import { FastifyInstance } from "fastify" import axios from "axios" import dayjs from "dayjs" import { secrets } from "../utils/secrets" import { insertHistoryItem } from "../utils/history" import { bankrequisitions, statementallocations, } from "../../db/schema" import { eq, and, } from "drizzle-orm" export default async function bankingRoutes(server: FastifyInstance) { // ------------------------------------------------------------------ // πŸ” GoCardLess Token Handling // ------------------------------------------------------------------ const goCardLessBaseUrl = secrets.GOCARDLESS_BASE_URL const goCardLessSecretId = secrets.GOCARDLESS_SECRET_ID const goCardLessSecretKey = secrets.GOCARDLESS_SECRET_KEY let tokenData: any = null const getToken = async () => { const res = await axios.post(`${goCardLessBaseUrl}/token/new/`, { secret_id: goCardLessSecretId, secret_key: goCardLessSecretKey, }) tokenData = res.data tokenData.created_at = new Date().toISOString() server.log.info("GoCardless token refreshed.") } const checkToken = async () => { if (!tokenData) return await getToken() const expired = dayjs(tokenData.created_at) .add(tokenData.access_expires, "seconds") .isBefore(dayjs()) if (expired) { server.log.info("Refreshing expired GoCardless token …") await getToken() } } // ------------------------------------------------------------------ // πŸ”— Create GoCardless Banking Link // ------------------------------------------------------------------ server.get("/banking/link/:institutionid", async (req, reply) => { try { await checkToken() const { institutionid } = req.params as { institutionid: string } const tenantId = req.user?.tenant_id if (!tenantId) return reply.code(401).send({ error: "Unauthorized" }) const { data } = await axios.post( `${goCardLessBaseUrl}/requisitions/`, { redirect: "https://app.fedeo.de/settings/banking", institution_id: institutionid, user_language: "de", }, { headers: { Authorization: `Bearer ${tokenData.access}` }, } ) // DB: Requisition speichern await server.db.insert(bankrequisitions).values({ id: data.id, tenant: tenantId, institutionId: institutionid, status: data.status, }) return reply.send({ link: data.link }) } catch (err: any) { server.log.error(err?.response?.data || err) return reply.code(500).send({ error: "Failed to generate link" }) } }) // ------------------------------------------------------------------ // 🏦 Check Bank Institutions // ------------------------------------------------------------------ server.get("/banking/institutions/:bic", async (req, reply) => { try { const { bic } = req.params as { bic: string } if (!bic) return reply.code(400).send("BIC missing") await checkToken() const { data } = await axios.get( `${goCardLessBaseUrl}/institutions/?country=de`, { headers: { Authorization: `Bearer ${tokenData.access}` } } ) const bank = data.find((i: any) => i.bic.toLowerCase() === bic.toLowerCase()) if (!bank) return reply.code(404).send("Bank not found") return reply.send(bank) } catch (err: any) { server.log.error(err?.response?.data || err) return reply.code(500).send("Failed to fetch institutions") } }) // ------------------------------------------------------------------ // πŸ“„ Get Requisition Details // ------------------------------------------------------------------ server.get("/banking/requisitions/:reqId", async (req, reply) => { try { const { reqId } = req.params as { reqId: string } if (!reqId) return reply.code(400).send("Requisition ID missing") await checkToken() const { data } = await axios.get( `${goCardLessBaseUrl}/requisitions/${reqId}`, { headers: { Authorization: `Bearer ${tokenData.access}` } } ) // Load account details if (data.accounts) { data.accounts = await Promise.all( data.accounts.map(async (accId: string) => { const { data: acc } = await axios.get( `${goCardLessBaseUrl}/accounts/${accId}`, { headers: { Authorization: `Bearer ${tokenData.access}` } } ) return acc }) ) } return reply.send(data) } catch (err: any) { server.log.error(err?.response?.data || err) return reply.code(500).send("Failed to fetch requisition details") } }) // ------------------------------------------------------------------ // πŸ’° Create Statement Allocation // ------------------------------------------------------------------ server.post("/banking/statements", async (req, reply) => { try { if (!req.user) return reply.code(401).send({ error: "Unauthorized" }) const { data: payload } = req.body as { data: any } const inserted = await server.db.insert(statementallocations).values({ ...payload, tenant: req.user.tenant_id }).returning() const createdRecord = inserted[0] await insertHistoryItem(server, { entity: "bankstatements", entityId: createdRecord.id, action: "created", created_by: req.user.user_id, tenant_id: req.user.tenant_id, oldVal: null, newVal: createdRecord, text: "Buchung erstellt", }) return reply.send(createdRecord) } catch (err) { console.error(err) return reply.code(500).send({ error: "Failed to create statement" }) } }) // ------------------------------------------------------------------ // πŸ—‘ Delete Statement Allocation // ------------------------------------------------------------------ server.delete("/banking/statements/:id", async (req, reply) => { try { if (!req.user) return reply.code(401).send({ error: "Unauthorized" }) const { id } = req.params as { id: string } const oldRecord = await server.db .select() .from(statementallocations) .where(eq(statementallocations.id, id)) .limit(1) const old = oldRecord[0] if (!old) return reply.code(404).send({ error: "Record not found" }) await server.db .delete(statementallocations) .where(eq(statementallocations.id, id)) await insertHistoryItem(server, { entity: "bankstatements", entityId: id, action: "deleted", created_by: req.user.user_id, tenant_id: req.user.tenant_id, oldVal: old, newVal: null, text: "Buchung gelΓΆscht", }) return reply.send({ success: true }) } catch (err) { console.error(err) return reply.code(500).send({ error: "Failed to delete statement" }) } }) }