Ö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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user