import { test, after } from 'node:test' import assert from 'node:assert/strict' import { startAppwriteMock } from './support/appwriteMock.js' import { startFakeStripe } from './support/fakeStripe.js' import { useTestEnv } from './support/testEnv.js' import { schemaKeys } from './support/schema.js' // Einrichtungs-Ticket mit Abrechnung (V7) und unlockedBy in featureView fuer alle Funktionen. // workorders, websiteProjects und customers legt das Setup-Skript nicht an: ohne Schema im Nachbau. const aw = await startAppwriteMock({ schema: { portalFeatures: schemaKeys('portalFeatures') } }) const st = await startFakeStripe() after(() => Promise.all([aw.close(), st.close()])) useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url }) const { createFeatureTicket, ABRECHNUNGS_ZEILEN } = await import('../server/services/ticketAdmin.js') const { featureView, ticketNachlegen } = await import('../server/services/siteFeatures.js') aw.seed('customers', { $id: 'c1', name: 'Kunde Eins', location: 'Berlin', email: 'kunde1@example.test' }) const ALT = ['createdAt', 'customerId', 'customerLocation', 'customerName', 'details', 'priority', 'serviceType', 'status', 'systemType', 'topic', 'type', 'woid'] const base = { customerId: 'c1', project: { $id: 'p1', projectName: 'Bäckerei' }, featureKey: 'termine', siteKey: 'abcdefghijklmnop', topic: 'T', details: 'Zeile 1' } const nutzer = (doc) => Object.keys(doc).filter((k) => !k.startsWith('$')).sort() // aw.docs() liefert eine Kopie des Standes zum Aufruf: das Ticket erst anlegen, dann nachsehen const ticketVon = async (args) => { const { ticketId } = await createFeatureTicket(args) return aw.docs('workorders').get(ticketId) } test('abrechnung abo und projekt: Felder und letzte Zeile', async () => { assert.deepEqual(ABRECHNUNGS_ZEILEN, { abo: 'Zeit im Arbeitsblatt erfassen und aufs Abo-Stundenkonto anrechnen (Einrichtung aus dem Abo, Vorschuss möglich).', projekt: 'Zeit im Arbeitsblatt erfassen, nicht aufs Abo anrechnen (im Projekt enthalten).', }) const t = await ticketVon({ ...base, abrechnung: 'abo' }) assert.deepEqual([t.einrichtungFuer, t.einrichtungAbrechnung], ['termine', 'abo']) assert.equal(t.details, `Zeile 1\n${ABRECHNUNGS_ZEILEN.abo}`) assert.ok((await ticketVon({ ...base, abrechnung: 'projekt' })).details.endsWith('\nZeit im Arbeitsblatt erfassen, nicht aufs Abo anrechnen (im Projekt enthalten).')) }) test('ohne abrechnung: keine neuen Felder, Details unverändert (alte Ansicht)', async () => { for (const abrechnung of [undefined, '']) { const t = await ticketVon({ ...base, abrechnung }) assert.deepEqual(nutzer(t), ALT) assert.equal(t.details, 'Zeile 1') } }) test('ungültiger Wert: TypeError, kein Ticket', async () => { const n = aw.docs('workorders').size for (const abrechnung of ['frei', 'Abo', null, 5]) await assert.rejects(() => createFeatureTicket({ ...base, abrechnung }), TypeError, String(abrechnung)) assert.equal(aw.docs('workorders').size, n) }) test('lange Details: gekürzt auf 7000, die Schlusszeile bleibt; auch im Notfallweg nach 400', async () => { const lang = Array.from({ length: 400 }, (_, i) => `Zeile ${i} ${'x'.repeat(30)}`).join('\n') const t = await ticketVon({ ...base, details: lang, abrechnung: 'abo' }) assert.ok(t.details.length <= 7000 && t.details.endsWith(ABRECHNUNGS_ZEILEN.abo)) assert.ok(t.details.length > 6800, 'es wird nicht zu stark gekürzt') aw.failNext({ collection: 'workorders', method: 'POST', status: 400, message: 'zu lang' }) const n = await ticketVon({ ...base, details: lang, fallbackDetails: 'Notfall', abrechnung: 'projekt' }) assert.ok(n.details.startsWith('Notfall') && n.details.endsWith(ABRECHNUNGS_ZEILEN.projekt)) }) test('featureView: unlockedBy für alle Funktionen, aboEndeAt/einbau nur bei Zeiten', async () => { assert.equal((await featureView({ state: 'setup', unlockedBy: 'abo' }, 'news')).unlockedBy, 'abo') assert.equal((await featureView({ state: 'setup', unlockedBy: 'projekt' }, 'shop')).unlockedBy, 'projekt') assert.equal((await featureView({ state: 'active', unlockedByPurchase: true }, 'termine')).unlockedBy, 'kauf') assert.equal((await featureView(undefined, 'news')).unlockedBy, null) assert.equal((await featureView({ state: 'off', unlockedBy: 'abo' }, 'news')).unlockedBy, null) assert.equal(Object.hasOwn(await featureView({ state: 'setup', unlockedBy: 'abo' }, 'news'), 'einbau'), false) assert.equal(Object.hasOwn(await featureView({ state: 'setup', unlockedBy: 'abo' }, 'news'), 'aboEndeAt'), false) const zeiten = await featureView({ state: 'setup', unlockedBy: 'abo', einbau: 'angestossen' }, 'zeiten') assert.deepEqual([zeiten.unlockedBy, zeiten.einbau, Object.hasOwn(zeiten, 'aboEndeAt')], ['abo', 'angestossen', true]) }) test('ersetztes Ticket trägt die Abrechnung des Dokuments', async () => { aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Bäckerei' }) aw.seed('portalFeatures', { $id: 'f_p1_news', customerId: 'c1', projectId: 'p1', featureKey: 'news', siteKey: 'abcdefghijklmnop', state: 'setup', enabled: false, unlockedBy: 'abo', einrichtungAbrechnung: 'abo', ticketId: 'weg' }) const { doc } = await ticketNachlegen({ customerId: 'c1', projectId: 'p1', key: 'news', by: 'Kenso' }) const ticket = aw.docs('workorders').get(doc.ticketId) assert.deepEqual([ticket.einrichtungAbrechnung, ticket.einrichtungFuer], ['abo', 'news']) assert.ok(ticket.details.endsWith(ABRECHNUNGS_ZEILEN.abo)) }) test('ersetztes Ticket ohne Abrechnung am Dokument: wie bisher, keine neuen Felder', async () => { aw.seed('portalFeatures', { $id: 'f_p1_termine', customerId: 'c1', projectId: 'p1', featureKey: 'termine', siteKey: 'bbcdefghijklmnop', state: 'setup', enabled: false, unlockedByPurchase: true, ticketId: 'weg2' }) const { doc } = await ticketNachlegen({ customerId: 'c1', projectId: 'p1', key: 'termine', by: 'Kenso' }) assert.deepEqual(nutzer(aw.docs('workorders').get(doc.ticketId)), ALT) })