From c3eef4747dd9c971aec281ade262aada9e39b3ec Mon Sep 17 00:00:00 2001 From: Kenso Grimm Date: Wed, 7 Oct 2026 14:55:32 +0000 Subject: [PATCH] =?UTF-8?q?Buchungskalender:=20Abgleich=20=E2=80=93=20T22/?= =?UTF-8?q?K34=20passen=20zu=20den=20Vorlagen,=20Config-Werte=20gepr=C3=BC?= =?UTF-8?q?ft?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- server/services/unterkunft/anfrageFunktion.js | 17 +++++++----- test/unterkunftAnfrageFunktion.test.js | 27 +++++++++++++++---- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/server/services/unterkunft/anfrageFunktion.js b/server/services/unterkunft/anfrageFunktion.js index 9163e4a..ddb9ce3 100644 --- a/server/services/unterkunft/anfrageFunktion.js +++ b/server/services/unterkunft/anfrageFunktion.js @@ -12,14 +12,14 @@ import { ladeEinstellungen, speichereEinstellungen } from './speicher.js' * portalFeatures-Dokuments): anfrage (JSON), anfrageStatus ('' | offen | angenommen | abgelehnt | zurueckgezogen), anfrageAt, * anfrageEntschiedenAt, anfrageEntschiedenVon, anfrageBegruendung. Sperre je Projekt: uk-anfrage:. * 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); die Vorlagen kommen aus Task 20. + * Mails: T22 ans Team (Anfrage eingegangen), K34 an den Kunden (Entscheidung). */ export const ANFRAGE_OFFEN_TEXT = 'Deine Anfrage für den Buchungskalender ist noch nicht freigegeben.' export const ARTEN = ['ferienwohnung', 'monteurzimmer', 'pension', 'sonstiges'] export const ART_NAMEN = { ferienwohnung: 'Ferienwohnung oder Ferienhaus', monteurzimmer: 'Monteurwohnung', pension: 'Pension oder Gästezimmer', sonstiges: 'Sonstiges' } -/** Das Portal sendet und liest 'monteur' (ui-erwartungen.md); gespeichert wird 'monteurzimmer'. */ +/** Das Portal sendet 'monteurzimmer'; 'monteur' (aeltere Fassung, ui-erwartungen.md) wird weiter als Alias angenommen und gelesen. */ const ART_ALIAS = { monteur: 'monteurzimmer' } export const PORTALE = ['booking', 'airbnb'] export const PORTAL_NAMEN = { booking: 'Booking.com', airbnb: 'Airbnb', andere: 'Portale (ohne Angabe)' } @@ -43,7 +43,7 @@ const MELDUNG = { const stop = (status, text, code) => Object.assign(new Error(text), { status, ...(code ? { body: { code } } : {}) }) const istObjekt = (wert) => wert !== null && typeof wert === 'object' && !Array.isArray(wert) -/** Versand (nur fuer Tests austauschbar: die Vorlagen K34 und T22 liegen auf einem anderen Strang). */ +/** Versand (nur fuer Tests austauschbar). */ export const _versand = { admin: (nachricht) => sendAdminNotice(nachricht), kunde: (an, nachricht) => sendCustomerMail(an, nachricht), @@ -193,10 +193,13 @@ async function meldeTeam({ customerId, projectId, project, anfrage, person }) { kunde_name: kundeName || 'Kunde', email: person?.email || '', projekt: project?.projectName || projectId, - art: ART_NAMEN[anfrage.art], - einheiten: String(anfrage.einheiten), - portale: anfrage.portale.length ? anfrage.portale.map((p) => PORTAL_NAMEN[p]).join(', ') : 'keine', - nachricht: anfrage.nachricht || '–', + // die Vorlage T22 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}`, + `Portale: ${anfrage.portale.length ? anfrage.portale.map((p) => PORTAL_NAMEN[p]).join(', ') : 'keine'}`, + ...(anfrage.nachricht ? ['', anfrage.nachricht] : []), + ].join('\n'), woms_url: `${config.womsBaseUrl}/customers/${encodeURIComponent(customerId)}?tab=funktionen`, }, }) diff --git a/test/unterkunftAnfrageFunktion.test.js b/test/unterkunftAnfrageFunktion.test.js index 23cccb5..6f472e1 100644 --- a/test/unterkunftAnfrageFunktion.test.js +++ b/test/unterkunftAnfrageFunktion.test.js @@ -17,6 +17,7 @@ const { config } = await import('../server/config.js') const { startTestApp, send, rawGet, post: rawPost } = await import('./support/testApp.js') const { customerCookie, kundenansichtCookie, seedFixtures, customers } = await import('./support/fixtures.js') const { deleteDocument } = await import('../server/services/appwriteAdmin.js') +const { renderMail } = await import('../server/services/mailTemplates.js') const af = await import('../server/services/unterkunft/anfrageFunktion.js') const { freischaltbar } = await import('../server/services/unterkunft/freischaltung.js') const { unterkunftTicketDetails } = await import('../server/services/unterkunft/ticketDetails.js') @@ -43,15 +44,17 @@ 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: Vorlagen K34/T22 liegen auf einem anderen Strang, der Test prueft nur den Aufruf -const aufrufe = { admin: [], kunde: [], adminFehler: false, kundeFehler: false } +// Mails: die Aufrufe werden festgehalten und gegen die echten Vorlagen K34/T22 gerendert (problems muss leer bleiben) +const aufrufe = { admin: [], kunde: [], probleme: [], adminFehler: false, kundeFehler: false } af._versand.admin = async (m) => { aufrufe.admin.push(m) + aufrufe.probleme.push(...renderMail(m.template, m.values, m.blocks ?? {}).problems.map((x) => `${m.template}: ${x}`)) if (aufrufe.adminFehler) throw new Error('SMTP') return { sent: true } } af._versand.kunde = async (an, m) => { aufrufe.kunde.push({ an, ...m }) + aufrufe.probleme.push(...renderMail(m.template, m.values, m.blocks ?? {}).problems.map((x) => `${m.template}: ${x}`)) if (aufrufe.kundeFehler) throw new Error('SMTP') } @@ -60,6 +63,7 @@ beforeEach(async () => { Object.assign(config.unterkunft, vorher, { mode: 'pilot', pilotEmails: [customers.c1.email] }) resetUnterkunftRoutesForTests() aufrufe.admin.length = 0 + aufrufe.probleme.length = 0 aufrufe.kunde.length = 0 aufrufe.adminFehler = aufrufe.kundeFehler = false aw.seed('websiteProjects', { $id: P, customerId: 'c1', projectName: 'Donau', archived: false }) @@ -101,9 +105,7 @@ test('Anfrage stellen: gespeichert, T22 ans Team, Status-Block fuer state off', assert.equal(aufrufe.admin.length, 1) assert.equal(aufrufe.admin[0].template, 'T22') assert.equal(aufrufe.admin[0].values.projekt, 'Donau') - assert.equal(aufrufe.admin[0].values.einheiten, '3') - assert.equal(aufrufe.admin[0].values.art, 'Monteurwohnung') - assert.equal(aufrufe.admin[0].values.portale, 'Booking.com, Airbnb') + 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`) assert.equal(aufrufe.kunde.length, 0) const s = (await status()).body.anfrage @@ -307,3 +309,18 @@ test('ticketDetails: siteKey, Snippet, Schritte, Verweis, Anfrage; wirft nie', a const viaConfig = await config.features.unterkunft.ticket.details({ doc: doc(), project: {}, customer: null, feature: { key: 'Buchungskalender' } }) assert.ok(viaConfig.includes('abcdefghijklmnop')) }) + +test('T22 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.kunde.map((m) => m.template), ['K34', 'K34']) + assert.deepEqual(aufrufe.probleme, []) + const t22 = renderMail('T22', 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) + assert.match(k34.text, /199 €/) +})