Öffnungszeiten: Mails K24–K27 heißen jetzt K27–K30, Feld k24At heißt einschaltMailAt
K24–K26 sind seit dem Webshop-Livegang (main) vergeben; der Mail-Lader sucht die Vorlage über den Dateianfang, doppelte Nummern hätten die falsche Mail geschickt. Das Feld ist noch nicht in der Produktion angelegt (zeiten-setup.mjs lief noch nicht), daher jetzt ein sprechender Name. Spec und Plan behalten die alten Nummern (Hinweis oben im Dokument). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ea4457e6a4
commit
c7cd436ec2
34 files changed
+359
-354
No files matched your search
@@ -6,9 +6,10 @@ 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); K24–K27 Öffnungszeiten live (eingeschaltet, Feiertage prüfen, Abo beendet, abgeschaltet; Öffnungszeiten live Task 13); T18 Bankzugang erneuern (Finanzen); T19 Erstattung fällig und T20 Terminplaner Betriebsmeldung (Terminplaner TP2, Team-Hinweise; T16 gilt je Funktion, T19 aus B9b); K04 und T09 gelten beim Widerruf einer Funktion je Funktion (produkt, T09 zusätzlich preis; Terminplaner TP2 F9)
|
||||
// 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); K27–K30 Öffnungszeiten live (eingeschaltet, Feiertage prüfen, Abo beendet, abgeschaltet; Öffnungszeiten live Task 13); T18 Bankzugang erneuern (Finanzen); T19 Erstattung fällig und T20 Terminplaner Betriebsmeldung (Terminplaner TP2, Team-Hinweise; T16 gilt je Funktion, T19 aus B9b); K04 und T09 gelten beim Widerruf einer Funktion je Funktion (produkt, T09 zusätzlich preis; Terminplaner TP2 F9)
|
||||
const ids = (prefix, count) => Array.from({ length: count }, (_, i) => `${prefix}${String(i + 1).padStart(2, '0')}`)
|
||||
const EXPECTED = [...ids('G', 8), ...ids('K', 27), ...ids('T', 19), 'T20']
|
||||
// K24–K26 gehoeren dem Webshop (auf main); dieser Zweig traegt bis zur Zusammenfuehrung nur die Oeffnungszeiten-Mails K27–K30
|
||||
const EXPECTED = [...ids('G', 8), ...ids('K', 23), 'K27', 'K28', 'K29', 'K30', ...ids('T', 19), 'T20']
|
||||
const GUEST = ids('G', 8)
|
||||
|
||||
const sampleValues = (tpl) => Object.fromEntries(tpl.placeholders.map((p) => [p, `Wert-${p}`]))
|
||||
@@ -527,8 +528,8 @@ test('T02 und T07 erinnern an UptimeRobot', () => {
|
||||
assert.ok(t07.text.includes('Monitor in UptimeRobot entfernen'), t07.text)
|
||||
})
|
||||
|
||||
test('Keine Vorlage nennt fest „Wartungs-Abo“ im Betreff außer K12, K14, T12 und K26 (Abo beendet, Öffnungszeiten live)', () => {
|
||||
for (const id of templateIds()) if (!['K12', 'K14', 'T12', 'K26'].includes(id)) assert.ok(!loadTemplate(id).subject.includes('Wartungs-Abo'), id)
|
||||
test('Keine Vorlage nennt fest „Wartungs-Abo“ im Betreff außer K12, K14, T12 und K29 (Abo beendet, Öffnungszeiten live)', () => {
|
||||
for (const id of templateIds()) if (!['K12', 'K14', 'T12', 'K29'].includes(id)) assert.ok(!loadTemplate(id).subject.includes('Wartungs-Abo'), id)
|
||||
})
|
||||
|
||||
test('K01-Anhang: Fassung 3 für v3-Abos, Fassung 4 sonst', () => {
|
||||
@@ -968,9 +969,9 @@ test('G08: Eingangsbestätigung mit Datum und Uhrzeit, Blöcke zugeordnet und na
|
||||
assert.match(ohne.text, /Waren Sie das nicht, können Sie diese Nachricht ignorieren\./)
|
||||
})
|
||||
|
||||
// Terminplaner TP2 (E3): K22 und K23 stehen in Du-Form (K02/K03 bleiben Sie-Form wie ihr Rest); Öffnungszeiten live: K24–K27 ebenso
|
||||
test('K22, K23, K24–K27: Du-Form fest, kein Sie und kein Ihr, auch mit allen Blöcken', () => {
|
||||
for (const id of ['K22', 'K23', 'K24', 'K25', 'K26', 'K27']) {
|
||||
// Terminplaner TP2 (E3): K22 und K23 stehen in Du-Form (K02/K03 bleiben Sie-Form wie ihr Rest); Öffnungszeiten live: K27–K30 ebenso
|
||||
test('K22, K23, K27–K30: Du-Form fest, kein Sie und kein Ihr, auch mit allen Blöcken', () => {
|
||||
for (const id of ['K22', 'K23', 'K27', 'K28', 'K29', 'K30']) {
|
||||
const tpl = loadTemplate(id)
|
||||
assert.ok(!tpl.blocks.includes('sie') && !tpl.blocks.includes('du'), `${id}: Du-Form fest, keine Anredeblöcke`)
|
||||
const mail = renderMail(id, sampleValues(tpl), allBlocks(tpl, true))
|
||||
|
||||
@@ -7,13 +7,13 @@ import { useTestEnv } from './support/testEnv.js'
|
||||
import { ZEITEN_DATETIMES } from '../server/services/zeiten/schema.js'
|
||||
|
||||
// Abo-Abgleich von Öffnungszeiten live (Aufgabe 21, Spec 5 "Abo-Abgleich" Schritte 1 bis 4 und Z11): gleicheZeitenAboAb(now) koppelt die im
|
||||
// Wartungs-Abo enthaltenen Zeiten (unlockedBy 'abo') an das Abo: Abo-Ende erkennen (aboEndeAt, K26), nach 30 Tagen abschalten (K27, Ersatz-Ticket),
|
||||
// bei Rückkehr des Abos wieder einschalten (K24 "wieder", Neubau nach dem Abschalten), 30 Tage nach dem Abschalten die Daten löschen.
|
||||
// Wartungs-Abo enthaltenen Zeiten (unlockedBy 'abo') an das Abo: Abo-Ende erkennen (aboEndeAt, K29), nach 30 Tagen abschalten (K30, Ersatz-Ticket),
|
||||
// bei Rückkehr des Abos wieder einschalten (K27 "wieder", Neubau nach dem Abschalten), 30 Tage nach dem Abschalten die Daten löschen.
|
||||
// Aufbau wie termineAboAbgleich.test.js: Appwrite-Nachbau mit Schema, Mails in der Senke, feste Zeit JETZT; Tage zählen nach Berliner Kalendertagen.
|
||||
// Datumsfelder wie in Appwrite: UTC mit "+00:00", Unlesbares oder '' wäre dort ein 400 (leeren geht nur mit null)
|
||||
const DATETIMES = {
|
||||
...ZEITEN_DATETIMES,
|
||||
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'k24At'],
|
||||
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'einschaltMailAt'],
|
||||
}
|
||||
const aw = await startAppwriteMock({
|
||||
schema: {
|
||||
@@ -122,7 +122,7 @@ const EIN_PLAN = () => ({
|
||||
})
|
||||
|
||||
/**
|
||||
* Funktion "Öffnungszeiten live" von kunde für Projekt nr, wie nach dem Einschalten im Wartungs-Abo: aktiv, Ticket geschlossen, K24 gesendet,
|
||||
* Funktion "Öffnungszeiten live" von kunde für Projekt nr, wie nach dem Einschalten im Wartungs-Abo: aktiv, Ticket geschlossen, K27 gesendet,
|
||||
* Zeiten gespeichert (daten, ersatzHash leer). Das Projekt "Praxis Muster" und das Einbau-Ticket legt die Hilfe mit an.
|
||||
*/
|
||||
async function legeFunktion(nr = 1, over = {}, { kunde = KUNDE, projekt = 'Praxis Muster', zeiten = true } = {}) {
|
||||
@@ -140,7 +140,7 @@ async function legeFunktion(nr = 1, over = {}, { kunde = KUNDE, projekt = 'Praxi
|
||||
siteKey: `abcdefghijklmno${'pqrstuv'[nr]}`,
|
||||
ticketId: `t_${nr}`,
|
||||
einbau: 'angestossen',
|
||||
k24At: ZUVOR,
|
||||
einschaltMailAt: ZUVOR,
|
||||
activatedAt: ZUVOR,
|
||||
...over,
|
||||
})
|
||||
@@ -167,9 +167,9 @@ async function laufe(now = JETZT) {
|
||||
const ERGEBNIS_LEER = { geprueft: 0, aboEnde: 0, abgeschaltet: 0, wiederEin: 0, geloescht: 0, mails: 0, fehler: 0 }
|
||||
const ergebnis = (over) => ({ ...ERGEBNIS_LEER, ...over })
|
||||
|
||||
const K26 = /^Dein Wartungs-Abo ist beendet/
|
||||
const K27 = 'Öffnungszeiten live ist abgeschaltet'
|
||||
const K24 = 'Öffnungszeiten live ist eingeschaltet'
|
||||
const K29 = /^Dein Wartungs-Abo ist beendet/
|
||||
const K30 = 'Öffnungszeiten live ist abgeschaltet'
|
||||
const K27 = 'Öffnungszeiten live ist eingeschaltet'
|
||||
const mails = () => sink.sent
|
||||
const mailsMit = (betreff) => mails().filter((m) => (betreff instanceof RegExp ? betreff.test(m.subject) : m.subject === betreff))
|
||||
const tickets = () => [...aw.docs('workorders').values()]
|
||||
@@ -203,7 +203,7 @@ async function mitEinwurf(passt, dazwischen, fn) {
|
||||
}
|
||||
const liste = (collection) => (url, methode) => methode === 'GET' && new RegExp(`/collections/${collection}/documents(\\?|$)`).test(url)
|
||||
|
||||
/** Die volle Folge bis zum Abschalten: Tag 0 Abo-Ende erkannt (K26), Tag 30 abgeschaltet (K27). */
|
||||
/** Die volle Folge bis zum Abschalten: Tag 0 Abo-Ende erkannt (K29), Tag 30 abgeschaltet (K30). */
|
||||
async function bisAbgeschaltet() {
|
||||
await legeFunktion(1)
|
||||
abgelaufen()
|
||||
@@ -212,9 +212,9 @@ async function bisAbgeschaltet() {
|
||||
assert.equal(funktion().state, 'disabled')
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Schritt 1: Abo-Ende erkennen, K26
|
||||
// ---------------------------------------------------------------- Schritt 1: Abo-Ende erkennen, K29
|
||||
|
||||
test('Abo endet: aboEndeAt gesetzt, K26 einmal', async () => {
|
||||
test('Abo endet: aboEndeAt gesetzt, K29 einmal', async () => {
|
||||
await legeFunktion(1)
|
||||
abgelaufen()
|
||||
|
||||
@@ -223,13 +223,13 @@ test('Abo endet: aboEndeAt gesetzt, K26 einmal', async () => {
|
||||
assert.equal(doc.state, 'active', 'die Funktion läuft bis zur Frist weiter')
|
||||
assert.equal(doc.enabled, true)
|
||||
assert.equal(ms(doc.aboEndeAt), JETZT.getTime(), 'aboEndeAt = Zeitpunkt der Erkennung')
|
||||
assert.equal(ms(doc.aboEndeMailAt), JETZT.getTime(), 'Marker der K26 erst nach dem Versand')
|
||||
assert.equal(ms(doc.aboEndeMailAt), JETZT.getTime(), 'Marker der K29 erst nach dem Versand')
|
||||
assert.equal(doc.unlockedBy, 'abo')
|
||||
|
||||
assert.equal(mails().length, 1)
|
||||
const mail = mails()[0]
|
||||
assert.deepEqual(mail.to.map((a) => a.address), ['silke@example.com'])
|
||||
assert.match(mail.subject, K26)
|
||||
assert.match(mail.subject, K29)
|
||||
assert.match(mail.text, /Silke Müller/)
|
||||
assert.match(mail.text, /Dein Wartungs-Abo ist beendet\./, 'Variante beendet')
|
||||
assert.match(mail.text, /Läuft weiter bis: 04\.11\.2026/, 'Frist = Tag der Erkennung + 30 Tage')
|
||||
@@ -255,7 +255,7 @@ test('Läuft das Abo, geschieht nichts: kein Schreiben, keine Mail', async () =>
|
||||
assert.equal(mails().length, 0)
|
||||
})
|
||||
|
||||
test('K26-Variante bei Wechsel zu Hosting und bei widerrufenem Wechsel', async () => {
|
||||
test('K29-Variante bei Wechsel zu Hosting und bei widerrufenem Wechsel', async () => {
|
||||
const varianten = [
|
||||
['Hosting-Abo', () => hosting('active'), /Dein Abo ist ein Hosting-Abo\./, /Du hast den Wechsel in das Wartungs-Abo widerrufen/],
|
||||
[
|
||||
@@ -274,14 +274,14 @@ test('K26-Variante bei Wechsel zu Hosting und bei widerrufenem Wechsel', async (
|
||||
await legeFunktion(1)
|
||||
abo()
|
||||
assert.deepEqual(await laufe(), ergebnis({ geprueft: 1, aboEnde: 1, mails: 1 }), name)
|
||||
assert.match(mails()[0].subject, K26, name)
|
||||
assert.match(mails()[0].subject, K29, name)
|
||||
assert.match(mails()[0].text, passt, name)
|
||||
assert.doesNotMatch(mails()[0].text, passtNicht, name)
|
||||
assert.equal(funktion().state, 'active', `${name}: die Frist läuft`)
|
||||
}
|
||||
})
|
||||
|
||||
test('K26 geht nur an die Adresse des Kundenkontos; ohne Adresse setzt der Lauf den Marker ohne Fehler', async () => {
|
||||
test('K29 geht nur an die Adresse des Kundenkontos; ohne Adresse setzt der Lauf den Marker ohne Fehler', async () => {
|
||||
await legeFunktion(1)
|
||||
aw.seed('customers', { $id: KUNDE, name: 'Silke Müller', email: '' })
|
||||
abgelaufen()
|
||||
@@ -291,7 +291,7 @@ test('K26 geht nur an die Adresse des Kundenkontos; ohne Adresse setzt der Lauf
|
||||
assert.deepEqual(await laufe(nach(1)), ergebnis({ geprueft: 1 }))
|
||||
})
|
||||
|
||||
test('K26 scheitert (Mail): aboEndeAt steht, der Marker fehlt, der nächste Lauf sendet nach (einmal)', async () => {
|
||||
test('K29 scheitert (Mail): aboEndeAt steht, der Marker fehlt, der nächste Lauf sendet nach (einmal)', async () => {
|
||||
await legeFunktion(1)
|
||||
abgelaufen()
|
||||
const roh = sink.sendMail
|
||||
@@ -305,16 +305,16 @@ test('K26 scheitert (Mail): aboEndeAt steht, der Marker fehlt, der nächste Lauf
|
||||
|
||||
sink.sendMail = roh
|
||||
assert.deepEqual(await laufe(nach(1)), ergebnis({ geprueft: 1, mails: 1 }))
|
||||
assert.equal(mailsMit(K26).length, 1)
|
||||
assert.equal(mailsMit(K29).length, 1)
|
||||
assert.equal(ms(funktion().aboEndeAt), JETZT.getTime(), 'aboEndeAt bleibt: die Frist wächst nicht')
|
||||
assert.match(mails()[0].text, /Läuft weiter bis: 04\.11\.2026/, 'dieselbe Frist wie beim ersten Versuch')
|
||||
assert.deepEqual(await laufe(nach(2)), ergebnis({ geprueft: 1 }))
|
||||
assert.equal(mails().length, 1)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Schritt 2: nach 30 Tagen abschalten, K27, Ersatz-Ticket
|
||||
// ---------------------------------------------------------------- Schritt 2: nach 30 Tagen abschalten, K30, Ersatz-Ticket
|
||||
|
||||
test('Tag 29: noch aktiv; Tag 30: disabled abo_ende, K27, Ersatz-Ticket', async () => {
|
||||
test('Tag 29: noch aktiv; Tag 30: disabled abo_ende, K30, Ersatz-Ticket', async () => {
|
||||
await legeFunktion(1)
|
||||
abgelaufen()
|
||||
await laufe(JETZT)
|
||||
@@ -334,12 +334,12 @@ test('Tag 29: noch aktiv; Tag 30: disabled abo_ende, K27, Ersatz-Ticket', async
|
||||
assert.equal(doc.disabledReason, 'abo_ende')
|
||||
assert.equal(ms(doc.disabledAt), tag30.getTime())
|
||||
assert.equal(ms(doc.aboEndeAt), JETZT.getTime(), 'aboEndeAt bleibt als Spur stehen')
|
||||
assert.equal(ms(doc.aboEndeMailAt), tag30.getTime(), 'Marker gilt jetzt der K27')
|
||||
assert.equal(ms(doc.aboEndeMailAt), tag30.getTime(), 'Marker gilt jetzt der K30')
|
||||
assert.match(doc.adminNote, /System: Abgeschaltet: Wartungs-Abo beendet/)
|
||||
|
||||
// K27: Datum der Löschung = Abschalten + 30 Tage
|
||||
assert.equal(mailsMit(K27).length, 1)
|
||||
const mail = mailsMit(K27)[0]
|
||||
// K30: Datum der Löschung = Abschalten + 30 Tage
|
||||
assert.equal(mailsMit(K30).length, 1)
|
||||
const mail = mailsMit(K30)[0]
|
||||
assert.deepEqual(mail.to.map((a) => a.address), ['silke@example.com'])
|
||||
assert.match(mail.text, /Eingaben gespeichert bis: 04\.12\.2026/)
|
||||
assert.match(mail.text, /30 €/)
|
||||
@@ -370,7 +370,7 @@ test('Frist nach Berliner Kalendertagen, nicht nach 24-Stunden-Schritten (Ende d
|
||||
// 23.11., 00:01 Berliner Zeit: der Tag der Frist ist da, auch vor 23:30 (24 x 30 Stunden später wäre erst 22:30 am 23.11.)
|
||||
assert.deepEqual(await laufe(new Date('2026-11-22T23:01:00.000Z')), ergebnis({ geprueft: 1, abgeschaltet: 1, mails: 1 }))
|
||||
assert.equal(funktion().state, 'disabled')
|
||||
assert.match(mailsMit(K27)[0].text, /Eingaben gespeichert bis: 23\.12\.2026/)
|
||||
assert.match(mailsMit(K30)[0].text, /Eingaben gespeichert bis: 23\.12\.2026/)
|
||||
})
|
||||
|
||||
test('Tag 30 mit zurückgekehrtem Abo: nicht abgeschaltet, sondern die Rückkehr', async () => {
|
||||
@@ -381,10 +381,10 @@ test('Tag 30 mit zurückgekehrtem Abo: nicht abgeschaltet, sondern die Rückkehr
|
||||
assert.deepEqual(await laufe(nach(30)), ergebnis({ geprueft: 1, wiederEin: 1, mails: 1 }))
|
||||
assert.equal(funktion().state, 'active')
|
||||
assert.equal(ersatzTickets().length, 0)
|
||||
assert.equal(mailsMit(K27).length, 0)
|
||||
assert.equal(mailsMit(K30).length, 0)
|
||||
})
|
||||
|
||||
test('K27 scheitert (Mail): abgeschaltet bleibt, der Marker fehlt, der nächste Lauf sendet nach; das Ersatz-Ticket entsteht nur einmal', async () => {
|
||||
test('K30 scheitert (Mail): abgeschaltet bleibt, der Marker fehlt, der nächste Lauf sendet nach; das Ersatz-Ticket entsteht nur einmal', async () => {
|
||||
await legeFunktion(1)
|
||||
abgelaufen()
|
||||
await laufe(JETZT)
|
||||
@@ -394,18 +394,18 @@ test('K27 scheitert (Mail): abgeschaltet bleibt, der Marker fehlt, der nächste
|
||||
}
|
||||
assert.deepEqual(await laufe(nach(30)), ergebnis({ geprueft: 1, abgeschaltet: 1, fehler: 1 }))
|
||||
assert.equal(funktion().state, 'disabled')
|
||||
assert.ok(!funktion().aboEndeMailAt || ms(funktion().aboEndeMailAt) < ms(funktion().disabledAt), 'der Marker der K26 gilt nicht für die K27')
|
||||
assert.ok(!funktion().aboEndeMailAt || ms(funktion().aboEndeMailAt) < ms(funktion().disabledAt), 'der Marker der K29 gilt nicht für die K30')
|
||||
assert.equal(ersatzTickets().length, 1)
|
||||
|
||||
sink.sendMail = roh
|
||||
assert.deepEqual(await laufe(nach(31)), ergebnis({ geprueft: 1, mails: 1 }))
|
||||
assert.equal(mailsMit(K27).length, 1)
|
||||
assert.match(mailsMit(K27)[0].text, /Eingaben gespeichert bis: 04\.12\.2026/, 'Löschdatum von der echten Abschaltung, nicht von heute')
|
||||
assert.equal(mailsMit(K30).length, 1)
|
||||
assert.match(mailsMit(K30)[0].text, /Eingaben gespeichert bis: 04\.12\.2026/, 'Löschdatum von der echten Abschaltung, nicht von heute')
|
||||
assert.equal(ersatzTickets().length, 1)
|
||||
assert.deepEqual(await laufe(nach(32)), ergebnis({ geprueft: 1 }))
|
||||
})
|
||||
|
||||
test('Ersatz-Ticket scheitert: abgeschaltet bleibt, fehler zählt, K27 und Marker fehlen; der nächste Lauf legt das Ticket an und sendet K27 genau einmal', async () => {
|
||||
test('Ersatz-Ticket scheitert: abgeschaltet bleibt, fehler zählt, K30 und Marker fehlen; der nächste Lauf legt das Ticket an und sendet K30 genau einmal', async () => {
|
||||
await legeFunktion(1)
|
||||
abgelaufen()
|
||||
await laufe(JETZT)
|
||||
@@ -414,21 +414,21 @@ test('Ersatz-Ticket scheitert: abgeschaltet bleibt, fehler zählt, K27 und Marke
|
||||
assert.deepEqual(await laufe(nach(30)), ergebnis({ geprueft: 1, abgeschaltet: 1, fehler: 1 }))
|
||||
assert.equal(funktion().state, 'disabled')
|
||||
assert.equal(ersatzTickets().length, 0)
|
||||
assert.equal(mailsMit(K27).length, 0, 'K27 erst nach dem Ersatz-Ticket: so geht sie nie ein zweites Mal raus')
|
||||
assert.ok(!funktion().aboEndeMailAt || ms(funktion().aboEndeMailAt) < ms(funktion().disabledAt), 'kein K27-Marker')
|
||||
assert.equal(mailsMit(K30).length, 0, 'K30 erst nach dem Ersatz-Ticket: so geht sie nie ein zweites Mal raus')
|
||||
assert.ok(!funktion().aboEndeMailAt || ms(funktion().aboEndeMailAt) < ms(funktion().disabledAt), 'kein K30-Marker')
|
||||
|
||||
// der nächste Lauf wiederholt beides (Ticket idempotent über den Hash) und sendet K27 einmal
|
||||
// der nächste Lauf wiederholt beides (Ticket idempotent über den Hash) und sendet K30 einmal
|
||||
assert.deepEqual(await laufe(nach(31)), ergebnis({ geprueft: 1, mails: 1 }))
|
||||
assert.equal(ersatzTickets().length, 1)
|
||||
assert.equal(mailsMit(K27).length, 1)
|
||||
assert.match(mailsMit(K27)[0].text, /Eingaben gespeichert bis: 04\.12\.2026/, 'Löschdatum von der echten Abschaltung')
|
||||
assert.ok(ms(funktion().aboEndeMailAt) >= ms(funktion().disabledAt), 'Marker gilt jetzt der K27')
|
||||
assert.equal(mailsMit(K30).length, 1)
|
||||
assert.match(mailsMit(K30)[0].text, /Eingaben gespeichert bis: 04\.12\.2026/, 'Löschdatum von der echten Abschaltung')
|
||||
assert.ok(ms(funktion().aboEndeMailAt) >= ms(funktion().disabledAt), 'Marker gilt jetzt der K30')
|
||||
assert.deepEqual(await laufe(nach(32)), ergebnis({ geprueft: 1 }))
|
||||
assert.equal(ersatzTickets().length, 1)
|
||||
assert.equal(mailsMit(K27).length, 1)
|
||||
assert.equal(mailsMit(K30).length, 1)
|
||||
})
|
||||
|
||||
test('Ersatz-Ticket scheitert mehrere Läufe: kein K27, jeder Lauf zählt fehler; sobald es klappt, genau eine K27', async () => {
|
||||
test('Ersatz-Ticket scheitert mehrere Läufe: kein K30, jeder Lauf zählt fehler; sobald es klappt, genau eine K30', async () => {
|
||||
await legeFunktion(1)
|
||||
abgelaufen()
|
||||
await laufe(JETZT)
|
||||
@@ -437,18 +437,18 @@ test('Ersatz-Ticket scheitert mehrere Läufe: kein K27, jeder Lauf zählt fehler
|
||||
await laufe(nach(30))
|
||||
aw.failNext({ collection: 'workorders', method: 'POST', status: 500, message: 'Ticket kaputt' })
|
||||
assert.deepEqual(await laufe(nach(31)), ergebnis({ geprueft: 1, fehler: 1 }))
|
||||
assert.equal(mailsMit(K27).length, 0)
|
||||
assert.equal(mailsMit(K30).length, 0)
|
||||
assert.deepEqual(await laufe(nach(32)), ergebnis({ geprueft: 1, mails: 1 }))
|
||||
assert.equal(mailsMit(K27).length, 1)
|
||||
assert.equal(mailsMit(K30).length, 1)
|
||||
assert.equal(ersatzTickets().length, 1)
|
||||
})
|
||||
|
||||
test('Abschalten gelingt, die Folge scheitert (Marker der K26 steht noch, älter als disabledAt): der nächste Lauf holt K27 und das Ersatz-Ticket nach, einmal', async () => {
|
||||
// So steht das Dokument, wenn nach disable() nichts mehr geschrieben wurde: disabled/abo_ende, der Marker ist der der K26
|
||||
test('Abschalten gelingt, die Folge scheitert (Marker der K29 steht noch, älter als disabledAt): der nächste Lauf holt K30 und das Ersatz-Ticket nach, einmal', async () => {
|
||||
// So steht das Dokument, wenn nach disable() nichts mehr geschrieben wurde: disabled/abo_ende, der Marker ist der der K29
|
||||
await legeFunktion(1, { state: 'disabled', enabled: false, disabledReason: 'abo_ende', disabledAt: JETZT.toISOString(), aboEndeAt: ZUVOR, aboEndeMailAt: ZUVOR })
|
||||
abgelaufen()
|
||||
assert.deepEqual(await laufe(nach(1)), ergebnis({ geprueft: 1, mails: 1 }))
|
||||
assert.equal(mailsMit(K27).length, 1)
|
||||
assert.equal(mailsMit(K30).length, 1)
|
||||
assert.equal(ersatzTickets().length, 1)
|
||||
assert.ok(ms(funktion().aboEndeMailAt) >= ms(funktion().disabledAt))
|
||||
assert.deepEqual(await laufe(nach(2)), ergebnis({ geprueft: 1 }))
|
||||
@@ -456,7 +456,7 @@ test('Abschalten gelingt, die Folge scheitert (Marker der K26 steht noch, älter
|
||||
assert.equal(ersatzTickets().length, 1)
|
||||
})
|
||||
|
||||
test('Wanduhr nach dem Laufstart (disabledAt später als now): der K27-Marker liegt nie davor, K27 geht nicht täglich raus', async () => {
|
||||
test('Wanduhr nach dem Laufstart (disabledAt später als now): der K30-Marker liegt nie davor, K30 geht nicht täglich raus', async () => {
|
||||
await legeFunktion(1)
|
||||
abgelaufen()
|
||||
await laufe(JETZT)
|
||||
@@ -470,12 +470,12 @@ test('Wanduhr nach dem Laufstart (disabledAt später als now): der K27-Marker li
|
||||
}
|
||||
assert.ok(ms(funktion().aboEndeMailAt) >= ms(funktion().disabledAt), 'Marker nicht vor disabledAt')
|
||||
assert.deepEqual(await laufe(nach(31)), ergebnis({ geprueft: 1 }))
|
||||
assert.equal(mailsMit(K27).length, 1)
|
||||
assert.equal(mailsMit(K30).length, 1)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Schritt 3: Rückkehr des Abos
|
||||
|
||||
test('Rückkehr an Tag 45: zurück auf active, Neubau, K24 wieder', async () => {
|
||||
test('Rückkehr an Tag 45: zurück auf active, Neubau, K27 wieder', async () => {
|
||||
await bisAbgeschaltet()
|
||||
mails().length = 0
|
||||
laeuft()
|
||||
@@ -492,9 +492,9 @@ test('Rückkehr an Tag 45: zurück auf active, Neubau, K24 wieder', async () =>
|
||||
|
||||
// Neubau nach dem Schreiben des Zustands und ausserhalb der Sperre
|
||||
assert.deepEqual(rebuilds, [{ projektId: 'p1', state: 'active', adminFrei: true }])
|
||||
// K24 mit dem Block wieder und dem Neubau-Text
|
||||
assert.equal(mailsMit(K24).length, 1)
|
||||
const mail = mailsMit(K24)[0]
|
||||
// K27 mit dem Block wieder und dem Neubau-Text
|
||||
assert.equal(mailsMit(K27).length, 1)
|
||||
const mail = mailsMit(K27)[0]
|
||||
assert.deepEqual(mail.to.map((a) => a.address), ['silke@example.com'])
|
||||
assert.match(mail.text, /Dein Wartungs-Abo läuft wieder/)
|
||||
assert.match(mail.text, /nach dem nächsten Veröffentlichen/)
|
||||
@@ -505,7 +505,7 @@ test('Rückkehr an Tag 45: zurück auf active, Neubau, K24 wieder', async () =>
|
||||
assert.equal(rebuilds.length, 1)
|
||||
})
|
||||
|
||||
test('Rückkehr aus disabled: ein nie freigeschaltetes Dokument (kein activatedAt) kommt auf setup zurück; einbau manuell sagt K24 "das Team baut ein"', async () => {
|
||||
test('Rückkehr aus disabled: ein nie freigeschaltetes Dokument (kein activatedAt) kommt auf setup zurück; einbau manuell sagt K27 "das Team baut ein"', async () => {
|
||||
await legeFunktion(1, { state: 'setup', enabled: false, activatedAt: null })
|
||||
abgelaufen()
|
||||
await laufe(JETZT)
|
||||
@@ -524,7 +524,7 @@ test('Rückkehr aus disabled: ein nie freigeschaltetes Dokument (kein activatedA
|
||||
assert.match(mails()[0].text, /baut unser Team/)
|
||||
})
|
||||
|
||||
test('Rückkehr vor dem Abschalten (Tag 10): aboEndeAt leer, Zustand bleibt, kein Neubau, K24 wieder ohne "Wie geht es weiter"', async () => {
|
||||
test('Rückkehr vor dem Abschalten (Tag 10): aboEndeAt leer, Zustand bleibt, kein Neubau, K27 wieder ohne "Wie geht es weiter"', async () => {
|
||||
for (const [name, over] of [
|
||||
['aktiv', {}],
|
||||
['in Einrichtung', { state: 'setup', enabled: false, activatedAt: null }],
|
||||
@@ -548,15 +548,15 @@ test('Rückkehr vor dem Abschalten (Tag 10): aboEndeAt leer, Zustand bleibt, kei
|
||||
assert.equal(doc.einbau, 'angestossen', name)
|
||||
assert.equal(rebuilds.length, 0, `${name}: die Leisten waren nie weg, kein Neubau`)
|
||||
|
||||
assert.equal(mailsMit(K24).length, 1, name)
|
||||
const text = mailsMit(K24)[0].text
|
||||
assert.equal(mailsMit(K27).length, 1, name)
|
||||
const text = mailsMit(K27)[0].text
|
||||
assert.match(text, /Dein Wartungs-Abo läuft wieder/, name)
|
||||
assert.doesNotMatch(text, /Wie geht es weiter/, `${name}: beide Blöcke aus, der Kasten entfällt`)
|
||||
assert.doesNotMatch(text, /nach dem nächsten Veröffentlichen|baut unser Team|Ohne gespeicherte Zeiten/, name)
|
||||
}
|
||||
})
|
||||
|
||||
test('Rückkehr ohne vorherige K26 (kein Marker): die Funktion wird bereinigt, der Kunde bekommt keine Mail über etwas, das er nie erfuhr', async () => {
|
||||
test('Rückkehr ohne vorherige K29 (kein Marker): die Funktion wird bereinigt, der Kunde bekommt keine Mail über etwas, das er nie erfuhr', async () => {
|
||||
await legeFunktion(1, { aboEndeAt: JETZT.toISOString(), behaltenSessionId: 'cs_test_offen' })
|
||||
laeuft()
|
||||
assert.deepEqual(await laufe(nach(3)), ergebnis({ geprueft: 1, wiederEin: 1 }))
|
||||
@@ -572,8 +572,8 @@ test('Rückkehr: das Abo kommt zwischen Vorauswahl und Sperre zurück ⇒ der fr
|
||||
const erg = await mitEinwurf(liste('subscriptions'), async () => laeuft(), () => laufe(nach(30)))
|
||||
assert.deepEqual(erg, ergebnis({ geprueft: 1, wiederEin: 1, mails: 1 }))
|
||||
assert.equal(funktion().state, 'active')
|
||||
assert.equal(mailsMit(K27).length, 0)
|
||||
assert.equal(mailsMit(K24).length, 1)
|
||||
assert.equal(mailsMit(K30).length, 0)
|
||||
assert.equal(mailsMit(K27).length, 1)
|
||||
assert.equal(ersatzTickets().length, 0)
|
||||
})
|
||||
|
||||
@@ -592,7 +592,7 @@ test('Löschung fällig, aber die Daten sind schon weg (Lauf nach dem Löschen u
|
||||
assert.deepEqual(await laufe(nach(60)), ergebnis({ geprueft: 1, geloescht: 1 }))
|
||||
assert.equal(funktion().state, 'off')
|
||||
assert.equal(rebuilds.length, 0)
|
||||
assert.equal(mailsMit(K24).length, 0)
|
||||
assert.equal(mailsMit(K27).length, 0)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------- Schritt 4: Löschung
|
||||
@@ -615,22 +615,22 @@ test('Tag 60 nach dem Abo-Ende (30 Tage nach der Abschaltung): Daten gelöscht,
|
||||
assert.equal(doc.state, 'off')
|
||||
assert.equal(doc.enabled, false)
|
||||
assert.deepEqual(
|
||||
[doc.ticketId ?? '', doc.einbau ?? '', doc.k24At ?? null, doc.aboEndeAt ?? null, doc.behaltenSessionId ?? '', doc.activatedAt ?? null],
|
||||
[doc.ticketId ?? '', doc.einbau ?? '', doc.einschaltMailAt ?? null, doc.aboEndeAt ?? null, doc.behaltenSessionId ?? '', doc.activatedAt ?? null],
|
||||
['', '', null, null, '', null],
|
||||
'ticketId, einbau, k24At, aboEndeAt, behaltenSessionId, activatedAt geleert (sonst käme ein späterer Lauf zu früh auf active)'
|
||||
'ticketId, einbau, einschaltMailAt, aboEndeAt, behaltenSessionId, activatedAt geleert (sonst käme ein späterer Lauf zu früh auf active)'
|
||||
)
|
||||
assert.match(doc.adminNote, /Öffnungszeiten gelöscht, 30 Tage nach dem Abschalten/)
|
||||
assert.equal(await ladeZeiten(feature.$id), null, 'das Zeiten-Dokument (samt ersatzHash) ist gelöscht')
|
||||
assert.equal(aw.docs('oeffnungszeiten').size, 0)
|
||||
assert.equal(aw.docs('zeitenErinnerungen').has('m1'), false, 'Erinnerungsmerker der Funktion gelöscht')
|
||||
assert.equal(aw.docs('zeitenErinnerungen').has('m2'), true, 'fremde Merker bleiben')
|
||||
assert.equal(mails().length, 0, 'die K27 hat die Löschung angekündigt, es gibt keine weitere Mail')
|
||||
assert.equal(mails().length, 0, 'die K30 hat die Löschung angekündigt, es gibt keine weitere Mail')
|
||||
|
||||
// aus ist aus: der nächste Lauf liest das Dokument nicht mehr
|
||||
assert.deepEqual(await laufe(nach(61)), ergebnis())
|
||||
})
|
||||
|
||||
test('nach der Löschung: ticketId, einbau, k24At, ersatzHash leer; Neubestellung legt ein neues Ticket an', async () => {
|
||||
test('nach der Löschung: ticketId, einbau, einschaltMailAt, ersatzHash leer; Neubestellung legt ein neues Ticket an', async () => {
|
||||
await bisAbgeschaltet()
|
||||
await laufe(nach(60))
|
||||
assert.equal(funktion().state, 'off')
|
||||
@@ -639,7 +639,7 @@ test('nach der Löschung: ticketId, einbau, k24At, ersatzHash leer; Neubestellun
|
||||
mails().length = 0
|
||||
rebuilds.length = 0
|
||||
|
||||
// Das Abo läuft wieder, der Kunde schaltet neu ein (Gratisweg): neues Ticket, Neubau, K24
|
||||
// Das Abo läuft wieder, der Kunde schaltet neu ein (Gratisweg): neues Ticket, Neubau, K27
|
||||
laeuft()
|
||||
const { doc, changed } = await einschaltenZeiten({ customerId: KUNDE, projectId: 'p1', by: 'kunde', now: nach(61) })
|
||||
assert.equal(changed, true)
|
||||
@@ -647,8 +647,8 @@ test('nach der Löschung: ticketId, einbau, k24At, ersatzHash leer; Neubestellun
|
||||
assert.ok(doc.ticketId && doc.ticketId !== 't_1', 'ein neues Einbau-Ticket')
|
||||
assert.equal(tickets().length, ticketsVorher + 1)
|
||||
assert.equal(doc.einbau, 'angestossen')
|
||||
assert.ok(doc.k24At, 'K24 neu gesendet')
|
||||
assert.equal(mailsMit(K24).length, 1)
|
||||
assert.ok(doc.einschaltMailAt, 'K27 neu gesendet')
|
||||
assert.equal(mailsMit(K27).length, 1)
|
||||
assert.equal(rebuilds.length, 1)
|
||||
})
|
||||
|
||||
@@ -790,14 +790,14 @@ test('ein unlesbares Datum bei einem Dokument stoppt die anderen nicht (je Dokum
|
||||
const vorher = [stand(1), stand(2)]
|
||||
|
||||
const erg = await laufe(nach(30))
|
||||
assert.deepEqual(erg, ergebnis({ geprueft: 3, aboEnde: 1, mails: 1, fehler: 2 }), 'das dritte Dokument ist trotzdem dran: Abo-Ende erkannt, K26')
|
||||
assert.deepEqual(erg, ergebnis({ geprueft: 3, aboEnde: 1, mails: 1, fehler: 2 }), 'das dritte Dokument ist trotzdem dran: Abo-Ende erkannt, K29')
|
||||
assert.deepEqual([stand(1), stand(2)], vorher, 'die kaputten Dokumente bleiben, wie sie sind')
|
||||
assert.deepEqual(mails().map((m) => m.to[0].address), ['eva@example.com'])
|
||||
assert.equal(ersatzTickets().length, 0)
|
||||
assert.equal(aw.docs('oeffnungszeiten').size, 3, 'nichts gelöscht')
|
||||
})
|
||||
|
||||
test('Kunde nicht lesbar beim Versand: fehler, das nächste Dokument ist trotzdem dran, der nächste Lauf sendet die K26 nach', async () => {
|
||||
test('Kunde nicht lesbar beim Versand: fehler, das nächste Dokument ist trotzdem dran, der nächste Lauf sendet die K29 nach', async () => {
|
||||
await legeFunktion(1)
|
||||
await legeFunktion(2, {}, { kunde: 'c2' })
|
||||
aw.seed('customers', { $id: 'c2', name: 'Karl Weber', email: 'karl@example.com' })
|
||||
@@ -817,13 +817,13 @@ test('Kunde nicht lesbar beim Versand: fehler, das nächste Dokument ist trotzde
|
||||
test('Mails und Neubau gehen ohne gehaltene Sperre feature-admin raus', async () => {
|
||||
await legeFunktion(1)
|
||||
abgelaufen()
|
||||
await laufe(JETZT) // K26
|
||||
await laufe(nach(30)) // K27
|
||||
await laufe(JETZT) // K29
|
||||
await laufe(nach(30)) // K30
|
||||
laeuft()
|
||||
await laufe(nach(45)) // Neubau und K24
|
||||
await laufe(nach(45)) // Neubau und K27
|
||||
assert.deepEqual(
|
||||
sperrenBeimVersand.map((s) => [s.subject === K27 ? 'K27' : s.subject === K24 ? 'K24' : 'K26', s.admin]),
|
||||
[['K26', true], ['K27', true], ['K24', true]]
|
||||
sperrenBeimVersand.map((s) => [s.subject === K30 ? 'K30' : s.subject === K27 ? 'K27' : 'K29', s.admin]),
|
||||
[['K29', true], ['K30', true], ['K27', true]]
|
||||
)
|
||||
assert.equal(rebuilds[0].adminFrei, true)
|
||||
})
|
||||
@@ -840,7 +840,7 @@ test('Abo-Abfragen: die Vorauswahl liest je Kunde einmal, dazu das frische Lesen
|
||||
aw.resetListCalls()
|
||||
assert.deepEqual(await laufe(), ergebnis({ geprueft: 3, aboEnde: 2, mails: 2 }))
|
||||
assert.equal(abfragenAufAbos(), 2 + 2, 'zwei Kunden in der Vorauswahl; frisch nur für die zwei Dokumente von c1 (das von c2 braucht nichts)')
|
||||
assert.equal(mailsMit(K26).length, 2)
|
||||
assert.equal(mailsMit(K29).length, 2)
|
||||
assert.deepEqual(mails().map((m) => m.to[0].address), ['silke@example.com', 'silke@example.com'])
|
||||
assert.ok(funktion(1).aboEndeAt && funktion(2).aboEndeAt)
|
||||
assert.ok(!funktion(3).aboEndeAt)
|
||||
@@ -887,12 +887,12 @@ test('activate() ohne extra lässt aboEndeAt und behaltenSessionId stehen; admin
|
||||
assert.deepEqual([team.aboEndeAt ?? null, team.behaltenSessionId ?? ''], [null, ''])
|
||||
})
|
||||
|
||||
test('abgeschaltet mit abo_ende, aber ohne Marker (Lauf nach dem Abschalten unterbrochen): der nächste Lauf holt K27 und das Ersatz-Ticket nach', async () => {
|
||||
test('abgeschaltet mit abo_ende, aber ohne Marker (Lauf nach dem Abschalten unterbrochen): der nächste Lauf holt K30 und das Ersatz-Ticket nach', async () => {
|
||||
await legeFunktion(1, { state: 'disabled', enabled: false, disabledReason: 'abo_ende', disabledAt: JETZT.toISOString(), aboEndeAt: ZUVOR })
|
||||
abgelaufen()
|
||||
assert.deepEqual(await laufe(nach(1)), ergebnis({ geprueft: 1, mails: 1 }))
|
||||
assert.equal(mailsMit(K27).length, 1)
|
||||
assert.match(mailsMit(K27)[0].text, /Eingaben gespeichert bis: 04\.11\.2026/, 'Löschdatum = disabledAt + 30 Tage')
|
||||
assert.equal(mailsMit(K30).length, 1)
|
||||
assert.match(mailsMit(K30)[0].text, /Eingaben gespeichert bis: 04\.11\.2026/, 'Löschdatum = disabledAt + 30 Tage')
|
||||
assert.equal(ersatzTickets().length, 1)
|
||||
assert.deepEqual(await laufe(nach(2)), ergebnis({ geprueft: 1 }))
|
||||
assert.equal(mails().length, 1)
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useTestEnv } from './support/testEnv.js'
|
||||
import { ZEITEN_FEATURE_ATTRIBUTES } from '../server/services/zeiten/schema.js'
|
||||
|
||||
// activate() ist der gemeinsame Schreibpfad aller Funktionen (News, Terminplaner, Zeiten). Die neuen Zeiten-Felder von portalFeatures
|
||||
// (unlockedBy, aboEndeAt, einbau, behaltenSessionId, k24At) legt erst scripts/zeiten-setup.mjs an: laeuft der Code vor diesem Skript,
|
||||
// (unlockedBy, aboEndeAt, einbau, behaltenSessionId, einschaltMailAt) legt erst scripts/zeiten-setup.mjs an: laeuft der Code vor diesem Skript,
|
||||
// darf die Aktivierung von News und Terminplaner nie ein Zeiten-Feld schreiben (Appwrite: "Unknown attribute"). Der Nachbau hier hat
|
||||
// darum genau das Schema OHNE die Zeiten-Felder; nur der Zeiten-Pfad (extra) schreibt sie.
|
||||
const ZEITEN_FELDER = ZEITEN_FEATURE_ATTRIBUTES.map((a) => a.key)
|
||||
|
||||
@@ -22,7 +22,7 @@ const aw = await startAppwriteMock({
|
||||
// Datumsfelder wie in Appwrite: Unlesbares oder '' waere dort ein 400 (leeren geht nur mit null)
|
||||
datetimes: {
|
||||
...ZEITEN_DATETIMES,
|
||||
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'k24At'],
|
||||
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'einschaltMailAt'],
|
||||
featureConsents: ['acceptedAt'],
|
||||
},
|
||||
defaultLimit: 25,
|
||||
|
||||
@@ -22,7 +22,7 @@ const aw = await startAppwriteMock({
|
||||
// Datumsfelder wie in Appwrite: Unlesbares oder '' waere dort ein 400 (leeren geht nur mit null)
|
||||
datetimes: {
|
||||
...ZEITEN_DATETIMES,
|
||||
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'k24At'],
|
||||
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'einschaltMailAt'],
|
||||
featureConsents: ['acceptedAt'],
|
||||
},
|
||||
defaultLimit: 25,
|
||||
@@ -286,13 +286,13 @@ test('Zahlung ⇒ setup, unlockedBy kauf, Neubau vor dem Ticket, einbau am Dokum
|
||||
assert.doesNotMatch(ticket.details, /bitte von Hand einsetzen/)
|
||||
assert.ok(ticket.details.includes(`data-site="${doc.siteKey}"`))
|
||||
|
||||
// K15 an den Kunden, T16 ans Team, je einmal; keine K24 (die gehört zum Gratisweg)
|
||||
// K15 an den Kunden, T16 ans Team, je einmal; keine K27 (die gehört zum Gratisweg)
|
||||
assert.equal(mailsMit(SUBJECT.K15).length, 1)
|
||||
assert.deepEqual(mailsMit(SUBJECT.K15)[0].to.map((a) => a.address), ['k@example.invalid'])
|
||||
assert.equal(mailsMit(SUBJECT.T16).length, 1)
|
||||
assert.doesNotMatch(mailsMit(SUBJECT.T16)[0].text, BEREITS_DA)
|
||||
assert.equal(mailsMit('Öffnungszeiten live ist eingeschaltet').length, 0)
|
||||
assert.equal(doc.k24At ?? null, null)
|
||||
assert.equal(doc.einschaltMailAt ?? null, null)
|
||||
})
|
||||
|
||||
test('Zahlung, Neubau nicht möglich (kein Repo): einbau manuell, Ticket sagt „bitte von Hand einsetzen“', async () => {
|
||||
|
||||
@@ -21,7 +21,7 @@ const aw = await startAppwriteMock({
|
||||
// Datumsfelder wie in Appwrite: Unlesbares oder '' waere dort ein 400 (leeren geht nur mit null)
|
||||
datetimes: {
|
||||
...ZEITEN_DATETIMES,
|
||||
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'k24At'],
|
||||
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'einschaltMailAt'],
|
||||
featureConsents: ['acceptedAt'],
|
||||
},
|
||||
defaultLimit: 25,
|
||||
@@ -127,7 +127,7 @@ test('Einschalten mit laufendem Abo: setup, unlockedBy abo, Nachweis ohne Sitzun
|
||||
assert.match(doc.siteKey, /^[a-z2-7]{16}$/)
|
||||
assert.equal(doc.einbau, 'angestossen')
|
||||
assert.ok(doc.ticketId)
|
||||
assert.ok(doc.k24At)
|
||||
assert.ok(doc.einschaltMailAt)
|
||||
assert.equal(featureDocs().length, 1, 'nur das Dokument der Zeiten')
|
||||
|
||||
// Nachweis: Fassung und Texte der Gratis-Zustimmung, ohne Stripe-Sitzung, mit dem handelnden Konto
|
||||
@@ -142,12 +142,12 @@ test('Einschalten mit laufendem Abo: setup, unlockedBy abo, Nachweis ohne Sitzun
|
||||
])
|
||||
})
|
||||
|
||||
test('Reihenfolge: Neubau vor Ticket, Ticket-Text nennt einbau, dann K24, dann k24At', async () => {
|
||||
test('Reihenfolge: Neubau vor Ticket, Ticket-Text nennt einbau, dann K27, dann einschaltMailAt', async () => {
|
||||
const beimVersand = []
|
||||
setMailTransportForTests({
|
||||
sent: sink.sent,
|
||||
async sendMail(mail) {
|
||||
beimVersand.push({ k24At: zeitenDoc()?.k24At ?? null, tickets: aw.docs('workorders').size })
|
||||
beimVersand.push({ einschaltMailAt: zeitenDoc()?.einschaltMailAt ?? null, tickets: aw.docs('workorders').size })
|
||||
return sink.sendMail(mail)
|
||||
},
|
||||
})
|
||||
@@ -163,19 +163,19 @@ test('Reihenfolge: Neubau vor Ticket, Ticket-Text nennt einbau, dann K24, dann k
|
||||
assert.match(ticket.details, /Einbau: automatisch eingesetzt \(nach dem nächsten Veröffentlichen prüfen\)/)
|
||||
assert.ok(ticket.details.includes(`data-site="${zeitenDoc().siteKey}"`))
|
||||
assert.equal(zeitenDoc().ticketId, ticket.$id)
|
||||
// K24: genau eine Mail, nach dem Ticket, k24At erst danach
|
||||
// K27: genau eine Mail, nach dem Ticket, einschaltMailAt erst danach
|
||||
assert.equal(sink.sent.length, 1)
|
||||
assert.deepEqual(beimVersand, [{ k24At: null, tickets: 1 }])
|
||||
assert.deepEqual(beimVersand, [{ einschaltMailAt: null, tickets: 1 }])
|
||||
const mail = sink.sent[0]
|
||||
assert.equal(mail.subject, 'Öffnungszeiten live ist eingeschaltet')
|
||||
assert.deepEqual(mail.to.map((a) => a.address), ['k@example.invalid'])
|
||||
assert.match(mail.text, /erscheinen nach dem nächsten Veröffentlichen/)
|
||||
assert.ok(mail.text.includes(`(Ticket #${ticket.woid})`))
|
||||
assert.doesNotMatch(mail.text, /nicht automatisch aktualisieren/)
|
||||
assert.ok(Date.parse(zeitenDoc().k24At) > 0)
|
||||
assert.ok(Date.parse(zeitenDoc().einschaltMailAt) > 0)
|
||||
})
|
||||
|
||||
test('Neubau nicht möglich: einbau manuell, Ticket und K24 sagen „Team baut ein“', async () => {
|
||||
test('Neubau nicht möglich: einbau manuell, Ticket und K27 sagen „Team baut ein“', async () => {
|
||||
aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Ohne Repo' }) // kein Repo ⇒ manuell
|
||||
const res = await einschalten(koerper())
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
@@ -333,7 +333,7 @@ test('Nachweis nicht speicherbar ⇒ 500 ohne Rohtext, Zustand bleibt off, kein
|
||||
|
||||
// ------------------------------------------------------------------------------------------- Zustaende
|
||||
|
||||
test('zweiter Klick ist idempotent und holt Fehlendes nach: Ticket, dann genau ein K24', async () => {
|
||||
test('zweiter Klick ist idempotent und holt Fehlendes nach: Ticket, dann genau ein K27', async () => {
|
||||
aw.failNext({ collection: 'workorders', method: 'POST', status: 500, message: 'Ticketsystem down' })
|
||||
const erster = await einschalten(koerper())
|
||||
// Zustand ist gespeichert, das Ticket fehlt: der Kunde bekommt einen Hinweis zum erneuten Versuch, nicht den Rohtext
|
||||
@@ -342,7 +342,7 @@ test('zweiter Klick ist idempotent und holt Fehlendes nach: Ticket, dann genau e
|
||||
assert.equal(zeitenDoc().state, 'setup')
|
||||
assert.equal(zeitenDoc().unlockedBy, 'abo')
|
||||
assert.ok(!zeitenDoc().ticketId)
|
||||
assert.ok(!zeitenDoc().k24At)
|
||||
assert.ok(!zeitenDoc().einschaltMailAt)
|
||||
assert.equal(tickets().length, 0)
|
||||
assert.equal(sink.sent.length, 0)
|
||||
assert.equal(nachweise().length, 1)
|
||||
@@ -353,7 +353,7 @@ test('zweiter Klick ist idempotent und holt Fehlendes nach: Ticket, dann genau e
|
||||
assert.equal(tickets().length, 1)
|
||||
assert.equal(zeitenDoc().ticketId, tickets()[0].$id)
|
||||
assert.equal(sink.sent.length, 1)
|
||||
assert.ok(zeitenDoc().k24At)
|
||||
assert.ok(zeitenDoc().einschaltMailAt)
|
||||
assert.equal(nachweise().length, 1, 'kein zweiter Nachweis')
|
||||
assert.equal(rebuilds.length, 1, 'einbau steht schon, kein zweiter Neubau')
|
||||
assert.equal(zweiter.body.einbau, 'angestossen')
|
||||
@@ -364,21 +364,21 @@ test('zweiter Klick ist idempotent und holt Fehlendes nach: Ticket, dann genau e
|
||||
assert.deepEqual([tickets().length, sink.sent.length, nachweise().length, rebuilds.length], [1, 1, 1, 1])
|
||||
})
|
||||
|
||||
test('K24 scheitert: Zustand und Ticket bleiben, k24At leer, der nächste Klick sendet nach (ein Ticket, eine Mail)', async () => {
|
||||
test('K27 scheitert: Zustand und Ticket bleiben, einschaltMailAt leer, der nächste Klick sendet nach (ein Ticket, eine Mail)', async () => {
|
||||
setMailTransportForTests({ sent: [], async sendMail() { throw new Error('SMTP down') } })
|
||||
const erster = await einschalten(koerper())
|
||||
assert.equal(erster.status, 200, 'Mail-Fehler kosten die Antwort nichts')
|
||||
assert.equal(tickets().length, 1)
|
||||
assert.ok(!zeitenDoc().k24At)
|
||||
assert.ok(!zeitenDoc().einschaltMailAt)
|
||||
setMailTransportForTests(sink)
|
||||
assert.equal((await einschalten(koerper())).status, 200)
|
||||
assert.equal(tickets().length, 1)
|
||||
assert.equal(sink.sent.length, 1)
|
||||
assert.ok(zeitenDoc().k24At)
|
||||
assert.ok(zeitenDoc().einschaltMailAt)
|
||||
assert.match(sink.sent[0].text, new RegExp(`Ticket #${tickets()[0].woid}`))
|
||||
})
|
||||
|
||||
test('zwei gleichzeitige Klicks: ein Nachweis, ein Neubau, ein Ticket, eine K24', async () => {
|
||||
test('zwei gleichzeitige Klicks: ein Nachweis, ein Neubau, ein Ticket, eine K27', async () => {
|
||||
const [a, b] = await Promise.all([einschalten(koerper()), einschalten(koerper())])
|
||||
assert.deepEqual([a.status, b.status], [200, 200], JSON.stringify([a.body, b.body]))
|
||||
assert.deepEqual([nachweise().length, rebuilds.length, tickets().length, sink.sent.length], [1, 1, 1, 1])
|
||||
@@ -438,7 +438,7 @@ test('Dokument im Zustand off mit Resten wird sauber auf setup gestellt', async
|
||||
|
||||
// --------------------------------------------------------------------------------------------- Team und Nachholen
|
||||
|
||||
test('Team: kein Abo nötig, unlockedBy team, kein Nachweis, Neubau, Ticket, keine K24, Notiz', async () => {
|
||||
test('Team: kein Abo nötig, unlockedBy team, kein Nachweis, Neubau, Ticket, keine K27, Notiz', async () => {
|
||||
aw.seed('subscriptions', hostingDoc)
|
||||
const { doc, changed } = await einschaltenZeiten({ customerId: 'c1', projectId: 'p1', by: 'team', staff: { name: 'Andrej' }, now: JETZT })
|
||||
assert.equal(changed, true)
|
||||
@@ -447,7 +447,7 @@ test('Team: kein Abo nötig, unlockedBy team, kein Nachweis, Neubau, Ticket, kei
|
||||
assert.equal(nachweise().length, 0)
|
||||
assert.equal(tickets().length, 1)
|
||||
assert.equal(sink.sent.length, 0)
|
||||
assert.ok(!zeitenDoc().k24At)
|
||||
assert.ok(!zeitenDoc().einschaltMailAt)
|
||||
assert.equal(rebuilds.length, 1)
|
||||
// ein zweiter Aufruf aendert nichts
|
||||
const nochmal = await einschaltenZeiten({ customerId: 'c1', projectId: 'p1', by: 'team', staff: { name: 'Andrej' }, now: JETZT })
|
||||
@@ -488,12 +488,12 @@ test('Team aus disabled bei einem gekauften Dokument: kauf und unlockedByPurchas
|
||||
assert.deepEqual([team.doc.unlockedBy, team.doc.unlockedByPurchase], ['team', false])
|
||||
})
|
||||
|
||||
test('holeZeitenNach: Ticket fehlt in setup ⇒ anlegen; K24 nur bei unlockedBy abo und leerem k24At, danach Marker', async () => {
|
||||
test('holeZeitenNach: Ticket fehlt in setup ⇒ anlegen; K27 nur bei unlockedBy abo und leerem einschaltMailAt, danach Marker', async () => {
|
||||
aw.seed('portalFeatures', { $id: KEY_P1, customerId: 'c1', projectId: 'p1', featureKey: 'zeiten', state: 'setup', enabled: false, siteKey: 'abcdefghijklmnop', unlockedBy: 'abo', einbau: 'manuell' })
|
||||
const doc = await holeZeitenNach(zeitenDoc(), { now: JETZT })
|
||||
assert.equal(tickets().length, 1)
|
||||
assert.equal(doc.ticketId, tickets()[0].$id)
|
||||
assert.equal(Date.parse(doc.k24At), JETZT.getTime())
|
||||
assert.equal(Date.parse(doc.einschaltMailAt), JETZT.getTime())
|
||||
assert.equal(sink.sent.length, 1)
|
||||
assert.deepEqual(rebuilds, [], 'einbau steht schon: kein Neubau')
|
||||
// noch einmal: nichts
|
||||
@@ -501,7 +501,7 @@ test('holeZeitenNach: Ticket fehlt in setup ⇒ anlegen; K24 nur bei unlockedBy
|
||||
assert.deepEqual([tickets().length, sink.sent.length], [1, 1])
|
||||
})
|
||||
|
||||
test('holeZeitenNach: Neubau nur bei leerem einbau; kauf, active und off bleiben unberührt; team bekommt keine K24', async () => {
|
||||
test('holeZeitenNach: Neubau nur bei leerem einbau; kauf, active und off bleiben unberührt; team bekommt keine K27', async () => {
|
||||
const seed = (id, felder) => {
|
||||
projekt(id)
|
||||
aw.seed('portalFeatures', { $id: `f_${id}_zeiten`, customerId: 'c1', projectId: id, featureKey: 'zeiten', siteKey: 'abcdefghijklmnop', enabled: false, ...felder })
|
||||
@@ -522,22 +522,22 @@ test('holeZeitenNach: Neubau nur bei leerem einbau; kauf, active und off bleiben
|
||||
const fertig = await holeZeitenNach(team, { now: JETZT })
|
||||
assert.deepEqual([rebuilds.map((r) => r.projektId), tickets().length, sink.sent.length], [['p_team'], 1, 0])
|
||||
assert.equal(fertig.einbau, 'angestossen')
|
||||
assert.ok(!fertig.k24At)
|
||||
assert.ok(!fertig.einschaltMailAt)
|
||||
assert.match(tickets()[0].details, /automatisch eingesetzt/, 'das Ticket kennt das frische einbau')
|
||||
})
|
||||
|
||||
test('holeZeitenNach: Kunde ohne Adresse oder gelöscht ⇒ k24At wird gesetzt (nichts zu senden), Lesefehler ⇒ wirft und lässt den Marker leer', async () => {
|
||||
test('holeZeitenNach: Kunde ohne Adresse oder gelöscht ⇒ einschaltMailAt wird gesetzt (nichts zu senden), Lesefehler ⇒ wirft und lässt den Marker leer', async () => {
|
||||
aw.seed('customers', { $id: 'c1', appwriteUserId: 'u1', email: '', name: 'Kunde Eins', status: 'active', portalAccessEnabled: true })
|
||||
aw.seed('portalFeatures', { $id: KEY_P1, customerId: 'c1', projectId: 'p1', featureKey: 'zeiten', state: 'setup', enabled: false, siteKey: 'abcdefghijklmnop', unlockedBy: 'abo', einbau: 'manuell', ticketId: 'ticket_da' })
|
||||
const doc = await holeZeitenNach(zeitenDoc(), { now: JETZT })
|
||||
assert.ok(doc.k24At)
|
||||
assert.ok(doc.einschaltMailAt)
|
||||
assert.equal(sink.sent.length, 0)
|
||||
|
||||
await updateDocument('portalFeatures', KEY_P1, { k24At: null })
|
||||
await updateDocument('portalFeatures', KEY_P1, { einschaltMailAt: null })
|
||||
aw.seed('customers', { $id: 'c1', appwriteUserId: 'u1', email: 'k@example.invalid', name: 'Kunde Eins', status: 'active', portalAccessEnabled: true })
|
||||
aw.failNext({ collection: 'customers', status: 500 })
|
||||
await assert.rejects(holeZeitenNach(zeitenDoc(), { now: JETZT }))
|
||||
assert.ok(!zeitenDoc().k24At)
|
||||
assert.ok(!zeitenDoc().einschaltMailAt)
|
||||
})
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------- Angebot
|
||||
|
||||
@@ -8,13 +8,13 @@ import { useTestEnv } from './support/testEnv.js'
|
||||
import { ZEITEN_DATETIMES } from '../server/services/zeiten/schema.js'
|
||||
|
||||
// Feiertags-Erinnerung von Öffnungszeiten live (Aufgabe 23, Spec 7 "Feiertags-Erinnerung", Z18): erinnereFeiertage(now) schickt je Funktion
|
||||
// (setup/active) eine Mail K25 mit allen neuen Tagen in 1–7 Tagen, die Feiertag oder "bitte prüfen" sind. Je Tag zuerst der Merker
|
||||
// (setup/active) eine Mail K28 mit allen neuen Tagen in 1–7 Tagen, die Feiertag oder "bitte prüfen" sind. Je Tag zuerst der Merker
|
||||
// sha1(featureId|datum) (409 = schon erinnert), danach erst die Mail: lieber einmal zu wenig als doppelt. Nur Schalter pilot/on (pilot: nur
|
||||
// freigegebene Kunden), nur 08:00 bis 20:00 Berliner Zeit, kein BCC ans Team, Merker älter als 30 Tage werden gelöscht.
|
||||
// Aufbau wie zeitenAboAbgleich.test.js: Appwrite-Nachbau mit Schema, Mails in der Senke, feste Zeit als Parameter.
|
||||
const DATETIMES = {
|
||||
...ZEITEN_DATETIMES,
|
||||
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'k24At'],
|
||||
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'einschaltMailAt'],
|
||||
}
|
||||
const aw = await startAppwriteMock({
|
||||
schema: {
|
||||
@@ -141,7 +141,7 @@ test('eine Mail für zwei Feiertage im Fenster', async () => {
|
||||
assert.match(mail.text, /Fr 26\.03\. – Karfreitag\nMo 29\.03\. – Ostermontag/, 'beide Tage, in Datumsfolge')
|
||||
assert.match(mail.text, /Praxis 1/, 'der Projektname steht in der Mail')
|
||||
assert.match(mail.text, /https:\/\/project\.webklar\.com\/dashboard\.html#zeiten/, 'Knopf zum Editor')
|
||||
assert.ok(!mail.bcc?.length, 'K25 geht ohne Kopie ans Team (R17c)')
|
||||
assert.ok(!mail.bcc?.length, 'K28 geht ohne Kopie ans Team (R17c)')
|
||||
|
||||
assert.deepEqual(merker().map((m) => m.datum).sort(), ['2027-03-26', '2027-03-29'])
|
||||
})
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ZEITEN_DATETIMES } from '../server/services/zeiten/schema.js'
|
||||
// Freischalten von Oeffnungszeiten live (Aufgabe 18, Spec 3.2 Selbstheilung und 4.4): der 5-Minuten-Job (runActivation) setzt 'active',
|
||||
// wenn das Einbau-Ticket geschlossen ist UND der Kunde Zeiten eingetragen hat (Haken freischaltbar: Hauptplan gespeichert, mindestens
|
||||
// ein Tag nicht "zu"). Bei active: K16 und der Ersatz-Hash (Haken nachFreischaltung). Dazu heilt der Job Dokumente in setup, denen
|
||||
// Ticket oder K24 fehlen (holeZeitenNach), je Dokument getrennt abgesichert.
|
||||
// Ticket oder K27 fehlen (holeZeitenNach), je Dokument getrennt abgesichert.
|
||||
const aw = await startAppwriteMock({
|
||||
schema: {
|
||||
portalFeatures: schemaKeys('portalFeatures'),
|
||||
@@ -18,7 +18,7 @@ const aw = await startAppwriteMock({
|
||||
},
|
||||
datetimes: {
|
||||
...ZEITEN_DATETIMES,
|
||||
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'k24At'],
|
||||
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'einschaltMailAt'],
|
||||
},
|
||||
defaultLimit: 25,
|
||||
})
|
||||
@@ -39,13 +39,13 @@ after(() => setMailTransportForTests(null))
|
||||
const Z = config.features.zeiten
|
||||
const JETZT = new Date('2026-10-05T08:00:00.000Z') // Montag, 10:00 Berlin
|
||||
const K16_BETREFF = /bereit/
|
||||
const K24_BETREFF = 'Öffnungszeiten live ist eingeschaltet'
|
||||
const K27_BETREFF = 'Öffnungszeiten live ist eingeschaltet'
|
||||
|
||||
const feld = (id, name) => aw.docs('portalFeatures').get(id)[name]
|
||||
const zustand = (id) => feld(id, 'state')
|
||||
const tickets = () => [...aw.docs('workorders').values()]
|
||||
const k16 = () => sink.sent.filter((m) => K16_BETREFF.test(m.subject))
|
||||
const k24 = () => sink.sent.filter((m) => m.subject === K24_BETREFF)
|
||||
const k27 = () => sink.sent.filter((m) => m.subject === K27_BETREFF)
|
||||
const logText = () => console.error.mock.calls.map((c) => c.arguments.join(' ')).join('\n')
|
||||
|
||||
function tage(modi) {
|
||||
@@ -69,7 +69,7 @@ const eingabe = (modi = WOCHE) => ({
|
||||
let nr = 0
|
||||
/**
|
||||
* Dokument der Zeiten im Zustand setup mit Projekt, Kunde und Ticket (Standard: geschlossen), optional mit gespeicherten Zeiten
|
||||
* (zeiten: Wochenplan wie in tage()). Standard: abo, einbau gesetzt und k24At gesetzt, damit nur die Freischaltung etwas bewegt.
|
||||
* (zeiten: Wochenplan wie in tage()). Standard: abo, einbau gesetzt und einschaltMailAt gesetzt, damit nur die Freischaltung etwas bewegt.
|
||||
*/
|
||||
async function einrichtung({ customerId = 'c1', ticket = 'Closed', zeiten = null, ...over } = {}) {
|
||||
nr += 1
|
||||
@@ -87,7 +87,7 @@ async function einrichtung({ customerId = 'c1', ticket = 'Closed', zeiten = null
|
||||
siteKey: 'abcdefghijklmnop',
|
||||
unlockedBy: 'abo',
|
||||
einbau: 'angestossen',
|
||||
k24At: JETZT.toISOString(),
|
||||
einschaltMailAt: JETZT.toISOString(),
|
||||
...(ticket ? { ticketId: `t_${projectId}` } : {}),
|
||||
...over,
|
||||
})
|
||||
@@ -174,7 +174,7 @@ test('Ticket zu und Zeiten gespeichert ⇒ active, genau eine K16, ersatzHash ge
|
||||
assert.equal(Date.parse(feld(id, 'activatedAt')), JETZT.getTime())
|
||||
assert.equal(k16().length, 1)
|
||||
assert.deepEqual(k16()[0].to.map((a) => a.address), ['k@example.invalid'])
|
||||
assert.equal(k24().length, 0)
|
||||
assert.equal(k27().length, 0)
|
||||
|
||||
const nachher = await ladeZeiten(id)
|
||||
assert.match(nachher.ersatzHash, /^[0-9a-f]{16}$/)
|
||||
@@ -306,8 +306,8 @@ test('Abnahme der Zeiten: mit eingetragenen Zeiten active, K16, Ersatz-Hash (nac
|
||||
|
||||
// ------------------------------------------------------------------------------------------- Selbstheilung
|
||||
|
||||
test('Selbstheilung: setup ohne ticketId ⇒ der Job legt das Ticket an (und sendet die K24)', async () => {
|
||||
const id = await einrichtung({ ticket: null, einbau: '', k24At: null })
|
||||
test('Selbstheilung: setup ohne ticketId ⇒ der Job legt das Ticket an (und sendet die K27)', async () => {
|
||||
const id = await einrichtung({ ticket: null, einbau: '', einschaltMailAt: null })
|
||||
assert.deepEqual(await sf.runActivation({ now: JETZT }), [])
|
||||
assert.equal(zustand(id), 'setup')
|
||||
assert.equal(tickets().length, 1)
|
||||
@@ -315,49 +315,49 @@ test('Selbstheilung: setup ohne ticketId ⇒ der Job legt das Ticket an (und sen
|
||||
assert.match(tickets()[0].topic, /^Öffnungszeiten live einbauen – Projekt \d+$/)
|
||||
assert.equal(feld(id, 'einbau'), 'angestossen')
|
||||
assert.equal(rebuilds.length, 1, 'einbau war leer: ein Neubau')
|
||||
assert.equal(k24().length, 1)
|
||||
assert.ok(feld(id, 'k24At'))
|
||||
assert.equal(k27().length, 1)
|
||||
assert.ok(feld(id, 'einschaltMailAt'))
|
||||
|
||||
// kein zweites Ticket, kein zweiter Neubau, keine zweite Mail
|
||||
await sf.runActivation({ now: JETZT })
|
||||
assert.deepEqual([tickets().length, rebuilds.length, k24().length], [1, 1, 1])
|
||||
assert.deepEqual([tickets().length, rebuilds.length, k27().length], [1, 1, 1])
|
||||
})
|
||||
|
||||
test('Selbstheilung: unlockedBy abo ohne k24At ⇒ der Job sendet die K24 genau einmal', async () => {
|
||||
const id = await einrichtung({ ticket: 'Open', k24At: null })
|
||||
test('Selbstheilung: unlockedBy abo ohne einschaltMailAt ⇒ der Job sendet die K27 genau einmal', async () => {
|
||||
const id = await einrichtung({ ticket: 'Open', einschaltMailAt: null })
|
||||
await sf.runActivation({ now: JETZT })
|
||||
await sf.runActivation({ now: JETZT })
|
||||
await sf.runActivation({ now: JETZT })
|
||||
assert.equal(k24().length, 1)
|
||||
assert.deepEqual(k24()[0].to.map((a) => a.address), ['k@example.invalid'])
|
||||
assert.equal(Date.parse(feld(id, 'k24At')), JETZT.getTime())
|
||||
assert.equal(k27().length, 1)
|
||||
assert.deepEqual(k27()[0].to.map((a) => a.address), ['k@example.invalid'])
|
||||
assert.equal(Date.parse(feld(id, 'einschaltMailAt')), JETZT.getTime())
|
||||
assert.equal(tickets().length, 1, 'das Ticket war da: kein neues')
|
||||
assert.deepEqual(rebuilds, [])
|
||||
assert.equal(zustand(id), 'setup')
|
||||
})
|
||||
|
||||
test('Selbstheilung: K24 nicht gesendet (Mail scheitert) ⇒ k24At bleibt leer, der nächste Lauf sendet nach', async () => {
|
||||
const id = await einrichtung({ ticket: 'Open', k24At: null })
|
||||
test('Selbstheilung: K27 nicht gesendet (Mail scheitert) ⇒ einschaltMailAt bleibt leer, der nächste Lauf sendet nach', async () => {
|
||||
const id = await einrichtung({ ticket: 'Open', einschaltMailAt: null })
|
||||
setMailTransportForTests({ async sendMail() { throw new Error('SMTP down') } })
|
||||
await sf.runActivation({ now: JETZT })
|
||||
assert.ok(!feld(id, 'k24At'))
|
||||
assert.ok(!feld(id, 'einschaltMailAt'))
|
||||
setMailTransportForTests(sink)
|
||||
await sf.runActivation({ now: JETZT })
|
||||
assert.equal(k24().length, 1)
|
||||
assert.ok(feld(id, 'k24At'))
|
||||
assert.equal(k27().length, 1)
|
||||
assert.ok(feld(id, 'einschaltMailAt'))
|
||||
})
|
||||
|
||||
test('Selbstheilung nur für setup und nicht für kauf; Team bekommt Ticket, aber keine K24', async () => {
|
||||
const kauf = await einrichtung({ ticket: null, einbau: '', k24At: null, unlockedBy: 'kauf', unlockedByPurchase: true })
|
||||
const aktiv = await einrichtung({ ticket: null, state: 'active', enabled: true, k24At: null })
|
||||
const aus = await einrichtung({ ticket: null, state: 'off', k24At: null })
|
||||
const team = await einrichtung({ ticket: null, einbau: '', k24At: null, unlockedBy: 'team' })
|
||||
test('Selbstheilung nur für setup und nicht für kauf; Team bekommt Ticket, aber keine K27', async () => {
|
||||
const kauf = await einrichtung({ ticket: null, einbau: '', einschaltMailAt: null, unlockedBy: 'kauf', unlockedByPurchase: true })
|
||||
const aktiv = await einrichtung({ ticket: null, state: 'active', enabled: true, einschaltMailAt: null })
|
||||
const aus = await einrichtung({ ticket: null, state: 'off', einschaltMailAt: null })
|
||||
const team = await einrichtung({ ticket: null, einbau: '', einschaltMailAt: null, unlockedBy: 'team' })
|
||||
await sf.runActivation({ now: JETZT })
|
||||
assert.equal(tickets().length, 1, 'nur das Team-Dokument bekam ein Ticket')
|
||||
assert.equal(tickets()[0].$id, feld(team, 'ticketId'))
|
||||
assert.equal(sink.sent.length, 0)
|
||||
for (const id of [kauf, aktiv, aus]) assert.ok(!feld(id, 'ticketId'), id)
|
||||
assert.ok(!feld(team, 'k24At'))
|
||||
assert.ok(!feld(team, 'einschaltMailAt'))
|
||||
})
|
||||
|
||||
test('Selbstheilung nur für Zeiten: ein Terminplaner in setup ohne Ticket bleibt unberührt', async () => {
|
||||
@@ -369,15 +369,15 @@ test('Selbstheilung nur für Zeiten: ein Terminplaner in setup ohne Ticket bleib
|
||||
})
|
||||
|
||||
test('Selbstheilung: ein Dokument scheitert (Ticketsystem down) ⇒ die übrigen werden geheilt, der Job wirft nicht, das Log nennt keine Kundendaten', async () => {
|
||||
const kaputt = await einrichtung({ ticket: null, einbau: '', k24At: null })
|
||||
const heil = await einrichtung({ ticket: null, einbau: '', k24At: null, customerId: 'c2' })
|
||||
const kaputt = await einrichtung({ ticket: null, einbau: '', einschaltMailAt: null })
|
||||
const heil = await einrichtung({ ticket: null, einbau: '', einschaltMailAt: null, customerId: 'c2' })
|
||||
aw.failNext({ collection: 'workorders', method: 'POST', status: 500, message: 'Ticketsystem down' })
|
||||
const done = await sf.runActivation({ now: JETZT })
|
||||
assert.deepEqual(done, [])
|
||||
assert.ok(!feld(kaputt, 'ticketId'))
|
||||
assert.ok(feld(heil, 'ticketId'), 'das zweite Dokument wurde trotzdem geheilt')
|
||||
assert.equal(k24().length, 1)
|
||||
assert.deepEqual(k24()[0].to.map((a) => a.address), ['k2@example.invalid'])
|
||||
assert.equal(k27().length, 1)
|
||||
assert.deepEqual(k27()[0].to.map((a) => a.address), ['k2@example.invalid'])
|
||||
const log = logText()
|
||||
assert.match(log, /Ticketsystem down/)
|
||||
assert.doesNotMatch(log, /example\.invalid|Kunde Eins|Kunde Zwei/)
|
||||
@@ -385,7 +385,7 @@ test('Selbstheilung: ein Dokument scheitert (Ticketsystem down) ⇒ die übrigen
|
||||
// der nächste Lauf holt das gescheiterte nach
|
||||
await sf.runActivation({ now: JETZT })
|
||||
assert.ok(feld(kaputt, 'ticketId'))
|
||||
assert.equal(k24().length, 2)
|
||||
assert.equal(k27().length, 2)
|
||||
})
|
||||
|
||||
test('Selbstheilung: Listenfehler beim Suchen der Dokumente ⇒ kein Wurf, K16 der schon freigeschalteten bleibt', async () => {
|
||||
@@ -399,24 +399,24 @@ test('Selbstheilung: Listenfehler beim Suchen der Dokumente ⇒ kein Wurf, K16 d
|
||||
})
|
||||
|
||||
test('Projektliste eines Kunden (runActivation mit customerId) schaltet nur frei: keine Selbstheilung, kein Neubau, kein Ticket, keine Mail', async () => {
|
||||
const unvollstaendig = await einrichtung({ ticket: null, einbau: '', k24At: null })
|
||||
const unvollstaendig = await einrichtung({ ticket: null, einbau: '', einschaltMailAt: null })
|
||||
const fertig = await einrichtung({ zeiten: WOCHE })
|
||||
const done = await sf.runActivation({ now: JETZT, customerId: 'c1' })
|
||||
assert.deepEqual(done.map((d) => d.$id), [fertig], 'Freischalten gilt weiter für den Kunden')
|
||||
assert.ok(!feld(unvollstaendig, 'ticketId'))
|
||||
assert.deepEqual([tickets().length, rebuilds.length, k24().length], [1, 0, 0], 'nur das geschlossene Ticket der fertigen Funktion')
|
||||
assert.deepEqual([tickets().length, rebuilds.length, k27().length], [1, 0, 0], 'nur das geschlossene Ticket der fertigen Funktion')
|
||||
assert.equal(k16().length, 1)
|
||||
// der Lauf des Jobs holt es dann nach
|
||||
await sf.runActivation({ now: JETZT })
|
||||
assert.ok(feld(unvollstaendig, 'ticketId'))
|
||||
assert.deepEqual([tickets().length, rebuilds.length, k24().length], [2, 1, 1])
|
||||
assert.deepEqual([tickets().length, rebuilds.length, k27().length], [2, 1, 1])
|
||||
})
|
||||
|
||||
test('Heilen und Freischalten im selben Lauf: aktiv gewordene Dokumente bekommen keine K24 mehr nachgeschoben', async () => {
|
||||
const id = await einrichtung({ zeiten: WOCHE, k24At: null })
|
||||
test('Heilen und Freischalten im selben Lauf: aktiv gewordene Dokumente bekommen keine K27 mehr nachgeschoben', async () => {
|
||||
const id = await einrichtung({ zeiten: WOCHE, einschaltMailAt: null })
|
||||
const done = await sf.runActivation({ now: JETZT })
|
||||
assert.deepEqual(done.map((d) => d.$id), [id])
|
||||
assert.equal(zustand(id), 'active')
|
||||
assert.equal(k24().length, 0)
|
||||
assert.equal(k27().length, 0)
|
||||
assert.equal(k16().length, 1)
|
||||
})
|
||||
@@ -11,7 +11,7 @@ import { ZEITEN_DATETIMES } from '../server/services/zeiten/schema.js'
|
||||
// Feiertags-Erinnerung) mit eigener Überlappsperre je Job, ohne Wurf, plus Quelltext-Pin der Einbindung in server/index.js.
|
||||
const DATETIMES = {
|
||||
...ZEITEN_DATETIMES,
|
||||
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'k24At'],
|
||||
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'einschaltMailAt'],
|
||||
}
|
||||
const aw = await startAppwriteMock({
|
||||
schema: {
|
||||
@@ -75,7 +75,7 @@ async function legeFunktion() {
|
||||
siteKey: 'abcdefghijklmnop',
|
||||
ticketId: 't_1',
|
||||
einbau: 'angestossen',
|
||||
k24At: ZUVOR,
|
||||
einschaltMailAt: ZUVOR,
|
||||
activatedAt: ZUVOR,
|
||||
})
|
||||
const doc = aw.docs('portalFeatures').get('f_p1_zeiten')
|
||||
@@ -145,7 +145,7 @@ function haengeAn() {
|
||||
|
||||
// ---------------------------------------------------------------- Abo-Job
|
||||
|
||||
test('zeitenAboJob: führt den Abo-Abgleich aus (Abo-Ende erkannt, K26) und gibt dessen Ergebnis zurück', async () => {
|
||||
test('zeitenAboJob: führt den Abo-Abgleich aus (Abo-Ende erkannt, K29) und gibt dessen Ergebnis zurück', async () => {
|
||||
await legeFunktion()
|
||||
abgelaufen()
|
||||
|
||||
|
||||
+73
-73
@@ -6,7 +6,7 @@ import { startFakeStripe } from './support/fakeStripe.js'
|
||||
import { mailSink } from './support/mailSink.js'
|
||||
import { useTestEnv } from './support/testEnv.js'
|
||||
|
||||
// Mails K24–K27 (Öffnungszeiten live: eingeschaltet, Feiertage prüfen, Abo beendet, abgeschaltet), ihre Builder und
|
||||
// Mails K27–K30 (Öffnungszeiten live: eingeschaltet, Feiertage prüfen, Abo beendet, abgeschaltet), ihre Builder und
|
||||
// sendeZeitenMail (server/services/zeiten/mails.js, Task 13) sowie die neuen Blöcke behalten/neu in K15 und abo/team in T16.
|
||||
// Die allgemeinen Vorlagenregeln (Kopf, Platzhalter, Blöcke, Logo, Text ohne Entities, Du-Form) prüft test/mailTemplates.test.js.
|
||||
const aw = await startAppwriteMock()
|
||||
@@ -18,7 +18,7 @@ const { loadTemplate, renderMail } = await import('../server/services/mailTempla
|
||||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||||
const reg = await import('../server/services/featureRegistry.js')
|
||||
const sf = await import('../server/services/siteFeatures.js')
|
||||
const { k24Mail, k25Mail, k26Mail, k27Mail, sendeZeitenMail } = await import('../server/services/zeiten/mails.js')
|
||||
const { k27Mail, k28Mail, k29Mail, k30Mail, sendeZeitenMail } = await import('../server/services/zeiten/mails.js')
|
||||
after(() => setMailTransportForTests(null))
|
||||
|
||||
const KUNDE = { $id: 'c1', name: 'Silke Müller', email: 'silke@example.com' }
|
||||
@@ -40,13 +40,13 @@ const an = (nachricht) => Object.entries(nachricht.blocks || {}).filter(([, on])
|
||||
// Ein Beispiel je Variante: genau diese Nachrichten gehen durch die Prüfung "Werte decken die Platzhalter ab"
|
||||
const FRIST = new Date('2026-11-04T09:00:00.000Z')
|
||||
const BEISPIELE = {
|
||||
'K24 angestossen': () => k24Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', einbau: 'angestossen', wieder: false, ticketNr: 100123 }),
|
||||
'K24 manuell mit Ticket': () => k24Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', einbau: 'manuell', wieder: false, ticketNr: 100123 }),
|
||||
'K24 wieder ohne Ticket': () => k24Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', einbau: 'angestossen', wieder: true }),
|
||||
'K24 wieder ohne Neubau': () => k24Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', einbau: null, wieder: true }),
|
||||
'K25 ein Tag': () => k25Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', tage: [{ tag: '2026-10-03', name: 'Tag der Deutschen Einheit', art: 'feiertag' }] }),
|
||||
'K25 mehrere mit prüfen': () =>
|
||||
k25Mail({
|
||||
'K27 angestossen': () => k27Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', einbau: 'angestossen', wieder: false, ticketNr: 100123 }),
|
||||
'K27 manuell mit Ticket': () => k27Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', einbau: 'manuell', wieder: false, ticketNr: 100123 }),
|
||||
'K27 wieder ohne Ticket': () => k27Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', einbau: 'angestossen', wieder: true }),
|
||||
'K27 wieder ohne Neubau': () => k27Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', einbau: null, wieder: true }),
|
||||
'K28 ein Tag': () => k28Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', tage: [{ tag: '2026-10-03', name: 'Tag der Deutschen Einheit', art: 'feiertag' }] }),
|
||||
'K28 mehrere mit prüfen': () =>
|
||||
k28Mail({
|
||||
kunde: KUNDE.name,
|
||||
projekt: 'Praxis Muster',
|
||||
tage: [
|
||||
@@ -54,18 +54,18 @@ const BEISPIELE = {
|
||||
{ tag: '2026-12-25', name: '1. Weihnachtstag', art: 'feiertag' },
|
||||
],
|
||||
}),
|
||||
'K26 beendet': () => k26Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', art: 'beendet', frist: FRIST, preis: '30 €' }),
|
||||
'K26 hosting': () => k26Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', art: 'hosting', frist: FRIST, preis: '30 €' }),
|
||||
'K26 Wechsel widerrufen': () => k26Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', art: 'wechsel_widerrufen', frist: FRIST, preis: '30 €' }),
|
||||
K27: () => k27Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', loeschdatum: FRIST, preis: '30 €' }),
|
||||
'K29 beendet': () => k29Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', art: 'beendet', frist: FRIST, preis: '30 €' }),
|
||||
'K29 hosting': () => k29Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', art: 'hosting', frist: FRIST, preis: '30 €' }),
|
||||
'K29 Wechsel widerrufen': () => k29Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', art: 'wechsel_widerrufen', frist: FRIST, preis: '30 €' }),
|
||||
K30: () => k30Mail({ kunde: KUNDE.name, projekt: 'Praxis Muster', loeschdatum: FRIST, preis: '30 €' }),
|
||||
}
|
||||
|
||||
test('Vorlagen K24–K27: Datei, Betreff, Platzhalter und Blöcke stehen fest', () => {
|
||||
test('Vorlagen K27–K30: Datei, Betreff, Platzhalter und Blöcke stehen fest', () => {
|
||||
const erwartet = {
|
||||
K24: { file: 'K24 Zeiten eingeschaltet.html', subject: 'Öffnungszeiten live ist eingeschaltet', placeholders: ['kunde_name', 'projekt', 'ticket', 'portal_url'], blocks: ['angestossen', 'manuell', 'wieder', 'weiter'] },
|
||||
K25: { file: 'K25 Zeiten Feiertage pruefen.html', subject: 'Feiertage stehen an – stimmen Deine Zeiten?', placeholders: ['kunde_name', 'projekt', 'tage', 'portal_url'], blocks: ['pruefen', 'mehrere'] },
|
||||
K26: { file: 'K26 Zeiten Abo beendet.html', subject: 'Dein Wartungs-Abo ist beendet – Öffnungszeiten live behalten?', placeholders: ['kunde_name', 'projekt', 'frist', 'preis', 'portal_url'], blocks: ['abo_ende', 'hosting', 'wechsel_widerrufen'] },
|
||||
K27: { file: 'K27 Zeiten abgeschaltet.html', subject: 'Öffnungszeiten live ist abgeschaltet', placeholders: ['kunde_name', 'projekt', 'loeschdatum', 'preis', 'portal_url'], blocks: [] },
|
||||
K27: { file: 'K27 Zeiten eingeschaltet.html', subject: 'Öffnungszeiten live ist eingeschaltet', placeholders: ['kunde_name', 'projekt', 'ticket', 'portal_url'], blocks: ['angestossen', 'manuell', 'wieder', 'weiter'] },
|
||||
K28: { file: 'K28 Zeiten Feiertage pruefen.html', subject: 'Feiertage stehen an – stimmen Deine Zeiten?', placeholders: ['kunde_name', 'projekt', 'tage', 'portal_url'], blocks: ['pruefen', 'mehrere'] },
|
||||
K29: { file: 'K29 Zeiten Abo beendet.html', subject: 'Dein Wartungs-Abo ist beendet – Öffnungszeiten live behalten?', placeholders: ['kunde_name', 'projekt', 'frist', 'preis', 'portal_url'], blocks: ['abo_ende', 'hosting', 'wechsel_widerrufen'] },
|
||||
K30: { file: 'K30 Zeiten abgeschaltet.html', subject: 'Öffnungszeiten live ist abgeschaltet', placeholders: ['kunde_name', 'projekt', 'loeschdatum', 'preis', 'portal_url'], blocks: [] },
|
||||
}
|
||||
for (const [id, soll] of Object.entries(erwartet)) {
|
||||
const tpl = loadTemplate(id)
|
||||
@@ -92,22 +92,22 @@ test('Builder decken genau die Platzhalter und Blöcke der Vorlage ab', () => {
|
||||
})
|
||||
|
||||
test('Anrede: Kunde als Name oder als Kundendokument, ohne Namen bleibt der Wert leer (wie K22/K23)', () => {
|
||||
assert.equal(k27Mail({ kunde: KUNDE, projekt: 'P', loeschdatum: FRIST, preis: '30 €' }).values.kunde_name, 'Silke Müller')
|
||||
assert.equal(k30Mail({ kunde: KUNDE, projekt: 'P', loeschdatum: FRIST, preis: '30 €' }).values.kunde_name, 'Silke Müller')
|
||||
for (const kunde of [undefined, null, '', ' ', {}, { name: null }]) {
|
||||
assert.equal(k27Mail({ kunde, projekt: 'P', loeschdatum: FRIST, preis: '30 €' }).values.kunde_name, '', JSON.stringify(kunde))
|
||||
assert.equal(k30Mail({ kunde, projekt: 'P', loeschdatum: FRIST, preis: '30 €' }).values.kunde_name, '', JSON.stringify(kunde))
|
||||
}
|
||||
})
|
||||
|
||||
test('K24: Text je nach einbau (angestossen: nach dem nächsten Veröffentlichen, manuell: das Team baut ein)', () => {
|
||||
const auto = BEISPIELE['K24 angestossen']()
|
||||
assert.equal(auto.template, 'K24')
|
||||
test('K27: Text je nach einbau (angestossen: nach dem nächsten Veröffentlichen, manuell: das Team baut ein)', () => {
|
||||
const auto = BEISPIELE['K27 angestossen']()
|
||||
assert.equal(auto.template, 'K27')
|
||||
assert.deepEqual(auto.blocks, { angestossen: true, manuell: false, wieder: false, weiter: true })
|
||||
const textAuto = render(auto).text
|
||||
assert.match(textAuto, /nach dem nächsten Veröffentlichen/)
|
||||
assert.doesNotMatch(textAuto, /baut unser Team/)
|
||||
assert.match(textAuto, /Ticket #100123/, 'Ticketnummer steht in beiden Fällen in der Mail')
|
||||
|
||||
const hand = BEISPIELE['K24 manuell mit Ticket']()
|
||||
const hand = BEISPIELE['K27 manuell mit Ticket']()
|
||||
assert.deepEqual(hand.blocks, { angestossen: false, manuell: true, wieder: false, weiter: true })
|
||||
const textHand = render(hand).text
|
||||
assert.match(textHand, /baut unser Team/)
|
||||
@@ -116,13 +116,13 @@ test('K24: Text je nach einbau (angestossen: nach dem nächsten Veröffentlichen
|
||||
|
||||
// ohne bekannten Einbau sagt die Mail das Vorsichtige: das Team baut ein (null gilt nur mit wieder als "kein Einbau nötig", siehe unten)
|
||||
for (const einbau of [undefined, null, '', 'unbekannt']) {
|
||||
const unklar = k24Mail({ kunde: 'K', projekt: 'P', einbau })
|
||||
const unklar = k27Mail({ kunde: 'K', projekt: 'P', einbau })
|
||||
assert.deepEqual(unklar.blocks, { angestossen: false, manuell: true, wieder: false, weiter: true }, String(einbau))
|
||||
}
|
||||
})
|
||||
|
||||
test('K24 wieder ohne Neubau (einbau null, Abo kehrt vor dem Abschalten zurück): beide Einbau-Blöcke und der Kasten "Wie geht es weiter" entfallen', () => {
|
||||
const ohne = BEISPIELE['K24 wieder ohne Neubau']()
|
||||
test('K27 wieder ohne Neubau (einbau null, Abo kehrt vor dem Abschalten zurück): beide Einbau-Blöcke und der Kasten "Wie geht es weiter" entfallen', () => {
|
||||
const ohne = BEISPIELE['K27 wieder ohne Neubau']()
|
||||
assert.deepEqual(ohne.blocks, { angestossen: false, manuell: false, wieder: true, weiter: false })
|
||||
const mail = render(ohne)
|
||||
assert.deepEqual(mail.problems, [])
|
||||
@@ -136,40 +136,40 @@ test('K24 wieder ohne Neubau (einbau null, Abo kehrt vor dem Abschalten zurück)
|
||||
|
||||
// mit Neubau (angestossen oder manuell) bleibt der Kasten, auch beim Wiedereinschalten
|
||||
for (const einbau of ['angestossen', 'manuell']) {
|
||||
const text = render(k24Mail({ kunde: 'K', projekt: 'P', einbau, wieder: true })).text
|
||||
const text = render(k27Mail({ kunde: 'K', projekt: 'P', einbau, wieder: true })).text
|
||||
assert.match(text, /Wie geht es weiter/, einbau)
|
||||
}
|
||||
// einbau null ohne wieder ist kein Fall der Mail: es bleibt beim Vorsichtigen
|
||||
assert.deepEqual(k24Mail({ kunde: 'K', projekt: 'P', einbau: null }).blocks, { angestossen: false, manuell: true, wieder: false, weiter: true })
|
||||
assert.deepEqual(k27Mail({ kunde: 'K', projekt: 'P', einbau: null }).blocks, { angestossen: false, manuell: true, wieder: false, weiter: true })
|
||||
})
|
||||
|
||||
test('K24: ohne Ticketnummer steht kein leeres Ticket in der Mail; wieder nennt das Abo', () => {
|
||||
const ohne = k24Mail({ kunde: 'K', projekt: 'P', einbau: 'manuell' })
|
||||
test('K27: ohne Ticketnummer steht kein leeres Ticket in der Mail; wieder nennt das Abo', () => {
|
||||
const ohne = k27Mail({ kunde: 'K', projekt: 'P', einbau: 'manuell' })
|
||||
assert.equal(ohne.values.ticket, '')
|
||||
assert.doesNotMatch(render(ohne).text, /Ticket|\(\)|\s\./)
|
||||
const leer = k24Mail({ kunde: 'K', projekt: 'P', einbau: 'manuell', ticketNr: ' ' })
|
||||
const leer = k27Mail({ kunde: 'K', projekt: 'P', einbau: 'manuell', ticketNr: ' ' })
|
||||
assert.equal(leer.values.ticket, '')
|
||||
assert.equal(k24Mail({ kunde: 'K', projekt: 'P', einbau: 'manuell', ticketNr: '#100124' }).values.ticket, ' (Ticket #100124)')
|
||||
assert.equal(k27Mail({ kunde: 'K', projekt: 'P', einbau: 'manuell', ticketNr: '#100124' }).values.ticket, ' (Ticket #100124)')
|
||||
|
||||
const wieder = BEISPIELE['K24 wieder ohne Ticket']()
|
||||
const wieder = BEISPIELE['K27 wieder ohne Ticket']()
|
||||
assert.deepEqual(wieder.blocks, { angestossen: true, manuell: false, wieder: true, weiter: true })
|
||||
const mitWieder = render(wieder).text
|
||||
assert.match(mitWieder, /Dein Wartungs-Abo läuft wieder/)
|
||||
assert.match(mitWieder, /wieder eingeschaltet/)
|
||||
assert.match(mitWieder, /Zeiten und Einstellungen sind unverändert/)
|
||||
assert.doesNotMatch(render(BEISPIELE['K24 angestossen']()).text, /Wartungs-Abo läuft wieder/)
|
||||
assert.doesNotMatch(render(BEISPIELE['K27 angestossen']()).text, /Wartungs-Abo läuft wieder/)
|
||||
})
|
||||
|
||||
test('K24: Betreff, Projekt und Knopf', () => {
|
||||
const mail = render(BEISPIELE['K24 angestossen']())
|
||||
test('K27: Betreff, Projekt und Knopf', () => {
|
||||
const mail = render(BEISPIELE['K27 angestossen']())
|
||||
assert.equal(mail.subject, 'Öffnungszeiten live ist eingeschaltet')
|
||||
assert.match(mail.text, /Projekt: Praxis Muster/)
|
||||
assert.match(mail.html, new RegExp(`<a href="${PORTAL.replaceAll('.', '\\.')}"`))
|
||||
})
|
||||
|
||||
test('K25: eine Zeile je Tag mit Wochentag, Datum und Namen; „bitte prüfen“ und Mehrzahl nur bei Bedarf', () => {
|
||||
const ein = BEISPIELE['K25 ein Tag']()
|
||||
assert.equal(ein.template, 'K25')
|
||||
test('K28: eine Zeile je Tag mit Wochentag, Datum und Namen; „bitte prüfen“ und Mehrzahl nur bei Bedarf', () => {
|
||||
const ein = BEISPIELE['K28 ein Tag']()
|
||||
assert.equal(ein.template, 'K28')
|
||||
assert.equal(ein.values.tage, 'Sa 03.10. – Tag der Deutschen Einheit')
|
||||
assert.deepEqual(an(ein), [])
|
||||
assert.deepEqual(ein.blocks, { pruefen: false, mehrere: false })
|
||||
@@ -179,7 +179,7 @@ test('K25: eine Zeile je Tag mit Wochentag, Datum und Namen; „bitte prüfen“
|
||||
assert.match(textEin, /Stimmen Deine Zeiten/)
|
||||
assert.match(textEin, /Google/, 'Hinweis: bei Google auch eintragen')
|
||||
|
||||
const viele = BEISPIELE['K25 mehrere mit prüfen']()
|
||||
const viele = BEISPIELE['K28 mehrere mit prüfen']()
|
||||
assert.equal(viele.values.tage, 'Do 24.12. – Heiligabend (bitte prüfen)\nFr 25.12. – 1. Weihnachtstag')
|
||||
assert.deepEqual(viele.blocks, { pruefen: true, mehrere: true })
|
||||
const mail = render(viele)
|
||||
@@ -189,8 +189,8 @@ test('K25: eine Zeile je Tag mit Wochentag, Datum und Namen; „bitte prüfen“
|
||||
assert.match(mail.html, /Heiligabend \(bitte prüfen\)<br>Fr 25\.12\./, 'Zeilenumbrüche werden zu <br>')
|
||||
})
|
||||
|
||||
test('K25: Namen werden HTML-sicher eingesetzt, Wochentage stimmen über die Woche', () => {
|
||||
const nachricht = k25Mail({
|
||||
test('K28: Namen werden HTML-sicher eingesetzt, Wochentage stimmen über die Woche', () => {
|
||||
const nachricht = k28Mail({
|
||||
kunde: 'K',
|
||||
projekt: 'P',
|
||||
tage: [
|
||||
@@ -209,15 +209,15 @@ test('K25: Namen werden HTML-sicher eingesetzt, Wochentage stimmen über die Woc
|
||||
assert.doesNotMatch(mail.html, /<b>fett/)
|
||||
})
|
||||
|
||||
test('K25 geht ohne BCC ans Team, K24 mit', async () => {
|
||||
const k25 = await sendeZeitenMail(KUNDE, BEISPIELE['K25 ein Tag'](), { bccTeam: false })
|
||||
assert.deepEqual(k25, { sent: true })
|
||||
test('K28 geht ohne BCC ans Team, K27 mit', async () => {
|
||||
const k28 = await sendeZeitenMail(KUNDE, BEISPIELE['K28 ein Tag'](), { bccTeam: false })
|
||||
assert.deepEqual(k28, { sent: true })
|
||||
assert.equal(sink.sent.length, 1)
|
||||
assert.deepEqual(sink.sent[0].to.map((t) => t.address), ['silke@example.com'])
|
||||
assert.ok(!sink.sent[0].bcc, 'keine Kopie ans Team')
|
||||
assert.equal(sink.sent[0].subject, 'Feiertage stehen an – stimmen Deine Zeiten?')
|
||||
|
||||
assert.deepEqual(await sendeZeitenMail(KUNDE, BEISPIELE['K24 angestossen']()), { sent: true })
|
||||
assert.deepEqual(await sendeZeitenMail(KUNDE, BEISPIELE['K27 angestossen']()), { sent: true })
|
||||
assert.equal(sink.sent.length, 2)
|
||||
assert.deepEqual(sink.sent[1].bcc.map((t) => t.address), config.chatNotify.emails, 'Standard: Kopie ans Team')
|
||||
assert.ok(sink.sent[1].bcc.length > 0)
|
||||
@@ -225,15 +225,15 @@ test('K25 geht ohne BCC ans Team, K24 mit', async () => {
|
||||
assert.match(sink.sent[1].html, /Hallo Silke Müller,/)
|
||||
})
|
||||
|
||||
test('K26: je Variante nur der passende Grund, Frist, Preis und Behalten-Knopf', () => {
|
||||
test('K29: je Variante nur der passende Grund, Frist, Preis und Behalten-Knopf', () => {
|
||||
const faelle = [
|
||||
['K26 beendet', { abo_ende: true, hosting: false, wechsel_widerrufen: false }, /Dein Wartungs-Abo ist beendet/, /Hosting-Abo|Wechsel/],
|
||||
['K26 hosting', { abo_ende: false, hosting: true, wechsel_widerrufen: false }, /Hosting-Abo/, /Dein Wartungs-Abo ist beendet|widerrufen/],
|
||||
['K26 Wechsel widerrufen', { abo_ende: false, hosting: false, wechsel_widerrufen: true }, /Wechsel.*widerrufen/, /Dein Wartungs-Abo ist beendet|Dein Abo ist ein Hosting-Abo/],
|
||||
['K29 beendet', { abo_ende: true, hosting: false, wechsel_widerrufen: false }, /Dein Wartungs-Abo ist beendet/, /Hosting-Abo|Wechsel/],
|
||||
['K29 hosting', { abo_ende: false, hosting: true, wechsel_widerrufen: false }, /Hosting-Abo/, /Dein Wartungs-Abo ist beendet|widerrufen/],
|
||||
['K29 Wechsel widerrufen', { abo_ende: false, hosting: false, wechsel_widerrufen: true }, /Wechsel.*widerrufen/, /Dein Wartungs-Abo ist beendet|Dein Abo ist ein Hosting-Abo/],
|
||||
]
|
||||
for (const [name, blocks, passt, fremd] of faelle) {
|
||||
const nachricht = BEISPIELE[name]()
|
||||
assert.equal(nachricht.template, 'K26')
|
||||
assert.equal(nachricht.template, 'K29')
|
||||
assert.deepEqual(nachricht.blocks, blocks, name)
|
||||
assert.equal(nachricht.values.frist, '04.11.2026', `${name}: Datum TT.MM.JJJJ`)
|
||||
assert.equal(nachricht.values.preis, '30 €')
|
||||
@@ -245,22 +245,22 @@ test('K26: je Variante nur der passende Grund, Frist, Preis und Behalten-Knopf',
|
||||
assert.match(text, /Für 30 € behalten: .*#zeiten/, `${name}: Knopf zum Behalten`)
|
||||
}
|
||||
// unbekannte Lage: lieber die Standard-Fassung (Abo beendet) als gar kein Grund
|
||||
assert.deepEqual(k26Mail({ kunde: 'K', projekt: 'P', art: 'was-neues', frist: FRIST, preis: '30 €' }).blocks, { abo_ende: true, hosting: false, wechsel_widerrufen: false })
|
||||
assert.equal(render(BEISPIELE['K26 beendet']()).subject, 'Dein Wartungs-Abo ist beendet – Öffnungszeiten live behalten?')
|
||||
assert.deepEqual(k29Mail({ kunde: 'K', projekt: 'P', art: 'was-neues', frist: FRIST, preis: '30 €' }).blocks, { abo_ende: true, hosting: false, wechsel_widerrufen: false })
|
||||
assert.equal(render(BEISPIELE['K29 beendet']()).subject, 'Dein Wartungs-Abo ist beendet – Öffnungszeiten live behalten?')
|
||||
})
|
||||
|
||||
test('K26/K27: Datum aus Date (Berliner Tag), ungültiges Datum wirft früh', () => {
|
||||
test('K29/K30: Datum aus Date (Berliner Tag), ungültiges Datum wirft früh', () => {
|
||||
// Berliner Tag, nicht UTC: 22:30 UTC am 30.10. ist 23:30 MEZ (noch der 30.), 23:30 UTC am 31.10. ist 00:30 MEZ (schon der 1.11.)
|
||||
assert.equal(k26Mail({ kunde: 'K', projekt: 'P', art: 'beendet', frist: new Date('2026-10-30T22:30:00.000Z'), preis: '30 €' }).values.frist, '30.10.2026')
|
||||
assert.equal(k26Mail({ kunde: 'K', projekt: 'P', art: 'beendet', frist: new Date('2026-10-31T23:30:00.000Z'), preis: '30 €' }).values.frist, '01.11.2026')
|
||||
assert.throws(() => k26Mail({ kunde: 'K', projekt: 'P', art: 'beendet', frist: new Date('kaputt'), preis: '30 €' }), TypeError)
|
||||
assert.throws(() => k26Mail({ kunde: 'K', projekt: 'P', art: 'beendet', preis: '30 €' }), TypeError)
|
||||
assert.throws(() => k27Mail({ kunde: 'K', projekt: 'P', loeschdatum: 'morgen', preis: '30 €' }), TypeError)
|
||||
assert.equal(k29Mail({ kunde: 'K', projekt: 'P', art: 'beendet', frist: new Date('2026-10-30T22:30:00.000Z'), preis: '30 €' }).values.frist, '30.10.2026')
|
||||
assert.equal(k29Mail({ kunde: 'K', projekt: 'P', art: 'beendet', frist: new Date('2026-10-31T23:30:00.000Z'), preis: '30 €' }).values.frist, '01.11.2026')
|
||||
assert.throws(() => k29Mail({ kunde: 'K', projekt: 'P', art: 'beendet', frist: new Date('kaputt'), preis: '30 €' }), TypeError)
|
||||
assert.throws(() => k29Mail({ kunde: 'K', projekt: 'P', art: 'beendet', preis: '30 €' }), TypeError)
|
||||
assert.throws(() => k30Mail({ kunde: 'K', projekt: 'P', loeschdatum: 'morgen', preis: '30 €' }), TypeError)
|
||||
})
|
||||
|
||||
test('K27: Leisten weg, feste Zeiten, Daten bis zum Löschdatum, Wiedereinschalten für den Preis', () => {
|
||||
const nachricht = BEISPIELE.K27()
|
||||
assert.equal(nachricht.template, 'K27')
|
||||
test('K30: Leisten weg, feste Zeiten, Daten bis zum Löschdatum, Wiedereinschalten für den Preis', () => {
|
||||
const nachricht = BEISPIELE.K30()
|
||||
assert.equal(nachricht.template, 'K30')
|
||||
assert.equal(nachricht.values.loeschdatum, '04.11.2026')
|
||||
assert.equal(nachricht.values.preis, '30 €')
|
||||
const mail = render(nachricht)
|
||||
@@ -271,15 +271,15 @@ test('K27: Leisten weg, feste Zeiten, Daten bis zum Löschdatum, Wiedereinschalt
|
||||
assert.match(mail.text, /Projekt: Praxis Muster/)
|
||||
})
|
||||
|
||||
test('K26 und K27 gehen mit Kopie ans Team', async () => {
|
||||
assert.equal((await sendeZeitenMail(KUNDE, BEISPIELE['K26 beendet']())).sent, true)
|
||||
assert.equal((await sendeZeitenMail(KUNDE, BEISPIELE.K27())).sent, true)
|
||||
test('K29 und K30 gehen mit Kopie ans Team', async () => {
|
||||
assert.equal((await sendeZeitenMail(KUNDE, BEISPIELE['K29 beendet']())).sent, true)
|
||||
assert.equal((await sendeZeitenMail(KUNDE, BEISPIELE.K30())).sent, true)
|
||||
for (const mail of sink.sent) assert.ok(mail.bcc?.length > 0)
|
||||
})
|
||||
|
||||
test('ohne E-Mail-Adresse: nichts senden, nichts werfen', async () => {
|
||||
for (const kunde of [{ name: 'Silke' }, { name: 'Silke', email: '' }, { email: ' ' }, { email: null }, null, undefined]) {
|
||||
assert.deepEqual(await sendeZeitenMail(kunde, BEISPIELE.K27()), { sent: false, reason: 'keine_adresse' }, JSON.stringify(kunde))
|
||||
assert.deepEqual(await sendeZeitenMail(kunde, BEISPIELE.K30()), { sent: false, reason: 'keine_adresse' }, JSON.stringify(kunde))
|
||||
}
|
||||
assert.equal(sink.sent.length, 0)
|
||||
})
|
||||
@@ -290,13 +290,13 @@ test('Versandfehler werden gemeldet, nie geworfen; der Grund nennt keine Adresse
|
||||
throw new Error('550 Recipient silke@example.com rejected for Silke Müller')
|
||||
},
|
||||
})
|
||||
const ergebnis = await sendeZeitenMail(KUNDE, BEISPIELE.K27())
|
||||
const ergebnis = await sendeZeitenMail(KUNDE, BEISPIELE.K30())
|
||||
assert.equal(ergebnis.sent, false)
|
||||
assert.equal(ergebnis.reason, 'fehler')
|
||||
assert.doesNotMatch(JSON.stringify(ergebnis), /silke|Müller/i)
|
||||
// auch ohne SMTP-Konfiguration (sendCustomerMail wirft) und mit unbrauchbarer Nachricht
|
||||
setMailTransportForTests(null)
|
||||
assert.equal((await sendeZeitenMail(KUNDE, BEISPIELE.K27())).sent, false)
|
||||
assert.equal((await sendeZeitenMail(KUNDE, BEISPIELE.K30())).sent, false)
|
||||
setMailTransportForTests(sink)
|
||||
assert.equal((await sendeZeitenMail(KUNDE, undefined)).sent, false)
|
||||
assert.equal((await sendeZeitenMail(KUNDE, { template: 'K99' })).sent, false)
|
||||
@@ -309,19 +309,19 @@ test('Log enthält weder Adresse noch Namen', async () => {
|
||||
throw new Error('550 Recipient silke@example.com rejected for Silke Müller (Praxis Muster)')
|
||||
},
|
||||
})
|
||||
const ergebnis = await sendeZeitenMail(KUNDE, BEISPIELE['K26 beendet']())
|
||||
const ergebnis = await sendeZeitenMail(KUNDE, BEISPIELE['K29 beendet']())
|
||||
assert.equal(ergebnis.sent, false)
|
||||
const text = logText()
|
||||
assert.match(text, /K26/, 'das Log nennt die Vorlage')
|
||||
assert.match(text, /K29/, 'das Log nennt die Vorlage')
|
||||
assert.ok(text.length > 0)
|
||||
assert.doesNotMatch(text, /silke|example\.com|Müller/i, text)
|
||||
// auch der Fall ohne Adresse loggt höchstens ohne Namen
|
||||
await sendeZeitenMail({ name: 'Silke Müller' }, BEISPIELE.K27())
|
||||
await sendeZeitenMail({ name: 'Silke Müller' }, BEISPIELE.K30())
|
||||
assert.doesNotMatch(logText(), /silke|Müller/i)
|
||||
})
|
||||
|
||||
test('Erfolg schreibt nichts ins Log', async () => {
|
||||
await sendeZeitenMail(KUNDE, BEISPIELE['K24 angestossen']())
|
||||
await sendeZeitenMail(KUNDE, BEISPIELE['K27 angestossen']())
|
||||
assert.equal(logText(), '')
|
||||
})
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ const { createDocument, updateDocument, getDocument } = await import('../server/
|
||||
after(() => Promise.all([aw.close(), st.close()]))
|
||||
|
||||
const NAMES = ['oeffnungszeiten', 'zeitenErinnerungen']
|
||||
const FEATURE_KEYS = ['unlockedBy', 'aboEndeAt', 'einbau', 'behaltenSessionId', 'k24At']
|
||||
const FEATURE_KEYS = ['unlockedBy', 'aboEndeAt', 'einbau', 'behaltenSessionId', 'einschaltMailAt']
|
||||
const keysOf = (name) => ZEITEN_COLLECTIONS[name].attributes.map((a) => a.key)
|
||||
|
||||
/**
|
||||
@@ -120,13 +120,13 @@ test('zeitenErinnerungen: featureId, datum, createdAt', () => {
|
||||
])
|
||||
})
|
||||
|
||||
test('portalFeatures bekommt unlockedBy(8), aboEndeAt, einbau(12), behaltenSessionId(128), k24At', () => {
|
||||
test('portalFeatures bekommt unlockedBy(8), aboEndeAt, einbau(12), behaltenSessionId(128), einschaltMailAt', () => {
|
||||
assert.deepEqual(ZEITEN_FEATURE_ATTRIBUTES, [
|
||||
{ key: 'unlockedBy', type: 'string', size: 8, required: false },
|
||||
{ key: 'aboEndeAt', type: 'datetime', required: false },
|
||||
{ key: 'einbau', type: 'string', size: 12, required: false },
|
||||
{ key: 'behaltenSessionId', type: 'string', size: 128, required: false },
|
||||
{ key: 'k24At', type: 'datetime', required: false },
|
||||
{ key: 'einschaltMailAt', type: 'datetime', required: false },
|
||||
])
|
||||
})
|
||||
|
||||
@@ -206,7 +206,7 @@ test('main legt auf leerer Datenbank 2 Collections, alle Attribute und Indizes a
|
||||
assert.deepEqual(client.calls.attrs.filter((k) => k.startsWith(`${featuresId}.`)), FEATURE_KEYS.map((key) => `${featuresId}.${key}`))
|
||||
const features = client.state.get(featuresId).attrs
|
||||
assert.deepEqual(features.get('unlockedBy'), { key: 'unlockedBy', type: 'string', size: 8, required: false })
|
||||
assert.deepEqual(features.get('k24At'), { key: 'k24At', type: 'datetime', required: false })
|
||||
assert.deepEqual(features.get('einschaltMailAt'), { key: 'einschaltMailAt', type: 'datetime', required: false })
|
||||
})
|
||||
|
||||
test('main: zweiter Lauf legt nichts mehr an', async () => {
|
||||
@@ -232,7 +232,7 @@ test('main legt nur fehlende Attribute und Indizes einer vorhandenen Collection
|
||||
)
|
||||
assert.deepEqual(
|
||||
client.calls.attrs.filter((k) => k.startsWith(`${featuresId}.`)),
|
||||
[`${featuresId}.aboEndeAt`, `${featuresId}.behaltenSessionId`, `${featuresId}.k24At`],
|
||||
[`${featuresId}.aboEndeAt`, `${featuresId}.behaltenSessionId`, `${featuresId}.einschaltMailAt`],
|
||||
)
|
||||
assert.equal(client.calls.collections.length, 1)
|
||||
})
|
||||
@@ -256,9 +256,9 @@ test('--check meldet bei vollständigem Stand nichts und ein einzelnes fehlendes
|
||||
const client = fakeClient()
|
||||
await main({ client, log: quiet })
|
||||
assert.deepEqual(await main({ check: true, client, log: quiet }), { missing: [], missingCollections: [], missingIndexes: [] })
|
||||
client.state.get(featuresId).attrs.delete('k24At')
|
||||
client.state.get(featuresId).attrs.delete('einschaltMailAt')
|
||||
const result = await main({ check: true, client, log: quiet })
|
||||
assert.deepEqual(result.missing.map((a) => `${a.collection}.${a.key}`), [`${featuresId}.k24At`])
|
||||
assert.deepEqual(result.missing.map((a) => `${a.collection}.${a.key}`), [`${featuresId}.einschaltMailAt`])
|
||||
assert.deepEqual(result.missingCollections, [])
|
||||
})
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ const aw = await startAppwriteMock({
|
||||
// Datumsfelder wie in Appwrite: Unlesbares oder '' waere dort ein 400 (leeren geht nur mit null)
|
||||
datetimes: {
|
||||
...ZEITEN_DATETIMES,
|
||||
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'k24At'],
|
||||
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'einschaltMailAt'],
|
||||
},
|
||||
defaultLimit: 25,
|
||||
})
|
||||
@@ -123,7 +123,7 @@ afterEach(() => {
|
||||
|
||||
// ------------------------------------------------------------------------------------------------ Aktivieren
|
||||
|
||||
test('Team aktiviert zeiten: setup, unlockedBy team, Neubau, Ticket, kein K24', async () => {
|
||||
test('Team aktiviert zeiten: setup, unlockedBy team, Neubau, Ticket, kein K27', async () => {
|
||||
const res = await aktion('activate', { note: 'Pilot' })
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
assert.deepEqual(res.body.projects.map((p) => p.projectId), ['p1'])
|
||||
@@ -142,7 +142,7 @@ test('Team aktiviert zeiten: setup, unlockedBy team, Neubau, Ticket, kein K24',
|
||||
assert.match(doc.siteKey, /^[a-z2-7]{16}$/)
|
||||
assert.equal(doc.einbau, 'angestossen')
|
||||
assert.ok(doc.ticketId)
|
||||
assert.ok(!doc.k24At, 'keine K24 an den Kunden')
|
||||
assert.ok(!doc.einschaltMailAt, 'keine K27 an den Kunden')
|
||||
assert.equal(aw.docs('portalFeatures').size, 1, 'kein Dokument fuer News oder Terminplaner')
|
||||
assert.equal(aw.docs('featureConsents').size, 0, 'kein Zustimmungsnachweis fuer das Team')
|
||||
|
||||
@@ -202,7 +202,7 @@ test('Team „Freischalten (Abnahme)“ der Zeiten: ohne Zeiten 409 (bleibt setu
|
||||
})
|
||||
|
||||
// Review N8 / I1: „Ticket nachlegen“ lief für Abo und Team in den Zahlungsweg und scheiterte immer mit 409 „keine Zahlung vermerkt“
|
||||
test('Team „Ticket nachlegen“ der Zeiten (Team, setup, ohne Ticket): kein 409, Neubau und Einbau-Ticket kommen über das Nachholen, keine K24', async () => {
|
||||
test('Team „Ticket nachlegen“ der Zeiten (Team, setup, ohne Ticket): kein 409, Neubau und Einbau-Ticket kommen über das Nachholen, keine K27', async () => {
|
||||
aw.seed('portalFeatures', featureDoc('p1', { state: 'setup', unlockedBy: 'team', einbau: '' }))
|
||||
const res = await aktion('ticket')
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
@@ -213,7 +213,7 @@ test('Team „Ticket nachlegen“ der Zeiten (Team, setup, ohne Ticket): kein 40
|
||||
assert.equal(rebuilds.length, 1)
|
||||
assert.equal(res.body.projects[0].features.zeiten.ticketMissing, false)
|
||||
assert.match(doc.adminNote, / Andrej: ticket$/)
|
||||
assert.equal(sink.sent.length, 0, 'unlockedBy team: keine K24')
|
||||
assert.equal(sink.sent.length, 0, 'unlockedBy team: keine K27')
|
||||
|
||||
// zweiter Klick: das Ticket ist da, nichts entsteht neu
|
||||
assert.equal((await aktion('ticket')).status, 200)
|
||||
@@ -221,14 +221,14 @@ test('Team „Ticket nachlegen“ der Zeiten (Team, setup, ohne Ticket): kein 40
|
||||
assert.equal(rebuilds.length, 1)
|
||||
})
|
||||
|
||||
test('Team „Ticket nachlegen“ der Zeiten (Abo, setup, ohne Ticket): Ticket und K24 über das Nachholen, nicht über den Zahlungsweg', async () => {
|
||||
test('Team „Ticket nachlegen“ der Zeiten (Abo, setup, ohne Ticket): Ticket und K27 über das Nachholen, nicht über den Zahlungsweg', async () => {
|
||||
aw.seed('portalFeatures', featureDoc('p1', { state: 'setup', unlockedBy: 'abo', einbau: 'angestossen' }))
|
||||
const res = await aktion('ticket')
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
assert.ok(zeitenDoc().ticketId)
|
||||
assert.equal(tickets().length, 1)
|
||||
assert.equal(sink.sent.length, 1, 'K24 an den Kunden (unlockedBy abo)')
|
||||
assert.ok(zeitenDoc().k24At)
|
||||
assert.equal(sink.sent.length, 1, 'K27 an den Kunden (unlockedBy abo)')
|
||||
assert.ok(zeitenDoc().einschaltMailAt)
|
||||
})
|
||||
|
||||
test('Team „Ticket nachlegen“ der Zeiten (gekauft, ohne Zahlungsvermerk): bleibt der Zahlungsweg (409)', async () => {
|
||||
|
||||
@@ -21,7 +21,7 @@ const aw = await startAppwriteMock({
|
||||
},
|
||||
datetimes: {
|
||||
...ZEITEN_DATETIMES,
|
||||
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'k24At'],
|
||||
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'einschaltMailAt'],
|
||||
featureConsents: ['acceptedAt'],
|
||||
},
|
||||
defaultLimit: 25,
|
||||
|
||||
Reference in new issue
Block a user