This commit is contained in:
2026-05-06 21:34:21 -05:00
parent 9aae0e3c13
commit e01c5367bc
2 changed files with 532 additions and 237 deletions

View File

@@ -1,115 +1,169 @@
/**
* Accounting Routes
* /api/accounting/*
* Accounting Routes — /api/accounting/*
*
* Phase 1 — read-only:
* GET /accounts
* POST /sync-accounts (Phase-1 Stub)
* GET /register
* GET /reports/profit-loss
* GET /reports/balance-sheet
* Phase 1 + Phase 2 Lieferung 1 (Sync, Cache-Reads, Sync-Status)
*/
const express = require('express');
const router = express.Router();
const accountingService = require('../services/accounting-service');
/**
* Helper: einheitliches Error-Mapping QBO → HTTP
* Fault-Details landen im Server-Log, der User sieht die kurze Message.
*/
// ────────────────────────────────────────────────────────────────────
function handleQboError(err, res, context) {
console.error(`❌ Accounting/${context} error:`, err.message);
if (err.qboFault) {
console.error(' QBO Fault detail:', JSON.stringify(err.qboFault));
}
if (err.qboFault) console.error(' QBO Fault detail:', JSON.stringify(err.qboFault));
if (err.stack) console.error(err.stack);
res.status(500).json({
error: err.message || 'QBO request failed',
context
});
res.status(500).json({ error: err.message || 'QBO request failed', context });
}
// ─── GET /api/accounting/accounts ───────────────────────────────────
// Optional ?type=Bank|CreditCard|Expense|Income|... ?activeOnly=false
// ════════════════════════════════════════════════════════════════════
// Phase 1 — read-only
// ════════════════════════════════════════════════════════════════════
router.get('/accounts', async (req, res) => {
try {
const type = req.query.type || null;
const activeOnly = req.query.activeOnly === 'false' ? false : true;
const accounts = await accountingService.listAccounts({ type, activeOnly });
const accounts = await accountingService.listAccounts({
type: req.query.type || null,
activeOnly: req.query.activeOnly !== 'false'
});
res.json(accounts);
} catch (err) {
handleQboError(err, res, 'accounts');
}
} catch (err) { handleQboError(err, res, 'accounts'); }
});
// ─── POST /api/accounting/sync-accounts ─────────────────────────────
// Phase-1 Stub. Voll implementiert in Phase 2 mit qbo_account_cache.
router.post('/sync-accounts', (req, res) => {
res.json({
success: true,
synced: 0,
cached: false,
message:
'Account-Sync wird in Phase 2 aktiviert (qbo_account_cache). ' +
'In Phase 1 werden Accounts direkt live aus QBO geladen.'
});
});
// ─── GET /api/accounting/register ───────────────────────────────────
// ?accountId=<id>&startDate=YYYY-MM-DD&endDate=YYYY-MM-DD
router.get('/register', async (req, res) => {
const { accountId, startDate, endDate } = req.query;
if (!accountId) {
return res.status(400).json({ error: 'accountId is required' });
}
if (!accountId) return res.status(400).json({ error: 'accountId is required' });
try {
const result = await accountingService.getRegister({
accountId,
startDate,
endDate
});
res.json(result);
} catch (err) {
handleQboError(err, res, 'register');
}
res.json(await accountingService.getRegister({ accountId, startDate, endDate }));
} catch (err) { handleQboError(err, res, 'register'); }
});
// ─── GET /api/accounting/reports/profit-loss ────────────────────────
// ?startDate=YYYY-MM-DD&endDate=YYYY-MM-DD&accountingMethod=Accrual|Cash
router.get('/reports/profit-loss', async (req, res) => {
const { startDate, endDate, accountingMethod } = req.query;
try {
const data = await accountingService.getProfitAndLoss({
startDate,
endDate,
accountingMethod: accountingMethod || 'Accrual'
});
res.json(data);
} catch (err) {
handleQboError(err, res, 'profit-loss');
}
res.json(await accountingService.getProfitAndLoss({
startDate: req.query.startDate,
endDate: req.query.endDate,
accountingMethod: req.query.accountingMethod || 'Accrual'
}));
} catch (err) { handleQboError(err, res, 'profit-loss'); }
});
// ─── GET /api/accounting/reports/balance-sheet ──────────────────────
// ?asOfDate=YYYY-MM-DD&accountingMethod=Accrual|Cash
router.get('/reports/balance-sheet', async (req, res) => {
const { asOfDate, accountingMethod } = req.query;
try {
const data = await accountingService.getBalanceSheet({
asOfDate,
accountingMethod: accountingMethod || 'Accrual'
res.json(await accountingService.getBalanceSheet({
asOfDate: req.query.asOfDate,
accountingMethod: req.query.accountingMethod || 'Accrual'
}));
} catch (err) { handleQboError(err, res, 'balance-sheet'); }
});
// ════════════════════════════════════════════════════════════════════
// Phase 2 Lieferung 1 — Sync + Cache-Reads
// ════════════════════════════════════════════════════════════════════
// ─── POST /api/accounting/sync-accounts ─────────────────────────────
// Triggert den Account-Cache-Sync. Vorher: No-Op-Stub. Jetzt: voll.
router.post('/sync-accounts', async (req, res) => {
try {
console.log('🔄 Syncing QBO accounts cache...');
const result = await accountingService.syncAccountsCache();
console.log(`✅ Synced ${result.synced} accounts in ${result.durationMs}ms`);
res.json({
success: true,
cacheName: 'accounts',
synced: result.synced,
durationMs: result.durationMs
});
} catch (err) { handleQboError(err, res, 'sync-accounts'); }
});
// ─── POST /api/accounting/sync-vendors ──────────────────────────────
router.post('/sync-vendors', async (req, res) => {
try {
console.log('🔄 Syncing QBO vendors cache...');
const result = await accountingService.syncVendorsCache();
console.log(`✅ Synced ${result.synced} vendors in ${result.durationMs}ms`);
res.json({
success: true,
cacheName: 'vendors',
synced: result.synced,
durationMs: result.durationMs
});
} catch (err) { handleQboError(err, res, 'sync-vendors'); }
});
// ─── GET /api/accounting/sync-status ────────────────────────────────
// Liefert Status beider Caches + Flag, ob heute schon synchronisiert wurde.
router.get('/sync-status', async (req, res) => {
try {
const accounts = await accountingService.getCacheStatus('accounts');
const vendors = await accountingService.getCacheStatus('vendors');
const accountsStaleToday = await accountingService.cacheIsStaleToday('accounts');
const vendorsStaleToday = await accountingService.cacheIsStaleToday('vendors');
res.json({
accounts: { ...accounts, staleToday: accountsStaleToday },
vendors: { ...vendors, staleToday: vendorsStaleToday }
});
res.json(data);
} catch (err) {
handleQboError(err, res, 'balance-sheet');
console.error('❌ sync-status error:', err.message);
res.status(500).json({ error: err.message });
}
});
module.exports = router;
// ─── GET /api/accounting/vendors ────────────────────────────────────
// Aus Cache. Optional ?search=<q>&activeOnly=true&limit=200
router.get('/vendors', async (req, res) => {
try {
const vendors = await accountingService.getVendorsFromCache({
search: req.query.search || '',
activeOnly: req.query.activeOnly !== 'false',
limit: req.query.limit ? Math.min(parseInt(req.query.limit, 10) || 200, 1000) : 200
});
res.json(vendors);
} catch (err) {
console.error('❌ vendors error:', err.message);
res.status(500).json({ error: err.message });
}
});
// ─── GET /api/accounting/expense-accounts ───────────────────────────
// Aus Cache. Liefert alle Expense-Accounts (für Category-Dropdown).
router.get('/expense-accounts', async (req, res) => {
try {
const accounts = await accountingService.getExpenseAccountsFromCache({
activeOnly: req.query.activeOnly !== 'false'
});
res.json(accounts);
} catch (err) {
console.error('❌ expense-accounts error:', err.message);
res.status(500).json({ error: err.message });
}
});
// ─── GET /api/accounting/payment-accounts ───────────────────────────
// Aus Cache. Liefert Bank- und Credit-Card-Accounts (für Payment-Account-Dropdown im Expense-Modal).
router.get('/payment-accounts', async (req, res) => {
try {
const accounts = await accountingService.getPaymentAccountsFromCache({
activeOnly: req.query.activeOnly !== 'false'
});
res.json(accounts);
} catch (err) {
console.error('❌ payment-accounts error:', err.message);
res.status(500).json({ error: err.message });
}
});
// ─── GET /api/accounting/payment-methods ────────────────────────────
// Live aus QBO (kleine, selten ändernde Liste — kein Cache nötig).
router.get('/payment-methods', async (req, res) => {
try {
const methods = await accountingService.getPaymentMethods({
activeOnly: req.query.activeOnly !== 'false'
});
res.json(methods);
} catch (err) { handleQboError(err, res, 'payment-methods'); }
});
module.exports = router;