Kundenansicht P6: K38 an den Kunden und T24 ans Team beim Einschalten
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cdc8f5c424
commit
408f7d0cc2
4 files changed
+244
-7
No files matched your search
@@ -0,0 +1,116 @@
|
||||
import { test, after, beforeEach, afterEach, mock } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { startAppwriteMock } from './support/appwriteMock.js'
|
||||
import { startFakeStripe } from './support/fakeStripe.js'
|
||||
import { mailSink } from './support/mailSink.js'
|
||||
import { schemaKeys } from './support/schema.js'
|
||||
import { useTestEnv } from './support/testEnv.js'
|
||||
import { ZEITEN_DATETIMES } from '../server/services/zeiten/schema.js'
|
||||
|
||||
// Meldung beim Einschalten im Abo-Modell (P6 Task 6): K38 an den Kunden, T24 ans Team (aboFunktionenMeldung.js, Aufruf am Ende
|
||||
// von einschalten). Aufbau wie test/funktionEinschalten.test.js, dazu hourCredits und die Mail-Senke.
|
||||
const aw = await startAppwriteMock({
|
||||
schema: {
|
||||
portalFeatures: schemaKeys('portalFeatures'),
|
||||
subscriptions: schemaKeys('subscriptions'),
|
||||
contractEvents: schemaKeys('contractEvents'),
|
||||
featureConsents: schemaKeys('featureConsents'),
|
||||
oeffnungszeiten: schemaKeys('oeffnungszeiten'),
|
||||
zeitenErinnerungen: schemaKeys('zeitenErinnerungen'),
|
||||
hourCredits: schemaKeys('hourCredits'),
|
||||
},
|
||||
datetimes: {
|
||||
...ZEITEN_DATETIMES,
|
||||
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'einschaltMailAt'],
|
||||
featureConsents: ['acceptedAt'],
|
||||
},
|
||||
defaultLimit: 25,
|
||||
})
|
||||
const st = await startFakeStripe()
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
const { config } = await import('../server/config.js')
|
||||
const { customers, hostingDoc, seedFixtures, subscriptionDoc, wartungDoc } = await import('./support/fixtures.js')
|
||||
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
|
||||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||||
const { zeitenDeps } = await import('../server/services/zeiten/neubau.js')
|
||||
const { resetZeitenCacheForTests } = await import('../server/services/zeiten/cache.js')
|
||||
const { einschalten } = await import('../server/services/funktionEinschalten.js')
|
||||
const { meldeEingeschaltet, meldungDeps } = await import('../server/services/aboFunktionenMeldung.js')
|
||||
after(() => Promise.all([aw.close(), st.close()]))
|
||||
after(() => setMailTransportForTests(null))
|
||||
|
||||
const JETZT = new Date('2026-10-17T08:00:00.000Z') // ein Samstag
|
||||
const modusZeiten = { mode: config.zeiten.mode, pilotEmails: [...config.zeiten.pilotEmails] }
|
||||
after(() => {
|
||||
config.zeiten.mode = modusZeiten.mode
|
||||
config.zeiten.pilotEmails = modusZeiten.pilotEmails
|
||||
})
|
||||
|
||||
const E = (over = {}) => einschalten({ customerId: 'c1', projectId: 'p1', key: 'news', by: 'kunde', abrechnung: 'abo', now: JETZT, ...over })
|
||||
const projekt = (id, customerId = 'c1') => aw.seed('websiteProjects', { $id: id, customerId, projectName: `Projekt ${id}`, subdomain: id, repoFullName: `JUSN/${id}`, previewMode: 'auto', previewDetected: 'static' })
|
||||
const kunde = (over = {}) => ({ doc: aw.docs('portalFeatures').get('f_p1_news'), ticketWoid: '100150', by: 'kunde', abrechnung: 'abo', ...over })
|
||||
|
||||
let mails
|
||||
const betreff = (start) => mails.sent.filter((m) => m.subject.startsWith(start))
|
||||
const originalRebuild = zeitenDeps.rebuild
|
||||
beforeEach(async () => {
|
||||
for (const col of ['subscriptions', 'portalFeatures', 'featureConsents', 'oeffnungszeiten', 'workorders', 'websiteProjects', 'contractEvents', 'hourCredits']) {
|
||||
for (const id of aw.docs(col).keys()) await deleteDocument(col, id)
|
||||
}
|
||||
resetZeitenCacheForTests()
|
||||
seedFixtures(aw)
|
||||
aw.seed('subscriptions', subscriptionDoc('sub_h', wartungDoc))
|
||||
for (const id of ['p1', 'p2']) projekt(id)
|
||||
aw.seed('customers', { ...customers.c1 })
|
||||
config.zeiten.mode = 'pilot'
|
||||
config.zeiten.pilotEmails = ['*']
|
||||
mails = mailSink()
|
||||
setMailTransportForTests(mails)
|
||||
zeitenDeps.rebuild = async () => ({ queued: true })
|
||||
mock.method(console, 'error', () => {})
|
||||
mock.method(console, 'warn', () => {})
|
||||
mock.method(console, 'log', () => {})
|
||||
})
|
||||
afterEach(() => {
|
||||
mock.restoreAll()
|
||||
zeitenDeps.rebuild = originalRebuild
|
||||
})
|
||||
|
||||
test('Kunde, Wartung ohne Guthaben: K38 mit Konto-Satz an den Kunden, T24 mit Vorschuss ans Team', async () => {
|
||||
await E() // einschalten wie P5 Task 9: ruft die Meldung selbst
|
||||
const [k38] = betreff('Deine Funktion wird eingerichtet: News-Pinnwand'); assert.deepEqual(k38.to.map((a) => a.address), ['k@example.invalid'])
|
||||
assert.match(k38.text, /geht von deinem Stundenkonto ab/); assert.match(k38.text, /\(Ticket #\w+\)/)
|
||||
const [t24] = betreff('[Abo] Funktion eingeschaltet: News-Pinnwand'); assert.match(t24.text, /Guthaben 0 min, Vorschuss 0 min/)
|
||||
assert.equal(betreff('[Abo] Funktion eingeschaltet').length, 1)
|
||||
})
|
||||
test('Guthaben reicht (Gutschrift 300 min): T24 ohne Vorschuss-Satz; Hosting-Abo: auch keiner', async () => {
|
||||
aw.seed('hourCredits', { $id: 'h1', customerId: 'c1', minutes: 300, grantedAt: '2026-10-01T00:00:00.000Z', expiresAt: '2099-01-01T00:00:00.000Z' })
|
||||
await E(); assert.doesNotMatch(betreff('[Abo] Funktion eingeschaltet')[0].text, /reicht voraussichtlich nicht/)
|
||||
// ein Hosting-Abo hat kein Stundenkonto: auch ohne Guthaben kein Vorschuss-Satz
|
||||
await deleteDocument('hourCredits', 'h1'); await deleteDocument('subscriptions', 'sub_h'); aw.seed('subscriptions', subscriptionDoc('sub_h', hostingDoc))
|
||||
mails.sent.length = 0
|
||||
assert.deepEqual(await meldeEingeschaltet(kunde()), { k38: true, t24: true })
|
||||
assert.doesNotMatch(betreff('[Abo] Funktion eingeschaltet')[0].text, /reicht voraussichtlich nicht/)
|
||||
})
|
||||
test('Terminplaner: K38 mit zwei PDF-Anhängen; PDFs nicht lesbar: K38 ohne Anhang, T24 mit Hinweis', async () => {
|
||||
await E({ key: 'termine' }); const k = betreff('Deine Funktion wird eingerichtet')[0]
|
||||
assert.deepEqual(k.attachments.map((a) => a.contentType), ['application/pdf', 'application/pdf']); assert.match(k.text, /hängen als PDF an/)
|
||||
mails.sent.length = 0; const echt = meldungDeps.anhaenge; meldungDeps.anhaenge = async () => { throw new Error('weg') }
|
||||
try { await E({ key: 'termine', projectId: 'p2' }) } finally { meldungDeps.anhaenge = echt }
|
||||
assert.equal(betreff('Deine Funktion wird eingerichtet')[0].attachments.length, 0); assert.match(betreff('[Abo]')[0].text, /von Hand nachsenden/)
|
||||
})
|
||||
test('Team im Projekt: K38 mit Projektkosten-Satz, kein Konto-Satz, kein T24; Zeiten: nur T24 (K27 bleibt)', async () => {
|
||||
await E({ by: 'team', abrechnung: 'projekt', staff: { name: 'Andrej' }, nachweis: null })
|
||||
const k = betreff('Deine Funktion wird eingerichtet')[0]; assert.match(k.text, /Von deinem Stundenkonto geht nichts ab/); assert.match(k.text, /Die Funktion läuft, solange dein Wartungs-Abo läuft/); assert.doesNotMatch(k.text, /geht von deinem Stundenkonto ab/); assert.equal(betreff('[Abo]').length, 0)
|
||||
mails.sent.length = 0; await E({ key: 'zeiten', projectId: 'p2' })
|
||||
assert.deepEqual([betreff('Deine Funktion wird eingerichtet').length, betreff('[Abo] Funktion eingeschaltet').length], [0, 1])
|
||||
})
|
||||
test('Wiederholung und Heilung: höchstens eine Meldung je Einschalten; Versandfehler und Kunde ohne Adresse brechen nichts ab', async () => {
|
||||
await E(); await assert.rejects(E(), (e) => e.code === 'bereits_eingeschaltet'); assert.equal(betreff('Deine Funktion').length, 1)
|
||||
mails.sent.length = 0; aw.failNext({ collection: 'workorders', method: 'POST', status: 500 })
|
||||
await assert.rejects(E({ projectId: 'p2' }), (e) => e.status === 503); assert.equal(mails.sent.length, 0) // Ticket scheitert: noch keine Mail
|
||||
await E({ projectId: 'p2' }); assert.equal(betreff('Deine Funktion').length, 1) // Heilung: genau eine
|
||||
setMailTransportForTests({ sendMail: async () => { throw new Error('smtp') } })
|
||||
const r = await meldeEingeschaltet(kunde()); assert.deepEqual(r, { k38: false, t24: false })
|
||||
setMailTransportForTests(mails); aw.seed('customers', { ...customers.c1, email: '' }); assert.equal((await meldeEingeschaltet(kunde())).k38, false)
|
||||
})
|
||||
@@ -104,13 +104,15 @@ afterEach(() => {
|
||||
einschaltenDeps.jetzt = () => new Date()
|
||||
})
|
||||
|
||||
test('Erfolg (Betrieb, Wartung): Form, Zustand, Ticket, Nachweis mit Kundenart, keine Mail, kein Preis', async () => {
|
||||
test('Erfolg (Betrieb, Wartung): Form, Zustand, Ticket, Nachweis mit Kundenart, K38 und T24 (P6) ohne Preis, kein Stripe', async () => {
|
||||
aw.seed('customers', { ...customers.c1, kundenart: 'unternehmen' })
|
||||
const r = await go('news')
|
||||
assert.deepEqual([r.status, Object.keys(r.body).sort(), r.body.feature.state, r.body.feature.unlockedBy], [200, ['feature', 'ticketWoid'], 'setup', 'abo'])
|
||||
assert.equal(r.body.ticketWoid, r.body.feature.ticketWoid)
|
||||
assert.equal(aw.docs('workorders').size, 1)
|
||||
assert.deepEqual([sink.sent.length, st.calls.length], [0, 0], 'keine Mail, kein Stripe')
|
||||
// P6: das Einschalten meldet K38 an den Kunden und T24 ans Team (aboFunktionenMeldung.js); kein Preis, kein Stripe
|
||||
assert.deepEqual([sink.sent.map((m) => m.subject.split(':')[0]), st.calls.length], [['Deine Funktion wird eingerichtet', '[Abo] Funktion eingeschaltet'], 0])
|
||||
assert.doesNotMatch(JSON.stringify(sink.sent), /€/)
|
||||
const n = [...aw.docs('featureConsents').values()][0]
|
||||
assert.deepEqual([JSON.parse(n.texte).map((t) => t.id), n.kundenart, n.stripeSessionId], [['einschalten-zeile', 'knopf'], 'unternehmen', ''])
|
||||
assert.doesNotMatch(JSON.stringify(r.body), /€/)
|
||||
|
||||
Reference in new issue
Block a user