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' // Team-Aktion "Ticket nachlegen" auf Oeffnungszeiten live mit einer Behalten-Zahlung am Dokument (Merge mit TP2, Fix Runde 1): ticketNachlegen // holt die bezahlte Sitzung bei Stripe und fuettert sie dem Webhook-Weg. Eine Behalten-Sitzung (metadata.zweck 'behalten') ginge dort in // bucheBehalten, das `feature-admin:` nimmt, waehrend die Team-Route diese Sperre schon haelt (nicht wiederbetretbar => Stillstand, // der den Schluessel dauerhaft blockiert). Sie wird darum mit 409 abgelehnt, ohne etwas zu veraendern. const aw = await startAppwriteMock({ schema: { portalFeatures: schemaKeys('portalFeatures'), subscriptions: schemaKeys('subscriptions'), featureConsents: schemaKeys('featureConsents'), oeffnungszeiten: schemaKeys('oeffnungszeiten'), zeitenErinnerungen: schemaKeys('zeitenErinnerungen'), }, 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 { startTestApp, post } = await import('./support/testApp.js') const { seedFixtures } = await import('./support/fixtures.js') const { deleteDocument } = await import('../server/services/appwriteAdmin.js') const { withLocks } = await import('../server/services/keyedLock.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 { default: teamRoutes } = await import('../server/routes/team/index.js') const app = await startTestApp({ '/api/team': teamRoutes }) after(() => Promise.all([app.close(), aw.close(), st.close()])) after(() => setMailTransportForTests(null)) seedFixtures(aw) const STAFF = { headers: { Authorization: 'Bearer jwt-staff' } } const doc = () => aw.docs('portalFeatures').get('f_p1_zeiten') const tickets = () => [...aw.docs('workorders').values()] const ticketAktion = () => post('/api/team/funktionen/c1/zeiten', { projectId: 'p1', action: 'ticket' }, STAFF) /** Antwortet die Anfrage nicht binnen 4 s, ist sie haengengeblieben (Stillstand statt 409). */ const binnen4s = (promise) => Promise.race([promise, new Promise((resolve) => setTimeout(() => resolve('HANG'), 4000))]) let sink let rebuilds beforeEach(async () => { for (const col of ['portalFeatures', 'oeffnungszeiten', 'zeitenErinnerungen', 'workorders', 'websiteProjects']) { for (const id of [...aw.docs(col).keys()]) await deleteDocument(col, id) } resetZeitenCacheForTests() st.checkoutSessions.clear() aw.seed('customers', { $id: 'c1', appwriteUserId: 'u1', email: 'k@example.invalid', name: 'Kunde Eins', status: 'active', portalAccessEnabled: true }) aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Projekt p1', subdomain: 'sub-p1', repoFullName: 'JUSN/p1', previewMode: 'auto', previewDetected: 'static' }) sink = mailSink() setMailTransportForTests(sink) rebuilds = [] zeitenDeps.rebuild = async (p) => { rebuilds.push(p.$id) return { queued: true } } mock.method(console, 'error', () => {}) mock.method(console, 'warn', () => {}) mock.method(console, 'log', () => {}) }) afterEach(() => mock.restoreAll()) /** Zustand, den bucheBehalten hinterlaesst: setup ohne Ticket, stripeSessionId = bezahlte Behalten-Sitzung, unlockedBy kauf. */ function behaltenDokument(sitzung = { payment_status: 'paid' }) { aw.seed('portalFeatures', { $id: 'f_p1_zeiten', customerId: 'c1', projectId: 'p1', featureKey: 'zeiten', state: 'setup', enabled: false, siteKey: 'abcdefghijklmnop', unlockedBy: 'kauf', unlockedByPurchase: true, purchaseStatus: 'paid', stripeSessionId: 'cs_beh', ticketId: '', einbau: 'angestossen', }) st.checkoutSessions.set('cs_beh', { id: 'cs_beh', object: 'checkout.session', mode: 'payment', status: 'complete', invoice: null, metadata: { customerId: 'c1', projectId: 'p1', featureKey: 'zeiten', zweck: 'behalten' }, ...sitzung, }) } test('ticket auf Zeiten mit bezahlter Behalten-Sitzung: sofort 409, nichts verändert, die Sperre feature-admin bleibt frei', async () => { behaltenDokument() const vorher = { ...doc() } const res = await binnen4s(ticketAktion()) assert.notEqual(res, 'HANG', 'die Anfrage darf nicht an feature-admin haengenbleiben') assert.equal(res.status, 409, JSON.stringify(res.body)) assert.match(res.body.error, /Behalten-Zahlung/) assert.deepEqual(doc(), vorher, 'Dokument unveraendert') assert.equal(tickets().length, 0, 'kein Ticket') assert.deepEqual(rebuilds, []) assert.equal(sink.sent.length, 0, 'keine Mail') // die Sperre ist frei: ein Probelauf und danach eine weitere Team-Aktion am selben Projekt kommen sofort durch assert.equal(await binnen4s(withLocks(['feature-admin:p1'], async () => 'frei')), 'frei') const aus = await binnen4s(post('/api/team/funktionen/c1/zeiten', { projectId: 'p1', action: 'disable' }, STAFF)) assert.notEqual(aus, 'HANG', 'die folgende Team-Aktion darf nicht blockiert sein') assert.equal(aus.status, 200, JSON.stringify(aus.body)) assert.equal(doc().state, 'disabled') }) test('ticket auf Zeiten mit noch nicht bezahlter Behalten-Sitzung: ebenfalls 409 und kein Stillstand', async () => { behaltenDokument({ payment_status: 'unpaid', status: 'open' }) const res = await binnen4s(ticketAktion()) assert.notEqual(res, 'HANG') assert.equal(res.status, 409, JSON.stringify(res.body)) assert.equal(await binnen4s(withLocks(['feature-admin:p1'], async () => 'frei')), 'frei') })