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:
1 parent
6d35674c1e
commit
1d9515823c
13 files changed
+123
-14
No files matched your search
@@ -331,8 +331,10 @@ router.post(
|
||||
const pruefung = pruefeFormToken(siteKey, b.formToken, now)
|
||||
if (pruefung === 'zu_schnell') return { status: 201, body: { nummer: scheinNummer() } }
|
||||
if (pruefung !== 'ok') throw fehlerMit(400, 'neu_laden')
|
||||
if (!zaehle(`uk-anfrage:${siteKey}`, config.unterkunft.anfragenJeSiteStunde, STUNDE_MS, now)) throw zuViele(3600)
|
||||
if (!ID_RE.test(text(b.unterkunft))) throw ungueltig({ unterkunft: 'Ungültige Unterkunft.' })
|
||||
if (!istTag(b.von) || !istTag(b.bis)) throw ungueltig({ zeitraum: 'Ungültige Daten.' })
|
||||
// je Website erst nach der Feldpruefung zaehlen: fehlerhafte Formulare verbrauchen das Stundenkontingent nicht
|
||||
if (!zaehle(`uk-anfrage:${siteKey}`, config.unterkunft.anfragenJeSiteStunde, STUNDE_MS, now)) throw zuViele(3600)
|
||||
const personen = typeof b.personen === 'string' && /^\d{1,3}$/.test(b.personen) ? Number(b.personen) : b.personen
|
||||
const anfrage = await legeAnfrageAn(
|
||||
ctx,
|
||||
|
||||
@@ -12,7 +12,7 @@ import { istFrei } from '../services/unterkunft/belegung.js'
|
||||
import { istTag, naechte } from '../services/unterkunft/datum.js'
|
||||
import { anfrageAnsicht as funktionAnfrageAnsicht, stelleAnfrage, zieheZurueck } from '../services/unterkunft/anfrageFunktion.js'
|
||||
import { UkFehler } from '../services/unterkunft/fehler.js'
|
||||
import { ladeFuerProjekt } from '../services/unterkunft/kontext.js'
|
||||
import { ladeFuerProjekt, leereCache } from '../services/unterkunft/kontext.js'
|
||||
import { berechnePreis, preiseVollstaendig, summiere } from '../services/unterkunft/preis.js'
|
||||
import { unterkunftFuerKunde } from '../services/unterkunft/schalter.js'
|
||||
import * as sp from '../services/unterkunft/speicher.js'
|
||||
@@ -330,6 +330,8 @@ router.put('/einstellungen', ...schreib, route(async (req, res) => {
|
||||
if (rechnungAuftrag === false) werte.rechnungAuftragAt = null
|
||||
return sp.speichereEinstellungen(ctx.featureId, werte)
|
||||
})
|
||||
// die oeffentliche Schnittstelle (Farbe, Firma, Online-Zahlung) merkt sich den Kontext 30 s: sofort frisch
|
||||
if (ctx.siteKey) leereCache(ctx.siteKey)
|
||||
res.json(await einstellungenAntwort(ctx, neu))
|
||||
}))
|
||||
|
||||
|
||||
@@ -6,12 +6,12 @@ import { statusAusKonto } from '../services/shop/connect.js'
|
||||
import { ladeFuerProjekt } from '../services/unterkunft/kontext.js'
|
||||
import { unterkunftModus } from '../services/unterkunft/schalter.js'
|
||||
import { aktualisiereAnfrage, ladeAnfrage, ladeEinstellungen } from '../services/unterkunft/speicher.js'
|
||||
import { verbucheZahlung } from '../services/unterkunft/zahlung.js'
|
||||
import { erstatteUeberzaehligeZahlung, verbucheZahlung, zahlungPasst } from '../services/unterkunft/zahlung.js'
|
||||
|
||||
/**
|
||||
* Stripe-Ereignisse des Buchungskalenders. Kein eigener Endpunkt: routes/shopWebhook.js (Connect-Webhook, ein Endpunkt je Konto)
|
||||
* leitet Ereignisse mit metadata.wkUk === '1' (Checkout-Sitzungen) oder metadata.webklar === 'unterkunft' (Erstattungen) hierher
|
||||
* und ruft bei account.updated zusaetzlich kontoAktualisiertUk. Alles idempotent; Fehler => 500 (Stripe wiederholt).
|
||||
* und ruft bei account.updated zusaetzlich kontoAktualisiertUk. Alles idempotent; ein Fehler bei Checkout- und Erstattungsereignissen => 500 (Stripe wiederholt), bei account.updated nur ein Log-Eintrag (shopWebhook.js).
|
||||
*/
|
||||
|
||||
const ACCOUNT_RE = /^acct_[A-Za-z0-9]+$/
|
||||
@@ -48,11 +48,12 @@ async function zahlungEingegangen(event, session, now) {
|
||||
const treffer = await anfrageZu(event, session.metadata?.anfrageId)
|
||||
if (!treffer) return
|
||||
const { anfrage, ctx } = treffer
|
||||
const brutto = anfrage.summen?.bruttoCent
|
||||
if (Number.isInteger(session.amount_total) && Number.isInteger(brutto) && session.amount_total !== brutto) {
|
||||
console.warn(`[unterkunft] Webhook: Betrag weicht ab (Buchung ${anfrage.nummer})`)
|
||||
}
|
||||
const pi = typeof session.payment_intent === 'string' ? session.payment_intent : session.payment_intent?.id
|
||||
if (!zahlungPasst(anfrage, session)) {
|
||||
console.warn(`[unterkunft] Webhook: Betrag oder Währung weicht ab (Buchung ${anfrage.nummer}), Zahlung wird erstattet`)
|
||||
await erstatteUeberzaehligeZahlung(ctx, pi)
|
||||
return
|
||||
}
|
||||
await verbucheZahlung(ctx, anfrage.$id, pi, { now })
|
||||
}
|
||||
|
||||
@@ -60,7 +61,8 @@ async function erstattungGebucht(event, charge) {
|
||||
const treffer = await anfrageZu(event, charge.metadata?.anfrageId)
|
||||
if (!treffer) return
|
||||
const { anfrage } = treffer
|
||||
if (anfrage.stripePaymentIntentId && typeof charge.payment_intent === 'string' && charge.payment_intent !== anfrage.stripePaymentIntentId) return
|
||||
// nur die Zahlung, die diese Buchung bezahlt hat (fremde oder ueberzaehlige Zahlungen mit gleicher anfrageId zaehlen nicht)
|
||||
if (!anfrage.stripePaymentIntentId || typeof charge.payment_intent !== 'string' || charge.payment_intent !== anfrage.stripePaymentIntentId) return
|
||||
const stand = charge.amount_refunded
|
||||
if (!Number.isInteger(stand) || stand <= 0) return
|
||||
await withLocks(['unterkunft:' + anfrage.unterkunftId], async () => {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() : ''
|
||||
|
||||
Reference in new issue
Block a user