diff --git a/backend/src/modules/cron/bankstatementsync.service.ts b/backend/src/modules/cron/bankstatementsync.service.ts index 0841dff..71aa17d 100644 --- a/backend/src/modules/cron/bankstatementsync.service.ts +++ b/backend/src/modules/cron/bankstatementsync.service.ts @@ -1,8 +1,6 @@ // /services/bankStatementService.ts import axios from "axios" import { centralServicesClient } from "../push-server.client" -import dayjs from "dayjs" -import utc from "dayjs/plugin/utc.js" import {secrets} from "../../utils/secrets" import {FastifyInstance} from "fastify" @@ -15,16 +13,8 @@ import { import { eq, and, - isNull, } from "drizzle-orm" -dayjs.extend(utc) - -interface BalanceAmount { - amount: string - currency: string -} - interface BookedTransaction { bookingDate: string valueDate: string @@ -49,6 +39,13 @@ interface TransactionsResponse { } } +export interface BankStatementSyncResult { + accountsFound: number + accountsSynced: number + transactionsImported: number + errors: Array<{ accountId: string; message: string }> +} + const normalizeDate = (val: any) => { if (!val) return null const d = new Date(val) @@ -81,7 +78,7 @@ export function bankStatementService(server: FastifyInstance) { // ----------------------------------------------- // ✔ Salden laden // ----------------------------------------------- - const getBalanceData = async (accountId: string): Promise => { + const getBalanceData = async (accountId: string): Promise => { try { if (useCentralBanking) return await centralServicesClient.getBankingBalances(accountId) const {data} = await axios.get( @@ -109,7 +106,7 @@ export function bankStatementService(server: FastifyInstance) { .where(eq(bankaccounts.accountId, accountId)) } - return false + throw err } } @@ -135,54 +132,61 @@ export function bankStatementService(server: FastifyInstance) { return data.transactions.booked } catch (err: any) { server.log.error(err.response?.data ?? err) - return null + throw err } } // ----------------------------------------------- // ✔ Haupt-Sync-Prozess // ----------------------------------------------- - const syncAccounts = async (tenantId:number) => { - try { - console.log("Starting account sync…") + const syncAccounts = async (tenantId:number): Promise => { + console.log("Starting account sync…") - // 🟦 DB: Aktive Accounts - const accounts = await server.db - .select() - .from(bankaccounts) - .where(and(eq(bankaccounts.expired, false),eq(bankaccounts.tenant, tenantId))) + // 🟦 DB: Aktive Accounts + const accounts = await server.db + .select() + .from(bankaccounts) + .where(and(eq(bankaccounts.expired, false),eq(bankaccounts.tenant, tenantId))) - if (!accounts.length) return + const result: BankStatementSyncResult = { + accountsFound: accounts.length, + accountsSynced: 0, + transactionsImported: 0, + errors: [], + } - const allNewTransactions: any[] = [] + if (!accounts.length) return result - for (const account of accounts) { + const allNewTransactions: any[] = [] + + for (const account of accounts) { + try { // --------------------------- // 1. BALANCE SYNC // --------------------------- const balData = await getBalanceData(account.accountId) - if (balData === false) break - if (balData) { const closing = balData.balances.find( (i: any) => i.balanceType === "closingBooked" ) - const bookedBal = Number(closing.balanceAmount.amount) - - await server.db - .update(bankaccounts) - .set({balance: bookedBal}) - .where(eq(bankaccounts.id, account.id)) + if (closing?.balanceAmount?.amount !== undefined) { + const bookedBal = Number(closing.balanceAmount.amount) + if (Number.isFinite(bookedBal)) { + await server.db + .update(bankaccounts) + .set({balance: bookedBal}) + .where(eq(bankaccounts.id, account.id)) + } + } } // --------------------------- // 2. TRANSACTIONS // --------------------------- let transactions = await getTransactionData(account.accountId) - if (!transactions) continue //@ts-ignore transactions = transactions.map((item) => ({ @@ -217,45 +221,37 @@ export function bankStatementService(server: FastifyInstance) { ) allNewTransactions.push(...filtered) + + await server.db + .update(bankaccounts) + .set({syncedAt: new Date()}) + .where(eq(bankaccounts.id, account.id)) + result.accountsSynced++ + } catch (error: any) { + const message = error?.message || String(error) + result.errors.push({accountId: account.accountId, message}) + server.log.error({err: error, accountId: account.accountId}, "Bankkonto konnte nicht synchronisiert werden") } - - // --------------------------- - // 3. NEW TRANSACTIONS → DB - // --------------------------- - if (allNewTransactions.length > 0) { - await server.db.insert(bankstatements).values(allNewTransactions) - - const affectedAccounts = [ - ...new Set(allNewTransactions.map((t) => t.account)), - ] - - const normalizeDate = (val: any) => { - if (!val) return null - const d = new Date(val) - return isNaN(d.getTime()) ? null : d - } - - for (const accId of affectedAccounts) { - await server.db - .update(bankaccounts) - //@ts-ignore - .set({syncedAt: normalizeDate(dayjs())}) - .where(eq(bankaccounts.id, accId)) - } - } - - console.log("Bank statement sync completed.") - } catch (error) { - console.error(error) } + // --------------------------- + // 3. NEW TRANSACTIONS → DB + // --------------------------- + if (allNewTransactions.length > 0) { + await server.db.insert(bankstatements).values(allNewTransactions) + result.transactionsImported = allNewTransactions.length + } + + console.log("Bank statement sync completed.") + return result } return { run: async (tenant) => { await getToken() - await syncAccounts(tenant) + const result = await syncAccounts(tenant) console.log("Service: Bankstatement sync finished") + return result } } } diff --git a/backend/src/routes/functions.ts b/backend/src/routes/functions.ts index 23cf6f4..2cf1573 100644 --- a/backend/src/routes/functions.ts +++ b/backend/src/routes/functions.ts @@ -308,7 +308,16 @@ export default async function functionRoutes(server: FastifyInstance) { }) server.post('/functions/services/bankstatementsync', async (req, reply) => { - await server.services.bankStatements.run(req.user.tenant_id); + const result = await server.services.bankStatements.run(req.user.tenant_id); + if (result.errors.length > 0) { + const firstError = result.errors[0]?.message + return reply.code(502).send({ + error: 'bank_sync_failed', + message: `${result.errors.length} von ${result.accountsFound} Bankkonten konnten nicht synchronisiert werden.${firstError ? ` Ursache: ${firstError}` : ''}`, + ...result, + }) + } + return result }) server.post('/functions/services/prepareincominginvoices', async (req, reply) => { diff --git a/frontend/pages/banking/index.vue b/frontend/pages/banking/index.vue index 591cf7e..fae1329 100644 --- a/frontend/pages/banking/index.vue +++ b/frontend/pages/banking/index.vue @@ -174,11 +174,15 @@ watch([selectedPeriod, dateRange], () => { const syncBankStatements = async () => { isSyncing.value = true try { - await $api('/api/functions/services/bankstatementsync', {method: 'POST'}) - toast.add({title: 'Erfolg', description: 'Bankdaten synchronisiert.', color: 'green'}) + const result = await $api('/api/functions/services/bankstatementsync', {method: 'POST'}) + const description = result.accountsFound === 0 + ? 'Es sind keine aktiven Bankkonten eingerichtet.' + : `${result.accountsSynced} Konto/Konten synchronisiert, ${result.transactionsImported} neue Umsätze importiert.` + toast.add({title: 'Bankdaten synchronisiert', description, color: 'green'}) await setupPage() } catch (error) { - toast.add({title: 'Fehler', description: 'Fehler beim Abruf.', color: 'red'}) + const description = error?.data?.message || error?.message || 'Fehler beim Abruf.' + toast.add({title: 'Bank-Synchronisierung fehlgeschlagen', description, color: 'red'}) } finally { isSyncing.value = false }