Files

131 lines
8.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, after, beforeEach } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { mailSink } from './support/mailSink.js'
import { useTestEnv } from './support/testEnv.js'
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 = ['customers', 'ukUnterkuenfte', 'ukPreise', 'ukAnfragen', 'ukRechnungen', 'ukEinstellungen', 'termineOutbox']
const aw = await startAppwriteMock({
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,
})
after(() => aw.close())
useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1/v1' })
const { config } = await import('../server/config.js')
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const { setMailTransportForTests } = await import('../server/services/mailer.js')
const { warteAufVersand, resetOutboxForTests } = await import('../server/services/termine/outbox.js')
const mails = await import('../server/services/unterkunft/mails.js')
const sp = await import('../server/services/unterkunft/speicher.js')
after(() => setMailTransportForTests(null))
const EIN = { firma: 'Haus am See', strasse: 'Weg 1', plz: '10115', ort: 'Berlin', email: 'vermieter@example.com', telefon: '0123', akzentfarbe: '#336699' }
const ctx = { featureId: 'f1', customerId: 'c1', projectId: 'p1', siteKey: 'abcdefghijklmnop', besitzerEmail: 'besitzer@example.com', einstellungen: EIN }
const POSTEN = [{ text: 'Übernachtung, 2 Nächte', menge: 2, einzelCent: 10000, ustProzent: 7 }, { text: 'Endreinigung', menge: 1, einzelCent: 5000, ustProzent: 19 }]
const anfrage = (extra = {}) => ({
$id: 'a1', featureId: 'f1', unterkunftId: 'u1', nummer: 'A-K7QM', von: '2026-11-12', bis: '2026-11-14', personen: 2, status: 'freigegeben',
gast: { name: 'Eva Gast', email: 'eva@example.com', nachricht: 'Hallo' }, posten: POSTEN, summen: { bruttoCent: 25000 },
fristAt: '2026-11-05T16:00:00.000Z', stornobedingungenText: 'Kostenlos bis 7 Tage vorher.', nachrichtVermieter: '', ...extra,
})
let sink
beforeEach(async () => {
for (const c of ['termineOutbox', 'ukRechnungen', 'ukUnterkuenfte', 'ukEinstellungen']) for (const id of aw.docs(c).keys()) await deleteDocument(c, id)
config.smtp.from = 'noreply@webklar.com'
resetOutboxForTests()
mails._aufrufe.length = 0
sink = mailSink()
setMailTransportForTests(sink)
aw.seed('ukUnterkuenfte', { $id: 'u1', featureId: 'f1', name: 'Wohnung See', maxPersonen: 4, mindestNaechte: 1, aktiv: true })
})
const gesendet = async () => {
await warteAufVersand()
return sink.sent
}
test('G17: Absender = Firma, Antworten an den Vermieter, Buchungslink mit Token, Preis und Frist', async () => {
await mails.anGast('G17', ctx, anfrage(), { token: 'tok123' })
const [m] = await gesendet()
assert.equal(m.from.name, 'Haus am See')
assert.equal(m.replyTo[0].address, 'vermieter@example.com')
assert.equal(m.to[0].address, 'eva@example.com')
assert.ok(m.text.includes(`${config.publicBaseUrl}/unterkunft/abcdefghijklmnop/buchung/a1?t=tok123`))
assert.ok(m.text.includes('250,00 €'))
assert.ok(m.text.includes('12.11.2026 – 14.11.2026'))
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.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 () => {
aw.seed('ukEinstellungen', { $id: 'f1', featureId: 'f1', ...EIN, firma: 'Geladene Firma' })
await mails.anGast('G21', { featureId: 'f1' }, anfrage())
const [m] = await gesendet()
assert.equal(m.from.name, 'Geladene Firma')
assert.ok(m.text.includes('Wohnung See'))
assert.doesNotMatch(m.text, /buchung\/a1/)
})
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=/)
})
test('G19 hängt die Rechnung als PDF an (online, unbezahlt: Bezahl-Hinweis), G22 die Stornorechnung mit Erstattung', async () => {
const inhalt = { aussteller: { firma: 'Haus am See' }, empfaenger: { name: 'Eva Gast' }, unterkunft: { name: 'Wohnung See' }, von: '2026-11-12', bis: '2026-11-14', naechte: 2, personen: 2, posten: POSTEN, summen: { bruttoCent: 25000, nettoCent: 23000, ust: [] }, steuerart: 'kleinunternehmer', zahlart: 'online' }
const re = await sp.legeRechnungAn({ featureId: 'f1', anfrageId: 'a1', art: 'rechnung', nummer: 'RE-2026-0001', datum: '2026-11-01', bruttoCent: 25000, inhalt })
const st = await sp.legeRechnungAn({ featureId: 'f1', anfrageId: 'a1', art: 'storno', nummer: 'RE-2026-0002', datum: '2026-11-02', bruttoCent: -25000, inhalt: { ...inhalt, bezugNummer: 'RE-2026-0001' } })
await mails.anGast('G19', ctx, anfrage({ status: 'gebucht', zahlart: 'online' }), { rechnung: re })
await mails.anGast('G22', ctx, anfrage({ status: 'storniert' }), { stornoRechnung: st, erstattetCent: 25000, grund: 'Wasserschaden' })
const sent = await gesendet()
const g19 = sent.find((m) => m.subject.includes('bestätigt'))
const g22 = sent.find((m) => m.subject.includes('storniert'))
assert.equal(g19.attachments.length, 1)
assert.equal(g19.attachments[0].filename, 'Rechnung-RE-2026-0001.pdf')
assert.ok(Buffer.from(g19.attachments[0].content, 'base64').subarray(0, 4).toString() === '%PDF')
assert.ok(g19.text.includes('Den Bezahl-Link finden Sie auf der Buchungsseite'))
assert.equal(g22.attachments[0].filename, 'Stornorechnung-RE-2026-0002.pdf')
assert.ok(g22.text.includes('Eine Erstattung von 250,00 € wird veranlasst.'))
assert.ok(g22.text.includes('Wasserschaden'))
})
test('G22 ohne Erstattung: kein Erstattungssatz; G19 vor Ort bezahlt: kein Bezahl-Hinweis', async () => {
await mails.anGast('G22', ctx, anfrage({ status: 'storniert' }), { erstattetCent: 0 })
await mails.anGast('G19', ctx, anfrage({ status: 'gebucht', zahlart: 'vorort' }), {})
const sent = await gesendet()
assert.ok(sent.every((m) => !m.text.includes('Erstattung von') && !m.text.includes('Bezahl-Link')))
})
test('Vermieter: K36 geht an den Besitzer ohne BCC, mit Portal-Link; T23 ans Team; unbekannte Gastart wirft', async () => {
await mails.anVermieter('K36', ctx, { anfrage: anfrage({ status: 'neu' }) })
const [k] = await gesendet()
assert.equal(k.to[0].address, 'besitzer@example.com')
assert.ok(!k.bcc || k.bcc.length === 0)
assert.ok(k.text.includes(`${config.publicBaseUrl}/dashboard.html#buchungen?projekt=p1`))
assert.ok(k.text.includes('Eva Gast'))
await assert.rejects(() => mails.anGast('K36', ctx, anfrage()), TypeError)
})
test('Gastname wird in der Mail maskiert', async () => {
await mails.anGast('G16', ctx, anfrage({ gast: { name: '<b>x</b>', email: 'eva@example.com' } }))
const [m] = await gesendet()
assert.ok(!m.html.includes('<b>x</b>'))
assert.ok(m.html.includes('&lt;b&gt;x&lt;/b&gt;'))
})
test('K36: 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('K36', ctx, { anfrage: anfrage({ status: 'neu' }) })
const [k] = await gesendet()
assert.ok(k.text.includes('Hallo Mimi Muster'))
})