Ausgehende SEPA-Mandate einführen #183

This commit is contained in:
2026-05-15 17:47:11 +02:00
parent 683d073b6e
commit 44017a768b
19 changed files with 513 additions and 8 deletions

View File

@@ -27,6 +27,7 @@ const columnMap: Record<string, any> = {
customerspaces: historyitems.customerspace,
customerinventoryitems: historyitems.customerinventoryitem,
memberrelations: historyitems.memberrelation,
outgoingsepamandates: historyitems.outgoingsepamandate,
};
const insertFieldMap: Record<string, string> = {
@@ -53,6 +54,7 @@ const insertFieldMap: Record<string, string> = {
customerspaces: "customerspace",
customerinventoryitems: "customerinventoryitem",
memberrelations: "memberrelation",
outgoingsepamandates: "outgoingsepamandate",
}
const parseId = (value: string) => {

View File

@@ -11,7 +11,7 @@ import {
sql,
} from "drizzle-orm"
import { authProfiles, costcentres } from "../../../db/schema";
import { authProfiles, costcentres, customers, entitybankaccounts } from "../../../db/schema";
import { resourceConfig } from "../../utils/resource.config";
import { useNextNumberRangeNumber } from "../../utils/functions";
import { getHistoryEntityLabel, insertHistoryItem } from "../../utils/history";
@@ -365,6 +365,50 @@ function prepareEntityBankAccountPayload(payload: Record<string, any>, requireAl
return { data: result }
}
async function validateOutgoingSepaMandatePayload(
server: FastifyInstance,
tenantId: number,
payload: Record<string, any>,
existing: Record<string, any> | null = null
) {
const customerId = Number(payload.customer ?? existing?.customer)
const bankaccountId = Number(payload.bankaccount ?? existing?.bankaccount)
if (!customerId || !bankaccountId) {
return "Kunde und Bankverbindung sind Pflichtfelder."
}
const [customer] = await server.db
.select()
.from(customers)
.where(and(eq(customers.id, customerId), eq(customers.tenant, tenantId)))
.limit(1)
if (!customer) {
return "Kunde nicht gefunden."
}
const [bankaccount] = await server.db
.select()
.from(entitybankaccounts)
.where(and(eq(entitybankaccounts.id, bankaccountId), eq(entitybankaccounts.tenant, tenantId)))
.limit(1)
if (!bankaccount) {
return "Bankverbindung nicht gefunden."
}
const assignedBankAccountIds = Array.isArray((customer.infoData as any)?.bankAccountIds)
? (customer.infoData as any).bankAccountIds.map((id: any) => Number(id))
: []
if (!assignedBankAccountIds.includes(bankaccountId)) {
return "Die Bankverbindung ist dem ausgewählten Kunden nicht zugeordnet."
}
return null
}
export default async function resourceRoutes(server: FastifyInstance) {
// -------------------------------------------------------------
@@ -796,6 +840,13 @@ export default async function resourceRoutes(server: FastifyInstance) {
}
}
if (resource === "outgoingsepamandates") {
const validationError = await validateOutgoingSepaMandatePayload(server, req.user.tenant_id, createData)
if (validationError) {
return reply.code(400).send({ error: validationError })
}
}
if (config.numberRangeHolder && !body[config.numberRangeHolder]) {
const numberRangeResource = resource === "members" ? "customers" : resource
const result = await useNextNumberRangeNumber(server, req.user.tenant_id, numberRangeResource)
@@ -809,6 +860,17 @@ export default async function resourceRoutes(server: FastifyInstance) {
const [created] = await server.db.insert(table).values(createData).returning()
if (resource === "outgoingsepamandates" && created?.defaultMandate) {
await server.db
.update(table)
.set({ defaultMandate: false })
.where(and(
eq(table.tenant, req.user.tenant_id),
eq(table.customer, created.customer),
sql`${table.id} <> ${created.id}`
))
}
if (["products", "services", "hourrates"].includes(resource)) {
await recalculateServicePricesForTenant(server, req.user.tenant_id, req.user?.user_id || null);
}
@@ -917,6 +979,13 @@ export default async function resourceRoutes(server: FastifyInstance) {
}
}
if (resource === "outgoingsepamandates") {
const validationError = await validateOutgoingSepaMandatePayload(server, tenantId, data, oldRecord)
if (validationError) {
return reply.code(400).send({ error: validationError })
}
}
Object.keys(data).forEach((key) => {
const value = data[key]
const shouldNormalize =
@@ -934,6 +1003,17 @@ export default async function resourceRoutes(server: FastifyInstance) {
updateWhereCond = applyResourceWhereFilters(resource, table, updateWhereCond)
const [updated] = await server.db.update(table).set(data).where(updateWhereCond).returning()
if (resource === "outgoingsepamandates" && updated?.defaultMandate) {
await server.db
.update(table)
.set({ defaultMandate: false })
.where(and(
eq(table.tenant, tenantId),
eq(table.customer, updated.customer),
sql`${table.id} <> ${updated.id}`
))
}
if (["products", "services", "hourrates"].includes(resource)) {
await recalculateServicePricesForTenant(server, tenantId, userId);
}

View File

@@ -34,6 +34,7 @@ const HISTORY_ENTITY_LABELS: Record<string, string> = {
files: "Dateien",
memberrelations: "Mitgliedsverhältnisse",
teams: "Teams",
outgoingsepamandates: "Ausgehende SEPA-Mandate",
}
export function getHistoryEntityLabel(entity: string) {
@@ -94,6 +95,7 @@ export async function insertHistoryItem(
incominginvoices: "incomingInvoice",
files: "file",
memberrelations: "memberrelation",
outgoingsepamandates: "outgoingsepamandate",
}
const fkColumn = columnMap[params.entity]

View File

@@ -25,6 +25,7 @@ import {
letterheads,
memberrelations,
ownaccounts,
outgoingsepamandates,
plants,
productcategories,
products,
@@ -53,7 +54,7 @@ export const resourceConfig = {
},
customers: {
searchColumns: ["name", "nameAddition", "customerNumber", "firstname", "lastname", "notes"],
mtmLoad: ["contacts","projects","plants","createddocuments","contracts","customerinventoryitems","customerspaces"],
mtmLoad: ["contacts","projects","plants","createddocuments","contracts","outgoingsepamandates","customerinventoryitems","customerspaces"],
table: customers,
numberRangeHolder: "customerNumber",
},
@@ -77,7 +78,13 @@ export const resourceConfig = {
table: contracts,
searchColumns: ["name", "notes", "contractNumber", "paymentType", "billingInterval", "sepaRef", "bankingName"],
numberRangeHolder: "contractNumber",
mtoLoad: ["customer", "contracttype"],
mtoLoad: ["customer", "contracttype", "outgoingsepamandate"],
},
outgoingsepamandates: {
table: outgoingsepamandates,
searchColumns: ["reference", "status", "mandateType", "sequenceType", "notes"],
numberRangeHolder: "reference",
mtoLoad: ["customer", "bankaccount"],
},
contracttypes: {
table: contracttypes,
@@ -200,7 +207,7 @@ export const resourceConfig = {
},
createddocuments: {
table: createddocuments,
mtoLoad: ["customer", "project", "contact", "contract", "plant","letterhead","createddocument"],
mtoLoad: ["customer", "project", "contact", "contract", "plant","letterhead","createddocument", "outgoingsepamandate"],
mtmLoad: ["statementallocations","files","createddocuments"],
mtmListLoad: ["statementallocations", "files"],
},
@@ -235,6 +242,10 @@ export const resourceConfig = {
table: entitybankaccounts,
searchColumns: ["description"],
},
bankaccount: {
table: entitybankaccounts,
searchColumns: ["description"],
},
serialexecutions: {
table: serialExecutions
}