Buchungskalender: Abschluss – Kleinigkeiten Server (Betrag/Waehrung, Erstattungs-Webhook, Obergrenze, Zaehler, Cache, Kommentare, Setup-Check)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-10-07 15:29:55 +00:00
1 parent 6d35674c1e
commit 1d9515823c
13 files changed
+123 -14

No files matched your search

+2 -1
View File
@@ -6,7 +6,7 @@ import { istTag, naechte } from './datum.js'
import { istFrei } from './belegung.js'
import { UkFehler } from './fehler.js'
import * as mails from './mails.js'
import { berechnePreis, preiseVollstaendig, pruefePosten, summiere } from './preis.js'
import { BRUTTO_MAX_CENT, berechnePreis, preiseVollstaendig, pruefePosten, summiere } from './preis.js'
import { stelleAus } from './rechnungen.js'
import { aktualisiereAnfrage, ladeAnfrage, findeRechnung, ladeEinstellungen, ladePreise, ladeRechnung, ladeUnterkunft, legeAnfrageDokAn, listeAnfragen, listeAnfragenAlle } from './speicher.js'
import { linkGueltig, neuesToken, tokenPasst } from './token.js'
@@ -137,6 +137,7 @@ export async function gibFrei(ctx, anfrageId, { posten, fristTage, nachricht, no
const steuerart = preise?.steuerart ?? 'regel'
const summen = { ...summiere(sauber, steuerart), steuerart }
if (!(summen.bruttoCent > 0)) throw new UkFehler(400, 'ungueltig', { posten: 'Die Summe muss größer als 0 sein.' })
if (summen.bruttoCent > BRUTTO_MAX_CENT) throw new UkFehler(400, 'ungueltig', { posten: 'Die Summe ist zu hoch.' })
if (!(await istFrei(a.unterkunftId, a.von, a.bis, { ohneAnfrage: a.$id, now }))) throw new UkFehler(409, 'zeitraum_belegt')
const tage = Number.isInteger(fristTage) && fristTage >= 1 ? fristTage : preise?.fristTage || 3
const { token, hash } = neuesToken()
+6 -1
View File
@@ -62,6 +62,9 @@ export function summiere(posten, steuerart) {
return { bruttoCent, nettoCent, ust }
}
/** Obergrenze der Bruttosumme einer Anfrage (999.999,99 EUR): darunter bleiben Stripe-Betrag und Rechnung sicher darstellbar. */
export const BRUTTO_MAX_CENT = 99_999_999
export function pruefePosten(posten, steuerart) {
const fehler = (m) => ({ fehler: { posten: m } })
const maxPosten = config.unterkunft?.maxPosten ?? 30
@@ -79,7 +82,9 @@ export function pruefePosten(posten, steuerart) {
if (steuerart === 'kleinunternehmer' && p.ustProzent !== 0) return fehler('Bei Kleinunternehmern ist keine Umsatzsteuer erlaubt.')
sauber.push({ text, menge: p.menge, einzelCent: p.einzelCent, ustProzent: p.ustProzent })
}
if (sauber.reduce((a, p) => a + p.menge * p.einzelCent, 0) <= 0) return fehler('Die Summe muss größer als 0 sein.')
const summe = sauber.reduce((a, p) => a + p.menge * p.einzelCent, 0)
if (summe <= 0) return fehler('Die Summe muss größer als 0 sein.')
if (summe > BRUTTO_MAX_CENT) return fehler('Die Summe ist zu hoch.')
return { posten: sauber }
}
+11
View File
@@ -91,6 +91,12 @@ export async function beendeOffeneZahlung(ctx, anfrage) {
}
}
/** Stimmt eine bezahlte Sitzung mit der Buchung ueberein: Betrag = Brutto der Anfrage (Cent), Waehrung eur? Sonst gilt sie nicht als Bezahlung. */
export function zahlungPasst(anfrage, session) {
const brutto = anfrage?.summen?.bruttoCent
return Number.isInteger(brutto) && session?.amount_total === brutto && String(session?.currency ?? '').toLowerCase() === 'eur'
}
/** Volle Rueckerstattung einer Zahlung, die zu keiner offenen Buchung mehr passt (nach Storno, Ablauf oder doppelt gezahlt). */
export async function erstatteUeberzaehligeZahlung(ctx, paymentIntentId) {
const accountId = ctx?.einstellungen?.stripeAccountId
@@ -153,6 +159,11 @@ export async function schliesseAbWennBezahlt(ctx, anfrage, { now = new Date(), e
merkeAbgleich(schluessel, { at: nowMs, abgelaufen: session?.status === 'expired' })
if (session?.status === 'complete' && session.payment_status === 'paid') {
const pi = typeof session.payment_intent === 'string' ? session.payment_intent : session.payment_intent?.id
if (!zahlungPasst(anfrage, session)) {
console.warn(`[unterkunft] Abgleich: Betrag oder Währung weicht ab (Buchung ${anfrage.nummer}), Zahlung wird erstattet`)
await erstatteUeberzaehligeZahlung(ctx, pi)
return anfrage
}
return await verbucheZahlung(ctx, anfrage.$id, pi, { now })
}
} catch (err) {
+1 -1
View File
@@ -22,7 +22,7 @@ const ADRESSE = /[^\s<>"',;()]+@[^\s<>"',;()]+/g
/** Adresse des Editors im Kundenportal. */
const portalUrl = () => `${config.publicBaseUrl}/dashboard.html#zeiten`
/** Anrede-Wert: Name aus einem String oder einem Kundendokument, getrimmt; sonst '' (die Vorlage sagt dann „Hallo ,“, wie K22/K23). */
/** Anrede-Wert: Name aus einem String oder einem Kundendokument, getrimmt; sonst '' (renderMail macht aus „Hallo ,“ dann „Hallo,“, wie bei K22/K23). */
function kundenName(kunde) {
const name = typeof kunde === 'string' ? kunde : kunde?.name
return typeof name === 'string' ? name.trim() : ''