Bankstatementsync
This commit is contained in:
@@ -48,6 +48,10 @@ import {loadSecrets, secrets} from "./utils/secrets";
|
|||||||
import {initMailer} from "./utils/mailer"
|
import {initMailer} from "./utils/mailer"
|
||||||
import {initS3} from "./utils/s3";
|
import {initS3} from "./utils/s3";
|
||||||
|
|
||||||
|
//Services
|
||||||
|
import servicesPlugin from "./plugins/services";
|
||||||
|
import {prepareIncomingInvoices} from "./modules/cron/prepareIncomingInvoices";
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const app = Fastify({ logger: false });
|
const app = Fastify({ logger: false });
|
||||||
await loadSecrets();
|
await loadSecrets();
|
||||||
@@ -68,6 +72,7 @@ async function main() {
|
|||||||
await app.register(tenantPlugin);
|
await app.register(tenantPlugin);
|
||||||
await app.register(dayjsPlugin);
|
await app.register(dayjsPlugin);
|
||||||
await app.register(dbPlugin);
|
await app.register(dbPlugin);
|
||||||
|
await app.register(servicesPlugin);
|
||||||
|
|
||||||
app.addHook('preHandler', (req, reply, done) => {
|
app.addHook('preHandler', (req, reply, done) => {
|
||||||
console.log(req.method)
|
console.log(req.method)
|
||||||
|
|||||||
246
src/modules/cron/bankstatementsync.service.ts
Normal file
246
src/modules/cron/bankstatementsync.service.ts
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
// /services/bankStatementService.ts
|
||||||
|
import axios from "axios"
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
import utc from "dayjs/plugin/utc.js"
|
||||||
|
import {secrets} from "../../utils/secrets"
|
||||||
|
import {FastifyInstance} from "fastify"
|
||||||
|
|
||||||
|
// Drizzle imports
|
||||||
|
import {
|
||||||
|
bankaccounts,
|
||||||
|
bankstatements,
|
||||||
|
} from "../../../db/schema"
|
||||||
|
|
||||||
|
import {
|
||||||
|
eq,
|
||||||
|
and,
|
||||||
|
isNull,
|
||||||
|
} from "drizzle-orm"
|
||||||
|
|
||||||
|
dayjs.extend(utc)
|
||||||
|
|
||||||
|
interface BalanceAmount {
|
||||||
|
amount: string
|
||||||
|
currency: string
|
||||||
|
}
|
||||||
|
|
||||||
|
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[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeDate = (val: any) => {
|
||||||
|
if (!val) return null
|
||||||
|
const d = new Date(val)
|
||||||
|
return isNaN(d.getTime()) ? null : d
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bankStatementService(server: FastifyInstance) {
|
||||||
|
|
||||||
|
let accessToken: string | null = null
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// ✔ TOKEN LADEN
|
||||||
|
// -----------------------------------------------
|
||||||
|
const getToken = async () => {
|
||||||
|
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): Promise<any | false> => {
|
||||||
|
try {
|
||||||
|
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)
|
||||||
|
|
||||||
|
const expired =
|
||||||
|
err.response?.data?.summary?.includes("expired") ||
|
||||||
|
err.response?.data?.detail?.includes("expired")
|
||||||
|
|
||||||
|
if (expired) {
|
||||||
|
await server.db
|
||||||
|
.update(bankaccounts)
|
||||||
|
.set({expired: true})
|
||||||
|
.where(eq(bankaccounts.accountId, accountId))
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// ✔ Transaktionen laden
|
||||||
|
// -----------------------------------------------
|
||||||
|
const getTransactionData = async (accountId: string) => {
|
||||||
|
try {
|
||||||
|
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)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------
|
||||||
|
// ✔ Haupt-Sync-Prozess
|
||||||
|
// -----------------------------------------------
|
||||||
|
const syncAccounts = async (tenantId:number) => {
|
||||||
|
try {
|
||||||
|
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)))
|
||||||
|
|
||||||
|
if (!accounts.length) return
|
||||||
|
|
||||||
|
const allNewTransactions: any[] = []
|
||||||
|
|
||||||
|
for (const account of accounts) {
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// 2. TRANSACTIONS
|
||||||
|
// ---------------------------
|
||||||
|
let transactions = await getTransactionData(account.accountId)
|
||||||
|
if (!transactions) continue
|
||||||
|
|
||||||
|
//@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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------
|
||||||
|
// 3. NEW TRANSACTIONS → DB
|
||||||
|
// ---------------------------
|
||||||
|
if (allNewTransactions.length > 0) {
|
||||||
|
await server.db.insert(bankstatements).values(allNewTransactions)
|
||||||
|
|
||||||
|
const affectedAccounts = [
|
||||||
|
...new Set(allNewTransactions.map((t) => t.account)),
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const accId of affectedAccounts) {
|
||||||
|
await server.db
|
||||||
|
.update(bankaccounts)
|
||||||
|
.set({syncedAt: dayjs().utc().toISOString()})
|
||||||
|
.where(eq(bankaccounts.id, accId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Bank statement sync completed.")
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
run: async (tenant) => {
|
||||||
|
await getToken()
|
||||||
|
await syncAccounts(tenant)
|
||||||
|
console.log("Service: Bankstatement sync finished")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/plugins/services.ts
Normal file
24
src/plugins/services.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// /plugins/services.ts
|
||||||
|
import fp from "fastify-plugin";
|
||||||
|
import { bankStatementService } from "../modules/cron/bankstatementsync.service";
|
||||||
|
//import {initDokuboxClient, syncDokubox} from "../modules/cron/dokuboximport.service";
|
||||||
|
import { FastifyInstance } from "fastify";
|
||||||
|
import {prepareIncomingInvoices} from "../modules/cron/prepareIncomingInvoices";
|
||||||
|
|
||||||
|
declare module "fastify" {
|
||||||
|
interface FastifyInstance {
|
||||||
|
services: {
|
||||||
|
bankStatements: ReturnType<typeof bankStatementService>;
|
||||||
|
//dokuboxSync: ReturnType<typeof syncDokubox>;
|
||||||
|
//prepareIncomingInvoices: ReturnType<typeof prepareIncomingInvoices>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default fp(async function servicePlugin(server: FastifyInstance) {
|
||||||
|
server.decorate("services", {
|
||||||
|
bankStatements: bankStatementService(server),
|
||||||
|
//dokuboxSync: syncDokubox(server),
|
||||||
|
//prepareIncomingInvoices: prepareIncomingInvoices(server),
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -169,6 +169,10 @@ export default async function functionRoutes(server: FastifyInstance) {
|
|||||||
await finishManualGeneration(server,execution_id)
|
await finishManualGeneration(server,execution_id)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
server.post('/functions/services/bankstatementsync', async (req, reply) => {
|
||||||
|
await server.services.bankStatements.run(req.user.tenant_id);
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
/*server.post('/print/zpl/preview', async (req, reply) => {
|
/*server.post('/print/zpl/preview', async (req, reply) => {
|
||||||
const { zpl, widthMm = 50, heightMm = 30, dpmm = 8, asBase64 = false } = req.body as {zpl:string,widthMm:number,heightMm:number,dpmm:number,asBase64:string}
|
const { zpl, widthMm = 50, heightMm = 30, dpmm = 8, asBase64 = false } = req.body as {zpl:string,widthMm:number,heightMm:number,dpmm:number,asBase64:string}
|
||||||
|
|||||||
Reference in New Issue
Block a user