Kundenansicht P6: Zusage-Mails K42 und K43 für Webshop- und Buchungskalender-Anfrage ohne Preis
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bb1efa5c9e
commit
9a3ce7c8bf
10 files changed
+205
-7
No files matched your search
@@ -13,7 +13,7 @@ useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
const { config } = await import('../server/config.js')
|
||||
const { renderMail } = await import('../server/services/mailTemplates.js')
|
||||
const { anleitungsLink } = await import('../server/services/featureRegistry.js')
|
||||
const { k38Mail, k39Mail, t24Mail, loeschFristTage, kontostandText } = await import('../server/services/aboFunktionenMails.js')
|
||||
const { anfrageEntschiedenMail, k38Mail, k39Mail, t24Mail, loeschFristTage, kontostandText } = await import('../server/services/aboFunktionenMails.js')
|
||||
const { k29Mail, k30Mail } = await import('../server/services/zeiten/mails.js')
|
||||
|
||||
test('k38Mail: Werte und Blöcke je Abrechnung', () => {
|
||||
@@ -44,3 +44,12 @@ test('alle Builder-Ergebnisse rendern ohne Probleme und ohne €; Zeiten-Variant
|
||||
const k41 = k30Mail({ kunde: 'K', projekt: 'P', loeschdatum: frist, preis: '30 €', modell: true }); assert.equal(k41.template, 'K41'); assert.equal(renderMail('K41', k41.values, k41.blocks).problems.length, 0)
|
||||
assert.equal(k29Mail({ kunde: 'K', projekt: 'P', art: 'beendet', frist, preis: '30 €' }).template, 'K29') // Standard unverändert
|
||||
})
|
||||
test('anfrageEntschiedenMail: K42 und K43, Werte, Blöcke, Adresse; ohne Preis', () => {
|
||||
const m = anfrageEntschiedenMail({ kunde: { name: 'Erika' }, projekt: 'Töpferei', key: 'shop', projectId: 'pt1', angenommen: true })
|
||||
assert.equal(m.template, 'K42'); assert.deepEqual(m.blocks, { angenommen: true, abgelehnt: false })
|
||||
assert.deepEqual(m.values, { kunde_name: 'Erika', projekt: 'Töpferei', begruendung: '', portal_url: `${config.publicBaseUrl}/dashboard.html#webshop?projekt=pt1` })
|
||||
const u = anfrageEntschiedenMail({ kunde: { name: ' ' }, projekt: 'Donau', key: 'unterkunft', projectId: 'a b', angenommen: false, begruendung: 'Passt nicht.' })
|
||||
assert.deepEqual([u.template, u.blocks, u.values.kunde_name, u.values.begruendung, u.values.portal_url], ['K43', { angenommen: false, abgelehnt: true }, 'Kunde', 'Passt nicht.', `${config.publicBaseUrl}/dashboard.html#buchungen?projekt=a%20b`])
|
||||
assert.throws(() => anfrageEntschiedenMail({ kunde: 'K', projekt: 'P', key: 'termine', projectId: 'p', angenommen: true }), TypeError)
|
||||
for (const x of [m, u]) assert.deepEqual(renderMail(x.template, x.values, x.blocks).problems, [])
|
||||
})
|
||||
@@ -8,6 +8,8 @@ const SOLL = { // [Datei, Betreff, Platzhalter, Blöcke]
|
||||
K39: ['K39 Funktion beendet.html', 'Deine Funktion wurde beendet: {{funktion}}', ['kunde_name', 'projekt', 'funktion', 'loeschtag', 'portal_url'], ['abo_ende', 'hosting', 'termine', 'offene']],
|
||||
K40: ['K40 Zeiten Abo-Ende.html', 'Dein Abo ist beendet – Öffnungszeiten live läuft noch bis {{frist}}', ['kunde_name', 'projekt', 'frist', 'portal_url'], ['abo_ende', 'hosting', 'wechsel_widerrufen']],
|
||||
K41: ['K41 Zeiten abgeschaltet Abo.html', 'Öffnungszeiten live ist abgeschaltet', ['kunde_name', 'projekt', 'loeschdatum', 'portal_url'], []],
|
||||
K42: ['K42 Webshop-Anfrage Abo.html', 'Deine Webshop-Anfrage für {{projekt}}', ['kunde_name', 'projekt', 'begruendung', 'portal_url'], ['angenommen', 'abgelehnt']],
|
||||
K43: ['K43 Anfrage Buchungskalender Abo.html', 'Deine Anfrage zum Buchungskalender für {{projekt}}', ['kunde_name', 'projekt', 'begruendung', 'portal_url'], ['angenommen', 'abgelehnt']],
|
||||
T24: ['T24 Funktion eingeschaltet.html', '[Abo] Funktion eingeschaltet: {{funktion}} – {{kunde_name}}', ['kunde_name', 'email', 'projekt', 'funktion', 'woid', 'kontostand', 'woms_url'], ['vorschuss', 'anhang_fehlt']],
|
||||
}
|
||||
const mail = (id, on) => { const t = loadTemplate(id); return renderMail(id, Object.fromEntries(t.placeholders.map((p) => [p, `W-${p}`])), Object.fromEntries(t.blocks.map((b) => [b, on]))) }
|
||||
@@ -23,4 +25,13 @@ test('Festtexte', () => {
|
||||
assert.match(mail('K39', true).text, /Bitte informiere deine Gäste selbst\./); assert.doesNotMatch(mail('K39', false).text, /CSV|Offene Bestellungen/)
|
||||
assert.match(mail('T24', true).text, /Das Guthaben reicht voraussichtlich nicht \(W-kontostand\)\./); assert.doesNotMatch(mail('T24', false).text, /Vorschuss/)
|
||||
})
|
||||
test('templateIds enthält K38 bis K41 und T24', () => { for (const id of Object.keys(SOLL)) assert.ok(templateIds().includes(id)) })
|
||||
test('K42, K43: Zusage ohne Preis und ohne „bestellen“, Absage mit Begründung wie K24 und K34', () => {
|
||||
const werte = { kunde_name: 'Erika', projekt: 'Töpferei', begruendung: 'Wir verkaufen keine <Waffen>.', portal_url: 'https://portal.example/dashboard.html#x?projekt=p1' }
|
||||
for (const [id, satz, knopf] of [['K42', 'Du kannst den Webshop jetzt im Kundenportal einschalten; danach richten wir ihn ein.', 'Webshop einschalten'], ['K43', 'Du kannst ihn jetzt im Kundenportal einschalten; danach richten wir ihn ein.', 'Zum Kundenportal']]) {
|
||||
const zu = renderMail(id, werte, { angenommen: true, abgelehnt: false }); assert.deepEqual(zu.problems, [])
|
||||
assert.doesNotMatch(zu.html + zu.text, /€|UStG|bestell|Preis|Waffen/i); assert.ok(zu.text.includes(satz), id); assert.ok(zu.text.includes(knopf), id)
|
||||
const ab = renderMail(id, werte, { angenommen: false, abgelehnt: true }); assert.deepEqual(ab.problems, [])
|
||||
assert.match(ab.text, /leider/); assert.match(ab.text, /Wir verkaufen keine <Waffen>\./); assert.doesNotMatch(ab.text + ab.html, /€|UStG|bestell/i)
|
||||
}
|
||||
})
|
||||
test('templateIds enthält K38 bis K43 und T24', () => { for (const id of Object.keys(SOLL)) assert.ok(templateIds().includes(id)) })
|
||||
@@ -6,9 +6,9 @@ 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); 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]); K38–K41 und T24 Funktionen im Abo-Modell (Kundenansicht P6: K38 wird eingerichtet, K39 beendet, K40 und K41 Öffnungszeiten live Abo-Ende und abgeschaltet ohne Preis, T24 Team-Hinweis; Du-Form, Test in aboFunktionenVorlagen.test.js)
|
||||
// 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]); K42, K43 Zusage-Mails ohne Preis (Kundenansicht P6: Webshop- und Buchungskalender-Anfrage, Ersatz für K24 und K34 im Abo-Modell); K38–K41 und T24 Funktionen im Abo-Modell (Kundenansicht P6: K38 wird eingerichtet, K39 beendet, K40 und K41 Öffnungszeiten live Abo-Ende und abgeschaltet ohne Preis, T24 Team-Hinweis; Du-Form, Test in aboFunktionenVorlagen.test.js)
|
||||
const ids = (prefix, count) => Array.from({ length: count }, (_, i) => `${prefix}${String(i + 1).padStart(2, '0')}`)
|
||||
const EXPECTED = [...ids('G', 22), ...ids('K', 41), ...ids('T', 24)]
|
||||
const EXPECTED = [...ids('G', 22), ...ids('K', 43), ...ids('T', 24)]
|
||||
// GUEST = Gästemails des Terminplaners (gemeinsamer Fuß, Anrede-Blöcke); SHOP_GUEST = Käufermails des Webshops (G09–G14, siehe unten)
|
||||
const GUEST = ids('G', 8)
|
||||
const SHOP_GUEST = ['G09', 'G10', 'G11', 'G12', 'G13', 'G14', 'G15']
|
||||
|
||||
@@ -252,3 +252,17 @@ test('nach Ablehnung kann der Kunde neu anfragen, die alte Entscheidung ist gel
|
||||
assert.equal(doc.anfrageBegruendung, '')
|
||||
assert.equal((await entscheide({ entscheidung: 'annehmen' })).status, 200)
|
||||
})
|
||||
|
||||
const ansichtAlt = { ...config.kundenansichtNeu }; after(() => Object.assign(config.kundenansichtNeu, ansichtAlt))
|
||||
const neueAnsicht = (an) => { config.kundenansichtNeu.mode = an ? 'pilot' : 'off'; config.kundenansichtNeu.pilotEmails = [customers.c1.email] }
|
||||
test('Abo-Modell: Zusage als K42 ohne Preis und ohne „bestellen“, Absage mit Begründung; alte Ansicht bekommt weiter K24 mit 299 €', async () => {
|
||||
for (const an of [true, false]) {
|
||||
neueAnsicht(an); await leereAlles(); await stelle(); mails.sent.length = 0
|
||||
assert.equal((await entscheide({ entscheidung: 'annehmen' })).status, 200)
|
||||
const m = k24()[0]; assert.equal(m.subject, 'Deine Webshop-Anfrage für Töpferei'); assert.match(m.html, /dashboard\.html#webshop\?projekt=pt1"/)
|
||||
if (an) { assert.doesNotMatch(m.text + m.html, /€|UStG|bestell/i); assert.match(m.text, /jetzt im Kundenportal einschalten/) } else { assert.match(m.text, /299 €/); assert.match(m.text, /bestellen/) }
|
||||
}
|
||||
neueAnsicht(true); await leereAlles(); await stelle(); mails.sent.length = 0
|
||||
assert.equal((await entscheide({ entscheidung: 'ablehnen', begruendung: 'Passt nicht.' })).status, 200)
|
||||
assert.match(k24()[0].text, /leider[\s\S]*Passt nicht\./); assert.doesNotMatch(k24()[0].text, /299 €/)
|
||||
})
|
||||
@@ -326,3 +326,17 @@ test('T23 und K34 (angenommen und abgelehnt) passen zu den Vorlagen: keine Platz
|
||||
const k34 = renderMail('K34', aufrufe.kunde[1].values, aufrufe.kunde[1].blocks)
|
||||
assert.match(k34.text, /199 €/)
|
||||
})
|
||||
|
||||
const ansichtAlt = { ...config.kundenansichtNeu }; after(() => Object.assign(config.kundenansichtNeu, ansichtAlt))
|
||||
const neueAnsicht = (an) => { config.kundenansichtNeu.mode = an ? 'pilot' : 'off'; config.kundenansichtNeu.pilotEmails = [customers.c1.email] }
|
||||
test('Abo-Modell: Zusage als K43 ohne Preis und ohne „bestellen“, Absage unverändert; alte Ansicht bekommt weiter K34 mit 199 €', async () => {
|
||||
for (const an of [true, false]) {
|
||||
neueAnsicht(an); aufrufe.kunde.length = 0; await deleteDocument(config.collections.ukEinstellungen, FEATURE).catch(() => {}); assert.equal((await stelle()).status, 200); assert.equal((await entscheide({ entscheidung: 'annehmen' })).status, 200)
|
||||
const m = aufrufe.kunde[0]; const r = renderMail(m.template, m.values, m.blocks); assert.deepEqual(r.problems, [])
|
||||
assert.deepEqual([m.template, 'preis' in m.values, 'ust_hinweis' in m.values], an ? ['K43', false, false] : ['K34', true, true])
|
||||
assert.equal(m.values.portal_url, `${config.publicBaseUrl}/dashboard.html#buchungen?projekt=${P}`)
|
||||
if (an) assert.doesNotMatch(r.text + r.html, /€|UStG|bestell/i); else { assert.match(r.text, /199 €/); assert.match(r.text, /bestellen/) }
|
||||
}
|
||||
neueAnsicht(true); aufrufe.kunde.length = 0; await deleteDocument(config.collections.ukEinstellungen, FEATURE).catch(() => {}); await stelle(); assert.equal((await entscheide({ entscheidung: 'ablehnen', begruendung: 'Passt nicht.' })).status, 200)
|
||||
assert.deepEqual([aufrufe.kunde[0].template, aufrufe.kunde[0].values.begruendung, aufrufe.kunde[0].blocks], ['K43', 'Passt nicht.', { angenommen: false, abgelehnt: true }])
|
||||
})
|
||||
Reference in new issue
Block a user