Files
FEDEO/backend/src/routes/banking.ts
2026-01-06 12:07:43 +01:00

237 lines
7.8 KiB
TypeScript

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" })
}
})
}