Buchungskalender: Abgleich – G22/K33-Platzhalter

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-10-07 14:58:50 +00:00
1 parent 74543b75c3
commit e5793fd151
6 files changed
+15 -10

No files matched your search

@@ -7,7 +7,8 @@
Betreff: Der Kalenderabgleich für {{unterkunft}} funktioniert nicht
Absender: „WEBklar“ <Portal-Postfach>
Empfänger: Kunde (E-Mail des Kundenkontos) · BCC intern
Platzhalter: {{kunde_name}}, {{unterkunft}}, {{portal_url}}
Platzhalter: {{kunde_name}}, {{unterkunft}}, {{quelle}}, {{seit}}, {{portal_url}}
quelle = Name des externen Kalenders (z. B. Booking.com); seit = Datum und Uhrzeit des letzten erfolgreichen Abgleichs bzw. des ersten Fehlers.
Der Abgleich mit einem externen Kalender (iCal-Link, z. B. Airbnb oder Booking.com) schlägt wiederholt fehl; belegte Tage von dort fehlen sonst im Buchungskalender. Höchstens einmal je Tag.
Texte sind Entwurf für die Mail-Überarbeitung des Inhabers; verbindlich sind nur Platzhalter und Blöcke.
-->
@@ -34,7 +35,7 @@ a{color:#111111;}
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr><td class="px" style="padding:44px 40px 8px 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td style="background:#e4f2e6;border-radius:100px;padding:6px 12px;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#1d5b2c;">Hinweis</td></tr></table></td></tr>
<tr><td class="px ink" style="padding:16px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:28px;line-height:34px;mso-line-height-rule:exactly;font-weight:bold;letter-spacing:-0.5px;color:#111111;">Kalenderabgleich fehlgeschlagen</td></tr>
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Hallo {{kunde_name}},<br>wir konnten den externen Kalender von {{unterkunft}} nicht abrufen. Solange das nicht klappt, fehlen die dort belegten Tage im Buchungskalender, und Gäste könnten Zeiträume anfragen, die schon vergeben sind. Bitte prüfe den Kalender-Link in den Einstellungen.</td></tr>
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Hallo {{kunde_name}},<br>der Abgleich deines Kalenders „{{quelle}}“ für {{unterkunft}} schlägt seit {{seit}} fehl. Solange das nicht klappt, fehlen die dort belegten Tage im Buchungskalender, und Gäste könnten Zeiträume anfragen, die schon vergeben sind. Bitte prüfe den Kalender-Link in den Einstellungen.</td></tr>
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td class="btn" bgcolor="#111111" style="background:#111111;border-radius:10px;"><a href="{{portal_url}}" style="display:block;padding:14px 26px;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:bold;color:#ffffff;text-decoration:none;">Kalender prüfen &rarr;</a></td></tr></table></td></tr>
<tr><td class="px ink" style="padding:28px 40px 44px 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:24px;color:#111111;">Viele Grüße<br><strong>Dein WEBklar-Team</strong></td></tr>
</table>
+1
View File
@@ -111,6 +111,7 @@ export function renderMail(id, values = {}, blocks = {}) {
.replace(/\{\{(\w+)\}\}/g, (_, key) => escapeHtml(valueOf(key)).replace(/\r?\n/g, '<br>'))
// Anrede ohne bekannten Namen
.replace(/Guten Tag ,/g, 'Guten Tag,')
.replace(/Hallo ,/g, 'Hallo,')
const subject = template.subject
.replace(/\{\{(\w+)\}\}/g, (_, key) => valueOf(key))
+4 -4
View File
@@ -23,8 +23,8 @@ import { leereCache } from './kontext.js'
const TAG_MS = 24 * 60 * 60 * 1000
const STANDARD_DECKEL = 500
const SEITE = 100
const ANONYM_JAHRE = 3
const RECHNUNG_JAHRE = 8
const ANONYM_JAHRE = () => config.unterkunft.anonymisierenJahre ?? 3
const RECHNUNG_JAHRE = () => config.unterkunft.rechnungJahre ?? 8
const MARKER = 'Buchungsdaten gelöscht am '
const MARKER_RE = /Buchungsdaten gelöscht am (\d{4}-\d{2}-\d{2})/
const ANFRAGE_ANONYM = { gast: {}, nachrichtVermieter: '', ipHash: '' }
@@ -175,7 +175,7 @@ export async function anonymisiere(now, { deckel = config.unterkunft.aufbewahrun
[Query.equal('status', ['gebucht', 'bezahlt', 'storniert'])],
['status', 'rechnungId', 'anonymisiertAt', 'bis'],
// Abreisejahr Y: faellig, sobald das Berliner Jahr >= Y + 3 + 1 ist (3 Jahre nach Ende des Jahres Y)
(d) => !d.anonymisiertAt && gueltigerTag(d.bis) && gebuchtJahr >= jahrVon(d.bis) + ANONYM_JAHRE + 1 && (d.status !== 'storniert' || hatRechnung(d)),
(d) => !d.anonymisiertAt && gueltigerTag(d.bis) && gebuchtJahr >= jahrVon(d.bis) + ANONYM_JAHRE() + 1 && (d.status !== 'storniert' || hatRechnung(d)),
rest,
)
await bearbeite('anonymisieren', gebucht, async (d) => {
@@ -190,7 +190,7 @@ export async function anonymisiere(now, { deckel = config.unterkunft.aufbewahrun
await schritt('rechnungen', async () => {
const jahr = jahrVon(heute(new Date(nowMs)))
const RE = config.collections.ukRechnungen
const faellig = (r) => gueltigerTag(r.datum) && jahr >= jahrVon(r.datum) + RECHNUNG_JAHRE + 1
const faellig = (r) => gueltigerTag(r.datum) && jahr >= jahrVon(r.datum) + RECHNUNG_JAHRE() + 1
const gefunden = await sammle(RE, [], ['anfrageId', 'datum'], faellig, rest)
if (gefunden.gedeckelt) erg.gedeckelt = true
const erledigt = new Set()
+2 -1
View File
@@ -95,7 +95,7 @@ export async function anGast(art, ctx, anfrage, extra = {}) {
const blocks = {}
let anhang = null
if (art !== 'G16' && art !== 'G18' && art !== 'G21' && art !== 'G20') values.stornobedingungen = await stornoText(anfrage)
if (art !== 'G16' && art !== 'G18' && art !== 'G21' && art !== 'G20' && art !== 'G22') values.stornobedingungen = await stornoText(anfrage)
if (art === 'G17' || art === 'G19' || art === 'G20') values.gesamt = formatEuro(anfrage.summen?.bruttoCent ?? 0)
if (art === 'G17' || art === 'G19') values.posten_text = postenText(anfrage.posten)
if (art === 'G17') {
@@ -164,6 +164,7 @@ export async function anVermieter(template, ctx, values = {}) {
zahlart_text: ZAHLART[anfrage.zahlart] ?? LEER,
})
}
if (!anfrage) vorgabe.kunde_name = text(ctx?.kundeName) || (await kundenName(ctx))
const alle = { portal_url: portalUrl(ctx), ...vorgabe, ...eigene }
if (template === 'T22') {
const r = await sendAdminNotice({ template, values: nurPlatzhalter(template, alle) })
+1 -1
View File
@@ -1339,7 +1339,7 @@ test('K31–K34, T22: Dateien, Platzhalter, Du-Form', () => {
const erwartet = {
K31: ['kunde_name', 'gast_name', 'gast_email', 'unterkunft', 'zeitraum', 'naechte', 'personen', 'nummer', 'nachricht_gast', 'portal_url'],
K32: ['kunde_name', 'gast_name', 'unterkunft', 'zeitraum', 'naechte', 'personen', 'nummer', 'gesamt', 'zahlart_text', 'portal_url'],
K33: ['kunde_name', 'unterkunft', 'portal_url'],
K33: ['kunde_name', 'unterkunft', 'quelle', 'seit', 'portal_url'],
K34: ['kunde_name', 'projekt', 'preis', 'ust_hinweis', 'begruendung', 'portal_url'],
T22: ['kunde_name', 'email', 'projekt', 'nachricht', 'woms_url'],
}
+4 -2
View File
@@ -48,7 +48,7 @@ beforeEach(async () => {
sink = mailSink()
setMailTransportForTests(sink)
aw.seed('portalFeatures', { $id: FEATURE, customerId: 'c1', projectId: 'p1', featureKey: 'unterkunft', siteKey: 'abcdefghijklmnop', state: 'active', enabled: true })
aw.seed('customers', { $id: 'c1', email: 'mimi@example.com' })
aw.seed('customers', { $id: 'c1', email: 'mimi@example.com', name: 'Mimi Muster' })
aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Donau' })
})
afterEach(() => Object.assign(config.unterkunft, vorher))
@@ -237,7 +237,9 @@ test('K33: erst nach icalFehlerStunden ohne Erfolg, danach hoechstens 1x je 24 h
assert.deepEqual(Object.keys(K33()[0]).sort(), ['an', 'anfrageId', 'template'])
await warteAufVersand()
const k33 = sink.sent.find((m) => /Erste/.test(m.text ?? ''))
assert.ok(k33, 'Mail nennt die Unterkunft (K33 kennt keine Platzhalter quelle/seit)')
assert.ok(k33, 'Mail nennt die Unterkunft')
assert.match(k33.text, /Hallo Mimi Muster,/)
assert.match(k33.text, /„Booking\.com“ für Erste schlägt seit 07\.11\.2026, 06:00 fehl/)
assert.deepEqual(k33.to.map((x) => x.address), ['mimi@example.com'])
assert.ok(!JSON.stringify(k33).includes('geheim'))
assert.ok(!JSON.stringify(K33()[0]).includes('geheim'))