Buchungskalender: Mail-Nummern K36, K37, T23 (K31/K32/T22 gehören den Aufträgen)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fd971309a8
commit
3526909d61
17 files changed
+51
-47
No files matched your search
@@ -6,7 +6,7 @@ import { contractAttachmentFor, termsDocFor } from '../server/services/products.
|
||||
|
||||
const DIR = new URL('../server/mail-templates/', import.meta.url)
|
||||
// G01–G08 Gästemails des Terminplaners (Task 9, neutrales Layout im Auftrag eines Betriebs), K20 und K21 Hinweise an den Betrieb;
|
||||
// K12, K14 und T12 (Wechsel ins Wartungs-Abo) aus Task 9, K13 und T13 (Zusatzspeicher) aus Task 8 (Fix 1); Endfassung der Texte in Task 12; T14 „Löschfristen“ aus Task 21 (F39); T15 Push-Zeitmail; K15, K16, T16 Funktionen (News-Pinnwand, Terminplaner; K15, K16 und T16 gelten je Funktion, Terminplaner TP2 B9a und B9b); T17 Repo zuordnen; K17 Chat-Hinweis an Kunden; K18, K19 Website-Analyse; K22, K23 Terminplaner beendet/läuft wieder (Terminplaner TP2 E3); T18 Bankzugang erneuern (Finanzen); T19 Erstattung fällig und T20 Terminplaner Betriebsmeldung (Terminplaner TP2, Team-Hinweise; T16 gilt je Funktion, T19 aus B9b); T21 Neue Webshop-Anfrage (Webshop B1, Team-Hinweis); K24 Webshop-Anfrage entschieden (Webshop B2, Kunde); G09–G14 Käufermails des Webshops (Bestellbestätigung, versendet, abholbereit, Eingang Widerruf, Erstattung, Zahlung fehlgeschlagen; Webshop E4, eigener Fuß statt Terminplaner-Fuß, Sie-Form) und K25, K26 an den Händler (neue Bestellung, Widerruf; Webshop E4); K27–K30 Öffnungszeiten live (eingeschaltet, Feiertage prüfen, Abo beendet, abgeschaltet; Öffnungszeiten live Task 13); K04 und T09 gelten beim Widerruf einer Funktion je Funktion (produkt, T09 zusätzlich preis; Terminplaner TP2 F9); K31, K32 und T22 Auftragsbestätigung (Aufträge und Rechnungen, Strang L5; K31 und K32 in Sie-Form, T22 mit [Aufträge])
|
||||
// K12, K14 und T12 (Wechsel ins Wartungs-Abo) aus Task 9, K13 und T13 (Zusatzspeicher) aus Task 8 (Fix 1); Endfassung der Texte in Task 12; T14 „Löschfristen“ aus Task 21 (F39); T15 Push-Zeitmail; K15, K16, T16 Funktionen (News-Pinnwand, Terminplaner; K15, K16 und T16 gelten je Funktion, Terminplaner TP2 B9a und B9b); T17 Repo zuordnen; K17 Chat-Hinweis an Kunden; K18, K19 Website-Analyse; K22, K23 Terminplaner beendet/läuft wieder (Terminplaner TP2 E3); T18 Bankzugang erneuern (Finanzen); T19 Erstattung fällig und T20 Terminplaner Betriebsmeldung (Terminplaner TP2, Team-Hinweise; T16 gilt je Funktion, T19 aus B9b); T21 Neue Webshop-Anfrage (Webshop B1, Team-Hinweis); K24 Webshop-Anfrage entschieden (Webshop B2, Kunde); G09–G14 Käufermails des Webshops (Bestellbestätigung, versendet, abholbereit, Eingang Widerruf, Erstattung, Zahlung fehlgeschlagen; Webshop E4, eigener Fuß statt Terminplaner-Fuß, Sie-Form) und K25, K26 an den Händler (neue Bestellung, Widerruf; Webshop E4); K27–K30 Öffnungszeiten live (eingeschaltet, Feiertage prüfen, Abo beendet, abgeschaltet; Öffnungszeiten live Task 13); K04 und T09 gelten beim Widerruf einer Funktion je Funktion (produkt, T09 zusätzlich preis; Terminplaner TP2 F9); K33–K37 und T23 Buchungskalender (K36 und K37 Neue Anfrage und Neue Buchung an den Vermieter, T23 Team-Hinweis; zuvor K31, K32, T22, die den Aufträgen gehören); K31, K32 und T22 Auftragsbestätigung (Aufträge und Rechnungen, Strang L5; K31 und K32 in Sie-Form, T22 mit [Aufträge])
|
||||
const ids = (prefix, count) => Array.from({ length: count }, (_, i) => `${prefix}${String(i + 1).padStart(2, '0')}`)
|
||||
const EXPECTED = [...ids('G', 22), ...ids('K', 37), ...ids('T', 23)]
|
||||
// GUEST = Gästemails des Terminplaners (gemeinsamer Fuß, Anrede-Blöcke); SHOP_GUEST = Käufermails des Webshops (G09–G14, siehe unten)
|
||||
@@ -1282,7 +1282,7 @@ test('K26: Betreff, Platzhalter, Erklärung nur im Block, Hinweis auf die 14 Tag
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Buchungskalender (Task 20): G16–G22 an Gäste (Sie-Form, neutral), K31–K34 und T22 (Du-Form, WEBklar)
|
||||
// Buchungskalender (Task 20): G16–G22 an Gäste (Sie-Form, neutral), K33, K34, K36 und K37 und T23 (Du-Form, WEBklar)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const UK_GAST = ['G16', 'G17', 'G18', 'G19', 'G20', 'G21', 'G22']
|
||||
@@ -1335,14 +1335,14 @@ test('G22: Erstattung „wird veranlasst“ nur im Block', () => {
|
||||
assert.ok(!renderMail('G22', sampleValues(tpl), { erstattung: false }).text.includes('Erstattung von'))
|
||||
})
|
||||
|
||||
test('K31–K35, T22: Dateien, Platzhalter, Du-Form', () => {
|
||||
test('K33–K37, T23: Dateien, Platzhalter, Du-Form', () => {
|
||||
const erwartet = {
|
||||
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'],
|
||||
K36: ['kunde_name', 'gast_name', 'gast_email', 'unterkunft', 'zeitraum', 'naechte', 'personen', 'nummer', 'nachricht_gast', 'portal_url'],
|
||||
K37: ['kunde_name', 'gast_name', 'unterkunft', 'zeitraum', 'naechte', 'personen', 'nummer', 'gesamt', 'zahlart_text', 'portal_url'],
|
||||
K33: ['kunde_name', 'unterkunft', 'quelle', 'seit', 'portal_url'],
|
||||
K34: ['kunde_name', 'projekt', 'preis', 'ust_hinweis', 'begruendung', 'portal_url'],
|
||||
K35: ['kunde_name', 'projekt', 'loeschdatum', 'portal_url'],
|
||||
T22: ['kunde_name', 'email', 'projekt', 'nachricht', 'woms_url'],
|
||||
T23: ['kunde_name', 'email', 'projekt', 'nachricht', 'woms_url'],
|
||||
}
|
||||
for (const [id, p] of Object.entries(erwartet)) {
|
||||
const tpl = loadTemplate(id)
|
||||
@@ -1350,8 +1350,8 @@ test('K31–K35, T22: Dateien, Platzhalter, Du-Form', () => {
|
||||
const mail = renderMail(id, sampleValues(tpl), allBlocks(tpl, true))
|
||||
assert.deepEqual(mail.problems, [], id)
|
||||
}
|
||||
assert.equal(loadTemplate('K32').file, 'K32 Neue Buchung.html')
|
||||
assert.equal(loadTemplate('K37').file, 'K37 Neue Buchung.html')
|
||||
assert.equal(loadTemplate('K35').file, 'K35 Buchungskalender abgeschaltet.html')
|
||||
assert.equal(loadTemplate('K35').subject, 'Buchungskalender abgeschaltet – bitte sichere Deine Rechnungen')
|
||||
assert.equal(loadTemplate('T22').subject, '[Buchungskalender] Neue Anfrage: {{kunde_name}}')
|
||||
assert.equal(loadTemplate('T23').subject, '[Buchungskalender] Neue Anfrage: {{kunde_name}}')
|
||||
})
|
||||
@@ -397,7 +397,7 @@ async function szenario() {
|
||||
ok(Boolean(merk.eva && merk.paul), 'beide Anfragen den Gästen zugeordnet')
|
||||
await srv.warteAufVersand()
|
||||
ok(srv.gastArten().filter((x) => x === 'G16').length === 2, `G16 an beide Gäste (${srv.gastArten().join()})`)
|
||||
ok(srv.vermieterArten().includes('K31'), `K31 an den Vermieter (${srv.vermieterArten().join()})`)
|
||||
ok(srv.vermieterArten().includes('K36'), `K36 an den Vermieter (${srv.vermieterArten().join()})`)
|
||||
ok(srv.mailsMit().some((m) => m.to.some((x) => x.address === EVA.email)), 'G16 liegt in der Mail-Senke')
|
||||
})
|
||||
|
||||
@@ -489,7 +489,7 @@ async function szenario() {
|
||||
const g19 = srv.mailsMit().find((m) => m.to.some((x) => x.address === EVA.email) && (m.attachments || []).some((x) => /^Rechnung-.*\.pdf$/.test(x.filename)))
|
||||
ok(Boolean(g19), 'G19 mit Rechnungs-PDF im Anhang liegt in der Mail-Senke')
|
||||
ok(Boolean(g19) && Buffer.from(g19.attachments[0].content, 'base64').subarray(0, 4).toString() === '%PDF', 'Anhang der G19 ist ein PDF')
|
||||
ok(srv.vermieterArten().filter((x) => x === 'K32').length >= 1, `K32 an den Vermieter (${srv.vermieterArten().join()})`)
|
||||
ok(srv.vermieterArten().filter((x) => x === 'K37').length >= 1, `K37 an den Vermieter (${srv.vermieterArten().join()})`)
|
||||
ok(srv.rechnungen().length === 1 && srv.rechnungen()[0].art === 'rechnung', 'eine Rechnung in ukRechnungen')
|
||||
})
|
||||
|
||||
|
||||
@@ -55,11 +55,11 @@ const fehlerCode = async (p) => {
|
||||
return 'kein Fehler'
|
||||
}
|
||||
|
||||
test('Anfrage wird neu, Nummer-Format, Mails G16 + K31', async () => {
|
||||
test('Anfrage wird neu, Nummer-Format, Mails G16 + K36', async () => {
|
||||
const a = await anlegen()
|
||||
assert.equal(a.status, 'neu')
|
||||
assert.match(a.nummer, /^A-[A-Z2-9]{4}$/)
|
||||
assert.deepEqual(arten(), ['G16', 'K31'])
|
||||
assert.deepEqual(arten(), ['G16', 'K36'])
|
||||
})
|
||||
|
||||
test('Ungueltige Anfragen: nichts gespeichert', async () => {
|
||||
@@ -144,7 +144,7 @@ test('bucheVerbindlich zweimal parallel: genau eine Rechnung, gleiche Nummer', a
|
||||
assert.equal(x.rechnung.nummer, 'RE-2026-0001')
|
||||
assert.equal(y.rechnung.nummer, x.rechnung.nummer)
|
||||
assert.equal(x.anfrage.status, 'gebucht')
|
||||
assert.deepEqual(arten(), ['G19', 'K32'])
|
||||
assert.deepEqual(arten(), ['G19', 'K37'])
|
||||
})
|
||||
|
||||
test('bucheVerbindlich online ohne Stripe-Konto: 409 online_aus', async () => {
|
||||
|
||||
@@ -44,7 +44,7 @@ const status = (opts = { cookie }) => send('GET', `/api/unterkunft/status?projec
|
||||
const entscheide = (b, opts = staff) => rawPost(`/api/team/unterkunft/c1/anfrage`, { projectId: P, ...b }, opts)
|
||||
const einst = () => aw.docs('ukEinstellungen').get(FEATURE) ?? null
|
||||
|
||||
// Mails: die Aufrufe werden festgehalten und gegen die echten Vorlagen K34/T22 gerendert (problems muss leer bleiben)
|
||||
// Mails: die Aufrufe werden festgehalten und gegen die echten Vorlagen K34/T23 gerendert (problems muss leer bleiben)
|
||||
const aufrufe = { admin: [], kunde: [], probleme: [], adminFehler: false, kundeFehler: false }
|
||||
af._versand.admin = async (m) => {
|
||||
aufrufe.admin.push(m)
|
||||
@@ -89,7 +89,7 @@ test('normalisiereAnfrage: gueltig, Alias monteur, Fehler je Feld', () => {
|
||||
assert.ok(af.normalisiereAnfrage(null).fehler.art)
|
||||
})
|
||||
|
||||
test('Anfrage stellen: gespeichert, T22 ans Team, Status-Block fuer state off', async () => {
|
||||
test('Anfrage stellen: gespeichert, T23 ans Team, Status-Block fuer state off', async () => {
|
||||
const leer = await status()
|
||||
assert.equal(leer.status, 200)
|
||||
assert.equal(leer.body.state, 'off')
|
||||
@@ -103,7 +103,7 @@ test('Anfrage stellen: gespeichert, T22 ans Team, Status-Block fuer state off',
|
||||
assert.equal(JSON.parse(einst().anfrage).art, 'monteurzimmer')
|
||||
assert.equal(aw.docs('portalFeatures').get(FEATURE).state, 'off')
|
||||
assert.equal(aufrufe.admin.length, 1)
|
||||
assert.equal(aufrufe.admin[0].template, 'T22')
|
||||
assert.equal(aufrufe.admin[0].template, 'T23')
|
||||
assert.equal(aufrufe.admin[0].values.projekt, 'Donau')
|
||||
assert.match(aufrufe.admin[0].values.nachricht, /^Art: Monteurwohnung\nEinheiten: 3\nPortale: Booking\.com, Airbnb/)
|
||||
assert.equal(aufrufe.admin[0].values.woms_url, `${config.womsBaseUrl}/customers/c1?tab=funktionen`)
|
||||
@@ -145,7 +145,7 @@ test('Anfrage: doppelt 409, nur bei state off, Eingabefehler, Schalter, fremdes
|
||||
assert.equal(einst(), null)
|
||||
})
|
||||
|
||||
test('Anfrage: Mail-Fehler bei T22 verhindert nichts; Grenze 20 je Stunde', async () => {
|
||||
test('Anfrage: Mail-Fehler bei T23 verhindert nichts; Grenze 20 je Stunde', async () => {
|
||||
aufrufe.adminFehler = true
|
||||
assert.equal((await stelle()).status, 200)
|
||||
assert.equal(einst().anfrageStatus, 'offen')
|
||||
@@ -312,15 +312,15 @@ test('ticketDetails: siteKey, Snippet, Schritte, Verweis, Anfrage; wirft nie', a
|
||||
assert.ok(viaConfig.includes('abcdefghijklmnop'))
|
||||
})
|
||||
|
||||
test('T22 und K34 (angenommen und abgelehnt) passen zu den Vorlagen: keine Platzhalter-Probleme', async () => {
|
||||
test('T23 und K34 (angenommen und abgelehnt) passen zu den Vorlagen: keine Platzhalter-Probleme', async () => {
|
||||
assert.equal((await stelle({ nachricht: 'Hallo\nZeile 2' })).status, 200)
|
||||
assert.equal((await entscheide({ entscheidung: 'ablehnen', begruendung: 'Passt nicht.' })).status, 200)
|
||||
assert.equal((await stelle()).status, 200)
|
||||
assert.equal((await entscheide({ entscheidung: 'annehmen' })).status, 200)
|
||||
assert.deepEqual(aufrufe.admin.map((m) => m.template), ['T22', 'T22'])
|
||||
assert.deepEqual(aufrufe.admin.map((m) => m.template), ['T23', 'T23'])
|
||||
assert.deepEqual(aufrufe.kunde.map((m) => m.template), ['K34', 'K34'])
|
||||
assert.deepEqual(aufrufe.probleme, [])
|
||||
const t22 = renderMail('T22', aufrufe.admin[0].values, {})
|
||||
const t22 = renderMail('T23', aufrufe.admin[0].values, {})
|
||||
assert.match(t22.text, /Art: Ferienwohnung oder Ferienhaus/)
|
||||
assert.match(t22.text, /Hallo/)
|
||||
const k34 = renderMail('K34', aufrufe.kunde[1].values, aufrufe.kunde[1].blocks)
|
||||
|
||||
@@ -133,7 +133,7 @@ test('POST buchung vor Ort: gebucht, Rechnung; zweites POST gleiche Antwort ohne
|
||||
const r2 = await post(url(id, token), { rechnungsadresse: ADRESSE, zahlart: 'vorort' })
|
||||
assert.deepEqual(r2.body, r1.body)
|
||||
assert.equal(aw.docs('ukRechnungen').size, 1)
|
||||
assert.deepEqual(arten(), ['G19', 'K32'])
|
||||
assert.deepEqual(arten(), ['G19', 'K37'])
|
||||
const g = await send('GET', url(id, token))
|
||||
assert.equal(g.body.status, 'gebucht')
|
||||
assert.equal(g.body.rechnungNummer, 'RE-2026-0001')
|
||||
|
||||
@@ -105,14 +105,14 @@ test('G22 ohne Erstattung: kein Erstattungssatz; G19 vor Ort bezahlt: kein Bezah
|
||||
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' }) })
|
||||
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('K31', ctx, anfrage()), TypeError)
|
||||
await assert.rejects(() => mails.anGast('K36', ctx, anfrage()), TypeError)
|
||||
})
|
||||
|
||||
test('Gastname wird in der Mail maskiert', async () => {
|
||||
@@ -122,9 +122,9 @@ test('Gastname wird in der Mail maskiert', async () => {
|
||||
assert.ok(m.html.includes('<b>x</b>'))
|
||||
})
|
||||
|
||||
test('K31: Kundenname kommt aus dem Kundendokument, wenn ctx keinen hat', async () => {
|
||||
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('K31', ctx, { anfrage: anfrage({ status: 'neu' }) })
|
||||
await mails.anVermieter('K36', ctx, { anfrage: anfrage({ status: 'neu' }) })
|
||||
const [k] = await gesendet()
|
||||
assert.ok(k.text.includes('Hallo Mimi Muster'))
|
||||
})
|
||||
@@ -161,7 +161,7 @@ test('Anfrage: gueltig -> 201 {nummer} + Dokument neu, CORS-Origin gespiegelt',
|
||||
assert.equal(docs[0].status, 'neu')
|
||||
assert.equal(docs[0].nummer, r.body.nummer)
|
||||
assert.equal(docs[0].personen, 2)
|
||||
assert.deepEqual(mails._aufrufe.map((m) => m.art || m.template), ['G16', 'K31'])
|
||||
assert.deepEqual(mails._aufrufe.map((m) => m.art || m.template), ['G16', 'K36'])
|
||||
})
|
||||
|
||||
test('Anfrage: Honigtopf und zu schnelles Token -> 201 mit Schein-Nummer, nichts gespeichert', async () => {
|
||||
|
||||
Reference in new issue
Block a user