Files
wehub-resource-sync 9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

131 lines
6.7 KiB
JavaScript

import { cli, Strategy } from '@jackwener/opencli/registry';
import { CommandExecutionError } from '@jackwener/opencli/errors';
import {
MERCURY_EXPENSES_URL,
RECEIPT_INPUT_SELECTOR,
assertCreateExpenseSurface,
clickCreateExpenseButton,
assertLoggedIn,
fillReimbursementFields,
inspectMercury,
normalizeReimbursementInput,
receiptBasename,
reviewSnapshot,
} from './utils.js';
cli({
site: 'mercury',
name: 'reimbursement-draft',
description: 'Create a Mercury reimbursement draft from a local receipt, correct OCR fields, and stop at Review',
access: 'write',
example: 'opencli --profile <profile> mercury reimbursement-draft --receipt /tmp/receipt.png --amount 140.00 --currency CNY --date 2026-06-26 --merchant "Example Merchant" --category "Marketing & Advertising" --notes "Example business purpose." -f json',
domain: 'app.mercury.com',
strategy: Strategy.UI,
browser: true,
siteSession: 'persistent',
defaultWindowMode: 'foreground',
navigateBefore: false,
args: [
{ name: 'receipt', required: true, help: 'Local receipt/proof file path' },
{ name: 'amount', required: true, help: 'Original-currency amount, e.g. 140.00' },
{ name: 'currency', default: 'CNY', help: 'Original currency code' },
{ name: 'date', required: true, help: 'Expense date as YYYY-MM-DD' },
{ name: 'merchant', required: true, help: 'Merchant shown on the reimbursement' },
{ name: 'category', default: 'Marketing & Advertising', help: 'Mercury expense category' },
{ name: 'notes', required: true, help: 'Business purpose / reimbursement notes' },
{ name: 'ocr-wait-seconds', default: '8', help: 'Seconds to wait after receipt upload before correcting OCR-overwritten fields' },
{ name: 'close-after-review', type: 'boolean', default: false, help: 'Close the Review dialog after verification; final Submit is still never clicked' },
],
columns: ['status', 'url', 'receipt', 'uploaded', 'fieldsTouched', 'reviewReady', 'submitBlocked', 'warnings'],
func: async (page, kwargs) => {
const input = normalizeReimbursementInput(kwargs);
const before = await inspectMercury(page);
assertLoggedIn(before);
await page.goto(MERCURY_EXPENSES_URL, { waitUntil: 'load', settleMs: 1500 });
await page.wait({ time: 1 });
const createSurface = await assertCreateExpenseSurface(page);
if (createSurface.hasFinalSubmit && /review/i.test(createSurface.bodyPreview || '')) {
throw new CommandExecutionError('Mercury appears to have an existing expense review open; refusing to click a possible final Submit expense button');
}
const opened = await clickCreateExpenseButton(page);
if (!opened.clicked) {
throw new CommandExecutionError('Could not find the Mercury Submit expense or New expense button');
}
await page.wait({ time: 2 });
if (!page.uploadFiles) {
throw new CommandExecutionError('Mercury reimbursement-draft requires Browser Bridge uploadFiles support to verify the intended receipt input');
}
const upload = await page.uploadFiles(RECEIPT_INPUT_SELECTOR, [input.receipt]);
if (!upload || upload.uploaded !== true || upload.files !== 1) {
throw new CommandExecutionError('Mercury receipt upload did not confirm exactly one uploaded file');
}
if (upload.target !== RECEIPT_INPUT_SELECTOR || upload.matches_n !== 1) {
throw new CommandExecutionError('Mercury receipt upload did not confirm the intended receipt input');
}
const uploadedNames = Array.isArray(upload.file_names) ? upload.file_names : [];
if (!uploadedNames.includes(receiptBasename(input.receipt))) {
throw new CommandExecutionError('Mercury receipt upload did not confirm the expected receipt file');
}
if (input.ocrWaitSeconds > 0) await page.wait({ time: input.ocrWaitSeconds });
const fields = await fillReimbursementFields(page, input);
await page.wait({ time: 1 });
const expectedFields = ['amount', 'currency', 'date', 'merchant', 'category', 'notes'];
const missing = expectedFields.filter((key) => !fields.touched[key]);
if (missing.length > 0) {
throw new CommandExecutionError(`Mercury reimbursement form fields were not all filled: ${missing.join(', ')}`);
}
const reviewClick = await page.evaluate(`(() => {
const norm = (s) => String(s || '').replace(/\\s+/g, ' ').trim().toLowerCase();
const candidates = Array.from(document.querySelectorAll('button, a, [role="button"], [role="link"]'))
.filter((node) => {
const style = window.getComputedStyle(node);
return style.visibility !== 'hidden' && style.display !== 'none' && node.offsetParent !== null;
});
const el = candidates.find((node) => norm(node.innerText || node.textContent || '') === 'review');
if (!el) return { clicked: false };
el.click();
return { clicked: true, text: el.innerText || el.textContent || '' };
})()`);
if (!reviewClick || typeof reviewClick !== 'object' || typeof reviewClick.clicked !== 'boolean') {
throw new CommandExecutionError('Mercury returned malformed Review click result');
}
if (!reviewClick.clicked) {
throw new CommandExecutionError('Mercury Review button was not clicked; inspect the page for validation errors');
}
await page.wait({ time: 2 });
const review = await reviewSnapshot(page);
if (!review.hasReview || !review.hasSubmitExpenseButton) {
throw new CommandExecutionError('Mercury did not reach the Review step with the final Submit expense button visible');
}
if (input.closeAfterReview) {
await page.evaluate(`(() => {
const norm = (s) => String(s || '').replace(/\\s+/g, ' ').trim().toLowerCase();
const el = Array.from(document.querySelectorAll('button, a, [role="button"], [role="link"]'))
.find((node) => norm(node.innerText || node.textContent || '') === 'close');
el?.click();
return { clicked: Boolean(el) };
})()`);
await page.wait({ time: 1 });
}
return [{
status: 'review_ready',
url: review.url,
receipt: receiptBasename(input.receipt),
uploaded: true,
fieldsTouched: JSON.stringify(fields.touched),
reviewReady: true,
submitBlocked: true,
warnings: 'final Submit expense was intentionally not clicked',
}];
},
});