All checks were successful
Build and Push Docker Images / build-website (push) Successful in 23s
Build and Push Docker Images / build-central-services-admin (push) Successful in 22s
Build and Push Docker Images / build-docs (push) Successful in 22s
Build and Push Docker Images / build-backend (push) Successful in 44s
Build and Push Docker Images / build-frontend (push) Successful in 1m13s
Build and Push Docker Images / build-central-services-api (push) Successful in 21s
305 lines
11 KiB
TypeScript
305 lines
11 KiB
TypeScript
// /services/bankStatementService.ts
|
|
import axios from "axios"
|
|
import { centralServicesClient } from "../push-server.client"
|
|
import {secrets} from "../../utils/secrets"
|
|
import {FastifyInstance} from "fastify"
|
|
|
|
// Drizzle imports
|
|
import {
|
|
bankaccounts,
|
|
bankstatements,
|
|
} from "../../../db/schema"
|
|
|
|
import {
|
|
eq,
|
|
and,
|
|
} from "drizzle-orm"
|
|
|
|
interface BookedTransaction {
|
|
bookingDate: string
|
|
valueDate: string
|
|
internalTransactionId: string
|
|
transactionAmount: { amount: string; currency: string }
|
|
|
|
creditorAccount?: { iban?: string }
|
|
creditorName?: string
|
|
|
|
debtorAccount?: { iban?: string }
|
|
debtorName?: string
|
|
|
|
remittanceInformationUnstructured?: string
|
|
remittanceInformationStructured?: string
|
|
remittanceInformationStructuredArray?: string[]
|
|
additionalInformation?: string
|
|
}
|
|
|
|
interface TransactionsResponse {
|
|
transactions: {
|
|
booked: BookedTransaction[]
|
|
}
|
|
}
|
|
|
|
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)
|
|
return isNaN(d.getTime()) ? null : d
|
|
}
|
|
|
|
export const getBankAccountOwnerName = (account: any) => {
|
|
const ownerName = typeof account?.owner_name === "string" ? account.owner_name.trim() : ""
|
|
return ownerName || null
|
|
}
|
|
|
|
export const isExpiredBankingError = (error: any) => {
|
|
const values = [
|
|
error?.response?.data?.summary,
|
|
error?.response?.data?.detail,
|
|
error?.response?.data?.message,
|
|
error?.message,
|
|
]
|
|
return values.some((value) => typeof value === "string" && value.toLowerCase().includes("expired"))
|
|
}
|
|
|
|
export function bankStatementService(server: FastifyInstance) {
|
|
|
|
let accessToken: string | null = null
|
|
const useCentralBanking = Boolean(secrets.FEDEO_CENTRAL_SERVICES_ENABLED && centralServicesClient.configured())
|
|
|
|
// -----------------------------------------------
|
|
// ✔ TOKEN LADEN
|
|
// -----------------------------------------------
|
|
const getToken = async () => {
|
|
if (useCentralBanking) return
|
|
console.log("Fetching GoCardless token…")
|
|
|
|
const response = await axios.post(
|
|
`${secrets.GOCARDLESS_BASE_URL}/token/new/`,
|
|
{
|
|
secret_id: secrets.GOCARDLESS_SECRET_ID,
|
|
secret_key: secrets.GOCARDLESS_SECRET_KEY,
|
|
}
|
|
)
|
|
|
|
accessToken = response.data.access
|
|
}
|
|
|
|
// -----------------------------------------------
|
|
// ✔ Salden laden
|
|
// -----------------------------------------------
|
|
const getBalanceData = async (accountId: string, tenantId: number): Promise<any> => {
|
|
try {
|
|
if (useCentralBanking) return await centralServicesClient.getBankingBalances(accountId)
|
|
const {data} = await axios.get(
|
|
`${secrets.GOCARDLESS_BASE_URL}/accounts/${accountId}/balances`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
Accept: "application/json",
|
|
},
|
|
}
|
|
)
|
|
|
|
return data
|
|
} catch (err: any) {
|
|
server.log.error(err.response?.data ?? err)
|
|
|
|
if (isExpiredBankingError(err)) {
|
|
await server.db
|
|
.update(bankaccounts)
|
|
.set({expired: true})
|
|
.where(and(
|
|
eq(bankaccounts.accountId, accountId),
|
|
eq(bankaccounts.tenant, tenantId),
|
|
))
|
|
}
|
|
|
|
throw err
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------
|
|
// ✔ Kontoinhaber laden
|
|
// -----------------------------------------------
|
|
const getAccountData = async (accountId: string): Promise<any> => {
|
|
if (useCentralBanking) return await centralServicesClient.getBankingAccount(accountId)
|
|
const {data} = await axios.get(
|
|
`${secrets.GOCARDLESS_BASE_URL}/accounts/${accountId}`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
Accept: "application/json",
|
|
},
|
|
}
|
|
)
|
|
return data
|
|
}
|
|
|
|
// -----------------------------------------------
|
|
// ✔ Transaktionen laden
|
|
// -----------------------------------------------
|
|
const getTransactionData = async (accountId: string) => {
|
|
try {
|
|
if (useCentralBanking) {
|
|
const data = await centralServicesClient.getBankingTransactions(accountId)
|
|
return data.transactions.booked
|
|
}
|
|
const {data} = await axios.get<TransactionsResponse>(
|
|
`${secrets.GOCARDLESS_BASE_URL}/accounts/${accountId}/transactions`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${accessToken}`,
|
|
Accept: "application/json",
|
|
},
|
|
}
|
|
)
|
|
|
|
return data.transactions.booked
|
|
} catch (err: any) {
|
|
server.log.error(err.response?.data ?? err)
|
|
throw err
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------
|
|
// ✔ Haupt-Sync-Prozess
|
|
// -----------------------------------------------
|
|
const syncAccounts = async (tenantId:number): Promise<BankStatementSyncResult> => {
|
|
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)))
|
|
|
|
const result: BankStatementSyncResult = {
|
|
accountsFound: accounts.length,
|
|
accountsSynced: 0,
|
|
transactionsImported: 0,
|
|
errors: [],
|
|
}
|
|
|
|
if (!accounts.length) return result
|
|
|
|
const allNewTransactions: any[] = []
|
|
|
|
for (const account of accounts) {
|
|
try {
|
|
|
|
// ---------------------------
|
|
// 0. KONTOINHABER SYNC
|
|
// ---------------------------
|
|
try {
|
|
const accountData = await getAccountData(account.accountId)
|
|
const ownerName = getBankAccountOwnerName(accountData)
|
|
if (ownerName && ownerName !== account.ownerName) {
|
|
await server.db
|
|
.update(bankaccounts)
|
|
.set({ownerName})
|
|
.where(eq(bankaccounts.id, account.id))
|
|
}
|
|
} catch (error: any) {
|
|
server.log.warn({err: error, accountId: account.accountId}, "Kontoinhaber konnte nicht synchronisiert werden")
|
|
}
|
|
|
|
// ---------------------------
|
|
// 1. BALANCE SYNC
|
|
// ---------------------------
|
|
const balData = await getBalanceData(account.accountId, tenantId)
|
|
|
|
if (balData) {
|
|
const closing = balData.balances.find(
|
|
(i: any) => i.balanceType === "closingBooked"
|
|
)
|
|
|
|
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)
|
|
|
|
//@ts-ignore
|
|
transactions = transactions.map((item) => ({
|
|
account: account.id,
|
|
date: normalizeDate(item.bookingDate),
|
|
credIban: item.creditorAccount?.iban ?? null,
|
|
credName: item.creditorName ?? null,
|
|
text: `
|
|
${item.remittanceInformationUnstructured ?? ""}
|
|
${item.remittanceInformationStructured ?? ""}
|
|
${item.additionalInformation ?? ""}
|
|
${item.remittanceInformationStructuredArray?.join("") ?? ""}
|
|
`.trim(),
|
|
amount: Number(item.transactionAmount.amount),
|
|
tenant: account.tenant,
|
|
debIban: item.debtorAccount?.iban ?? null,
|
|
debName: item.debtorName ?? null,
|
|
gocardlessId: item.internalTransactionId,
|
|
currency: item.transactionAmount.currency,
|
|
valueDate: normalizeDate(item.valueDate),
|
|
}))
|
|
|
|
// Existierende Statements laden
|
|
const existing = await server.db
|
|
.select({gocardlessId: bankstatements.gocardlessId})
|
|
.from(bankstatements)
|
|
.where(eq(bankstatements.tenant, account.tenant))
|
|
|
|
const filtered = transactions.filter(
|
|
//@ts-ignore
|
|
(tx) => !existing.some((x) => x.gocardlessId === tx.gocardlessId)
|
|
)
|
|
|
|
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)
|
|
result.transactionsImported = allNewTransactions.length
|
|
}
|
|
|
|
console.log("Bank statement sync completed.")
|
|
return result
|
|
}
|
|
|
|
return {
|
|
run: async (tenant) => {
|
|
await getToken()
|
|
const result = await syncAccounts(tenant)
|
|
console.log("Service: Bankstatement sync finished")
|
|
return result
|
|
}
|
|
}
|
|
}
|