diff --git a/server/services/unterkunft/mails.js b/server/services/unterkunft/mails.js
index a679d11..821556a 100644
--- a/server/services/unterkunft/mails.js
+++ b/server/services/unterkunft/mails.js
@@ -1,5 +1,6 @@
import { config } from '../../config.js'
import { loadTemplate } from '../mailTemplates.js'
+import { customerOf } from '../siteFeatures.js'
import { sendAdminNotice, sendCustomerMail } from '../mailer.js'
import { einreihen } from '../termine/outbox.js'
import { formatTag } from './datum.js'
@@ -16,6 +17,8 @@ import { ladeEinstellungen, ladePreise, ladeUnterkunft } from './speicher.js'
* rechnung (G19: ukRechnungen-Dokument), stornoRechnung + erstattetCent + grund (G22), G21 braucht nichts.
*/
export const _aufrufe = []
+/** Protokoll nur im Test (UK_TESTPROTOKOLL, setzt useTestEnv); nie Token, Werte oder Gastdaten. */
+const testProtokoll = () => process.env.NODE_ENV === 'test' || process.env.UK_TESTPROTOKOLL === '1'
const LEER = '–'
const text = (v) => (typeof v === 'string' ? v.trim() : '')
@@ -81,10 +84,11 @@ async function stornoText(anfrage) {
}
export async function anGast(art, ctx, anfrage, extra = {}) {
- _aufrufe.push({ an: 'gast', art, anfrageId: anfrage?.$id, extra })
+ if (testProtokoll()) _aufrufe.push({ an: 'gast', art, anfrageId: anfrage?.$id, hatToken: Boolean(extra?.token), grund: extra?.grund })
if (!GAST_ARTEN.includes(art)) throw new TypeError(`${String(art).slice(0, 20)} ist keine Gastmail`)
extra = extra ?? {}
const gast = anfrage?.gast ?? {}
+ if (art === 'G17' && (!text(extra.token) || !text(ctx?.siteKey))) throw new TypeError('Token oder siteKey für den Buchungslink fehlt')
if (!text(gast.email)) throw new TypeError('Empfänger fehlt')
const { einstellungen: e, werte } = await werteZuAnfrage(ctx, anfrage)
const values = { ...werte }
@@ -100,7 +104,6 @@ export async function anGast(art, ctx, anfrage, extra = {}) {
values.buchungs_link = `${config.publicBaseUrl}/unterkunft/${ctx?.siteKey ?? ''}/buchung/${anfrage.$id}?t=${token}`
values.nachricht_vermieter = text(anfrage.nachrichtVermieter)
blocks.nachricht = values.nachricht_vermieter !== ''
- if (!token) throw new TypeError('Token für den Buchungslink fehlt')
}
if (art === 'G18') {
values.nachricht_vermieter = text(extra.text)
@@ -148,13 +151,13 @@ const portalUrl = (ctx) => `${config.publicBaseUrl}/dashboard.html#buchungen?pro
* values.an ersetzt die Adresse (Standard ctx.besitzerEmail). T22 geht an die Team-Adressen und wirft nie.
*/
export async function anVermieter(template, ctx, values = {}) {
- _aufrufe.push({ an: 'vermieter', template, values })
+ if (testProtokoll()) _aufrufe.push({ an: 'vermieter', template, anfrageId: values?.anfrage?.$id })
const { anfrage, an, ...eigene } = values ?? {}
const vorgabe = {}
if (anfrage) {
const { werte } = await werteZuAnfrage(ctx, anfrage)
Object.assign(vorgabe, werte, {
- kunde_name: text(ctx?.kundeName) || werte.firma,
+ kunde_name: text(ctx?.kundeName) || (await kundenName(ctx)) || werte.firma,
gast_email: text(anfrage.gast?.email) || LEER,
nachricht_gast: text(anfrage.gast?.nachricht) || LEER,
gesamt: formatEuro(anfrage.summen?.bruttoCent ?? 0),
@@ -177,3 +180,12 @@ function nurPlatzhalter(template, werte) {
const kennt = loadTemplate(template).placeholders
return Object.fromEntries(Object.entries(werte).filter(([k]) => kennt.includes(k) && k !== 'blocks'))
}
+
+/** Name des Kunden (Vermieters) aus dem Kundendokument; Fehler beim Laden machen die Mail nicht kaputt. */
+async function kundenName(ctx) {
+ try {
+ return text((await customerOf(ctx?.customerId))?.name)
+ } catch {
+ return ''
+ }
+}
diff --git a/server/services/unterkunft/zahlung.js b/server/services/unterkunft/zahlung.js
index d73ed70..c992c44 100644
--- a/server/services/unterkunft/zahlung.js
+++ b/server/services/unterkunft/zahlung.js
@@ -2,9 +2,11 @@
import { UkFehler } from './fehler.js'
export const _aufrufe = []
+/** Aufrufprotokoll nur im Test (UK_TESTPROTOKOLL, setzt useTestEnv); in Produktion waechst nichts. */
+const testProtokoll = () => process.env.NODE_ENV === 'test' || process.env.UK_TESTPROTOKOLL === '1'
export async function erstatte(ctx, anfrage, betragCent, { now, idempotencyKey } = {}) {
- _aufrufe.push({ anfrageId: anfrage?.$id, betragCent, idempotencyKey })
+ if (testProtokoll()) _aufrufe.push({ anfrageId: anfrage?.$id, betragCent, idempotencyKey })
if (!ctx?.einstellungen?.stripeAccountId) throw new UkFehler(409, 'online_aus')
return { erstattetCent: betragCent }
}
diff --git a/test/support/testEnv.js b/test/support/testEnv.js
index f6159b8..aca4b30 100644
--- a/test/support/testEnv.js
+++ b/test/support/testEnv.js
@@ -15,5 +15,6 @@ export function useTestEnv({ appwriteUrl, stripeUrl }) {
SMTP_HOST: '',
SESSION_SECRET: 'test-session-secret-mit-mindestens-32-zeichen',
ABO_PILOT_EMAILS: '*',
+ UK_TESTPROTOKOLL: '1',
})
}
diff --git a/test/unterkunftAblauf.test.js b/test/unterkunftAblauf.test.js
index fc9cc55..514d472 100644
--- a/test/unterkunftAblauf.test.js
+++ b/test/unterkunftAblauf.test.js
@@ -251,7 +251,7 @@ test('Storno: Erstattung scheitert -> storniert, eine Stornorechnung, 502; Wiede
assert.equal(a.erstattetCent, 0)
assert.equal(aw.docs('ukRechnungen').size, 2)
assert.deepEqual(arten(), ['G22'])
- assert.equal(mails._aufrufe[0].extra.grund, 'g')
+ assert.equal(mails._aufrufe[0].grund, 'g')
const r = await ab.storniere(sctx, id, { grund: 'g', erstattungCent: 20000, now: NOW })
assert.equal(r.anfrage.erstattetCent, 20000)
assert.equal(aw.docs('ukRechnungen').size, 2)
diff --git a/test/unterkunftMails.test.js b/test/unterkunftMails.test.js
index fc2a611..5a95b8b 100644
--- a/test/unterkunftMails.test.js
+++ b/test/unterkunftMails.test.js
@@ -7,9 +7,9 @@ import { schemaKeys } from './support/schema.js'
import { TERMINE_DATETIMES } from '../server/services/termine/schema.js'
// Buchungskalender Task 20: Mails an Gaeste (ueber die Warteschlange), Vermieter und Team
-const NAMEN = ['ukUnterkuenfte', 'ukPreise', 'ukAnfragen', 'ukRechnungen', 'ukEinstellungen', 'termineOutbox']
+const NAMEN = ['customers', 'ukUnterkuenfte', 'ukPreise', 'ukAnfragen', 'ukRechnungen', 'ukEinstellungen', 'termineOutbox']
const aw = await startAppwriteMock({
- schema: Object.fromEntries(NAMEN.map((n) => [n, schemaKeys(n)])),
+ schema: Object.fromEntries(NAMEN.map((n) => [n, n === 'customers' ? ['name', 'email'] : schemaKeys(n)])),
datetimes: { ...TERMINE_DATETIMES, ukAnfragen: ['fristAt', 'freigegebenAt', 'gebuchtAt', 'abgelehntAt', 'abgelaufenAt', 'storniertAt', 'bezahltAt'] },
defaultLimit: 25,
})
@@ -58,7 +58,8 @@ test('G17: Absender = Firma, Antworten an den Vermieter, Buchungslink mit Token,
assert.ok(m.text.includes('Kostenlos bis 7 Tage vorher.'))
assert.ok(m.text.includes('05.11.2026, 17:00 Uhr'))
assert.ok(m.text.includes('Erst mit dem Klick auf „Zahlungspflichtig buchen“'))
- assert.equal(mails._aufrufe[0].art, 'G17')
+ assert.deepEqual(mails._aufrufe[0], { an: 'gast', art: 'G17', anfrageId: 'a1', hatToken: true, grund: undefined })
+ assert.ok(!JSON.stringify(mails._aufrufe).includes('tok123'))
})
test('Minimaler ctx: Einstellungen und Unterkunft werden selbst geladen', async () => {
@@ -70,8 +71,10 @@ test('Minimaler ctx: Einstellungen und Unterkunft werden selbst geladen', async
assert.doesNotMatch(m.text, /buchung\/a1/)
})
-test('G17 ohne Token wirft, G16 hat keinen Link', async () => {
+test('G17 ohne Token oder ohne siteKey wirft vor dem Einreihen, G16 hat keinen Link', async () => {
await assert.rejects(() => mails.anGast('G17', ctx, anfrage(), {}), TypeError)
+ await assert.rejects(() => mails.anGast('G17', { ...ctx, siteKey: '' }, anfrage(), { token: 't' }), TypeError)
+ assert.equal(aw.docs('termineOutbox').size, 0)
await mails.anGast('G16', ctx, anfrage({ status: 'neu', posten: null, summen: null }))
const [m] = await gesendet()
assert.doesNotMatch(m.text, /\?t=/)
@@ -111,3 +114,17 @@ test('Vermieter: K31 geht an den Besitzer ohne BCC, mit Portal-Link; T22 ans Tea
assert.ok(k.text.includes('Eva Gast'))
await assert.rejects(() => mails.anGast('K31', ctx, anfrage()), TypeError)
})
+
+test('Gastname wird in der Mail maskiert', async () => {
+ await mails.anGast('G16', ctx, anfrage({ gast: { name: 'x', email: 'eva@example.com' } }))
+ const [m] = await gesendet()
+ assert.ok(!m.html.includes('x'))
+ assert.ok(m.html.includes('<b>x</b>'))
+})
+
+test('K31: Kundenname kommt aus dem Kundendokument, wenn ctx keinen hat', async () => {
+ aw.seed('customers', { $id: 'c1', name: 'Mimi Muster', email: 'besitzer@example.com' })
+ await mails.anVermieter('K31', ctx, { anfrage: anfrage({ status: 'neu' }) })
+ const [k] = await gesendet()
+ assert.ok(k.text.includes('Hallo Mimi Muster'))
+})