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
@@ -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
|
||||
|
||||
|
||||
@@ -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`).
|
||||
|
||||
+1
-1
@@ -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)
|
||||
+1
-1
@@ -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)
|
||||
+1
-1
@@ -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
|
||||
@@ -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}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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