Buchungskalender: Abgleich – G22/K33-Platzhalter
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
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
|
Betreff: Der Kalenderabgleich für {{unterkunft}} funktioniert nicht
|
||||||
Absender: „WEBklar“ <Portal-Postfach>
|
Absender: „WEBklar“ <Portal-Postfach>
|
||||||
Empfänger: Kunde (E-Mail des Kundenkontos) · BCC intern
|
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.
|
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.
|
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">
|
<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" 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 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 →</a></td></tr></table></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 →</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>
|
<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>
|
</table>
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ export function renderMail(id, values = {}, blocks = {}) {
|
|||||||
.replace(/\{\{(\w+)\}\}/g, (_, key) => escapeHtml(valueOf(key)).replace(/\r?\n/g, '<br>'))
|
.replace(/\{\{(\w+)\}\}/g, (_, key) => escapeHtml(valueOf(key)).replace(/\r?\n/g, '<br>'))
|
||||||
// Anrede ohne bekannten Namen
|
// Anrede ohne bekannten Namen
|
||||||
.replace(/Guten Tag ,/g, 'Guten Tag,')
|
.replace(/Guten Tag ,/g, 'Guten Tag,')
|
||||||
|
.replace(/Hallo ,/g, 'Hallo,')
|
||||||
|
|
||||||
const subject = template.subject
|
const subject = template.subject
|
||||||
.replace(/\{\{(\w+)\}\}/g, (_, key) => valueOf(key))
|
.replace(/\{\{(\w+)\}\}/g, (_, key) => valueOf(key))
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ import { leereCache } from './kontext.js'
|
|||||||
const TAG_MS = 24 * 60 * 60 * 1000
|
const TAG_MS = 24 * 60 * 60 * 1000
|
||||||
const STANDARD_DECKEL = 500
|
const STANDARD_DECKEL = 500
|
||||||
const SEITE = 100
|
const SEITE = 100
|
||||||
const ANONYM_JAHRE = 3
|
const ANONYM_JAHRE = () => config.unterkunft.anonymisierenJahre ?? 3
|
||||||
const RECHNUNG_JAHRE = 8
|
const RECHNUNG_JAHRE = () => config.unterkunft.rechnungJahre ?? 8
|
||||||
const MARKER = 'Buchungsdaten gelöscht am '
|
const MARKER = 'Buchungsdaten gelöscht am '
|
||||||
const MARKER_RE = /Buchungsdaten gelöscht am (\d{4}-\d{2}-\d{2})/
|
const MARKER_RE = /Buchungsdaten gelöscht am (\d{4}-\d{2}-\d{2})/
|
||||||
const ANFRAGE_ANONYM = { gast: {}, nachrichtVermieter: '', ipHash: '' }
|
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'])],
|
[Query.equal('status', ['gebucht', 'bezahlt', 'storniert'])],
|
||||||
['status', 'rechnungId', 'anonymisiertAt', 'bis'],
|
['status', 'rechnungId', 'anonymisiertAt', 'bis'],
|
||||||
// Abreisejahr Y: faellig, sobald das Berliner Jahr >= Y + 3 + 1 ist (3 Jahre nach Ende des Jahres Y)
|
// 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,
|
rest,
|
||||||
)
|
)
|
||||||
await bearbeite('anonymisieren', gebucht, async (d) => {
|
await bearbeite('anonymisieren', gebucht, async (d) => {
|
||||||
@@ -190,7 +190,7 @@ export async function anonymisiere(now, { deckel = config.unterkunft.aufbewahrun
|
|||||||
await schritt('rechnungen', async () => {
|
await schritt('rechnungen', async () => {
|
||||||
const jahr = jahrVon(heute(new Date(nowMs)))
|
const jahr = jahrVon(heute(new Date(nowMs)))
|
||||||
const RE = config.collections.ukRechnungen
|
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)
|
const gefunden = await sammle(RE, [], ['anfrageId', 'datum'], faellig, rest)
|
||||||
if (gefunden.gedeckelt) erg.gedeckelt = true
|
if (gefunden.gedeckelt) erg.gedeckelt = true
|
||||||
const erledigt = new Set()
|
const erledigt = new Set()
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export async function anGast(art, ctx, anfrage, extra = {}) {
|
|||||||
const blocks = {}
|
const blocks = {}
|
||||||
let anhang = null
|
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' || art === 'G20') values.gesamt = formatEuro(anfrage.summen?.bruttoCent ?? 0)
|
||||||
if (art === 'G17' || art === 'G19') values.posten_text = postenText(anfrage.posten)
|
if (art === 'G17' || art === 'G19') values.posten_text = postenText(anfrage.posten)
|
||||||
if (art === 'G17') {
|
if (art === 'G17') {
|
||||||
@@ -164,6 +164,7 @@ export async function anVermieter(template, ctx, values = {}) {
|
|||||||
zahlart_text: ZAHLART[anfrage.zahlart] ?? LEER,
|
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 }
|
const alle = { portal_url: portalUrl(ctx), ...vorgabe, ...eigene }
|
||||||
if (template === 'T22') {
|
if (template === 'T22') {
|
||||||
const r = await sendAdminNotice({ template, values: nurPlatzhalter(template, alle) })
|
const r = await sendAdminNotice({ template, values: nurPlatzhalter(template, alle) })
|
||||||
|
|||||||
@@ -1339,7 +1339,7 @@ test('K31–K34, T22: Dateien, Platzhalter, Du-Form', () => {
|
|||||||
const erwartet = {
|
const erwartet = {
|
||||||
K31: ['kunde_name', 'gast_name', 'gast_email', 'unterkunft', 'zeitraum', 'naechte', 'personen', 'nummer', 'nachricht_gast', 'portal_url'],
|
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'],
|
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'],
|
K34: ['kunde_name', 'projekt', 'preis', 'ust_hinweis', 'begruendung', 'portal_url'],
|
||||||
T22: ['kunde_name', 'email', 'projekt', 'nachricht', 'woms_url'],
|
T22: ['kunde_name', 'email', 'projekt', 'nachricht', 'woms_url'],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ beforeEach(async () => {
|
|||||||
sink = mailSink()
|
sink = mailSink()
|
||||||
setMailTransportForTests(sink)
|
setMailTransportForTests(sink)
|
||||||
aw.seed('portalFeatures', { $id: FEATURE, customerId: 'c1', projectId: 'p1', featureKey: 'unterkunft', siteKey: 'abcdefghijklmnop', state: 'active', enabled: true })
|
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' })
|
aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Donau' })
|
||||||
})
|
})
|
||||||
afterEach(() => Object.assign(config.unterkunft, vorher))
|
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'])
|
assert.deepEqual(Object.keys(K33()[0]).sort(), ['an', 'anfrageId', 'template'])
|
||||||
await warteAufVersand()
|
await warteAufVersand()
|
||||||
const k33 = sink.sent.find((m) => /Erste/.test(m.text ?? ''))
|
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.deepEqual(k33.to.map((x) => x.address), ['mimi@example.com'])
|
||||||
assert.ok(!JSON.stringify(k33).includes('geheim'))
|
assert.ok(!JSON.stringify(k33).includes('geheim'))
|
||||||
assert.ok(!JSON.stringify(K33()[0]).includes('geheim'))
|
assert.ok(!JSON.stringify(K33()[0]).includes('geheim'))
|
||||||
|
|||||||
Reference in new issue
Block a user