/** * 财务总账 / 期间关账 / 平台对账路由。 * * 总账是影子账:只读写 finance_*,不接管钱包余额。 */ import { Router } from 'express' import { closeFinancePeriod, getTrialBalance, listFinanceAccounts, listFinancePeriods, listJournalEntries, postManualVoucher, reopenFinancePeriod, syncFinanceLedgerFromFacts, } from '../../services/finance/finance-ledger-service.js' import { getFinanceReconciliation, importFinanceReconciliation, listFinanceReconciliations, resolveFinanceReconciliation, } from '../../services/finance/finance-reconciliation-service.js' import type { JsonObject } from '../../types/json.js' import { formatShanghaiDate } from '../../utils/finance-date-range.js' import { bodyOf, idOf, sessionOf } from './request-helpers.js' import { createJsonHandler, requireAdminPermission, requireAdminRoles } from './session.js' const router = Router() router.use('/finance', requireAdminRoles(['admin', 'operator', 'finance'])) router.get( '/finance/ledger/accounts', requireAdminPermission('finance.view'), createJsonHandler(() => listFinanceAccounts(), { successMessage: 'ok', errorMessage: '读取科目表失败', scope: '[admin/finance/ledger/accounts]', }), ) router.get( '/finance/ledger/trial-balance', requireAdminPermission('finance.view'), createJsonHandler( (req) => { const query = req.query as Record const today = formatShanghaiDate(new Date()) return getTrialBalance({ dateFrom: String(query.dateFrom || query.date_from || `${today.slice(0, 7)}-01`), dateTo: String(query.dateTo || query.date_to || today), }) }, { successMessage: 'ok', errorMessage: '读取试算平衡失败', scope: '[admin/finance/ledger/trial-balance]', }, ), ) router.get( '/finance/ledger/entries', requireAdminPermission('finance.view'), createJsonHandler((req) => listJournalEntries(req.query as JsonObject), { successMessage: 'ok', errorMessage: '读取总账凭证失败', scope: '[admin/finance/ledger/entries]', }), ) router.post( '/finance/ledger/sync', requireAdminPermission('finance.post'), createJsonHandler( (req) => { const body = bodyOf(req) const today = formatShanghaiDate(new Date()) return syncFinanceLedgerFromFacts({ dateFrom: String(body.dateFrom || body.date_from || `${today.slice(0, 7)}-01`), dateTo: String(body.dateTo || body.date_to || today), }) }, { successMessage: '总账已同步', errorMessage: '同步总账失败', scope: '[admin/finance/ledger/sync]', audit: (req, data) => ({ action: 'finance_ledger_synced', targetType: 'finance_ledger', targetId: 'sales_fact', data: data && typeof data === 'object' ? (data as JsonObject) : {}, }), }, ), ) router.post( '/finance/ledger/manual-voucher', requireAdminPermission('finance.post'), createJsonHandler((req) => postManualVoucher(bodyOf(req) as never, sessionOf(req)), { successMessage: '凭证已创建', errorMessage: '创建凭证失败', scope: '[admin/finance/ledger/manual-voucher]', audit: (req, data) => ({ action: 'finance_manual_voucher_created', targetType: 'finance_journal_entry', targetId: String((data as { entryId?: number })?.entryId || ''), data: data && typeof data === 'object' ? (data as JsonObject) : {}, }), }), ) router.get( '/finance/periods', requireAdminPermission('finance.view'), createJsonHandler(() => listFinancePeriods(), { successMessage: 'ok', errorMessage: '读取会计期间失败', scope: '[admin/finance/periods]', }), ) router.post( '/finance/periods/:periodKey/close', requireAdminPermission('finance.close'), createJsonHandler( (req) => { const body = bodyOf(req) return closeFinancePeriod( String((req.params as Record).periodKey || ''), sessionOf(req), String(body.reason || ''), ) }, { successMessage: '期间已关账', errorMessage: '关账失败', scope: '[admin/finance/periods/close]', audit: (req, data) => ({ action: 'finance_period_closed', targetType: 'finance_period', targetId: String((req.params as Record).periodKey || ''), data: data && typeof data === 'object' ? (data as JsonObject) : {}, }), }, ), ) router.post( '/finance/periods/:periodKey/reopen', requireAdminPermission('finance.close'), createJsonHandler( (req) => reopenFinancePeriod(String((req.params as Record).periodKey || '')), { successMessage: '期间已重新打开', errorMessage: '反关账失败', scope: '[admin/finance/periods/reopen]', audit: (req) => ({ action: 'finance_period_reopened', targetType: 'finance_period', targetId: String((req.params as Record).periodKey || ''), data: {}, }), }, ), ) router.get( '/finance/reconciliations', requireAdminPermission('finance.view'), createJsonHandler((req) => listFinanceReconciliations(req.query as JsonObject), { successMessage: 'ok', errorMessage: '读取对账批次失败', scope: '[admin/finance/reconciliations]', }), ) router.post( '/finance/reconciliations/import', requireAdminPermission('finance.reconcile'), createJsonHandler((req) => importFinanceReconciliation(bodyOf(req), sessionOf(req)), { successMessage: '账单已导入', errorMessage: '导入账单失败', scope: '[admin/finance/reconciliations/import]', audit: (req, data) => ({ action: 'finance_reconciliation_imported', targetType: 'finance_reconciliation', targetId: String((data as { reconciliationId?: number })?.reconciliationId || ''), data: data && typeof data === 'object' ? (data as JsonObject) : {}, }), }), ) router.get( '/finance/reconciliations/:id', requireAdminPermission('finance.view'), createJsonHandler((req) => getFinanceReconciliation(idOf(req, 'id')), { successMessage: 'ok', errorMessage: '读取对账明细失败', scope: '[admin/finance/reconciliations/detail]', }), ) router.post( '/finance/reconciliations/:id/resolve', requireAdminPermission('finance.reconcile'), createJsonHandler( (req) => { const body = bodyOf(req) return resolveFinanceReconciliation(idOf(req, 'id'), body.note, sessionOf(req)) }, { successMessage: '对账批次已标记解决', errorMessage: '标记对账解决失败', scope: '[admin/finance/reconciliations/resolve]', audit: (req) => ({ action: 'finance_reconciliation_resolved', targetType: 'finance_reconciliation', targetId: String(idOf(req, 'id') || ''), data: {}, }), }, ), ) export default router