G15 "Wir haben Ihre Bestellung erhalten" geht einmal raus, sobald Stripe die Sitzung abgeschlossen, die Zahlung aber noch nicht gemeldet hat (meldeEingang, Marker mailsEingangAt, aus dem Webhook und aus pruefeOffene). G09 sagt ausdruecklich, dass der Kaufvertrag mit der Mail geschlossen ist, trennt Bestell- und Zahlungsdatum und nennt die Telefonnummer des Anbieters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
402 lines
22 KiB
JavaScript
402 lines
22 KiB
JavaScript
import { test, after, beforeEach, afterEach, mock } from 'node:test'
|
||
import assert from 'node:assert/strict'
|
||
import { startAppwriteMock } from './support/appwriteMock.js'
|
||
import { mailSink } from './support/mailSink.js'
|
||
import { schemaKeys } from './support/schema.js'
|
||
import { useTestEnv } from './support/testEnv.js'
|
||
import { TERMINE_DATETIMES } from '../server/services/termine/schema.js'
|
||
|
||
// Webshop E4: Mails des Shops (services/shop/mails.js). Käufermails laufen durch die Warteschlange des Terminplaners
|
||
// (termineOutbox), Händlermails gehen direkt an das Kundenkonto. Mails landen in der Senke, nichts verlässt den Prozess.
|
||
const aw = await startAppwriteMock({
|
||
schema: { termineOutbox: schemaKeys('termineOutbox') },
|
||
datetimes: TERMINE_DATETIMES,
|
||
defaultLimit: 25,
|
||
})
|
||
after(() => aw.close())
|
||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' })
|
||
|
||
const { config } = await import('../server/config.js')
|
||
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
|
||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||
const { resetOutboxForTests, warteAufVersand } = await import('../server/services/termine/outbox.js')
|
||
const { STANDARD_EINSTELLUNGEN } = await import('../server/services/shop/einstellungen.js')
|
||
const { gewaehrleistungsHinweis } = await import('../server/services/shop/rechtstexte.js')
|
||
const M = await import('../server/services/shop/mails.js')
|
||
const { anKaeufer, anHaendler, kaeuferWerte, haendlerK25, haendlerK26 } = M
|
||
after(() => setMailTransportForTests(null))
|
||
|
||
const JETZT = new Date('2026-10-05T12:03:00.000Z') // 14:03 Berlin
|
||
const SITE_KEY = 'abcdefghijklmnop'
|
||
const ABSENDER_VORHER = config.smtp.from
|
||
const BASIS_VORHER = config.publicBaseUrl
|
||
|
||
const einstellungen = (over = {}) => ({
|
||
...JSON.parse(JSON.stringify(STANDARD_EINSTELLUNGEN)),
|
||
shopname: 'Töpferei Muster',
|
||
anbieter: { name: 'Erika Muster', strasse: 'Hauptstr. 1', plz: '67722', ort: 'Winnweiler', land: 'DE', email: 'info@toepferei.example', telefon: '', ustId: '' },
|
||
lieferzeit: '2–4 Werktage',
|
||
akzent: '#aa3300',
|
||
abholung: { aktiv: true, adresse: 'Werkstatt, Hauptstr. 1, 67722 Winnweiler', hinweis: 'Di–Fr 10–16 Uhr' },
|
||
...over,
|
||
})
|
||
const texte = (over = {}) => ({
|
||
agb: 'AGB Zeile 1\nAGB Zeile 2',
|
||
widerrufsbelehrung: 'Belehrung Zeile 1\nZeile 2',
|
||
widerrufsformular: 'An Töpferei Muster\nHiermit widerrufe ich',
|
||
impressumUrl: 'https://toepferei.example/impressum',
|
||
datenschutzUrl: 'https://toepferei.example/datenschutz',
|
||
...over,
|
||
})
|
||
const ctxMit = (e = einstellungen(), t = texte()) => ({
|
||
doc: { $id: 'f_shop01', customerId: 'c1', projectId: 'p1', featureKey: 'shop', siteKey: SITE_KEY },
|
||
shop: {
|
||
id: 'f_shop01',
|
||
customerId: 'c1',
|
||
projectId: 'p1',
|
||
siteKey: SITE_KEY,
|
||
einstellungen: e,
|
||
rechtstexte: { entwurf: texte(), bestaetigt: { texte: t, hash: 'h1', at: JETZT.toISOString(), von: 'c1' } },
|
||
},
|
||
})
|
||
|
||
const bestellung = (over = {}, snap = {}) => ({
|
||
$id: 'o_bestellung01',
|
||
bestellnummer: '26-K7QM2X',
|
||
kaeuferEmail: 'max@example.com',
|
||
bezahltAt: JETZT.toISOString(),
|
||
erstelltAt: new Date(JETZT.getTime() - 600000).toISOString(),
|
||
summeCent: 4140,
|
||
bestellung: JSON.stringify({
|
||
positionen: [
|
||
{ name: 'Tasse', varianteText: 'blau', menge: 2, summeCent: 2400, widerruf: 'gesetzlich' },
|
||
{ name: 'Teller', varianteText: '', menge: 1, summeCent: 1250, summeText: '12,50 €', widerruf: 'individuell' },
|
||
],
|
||
summen: { zwischensummeCent: 3650, versandCent: 490, gesamtCent: 4140, texte: { zwischensumme: '36,50 €', versand: '4,90 €', gesamt: '41,40 €', steuerHinweis: 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.' } },
|
||
lieferart: 'versand',
|
||
land: 'DE',
|
||
kaeufer: { name: 'Max Mustermann', email: 'max@example.com' },
|
||
lieferadresse: { name: 'Max Mustermann', line1: 'Musterweg 3', postal_code: '12345', city: 'Musterstadt', country: 'DE' },
|
||
zahlungsart: 'card',
|
||
...snap,
|
||
}),
|
||
...over,
|
||
})
|
||
|
||
let sink
|
||
let log
|
||
beforeEach(async () => {
|
||
for (const id of aw.docs('termineOutbox').keys()) await deleteDocument(config.collections.termineOutbox, id)
|
||
for (const id of aw.docs('customers').keys()) await deleteDocument(config.collections.customers, id)
|
||
resetOutboxForTests()
|
||
config.smtp.from = 'noreply@webklar.com'
|
||
config.publicBaseUrl = 'https://project.webklar.com'
|
||
sink = mailSink()
|
||
setMailTransportForTests(sink)
|
||
log = { error: mock.method(console, 'error', () => {}), warn: mock.method(console, 'warn', () => {}) }
|
||
aw.seed('customers', { $id: 'c1', name: 'Erika Muster', email: 'erika@toepferei.example' })
|
||
})
|
||
afterEach(async () => {
|
||
await warteAufVersand()
|
||
mock.restoreAll()
|
||
config.smtp.from = ABSENDER_VORHER
|
||
config.publicBaseUrl = BASIS_VORHER
|
||
})
|
||
|
||
const eintraege = () => [...aw.docs('termineOutbox').values()]
|
||
const warnLog = () => log.warn.mock.calls.map((aufruf) => aufruf.arguments.map(String).join(' '))
|
||
|
||
// ---------------------------------------------------------------- Werte
|
||
|
||
test('kaeuferWerte: Kopf, Fuß, Positionen, Summen, Lieferadresse und Rechtstexte aus Schnappschuss und Shop', () => {
|
||
const w = kaeuferWerte(ctxMit(), bestellung(), JETZT)
|
||
// euro() setzt ein geschütztes Leerzeichen vor das €-Zeichen
|
||
for (const [schluessel, wert] of Object.entries(w)) w[schluessel] = wert.replaceAll('\u00a0', ' ')
|
||
assert.equal(w.farbe, '#aa3300')
|
||
assert.equal(w.betrieb_name, 'Töpferei Muster')
|
||
assert.equal(w.anbieter_zeile, 'Erika Muster, Hauptstr. 1, 67722 Winnweiler, info@toepferei.example')
|
||
assert.equal(w.widerruf_url, `https://project.webklar.com/shop/${SITE_KEY}/widerruf`)
|
||
assert.equal(w.impressum_url, 'https://toepferei.example/impressum')
|
||
assert.equal(w.datenschutz_url, 'https://toepferei.example/datenschutz')
|
||
assert.equal(w.bestellnummer, '26-K7QM2X')
|
||
assert.equal(w.datum, '05.10.2026')
|
||
assert.equal(w.positionen, '2 × Tasse (blau) – 24,00 €\n1 × Teller – 12,50 €')
|
||
assert.deepEqual([w.zwischensumme, w.versand, w.gesamt], ['36,50 €', '4,90 €', '41,40 €'])
|
||
assert.equal(w.steuer_hinweis, 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.')
|
||
assert.equal(w.lieferart_text, 'Versand nach Deutschland')
|
||
assert.equal(w.lieferadresse, 'Max Mustermann\nMusterweg 3\n12345 Musterstadt')
|
||
assert.equal(w.abholort, 'Werkstatt, Hauptstr. 1, 67722 Winnweiler')
|
||
assert.equal(w.lieferzeit, '2–4 Werktage')
|
||
assert.equal(w.zahlungsart, 'Kreditkarte, Apple Pay, Google Pay')
|
||
assert.equal(w.ausschluss_text, 'Teller: Vom Widerruf ausgeschlossen: nach Kundenwunsch angefertigt')
|
||
assert.equal(w.agb, 'AGB Zeile 1\nAGB Zeile 2')
|
||
assert.equal(w.widerrufsbelehrung, 'Belehrung Zeile 1\nZeile 2')
|
||
assert.equal(w.widerrufsformular, 'An Töpferei Muster\nHiermit widerrufe ich')
|
||
assert.equal(w.gewaehrleistung, gewaehrleistungsHinweis())
|
||
})
|
||
|
||
test('kaeuferWerte: Links fallen ohne Impressum-/Datenschutz-Adresse auf den Shop zurück, ungültige Farbe auf Schwarz, ohne Bestellung "–"', () => {
|
||
const w = kaeuferWerte(ctxMit(einstellungen({ akzent: 'red;x' }), texte({ impressumUrl: '', datenschutzUrl: '' })), null, JETZT)
|
||
assert.equal(w.farbe, '#111111')
|
||
assert.equal(w.impressum_url, `https://project.webklar.com/shop/${SITE_KEY}`)
|
||
assert.equal(w.datenschutz_url, `https://project.webklar.com/shop/${SITE_KEY}`)
|
||
assert.equal(w.bestellnummer, '–')
|
||
assert.equal(w.positionen, '–')
|
||
assert.equal(w.lieferadresse, '–')
|
||
assert.equal(w.datum, '05.10.2026', 'ohne Bestellung gilt das übergebene Datum')
|
||
})
|
||
|
||
test('kaeuferWerte I-1: Bestelldatum ist der Tag der Bestellung, Zahlungsdatum der des Zahlungseingangs (SEPA Tage später)', () => {
|
||
const w = kaeuferWerte(ctxMit(), bestellung({ erstelltAt: '2026-10-05T10:00:00.000Z', bezahltAt: '2026-10-08T08:30:00.000Z' }), JETZT)
|
||
assert.equal(w.datum, '05.10.2026')
|
||
assert.equal(w.zahlungsdatum, '08.10.2026')
|
||
const offen = kaeuferWerte(ctxMit(), bestellung({ bezahltAt: undefined }), JETZT)
|
||
assert.equal(offen.zahlungsdatum, '05.10.2026', 'ohne Zahlung gilt das Bestelldatum')
|
||
})
|
||
|
||
test('kaeuferWerte M-5: Telefonnummer des Anbieters steht in der Anbieterzeile, wenn vorhanden', () => {
|
||
const mit = kaeuferWerte(ctxMit(einstellungen({ anbieter: { ...einstellungen().anbieter, telefon: '0 63 02 / 12 34' } })), bestellung(), JETZT)
|
||
assert.equal(mit.anbieter_zeile, 'Erika Muster, Hauptstr. 1, 67722 Winnweiler, info@toepferei.example, Tel. 0 63 02 / 12 34')
|
||
})
|
||
|
||
// ---------------------------------------------------------------- Käufermails
|
||
|
||
test('G09: wird mit Shopname als Absender und Antwort an den Händler eingereiht, kein BCC, Rechtstexte stehen in der Mail', async () => {
|
||
const erg = await anKaeufer('G09', ctxMit(), bestellung(), {}, JETZT)
|
||
assert.equal(erg.ok, true)
|
||
assert.equal(erg.gekuerzt, false)
|
||
await warteAufVersand()
|
||
|
||
const [doc] = eintraege()
|
||
assert.equal(doc.art, 'G09')
|
||
assert.equal(doc.featureId, 'f_shop01')
|
||
assert.equal(doc.bookingId, 'o_bestellung01')
|
||
assert.equal(doc.fromName, 'Töpferei Muster')
|
||
assert.equal(doc.replyTo, 'info@toepferei.example')
|
||
assert.equal(doc.status, 'gesendet')
|
||
assert.equal(doc.an, '', 'nach dem Versand ist die Adresse gelöscht')
|
||
|
||
assert.equal(sink.sent.length, 1)
|
||
const mail = sink.sent[0]
|
||
assert.deepEqual(mail.to.map((t) => t.address), ['max@example.com'])
|
||
assert.ok(!mail.bcc, 'kein BCC')
|
||
assert.equal(mail.from.name, 'Töpferei Muster')
|
||
assert.equal(mail.from.address, 'noreply@webklar.com')
|
||
assert.equal(mail.replyTo[0].address, 'info@toepferei.example')
|
||
assert.equal(mail.subject, 'Ihre Bestellung 26-K7QM2X bei Töpferei Muster')
|
||
for (const teil of ['2 × Tasse (blau) – 24,00 €', '41,40 €', 'Max Mustermann', 'Musterweg 3', 'Vom Widerruf ausgeschlossen', 'AGB Zeile 2', 'Belehrung Zeile 1', 'Hiermit widerrufe ich', 'GESETZLICHE GEWÄHRLEISTUNG', 'Vertrag widerrufen']) {
|
||
assert.ok(mail.text.includes(teil), teil)
|
||
}
|
||
assert.match(mail.html, new RegExp(`href="https://project\\.webklar\\.com/shop/${SITE_KEY}/widerruf"`))
|
||
assert.doesNotMatch(mail.text, /Abholort/)
|
||
assert.deepEqual(warnLog(), [], 'keine Platzhalter-Warnung der Vorlage')
|
||
})
|
||
|
||
test('G15: Eingang der Bestellung geht an die übergebene Adresse (noch keine Käuferadresse an der Bestellung), ohne Rechtstexte und ohne Vertragsschluss', async () => {
|
||
const offen = bestellung({ kaeuferEmail: undefined, bezahltAt: undefined, status: 'offen' })
|
||
const erg = await anKaeufer('G15', ctxMit(), offen, { an: 'sepa@example.com' }, JETZT)
|
||
assert.equal(erg.ok, true)
|
||
await warteAufVersand()
|
||
const [doc] = eintraege()
|
||
assert.equal(doc.art, 'G15')
|
||
assert.equal(doc.bookingId, 'o_bestellung01')
|
||
assert.equal(sink.sent.length, 1)
|
||
const mail = sink.sent[0]
|
||
assert.equal(mail.to[0].address, 'sepa@example.com')
|
||
assert.equal(mail.subject, 'Wir haben Ihre Bestellung 26-K7QM2X erhalten')
|
||
assert.match(mail.text, /noch in Bearbeitung/)
|
||
assert.match(mail.text, /2 × Tasse \(blau\) – 24,00/)
|
||
assert.doesNotMatch(mail.text, /Kaufvertrag ist damit geschlossen|AGB Zeile 1|Belehrung Zeile 1/)
|
||
})
|
||
|
||
test('G09 Abholung: Abholort statt Lieferadresse', async () => {
|
||
const erg = await anKaeufer('G09', ctxMit(), bestellung({}, { lieferart: 'abholung', lieferadresse: null, land: null, positionen: [{ name: 'Tasse', menge: 1, summeCent: 1200, widerruf: 'gesetzlich' }] }), {}, JETZT)
|
||
assert.equal(erg.ok, true)
|
||
await warteAufVersand()
|
||
const text = sink.sent[0].text
|
||
assert.match(text, /Lieferart: Abholung/)
|
||
assert.match(text, /Abholort: Werkstatt, Hauptstr\. 1, 67722 Winnweiler/)
|
||
assert.doesNotMatch(text, /Lieferadresse|Hinweis zum Widerrufsrecht/)
|
||
assert.deepEqual(warnLog(), [])
|
||
})
|
||
|
||
test('G09: zu lang für die Warteschlange ⇒ der AGB-Text wird durch einen Link auf die AGB ersetzt, Belehrung und Formular bleiben', async () => {
|
||
const lang = 'Dies ist ein langer Satz der Geschäftsbedingungen.\n'.repeat(560) // etwa 28 000 Zeichen
|
||
const t = texte({ agb: lang, widerrufsbelehrung: 'Belehrung Satz.\n'.repeat(1000), widerrufsformular: 'Formular Zeile\n'.repeat(300) })
|
||
const erg = await anKaeufer('G09', ctxMit(einstellungen(), t), bestellung(), {}, JETZT)
|
||
assert.equal(erg.ok, true)
|
||
assert.equal(erg.gekuerzt, true)
|
||
const doc = eintraege()[0]
|
||
assert.ok(doc.html.length <= 100000 && doc.text.length <= 50000, `${doc.html.length}/${doc.text.length}`)
|
||
assert.match(doc.text, new RegExp(`Die vollständigen AGB finden Sie unter https://project\\.webklar\\.com/shop/${SITE_KEY}\\?ansicht=agb`))
|
||
assert.doesNotMatch(doc.text, /langer Satz der Geschäftsbedingungen/)
|
||
assert.match(doc.text, /Belehrung Satz\./)
|
||
assert.match(doc.text, /Formular Zeile/)
|
||
assert.match(doc.text, /GESETZLICHE GEWÄHRLEISTUNG/)
|
||
})
|
||
|
||
test('G09: auch gekürzt zu lang ⇒ { ok: false } ohne Wurf und ohne Eintrag', async () => {
|
||
const t = texte({ widerrufsbelehrung: 'x'.repeat(60000) })
|
||
const erg = await anKaeufer('G09', ctxMit(einstellungen(), t), bestellung(), {}, JETZT)
|
||
assert.deepEqual(erg, { ok: false, fehler: 'Die E-Mail an den Käufer konnte nicht eingereiht werden.' })
|
||
assert.equal(eintraege().length, 0)
|
||
assert.equal(sink.sent.length, 0)
|
||
assert.doesNotMatch(warnLog().join('\n'), /max@example\.com/)
|
||
})
|
||
|
||
test('G10: Versanddienst und Sendungsnummer (Block sendung); ohne beide kein Block', async () => {
|
||
assert.equal((await anKaeufer('G10', ctxMit(), bestellung(), { versanddienst: 'DHL', sendungsnummer: '00340434' }, JETZT)).ok, true)
|
||
assert.equal((await anKaeufer('G10', ctxMit(), bestellung({ $id: 'o_zwei' }), {}, JETZT)).ok, true)
|
||
await warteAufVersand()
|
||
const [mit, ohne] = sink.sent
|
||
assert.equal(mit.subject, 'Ihre Bestellung 26-K7QM2X wurde versendet')
|
||
assert.match(mit.text, /Versanddienst: DHL/)
|
||
assert.match(mit.text, /Sendungsnummer: 00340434/)
|
||
assert.doesNotMatch(ohne.text, /Sendungsnummer/)
|
||
})
|
||
|
||
test('G11: Abholort und Hinweis des Händlers, ersatzweise ein Standardhinweis', async () => {
|
||
await anKaeufer('G11', ctxMit(), bestellung(), {}, JETZT)
|
||
const ohneHinweis = einstellungen({ abholung: { aktiv: true, adresse: 'Werkstatt', hinweis: '' } })
|
||
await anKaeufer('G11', ctxMit(ohneHinweis), bestellung({ $id: 'o_zwei' }), {}, JETZT)
|
||
await warteAufVersand()
|
||
const texte11 = sink.sent.map((m) => m.text)
|
||
assert.match(texte11[0], /Abholort: Werkstatt, Hauptstr\. 1/)
|
||
assert.match(texte11[0], /Hinweis: Di–Fr 10–16 Uhr/)
|
||
assert.match(texte11[1], /Hinweis: Bitte bringen Sie Ihre Bestellnummer mit\./)
|
||
assert.equal(sink.sent[0].subject, 'Ihre Bestellung 26-K7QM2X ist abholbereit')
|
||
})
|
||
|
||
test('G12: ohne passende Bestellung, an die Adresse der Erklärung, mit Datum und Uhrzeit; Vorrang-Klasse', async () => {
|
||
const erg = await anKaeufer('G12', ctxMit(), null, { an: 'kaeufer@example.com', bookingId: 'w_widerruf01', name: 'Kai Käufer', bestellnummer: '26-ZZZZZZ', eingang: JETZT, nachricht: 'Ich widerrufe.' }, JETZT)
|
||
assert.equal(erg.ok, true)
|
||
assert.equal(erg.eintrag.bookingId, 'w_widerruf01')
|
||
await warteAufVersand()
|
||
const mail = sink.sent[0]
|
||
assert.deepEqual(mail.to.map((t) => t.address), ['kaeufer@example.com'])
|
||
assert.equal(mail.subject, 'Eingangsbestätigung Ihres Widerrufs bei Töpferei Muster')
|
||
assert.match(mail.text, /Guten Tag Kai Käufer,/)
|
||
assert.match(mail.text, /Eingegangen am: 05\.10\.2026, 14:03 Uhr/)
|
||
assert.match(mail.text, /Bestellnummer: 26-ZZZZZZ/)
|
||
assert.match(mail.text, /Ihre Erklärung: Ich widerrufe\./)
|
||
assert.deepEqual(warnLog(), [])
|
||
})
|
||
|
||
test('G12 und K26 ohne Bestellnummer: „–“ statt leerer Stelle', async () => {
|
||
const erg = await anKaeufer('G12', ctxMit(), null, { an: 'kaeufer@example.com', bookingId: 'w_widerruf02', name: 'Kai Käufer', bestellnummer: '', eingang: JETZT, nachricht: 'Bestellt am 01.10., Tasse blau' }, JETZT)
|
||
assert.equal(erg.ok, true)
|
||
const k = haendlerK26(ctxMit(), { name: 'Kai', email: 'kai@example.com', bestellnummer: '', nachricht: 'Bestellt am 01.10., Tasse blau', eingang: JETZT })
|
||
assert.equal(k.values.bestellnummer, '–')
|
||
assert.equal((await anHaendler('K26', ctxMit(), k.values, k.blocks)).ok, true)
|
||
await warteAufVersand()
|
||
assert.match(sink.sent[0].text, /Bestellnummer: –/)
|
||
assert.match(sink.sent[1].text, /Bestellnummer: –/)
|
||
})
|
||
|
||
test('G13: erstatteter Betrag in Euro, Grund nur wenn angegeben; ohne Betrag { ok: false }', async () => {
|
||
assert.equal((await anKaeufer('G13', ctxMit(), bestellung(), { betragCent: 1250, grund: 'Artikel beschädigt' }, JETZT)).ok, true)
|
||
assert.equal((await anKaeufer('G13', ctxMit(), bestellung({ $id: 'o_zwei' }), { betragCent: 4140 }, JETZT)).ok, true)
|
||
assert.equal((await anKaeufer('G13', ctxMit(), bestellung({ $id: 'o_drei' }), {}, JETZT)).ok, false)
|
||
assert.equal((await anKaeufer('G13', ctxMit(), bestellung({ $id: 'o_vier' }), { betragCent: 12.5 }, JETZT)).ok, false)
|
||
await warteAufVersand()
|
||
assert.equal(sink.sent.length, 2)
|
||
assert.match(sink.sent[0].text, /Erstatteter Betrag: 12,50 €/)
|
||
assert.match(sink.sent[0].text, /Grund: Artikel beschädigt/)
|
||
assert.match(sink.sent[1].text, /Erstatteter Betrag: 41,40 €/)
|
||
assert.doesNotMatch(sink.sent[1].text, /Grund:/)
|
||
assert.match(sink.sent[1].text, /Die Gutschrift erfolgt über die ursprüngliche Zahlungsart\./)
|
||
})
|
||
|
||
test('G14: Zahlung fehlgeschlagen, nichts berechnet', async () => {
|
||
assert.equal((await anKaeufer('G14', ctxMit(), bestellung(), {}, JETZT)).ok, true)
|
||
await warteAufVersand()
|
||
assert.equal(sink.sent[0].subject, 'Ihre Bestellung 26-K7QM2X wurde nicht abgeschlossen')
|
||
assert.match(sink.sent[0].text, /es wurde nichts berechnet/)
|
||
})
|
||
|
||
test('anKaeufer wirft nie: unbekannte Art, fehlende Adresse und Speicherfehler ergeben { ok: false }', async () => {
|
||
assert.equal((await anKaeufer('G01', ctxMit(), bestellung(), {}, JETZT)).ok, false)
|
||
assert.equal((await anKaeufer('G09', ctxMit(), bestellung({ kaeuferEmail: '' }), {}, JETZT)).ok, false)
|
||
assert.equal((await anKaeufer('G09', ctxMit(), bestellung(), {}, new Date('kaputt'))).ok, false)
|
||
aw.failNext({ collection: 'termineOutbox', method: 'POST', status: 500, message: 'Nachbau kaputt' })
|
||
const erg = await anKaeufer('G09', ctxMit(), bestellung(), {}, JETZT)
|
||
assert.equal(erg.ok, false)
|
||
assert.equal(eintraege().length, 0)
|
||
assert.equal(sink.sent.length, 0)
|
||
})
|
||
|
||
test('Käufermails zählen als Gästemails: G09 und G12 mit Vorrang, die übrigen normal', async () => {
|
||
const { mailKlasse } = await import('../server/services/termine/outbox.js')
|
||
assert.deepEqual(['G09', 'G10', 'G11', 'G12', 'G13', 'G14'].map(mailKlasse), ['vorrang', 'normal', 'normal', 'vorrang', 'normal', 'normal'])
|
||
})
|
||
|
||
// ---------------------------------------------------------------- Händlermails
|
||
|
||
test('K25: geht an das Kundenkonto, ohne BCC ans Team, mit Käufer, Positionen und Portal-Link', async () => {
|
||
const { values, blocks } = haendlerK25(ctxMit(), bestellung())
|
||
assert.deepEqual(blocks, { ueberverkauft: false, sepa: false })
|
||
const erg = await anHaendler('K25', ctxMit(), values, blocks)
|
||
assert.deepEqual(erg, { ok: true })
|
||
assert.equal(sink.sent.length, 1)
|
||
const mail = sink.sent[0]
|
||
assert.deepEqual(mail.to.map((t) => t.address), ['erika@toepferei.example'])
|
||
assert.ok(!mail.bcc, 'kein BCC ans Team')
|
||
assert.equal(mail.subject, 'Neue Bestellung 26-K7QM2X in Deinem Webshop')
|
||
assert.match(mail.text, /Hallo Erika Muster/)
|
||
assert.match(mail.text, /Töpferei Muster/)
|
||
assert.match(mail.text, /Käufer: Max Mustermann/)
|
||
assert.match(mail.text, /Gesamt: 41,40 €/)
|
||
assert.match(mail.html.replaceAll('\u00a0', ' '), /2 × Tasse \(blau\) – 24,00 €<br>1 × Teller – 12,50 €/)
|
||
assert.match(mail.html, /href="https:\/\/project\.webklar\.com\/dashboard\.html#webshop\?projekt=p1"/)
|
||
assert.doesNotMatch(mail.text, /Achtung|Lastschrift/)
|
||
assert.deepEqual(warnLog(), [])
|
||
})
|
||
|
||
test('K25: Blöcke ueberverkauft und sepa', async () => {
|
||
const { values, blocks } = haendlerK25(ctxMit(), bestellung({}, { zahlungsart: 'sepa_debit' }), { ueberverkauft: true })
|
||
assert.deepEqual(blocks, { ueberverkauft: true, sepa: true })
|
||
assert.equal((await anHaendler('K25', ctxMit(), values, blocks)).ok, true)
|
||
assert.match(sink.sent[0].text, /Achtung: Bestand reichte nicht/)
|
||
assert.match(sink.sent[0].text, /Zahlung per Lastschrift/)
|
||
})
|
||
|
||
test('K26: Widerruf mit Eingang in Berliner Zeit und Erklärung, ohne BCC; ohne Erklärung kein Block', async () => {
|
||
const widerruf = { name: 'Kai <K>', email: 'kai@example.com', bestellnummer: '26-ZZZZZZ', nachricht: 'Ich widerrufe.', eingangAt: JETZT.toISOString() }
|
||
const { values, blocks } = haendlerK26(ctxMit(), widerruf)
|
||
assert.equal(values.eingang, '05.10.2026, 14:03 Uhr')
|
||
assert.deepEqual(blocks, { nachricht: true })
|
||
assert.equal((await anHaendler('K26', ctxMit(), values, blocks)).ok, true)
|
||
const ohne = haendlerK26(ctxMit(), { ...widerruf, nachricht: ' ' })
|
||
assert.deepEqual(ohne.blocks, { nachricht: false })
|
||
assert.equal((await anHaendler('K26', ctxMit(), ohne.values, ohne.blocks)).ok, true)
|
||
const [mit, kurz] = sink.sent
|
||
assert.ok(!mit.bcc)
|
||
assert.equal(mit.subject, 'Widerruf in Deinem Webshop eingegangen')
|
||
assert.match(mit.text, /Eingegangen am: 05\.10\.2026, 14:03 Uhr/)
|
||
assert.match(mit.text, /Käufer: Kai <K>/)
|
||
assert.match(mit.text, /E-Mail: kai@example\.com/)
|
||
assert.match(mit.text, /Erklärung: Ich widerrufe\./)
|
||
assert.match(mit.html, /Kai <K>/)
|
||
assert.doesNotMatch(kurz.text, /Erklärung:/)
|
||
})
|
||
|
||
test('anHaendler wirft nie: Kunde fehlt oder ohne E-Mail, Versandfehler ⇒ { ok: false } und Warnung ohne Adresse', async () => {
|
||
const { values, blocks } = haendlerK25(ctxMit(), bestellung())
|
||
const fremd = ctxMit()
|
||
fremd.shop.customerId = 'c_gibtsnicht'
|
||
assert.equal((await anHaendler('K25', fremd, values, blocks)).ok, false)
|
||
aw.seed('customers', { $id: 'c_ohne', name: 'Ohne', email: '' })
|
||
const ohneMail = ctxMit()
|
||
ohneMail.shop.customerId = 'c_ohne'
|
||
assert.equal((await anHaendler('K25', ohneMail, values, blocks)).ok, false)
|
||
setMailTransportForTests({
|
||
async sendMail() {
|
||
throw new Error('SMTP erika@toepferei.example kaputt')
|
||
},
|
||
})
|
||
assert.deepEqual(await anHaendler('K25', ctxMit(), values, blocks), { ok: false, fehler: 'Die E-Mail an den Händler konnte nicht gesendet werden.' })
|
||
assert.equal(sink.sent.length, 0)
|
||
assert.doesNotMatch(warnLog().join('\n'), /erika@toepferei\.example/, 'keine Adresse im Log')
|
||
assert.ok(warnLog().some((zeile) => /K25 nicht gesendet/.test(zeile)))
|
||
})
|