Files
Webklar-Kundenbereich/test/funktionenAdminAbo.test.js
T
ClaudeandClaude Sonnet 5.5 9c04f806eb Kundenansicht neu E-Fix F4: Team-Wiedereinschalten ohne gesendete Abrechnung nimmt die gespeicherte (Aufbauphase)
WOMS fragt nach einer Team-Sperre die Abrechnung nicht mehr ab und sendet keine; der Server setzte dann pauschal 'abo', pruefeAboDeckung warf
409 abo_fehlt, und eine in der Aufbauphase ('projekt', noch kein Abo) eingebaute Funktion liess sich nicht wieder einschalten.
Jetzt gilt bei disabled/team die am Dokument gespeicherte einrichtungAbrechnung, sonst wie bisher 'abo'; eine ausdrücklich gesendete
Abrechnung geht vor. Test in funktionenAdminAbo.test.js (Aufbauphase: ein, aus, ohne Angabe wieder ein = 200 mit warnung; mit abo hart).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 21:32:28 +00:00

136 lines
12 KiB
JavaScript

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 { useTestEnv } from './support/testEnv.js'
import { schemaKeys } from './support/schema.js'
// Team-Aktivierung im Abo-Modell (P6 Task 7, V10): POST /api/team/funktionen/:customerId/:key mit abrechnung. Pilotkunde c1 (Wartung),
// c2 außerhalb der neuen Ansicht. Aufbauphase (E9, N28): projekt ohne jedes bisherige Abo ergibt 200 mit warnung abo_fehlt.
const aw = await startAppwriteMock({
schema: {
portalFeatures: schemaKeys('portalFeatures'),
subscriptions: schemaKeys('subscriptions'),
contractEvents: schemaKeys('contractEvents'),
featureConsents: schemaKeys('featureConsents'),
},
})
const st = await startFakeStripe()
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { config } = await import('../server/config.js')
const { startTestApp, post } = await import('./support/testApp.js')
const { hostingDoc, seedFixtures, subscriptionDoc, wartungDoc } = await import('./support/fixtures.js')
const { default: teamRoutes } = await import('../server/routes/team/index.js')
const { deleteDocument, updateDocument } = await import('../server/services/appwriteAdmin.js')
const { setMailTransportForTests } = await import('../server/services/mailer.js')
const { ABRECHNUNGS_ZEILEN } = await import('../server/services/ticketAdmin.js')
const app = await startTestApp({ '/api/team': teamRoutes })
const mails = mailSink()
setMailTransportForTests(mails)
const MODUS_SHOP = { mode: config.shop.mode, pilotEmails: [...config.shop.pilotEmails] }
const MODUS_NEU = { mode: config.kundenansichtNeu.mode, pilotEmails: [...config.kundenansichtNeu.pilotEmails] }
after(() => {
config.shop.mode = MODUS_SHOP.mode
config.shop.pilotEmails = MODUS_SHOP.pilotEmails
config.kundenansichtNeu.mode = MODUS_NEU.mode
config.kundenansichtNeu.pilotEmails = MODUS_NEU.pilotEmails
setMailTransportForTests(null)
return Promise.all([app.close(), aw.close(), st.close()])
})
const auth = { headers: { authorization: 'Bearer jwt-staff' } }
const go = (cid, key, body = {}, pid = 'p1') => post(`/api/team/funktionen/${cid}/${key}`, { projectId: pid, action: 'activate', ...body }, auth)
const feat = (key, pid = 'p1') => aw.docs('portalFeatures').get(`f_${pid}_${key}`)
const nichts = () => assert.deepEqual([aw.docs('portalFeatures').size, aw.docs('workorders').size, aw.docs('featureConsents').size, mails.sent.length], [0, 0, 0, 0])
// aw.docs() liefert eine Kopie: Dokumente werden ueber die API geloescht
const leere = async (...collections) => {
for (const col of collections) for (const id of aw.docs(col).keys()) await deleteDocument(col, id)
}
beforeEach(async () => {
await leere('portalFeatures', 'workorders', 'featureConsents', 'subscriptions', 'websiteProjects')
mails.sent.length = 0
seedFixtures(aw)
aw.seed('employees', { $id: 'e_staff2', userId: 'u_staff2', displayName: 'Andrej', email: 'andrej@webklar.com' })
aw.seed('subscriptions', subscriptionDoc('sub_h', wartungDoc))
for (const [id, customerId] of [['p1', 'c1'], ['p2', 'c1'], ['p3', 'c2']]) {
aw.seed('websiteProjects', { $id: id, customerId, projectName: `Projekt ${id}`, subdomain: id, repoFullName: `JUSN/${id}`, previewMode: 'auto', previewDetected: 'static' })
}
config.kundenansichtNeu.mode = 'pilot'
config.kundenansichtNeu.pilotEmails = ['k@example.invalid']
mock.method(console, 'error', () => {})
mock.method(console, 'warn', () => {})
mock.method(console, 'log', () => {})
})
afterEach(() => mock.restoreAll())
test('Pilotkunde mit Wartung: setup, unlockedBy abo, Ticket mit Abo-Zeile, K38, kein T24; im Projekt: projekt', async () => {
const r = await go('c1', 'news'); assert.equal(r.status, 200, JSON.stringify(r.body)); assert.equal(r.body.warnung, undefined) // Abo deckt: keine Warnung
const d = feat('news'); assert.deepEqual([d.state, d.unlockedBy, d.einrichtungAbrechnung, d.unlockedByPurchase], ['setup', 'abo', 'abo', false])
const t = aw.docs('workorders').get(d.ticketId); assert.deepEqual([t.einrichtungAbrechnung, t.details.endsWith(ABRECHNUNGS_ZEILEN.abo)], ['abo', true])
assert.deepEqual([mails.sent.filter((m) => /^Deine Funktion wird eingerichtet/.test(m.subject)).length, mails.sent.filter((m) => m.subject.startsWith('[Abo]')).length], [1, 0])
assert.equal((await go('c1', 'shop', { abrechnung: 'projekt' })).status, 200); const s = feat('shop'); assert.deepEqual([s.unlockedBy, s.einrichtungAbrechnung], ['projekt', 'projekt'])
assert.ok(aw.docs('workorders').get(s.ticketId).details.endsWith(ABRECHNUNGS_ZEILEN.projekt))
})
test('kein deckendes Abo: 409 abo_fehlt mit Grund, nichts geschrieben (abo immer; projekt, sobald der Kunde ein Abo hat oder hatte); ungültige Abrechnung 400', async () => {
for (const [abo, grund, wechsel] of [[hostingDoc, 'falsches_abo', true], [{ ...wartungDoc, cancelAtPeriodEnd: true }, 'gekuendigt', false], [{ ...wartungDoc, status: 'canceled' }, 'kein_abo', false]]) {
await leere('subscriptions'); aw.seed('subscriptions', subscriptionDoc('sub_h', abo))
for (const abrechnung of ['abo', 'projekt']) { const r = await go('c1', 'termine', { abrechnung }); assert.deepEqual([r.status, r.body.code, r.body.grund, r.body.wechsel, typeof r.body.error], [409, 'abo_fehlt', grund, wechsel, 'string'], `${grund} ${abrechnung}`) }
nichts()
}
await leere('subscriptions') // noch nie ein Abo: aus dem Abo ist das Stundenkonto nötig, also hart
const ohne = await go('c1', 'termine'); assert.deepEqual([ohne.status, ohne.body.code, ohne.body.grund], [409, 'abo_fehlt', 'kein_abo']); nichts()
assert.equal((await go('c1', 'news', { abrechnung: 'frei' })).status, 400); nichts()
})
test('Projektstart ohne jedes Abo (E9, Aufbauphase): 200 mit warnung abo_fehlt, Funktion in setup mit Ticket; abgebrochene Bestellung zählt nicht; mit deckendem Abo keine Warnung', async () => {
await leere('subscriptions'); aw.seed('subscriptions', subscriptionDoc('sub_i', { ...wartungDoc, status: 'incomplete' }))
const r = await go('c1', 'shop', { abrechnung: 'projekt' }); assert.equal(r.status, 200, JSON.stringify(r.body)); assert.deepEqual([r.body.warnung, r.body.grund], ['abo_fehlt', 'kein_abo'])
const d = feat('shop'); assert.deepEqual([d.state, d.unlockedBy, d.einrichtungAbrechnung], ['setup', 'projekt', 'projekt']); assert.ok(aw.docs('workorders').get(d.ticketId))
assert.equal(mails.sent.filter((m) => /^Deine Funktion wird eingerichtet/.test(m.subject)).length, 1) // K38 mit dem Satz zur Bedingung (Task 3)
await leere('subscriptions'); const z = await go('c1', 'news', { abrechnung: 'projekt' }, 'p2'); assert.deepEqual([z.status, z.body.warnung], [200, 'abo_fehlt'])
aw.seed('subscriptions', subscriptionDoc('sub_h', wartungDoc)); const ok = await go('c1', 'news', { abrechnung: 'projekt' }); assert.deepEqual([ok.status, ok.body.warnung, ok.body.grund], [200, undefined, undefined])
})
test('Kunde außerhalb der neuen Ansicht: wie heute direkt aktiv, abrechnung im Body wird ignoriert', async () => {
const r = await go('c2', 'news', { abrechnung: 'projekt' }, 'p3'); assert.equal(r.status, 200)
const d = feat('news', 'p3'); assert.deepEqual([d.state, d.enabled, d.ticketId, d.unlockedBy, d.einrichtungAbrechnung], ['active', true, undefined, undefined, undefined]); assert.equal(aw.docs('workorders').size, 0)
config.kundenansichtNeu.mode = 'off'; await leere('portalFeatures'); assert.equal(feat('news'), undefined)
assert.equal((await go('c1', 'news')).status, 200); assert.equal(feat('news').state, 'active'); config.kundenansichtNeu.mode = 'pilot'
})
test('Bestand und vom Team abgeschaltet: kauf wie heute, disabled/team direkt aktiv (mit Abo), sonst 409', async () => {
aw.seed('portalFeatures', { $id: 'f_p2_termine', customerId: 'c1', projectId: 'p2', featureKey: 'termine', state: 'disabled', disabledReason: 'abo_ende', unlockedBy: 'kauf', unlockedByPurchase: true })
await leere('subscriptions'); const alt = await go('c1', 'termine', {}, 'p2'); assert.deepEqual([alt.status, alt.body.code, /Wartungs-Abo des Kunden läuft nicht/.test(alt.body.error)], [409, undefined, true])
aw.seed('portalFeatures', { $id: 'f_p1_shop', customerId: 'c1', projectId: 'p1', featureKey: 'shop', state: 'disabled', disabledReason: 'team', unlockedBy: 'abo', einrichtungAbrechnung: 'abo', activatedAt: '2026-10-01T09:00:00.000Z' })
assert.equal((await go('c1', 'shop')).body.code, 'abo_fehlt'); aw.seed('subscriptions', subscriptionDoc('sub_h', wartungDoc))
assert.equal((await go('c1', 'shop')).status, 200); assert.deepEqual([feat('shop').state, aw.docs('workorders').size], ['active', 0])
})
test('Wiedereinschalten nach Team-Sperre ohne gesendete abrechnung: die am Dokument gespeicherte Abrechnung gilt, nicht pauschal abo (F4)', async () => {
// Aufbauphase: der Kunde hatte nie ein Abo, das Team schaltet den Webshop 'im Projekt enthalten' ein, ab und wieder ein; WOMS fragt nach einer
// Team-Sperre die Abrechnung nicht mehr ab (die Abrechnung steht schon) und sendet keine abrechnung
await leere('subscriptions')
const an = await go('c1', 'shop', { abrechnung: 'projekt' }); assert.deepEqual([an.status, an.body.warnung], [200, 'abo_fehlt'], JSON.stringify(an.body))
const aus = await go('c1', 'shop', { action: 'disable' }); assert.equal(aus.status, 200, JSON.stringify(aus.body))
assert.deepEqual([feat('shop').state, feat('shop').disabledReason, feat('shop').einrichtungAbrechnung], ['disabled', 'team', 'projekt'])
const wieder = await go('c1', 'shop'); assert.equal(wieder.status, 200, JSON.stringify(wieder.body))
assert.deepEqual([wieder.body.warnung, wieder.body.grund, feat('shop').state], ['abo_fehlt', 'kein_abo', 'active'])
// wieder aus; eine ausdrücklich gesendete Abrechnung gilt vor der gespeicherten: abo ohne Abo bleibt hart
assert.equal((await go('c1', 'shop', { action: 'disable' })).status, 200)
const hart = await go('c1', 'shop', { abrechnung: 'abo' }); assert.deepEqual([hart.status, hart.body.code, hart.body.grund], [409, 'abo_fehlt', 'kein_abo']); assert.equal(feat('shop').state, 'disabled')
// gespeichert abo (oder ohne Angabe): ohne gesendete Abrechnung gilt abo wie bisher
await updateDocument('portalFeatures', 'f_p1_shop', { einrichtungAbrechnung: 'abo', unlockedBy: 'abo' })
const abo = await go('c1', 'shop'); assert.deepEqual([abo.status, abo.body.code, abo.body.grund], [409, 'abo_fehlt', 'kein_abo'])
await updateDocument('portalFeatures', 'f_p1_shop', { einrichtungAbrechnung: '' })
assert.equal((await go('c1', 'shop')).body.code, 'abo_fehlt')
})
test('Textform: Nachweis vor dem Zustand, K38 mit PDF-Anhängen, kein K16; Aktion ticket legt das Ticket ohne Stripe nach', async () => {
config.shop.mode = 'on'; config.shop.pilotEmails = []
const text = { textformDate: '2026-10-10', textformChannel: 'whatsapp' }
aw.failNext({ collection: 'featureConsents', method: 'POST', status: 500 }) // Nachweis scheitert: Funktion bleibt aus, kein Ticket, keine Mail
assert.ok((await go('c1', 'shop', text)).status >= 500); assert.deepEqual([feat('shop')?.state ?? 'off', aw.docs('workorders').size, mails.sent.length], ['off', 0, 0])
const r = await go('c1', 'shop', text); assert.equal(r.status, 200, JSON.stringify(r.body))
const n = [...aw.docs('featureConsents').values()].find((c) => c.featureKey === 'shop'); assert.match(n.konto, /^Mitarbeiter: /)
assert.ok(mails.sent.find((m) => /^Deine Funktion wird eingerichtet/.test(m.subject)).attachments.length >= 2); assert.equal(mails.sent.filter((m) => /ist bereit/.test(m.subject)).length, 0)
await deleteDocument('workorders', feat('shop').ticketId); await updateDocument('portalFeatures', 'f_p1_shop', { ticketId: '' }) // Ticket weg: nachlegen heilt über einschalten
const calls = st.calls.length; const t = await go('c1', 'shop', { action: 'ticket' }); assert.equal(t.status, 200, JSON.stringify(t.body)); assert.ok(feat('shop').ticketId); assert.equal(st.calls.length, calls)
})