131 lines
8.0 KiB
JavaScript
131 lines
8.0 KiB
JavaScript
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: K31 geht an den Besitzer ohne BCC, mit Portal-Link; T22 ans Team; unbekannte Gastart wirft', async () => {
|
||
await mails.anVermieter('K31', 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('K31', 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('<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'))
|
||
})
|