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) })