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:
knsoandClaude Opus 5.5 committed 2026-10-07 15:47:29 +00:00
1 parent fd971309a8
commit 3526909d61
17 files changed
+51 -47

No files matched your search

+1 -1
View File
@@ -55,7 +55,7 @@ Der Inhalt des `div` bleibt stehen, solange das Widget nicht lädt oder die Funk
- Auf dem Handy und am Desktop: Anreise und Abreise wählbar, belegte Nächte nicht wählbar, Mindestnächte und Höchstzahl der Personen werden geprüft, Anfrage senden zeigt die Nummer.
- Tastatur: Pfeiltasten im Kalender, Enter wählt; jeder Tag hat ein `aria-label` mit Datum und Status.
- Kein Konflikt mit globalen Stilen des Themes (Abschnitt 2).
- Testanfrage mit eigener Adresse: E-Mail G16 (Eingang) beim Gast, K31 beim Vermieter.
- Testanfrage mit eigener Adresse: E-Mail G16 (Eingang) beim Gast, K36 beim Vermieter.
## 6. Vorschau und Testanfrage
+3 -3
View File
@@ -9,7 +9,7 @@ Reihenfolge für den Inhaber. Jeder Schritt braucht seine Freigabe; nichts davon
## 0. Voraussetzung
- TP2, Webshop und Öffnungszeiten-Zweig liegen schon auf `origin/main` (Portal) bzw. `test` (WOMS) oder werden vorher gemerged: Das Funktionen-Register, die Mail-Warteschlange (`termineOutbox`) und die Connect-Anbindung stammen von dort. `git merge origin/main` im Worktree, Tests laufen lassen. Erwartbare Konflikte: `server/config.js` (features), `public/funktionen.js`, `test/mailTemplates.test.js` (Vorlagen-IDs: K31 bis K35, G16 bis G22, T22 sind für den Buchungskalender vergeben, vor dem Merge gegen `main` prüfen).
- TP2, Webshop und Öffnungszeiten-Zweig liegen schon auf `origin/main` (Portal) bzw. `test` (WOMS) oder werden vorher gemerged: Das Funktionen-Register, die Mail-Warteschlange (`termineOutbox`) und die Connect-Anbindung stammen von dort. `git merge origin/main` im Worktree, Tests laufen lassen. Erwartbare Konflikte: `server/config.js` (features), `public/funktionen.js`, `test/mailTemplates.test.js` (Vorlagen-IDs: K36 bis K35, G16 bis G22, T23 sind für den Buchungskalender vergeben, vor dem Merge gegen `main` prüfen).
- Die Rechtstexte der Fassung 2026-10-07 liegen als HTML und PDF im Zweig (`public/rechtliches/*-2026-10-07.*`, siehe Schritt 6).
## 1. Inhaber prüft die Texte
@@ -97,8 +97,8 @@ Das macht Claude selbst (Wunsch des Inhabers). Dafür legt der Inhaber vorher de
## 8. Pilot mit Mimi
1. Testanfrage im Widget (eigene Adresse): Mails G16 und K31 prüfen.
2. Im Portal freigeben (G17), Buchungslink öffnen, „vor Ort“ buchen (G19 mit Rechnung als PDF, K32).
1. Testanfrage im Widget (eigene Adresse): Mails G16 und K36 prüfen.
2. Im Portal freigeben (G17), Buchungslink öffnen, „vor Ort“ buchen (G19 mit Rechnung als PDF, K37).
3. Rechnung prüfen: Mimis Name und Anschrift, Steuernummer oder USt-ID, Nummer lückenlos, Steuerart, kein WEBklar-Hinweis.
4. Mit Stripe-Konto: Online-Zahlung mit kleinem Betrag testen (G20), dann stornieren mit Erstattung (G22, Stornorechnung).
5. Stornieren (Stornorechnung), Testbuchungen wieder entfernen; iCal-Import und -Export mit einem Test-Kalender prüfen.
@@ -1,5 +1,7 @@
# Buchungskalender (Funktion `unterkunft`) Implementation Plan
> **Hinweis (2026-10-07, Livegang-Vorbereitung):** Die Mail-Nummern K31, K32 und T22 gehören seit dem Merge mit `origin/main` den Aufträgen (Auftragsbestätigung). Im Buchungskalender heißen die Mails jetzt **K36** (Neue Buchungsanfrage, vorher K31), **K37** (Neue Buchung, vorher K32) und **T23** (Neue Anfrage Buchungskalender, vorher T22). Der folgende Text nennt noch die alten Nummern.
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Neue Portal-Funktion „Buchungskalender“ für Ferienwohnungen: Kalender-Widget mit Anfrage, Freigabe durch den Vermieter, verbindliche Buchung per Link, Rechnung im Namen des Vermieters, Zahlung über Stripe Connect oder vor Ort, iCal-Abgleich; erste Kundin Donau Home.
@@ -1,5 +1,7 @@
# Buchungskalender für Unterkünfte (Funktion `unterkunft`): Design
> **Hinweis (2026-10-07, Livegang-Vorbereitung):** Die Mail-Nummern K31, K32 und T22 gehören seit dem Merge mit `origin/main` den Aufträgen (Auftragsbestätigung). Im Buchungskalender heißen die Mails jetzt **K36** (Neue Buchungsanfrage, vorher K31), **K37** (Neue Buchung, vorher K32) und **T23** (Neue Anfrage Buchungskalender, vorher T22). Der folgende Text nennt noch die alten Nummern.
Stand 2026-10-07. Basis: Portal `origin/main` 8814c4a (TP2, Webshop, Öffnungszeiten im Pilot). Arbeitszweig
`feature/unterkunft`, Worktree `dev-unterkunft`. Pfade: **P** = Portal-Repo, **W** = WOMS-Repo, **D** = Website Donau Home
(`WEBklar/donau-home`, lokaler Klon `/root/reports/donau-home-website`).
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<!--
K31 · Neue Buchungsanfrage (an den Vermieter)
K36 · Neue Buchungsanfrage (an den Vermieter)
Betreff: Neue Buchungsanfrage {{nummer}} für {{unterkunft}}
Absender: „WEBklar“ <Portal-Postfach>
Empfänger: Kunde (E-Mail des Kundenkontos) · OHNE BCC (enthält Angaben des Gastes)
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<!--
K32 · Neue Buchung (an den Vermieter)
K37 · Neue Buchung (an den Vermieter)
Betreff: Neue Buchung {{nummer}} für {{unterkunft}}
Absender: „WEBklar“ <Portal-Postfach>
Empfänger: Kunde (E-Mail des Kundenkontos) · OHNE BCC (enthält Angaben des Gastes)
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<!--
T22 · Neue Anfrage Buchungskalender
T23 · Neue Anfrage Buchungskalender
Betreff: [Buchungskalender] Neue Anfrage: {{kunde_name}}
Absender: „WEBklar Kundenportal“ <Portal-Postfach>
Empfänger: kenso@webklar.com, andrej@webklar.com
+2 -2
View File
@@ -114,7 +114,7 @@ export async function legeAnfrageAn(ctx, eingabe, { now = new Date(), ipHash = '
})
})
await sende(() => mails.anGast('G16', ctx, anfrage))
await sende(() => mails.anVermieter('K31', ctx, { anfrage }))
await sende(() => mails.anVermieter('K36', ctx, { anfrage }))
return anfrage
}
@@ -229,7 +229,7 @@ export async function bucheVerbindlich(ctx, anfrageId, token, { rechnungsadresse
})
if (r.neu) {
await sende(() => mails.anGast('G19', ctx, r.anfrage, { rechnung: r.rechnung }))
await sende(() => mails.anVermieter('K32', ctx, { anfrage: r.anfrage }))
await sende(() => mails.anVermieter('K37', ctx, { anfrage: r.anfrage }))
}
return { anfrage: r.anfrage, rechnung: r.rechnung }
}
@@ -12,7 +12,7 @@ import { ladeEinstellungen, speichereEinstellungen } from './speicher.js'
* portalFeatures-Dokuments): anfrage (JSON), anfrageStatus ('' | offen | angenommen | abgelehnt | zurueckgezogen), anfrageAt,
* anfrageEntschiedenAt, anfrageEntschiedenVon, anfrageBegruendung. Sperre je Projekt: uk-anfrage:<projectId>.
* Fehler an den Kunden tragen status (4xx) und einen deutschen Text (Du-Form), optional err.body.code und err.fehler (je Feld).
* Mails: T22 ans Team (Anfrage eingegangen), K34 an den Kunden (Entscheidung).
* Mails: T23 ans Team (Anfrage eingegangen), K34 an den Kunden (Entscheidung).
*/
export const ANFRAGE_OFFEN_TEXT = 'Deine Anfrage für den Buchungskalender ist noch nicht freigegeben.'
@@ -153,7 +153,7 @@ async function pruefeZugang({ customerId, projectId, person, neu }) {
/**
* Anfrage stellen. Reihenfolge: Projekt (403/409 archiviert), Schalter (409), Eingabe (400 mit `fehler`), dann unter Sperre je
* Projekt: Funktion muss `off` sein (409), Status '' | abgelehnt | zurueckgezogen (409 sonst). Speichert, loescht eine fruehere
* Entscheidung und schickt T22 ans Team (ein Fehler dabei wird nur geloggt).
* Entscheidung und schickt T23 ans Team (ein Fehler dabei wird nur geloggt).
* @returns {Promise<object>} anfrageAnsicht des neuen Standes
*/
export async function stelleAnfrage({ customerId, projectId, eingabe, person = {}, now = new Date() }) {
@@ -182,18 +182,18 @@ export async function stelleAnfrage({ customerId, projectId, eingabe, person = {
return anfrageAnsicht(gespeichert)
}
/** T22 "Neue Anfrage Buchungskalender" ans Team; sendAdminNotice wirft nie, ein Fehlschlag steht nur im Log. */
/** T23 "Neue Anfrage Buchungskalender" ans Team; sendAdminNotice wirft nie, ein Fehlschlag steht nur im Log. */
async function meldeTeam({ customerId, projectId, project, anfrage, person }) {
try {
let kundeName = String(person?.name || '').trim()
if (!kundeName) kundeName = String((await customerOf(customerId))?.name || '').trim()
const result = await _versand.admin({
template: 'T22',
template: 'T23',
values: {
kunde_name: kundeName || 'Kunde',
email: person?.email || '',
projekt: project?.projectName || projectId,
// die Vorlage T22 kennt nur kunde_name, email, projekt, nachricht, woms_url: die Angaben stehen zusammen in der Nachricht
// die Vorlage T23 kennt nur kunde_name, email, projekt, nachricht, woms_url: die Angaben stehen zusammen in der Nachricht
nachricht: [
`Art: ${ART_NAMEN[anfrage.art]}`,
`Einheiten: ${anfrage.einheiten}`,
@@ -203,9 +203,9 @@ async function meldeTeam({ customerId, projectId, project, anfrage, person }) {
woms_url: `${config.womsBaseUrl}/customers/${encodeURIComponent(customerId)}?tab=funktionen`,
},
})
if (!result?.sent) console.warn(`[unterkunft] Team-Mail T22 nicht gesendet: ${result?.reason || 'unbekannt'}`)
if (!result?.sent) console.warn(`[unterkunft] Team-Mail T23 nicht gesendet: ${result?.reason || 'unbekannt'}`)
} catch (err) {
console.warn(`[unterkunft] Team-Mail T22 nicht gesendet: ${err?.message}`)
console.warn(`[unterkunft] Team-Mail T23 nicht gesendet: ${err?.message}`)
}
}
+4 -4
View File
@@ -9,7 +9,7 @@ import { ladeEinstellungen, ladePreise, ladeUnterkunft } from './speicher.js'
/**
* Mails des Buchungskalenders: G16-G22 an Gaeste (ueber die Mail-Warteschlange termineOutbox, Absendername = Firma des Vermieters,
* Antworten an dessen E-Mail), K31-K34 an den Vermieter (Kunde des Portals, ohne BCC bei Gastdaten) und T22 ans Team.
* Antworten an dessen E-Mail), K33, K34, K36 und K37 an den Vermieter (Kunde des Portals, ohne BCC bei Gastdaten) und T23 ans Team.
* `_aufrufe` protokolliert jeden Aufruf fuer Tests (die Mail geht trotzdem raus). Fehler werden geworfen; der Aufrufer
* (ablauf.js) faengt sie ab, weil der gespeicherte Schritt nicht rueckgaengig gemacht wird.
*
@@ -147,8 +147,8 @@ export async function anGast(art, ctx, anfrage, extra = {}) {
const portalUrl = (ctx) => `${config.publicBaseUrl}/dashboard.html#buchungen?projekt=${encodeURIComponent(ctx?.projectId ?? '')}`
/**
* Mail an den Vermieter (K31-K34, Kunde des Portals) bzw. ans Team (T22). K31/K32: values.anfrage; die Werte lassen sich ueberschreiben,
* values.an ersetzt die Adresse (Standard ctx.besitzerEmail). T22 geht an die Team-Adressen und wirft nie.
* Mail an den Vermieter (K33, K34, K36 und K37, Kunde des Portals) bzw. ans Team (T23). K36/K37: values.anfrage; die Werte lassen sich ueberschreiben,
* values.an ersetzt die Adresse (Standard ctx.besitzerEmail). T23 geht an die Team-Adressen und wirft nie.
*/
export async function anVermieter(template, ctx, values = {}) {
if (testProtokoll()) _aufrufe.push({ an: 'vermieter', template, anfrageId: values?.anfrage?.$id })
@@ -166,7 +166,7 @@ export async function anVermieter(template, ctx, values = {}) {
}
if (!anfrage) vorgabe.kunde_name = text(ctx?.kundeName) || (await kundenName(ctx))
const alle = { portal_url: portalUrl(ctx), ...vorgabe, ...eigene }
if (template === 'T22') {
if (template === 'T23') {
const r = await sendAdminNotice({ template, values: nurPlatzhalter(template, alle) })
if (!r?.sent) console.warn(`[unterkunft] Team-Mail ${template} nicht gesendet: ${r?.reason || 'unbekannt'}`)
return r
+8 -8
View File
@@ -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}}')
})
+2 -2
View File
@@ -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')
})
+3 -3
View File
@@ -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 () => {
+7 -7
View File
@@ -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)
+1 -1
View File
@@ -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')
+5 -5
View File
@@ -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('&lt;b&gt;x&lt;/b&gt;'))
})
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'))
})
+1 -1
View File
@@ -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 () => {