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' import { mailSink } from './support/mailSink.js' // K01 und K12 behaupten für Betriebe (Fassung 6 ohne Sofortbeginn-Zeitstempel) kein „ausdrücklich verlangt“ const aw = await startAppwriteMock({ schema: { subscriptions: schemaKeys('subscriptions'), contractEvents: schemaKeys('contractEvents'), hourCredits: schemaKeys('hourCredits'), featureConsents: schemaKeys('featureConsents'), }, }) const st = await startFakeStripe() useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url }) Object.assign(process.env, { KUNDENANSICHT_NEU: 'pilot', KUNDENANSICHT_NEU_PILOT_EMAILS: 'K@example.invalid, k3@example.invalid' }) const { config } = await import('../server/config.js') const { startTestApp, postWebhook } = await import('./support/testApp.js') const { hostingDoc, consent, seedFixtures, stripeSubscription } = await import('./support/fixtures.js') const { default: aboRoutes } = await import('../server/routes/abo.js') const { switchToWartung } = await import('../server/services/planSwitch.js') const { listContractEvents } = await import('../server/services/contractEvents.js') const { deleteDocument } = await import('../server/services/appwriteAdmin.js') const { setMailTransportForTests } = await import('../server/services/mailer.js') const { loadTemplate, renderMail } = await import('../server/services/mailTemplates.js') const app = await startTestApp({ '/api/abo': aboRoutes }) const mails = mailSink() setMailTransportForTests(mails) after(() => { setMailTransportForTests(null) return Promise.all([app.close(), aw.close(), st.close()]) }) seedFixtures(aw) const price = (id, lookup_key, unit_amount) => ({ id, object: 'price', active: true, lookup_key, unit_amount, recurring: { interval: 'month' } }) st.prices.set('price_hosting', price('price_hosting', config.products.hosting.lookupKey, config.products.hosting.amountCents)) st.prices.set('price_wartung', price('price_wartung', config.products.wartung.lookupKey, config.products.wartung.amountCents)) st.prices.set('price_mailStorage', price('price_mailStorage', config.mailStorage.lookupKey, config.mailStorage.amountCents)) const nowUnix = () => Math.floor(Date.now() / 1000) const K12_SUBJECT = 'Ihr Wechsel ins WEBklar Wartungs-Abo – Bestätigung' const CARD = { id: 'pm_card_visa', object: 'payment_method', type: 'card' } const sampleValues = (tpl) => Object.fromEntries(tpl.placeholders.map((p) => [p, `Wert-${p}`])) const switchInvoice = (overrides = {}) => ({ id: 'in_wechsel', object: 'invoice', status: 'paid', amount_due: 4100, billing_reason: 'subscription_update', hosted_invoice_url: 'https://invoice.stripe.test/i/in_wechsel', ...overrides }) async function clearEvents(customerId) { for (const e of await listContractEvents(customerId)) await deleteDocument(config.collections.contractEvents, e.$id) } async function resetHosting() { await clearEvents('c1') aw.seed('subscriptions', { ...hostingDoc }) st.subscriptions.set('sub_h', stripeSubscription({ id: 'sub_h', customerId: 'c1', productKey: 'hosting', default_payment_method: CARD })) st.nextSubscriptionUpdate = null mails.sent.length = 0 } test('Vorlage K12: der Absatz „ausdrücklich verlangt“ hängt am Block sofortverlangen', () => { const tpl = loadTemplate('K12') const werte = sampleValues(tpl) assert.ok(tpl.blocks.includes('sofortverlangen')) assert.match(renderMail('K12', werte, { sofortverlangen: true }).text, /Sie haben ausdrücklich verlangt, dass wir vor Ablauf der Widerrufsfrist mit dem Wartungs-Abo beginnen/) assert.doesNotMatch(renderMail('K12', werte, {}).text, /ausdrücklich verlangt|Wertersatz/) assert.match(renderMail('K12', werte, {}).text, /haben wir am Wert-zustimmung_zeitpunkt Uhr erhalten\./, 'die Zustimmungszeile bleibt') assert.deepEqual(renderMail('K12', werte, { sofortverlangen: true }).problems, []) assert.deepEqual(renderMail('K12', werte, {}).problems, []) }) test('K12 im Betrieb-Wechsel ohne den Satz, im Privat-Wechsel und in der alten Ansicht mit dem Satz', async () => { const text = async (consentExtra) => { await resetHosting() st.nextSubscriptionUpdate = { latest_invoice: switchInvoice() } const { earlyStartConsentAt: _x, ...ohne } = consent await switchToWartung('c1', { ...ohne, ...consentExtra }, nowUnix()) return mails.sent.find((m) => m.subject === K12_SUBJECT).text } assert.doesNotMatch(await text({ kundenart: 'unternehmen' }), /ausdrücklich verlangt|Wertersatz/) assert.match(await text({ kundenart: 'privat', earlyStartConsentAt: consent.consentAt }), /Sie haben ausdrücklich verlangt/) assert.match(await text({ earlyStartConsentAt: consent.consentAt }), /Sie haben ausdrücklich verlangt/) }) // Abo-IDs sind 28 Zeichen wie bei Stripe (sub_ + 24) const SUB_NEW = 'sub_1SD6vPRxAbCdEfGhIjKlMnOp' const checkoutCompleted = (subscriptionId, metadata = {}) => ({ id: `evt_${subscriptionId}`, object: 'event', type: 'checkout.session.completed', created: Math.floor(Date.now() / 1000), data: { object: { id: `cs_${subscriptionId}`, object: 'checkout.session', mode: 'subscription', subscription: subscriptionId, customer: 'cus_c3', customer_details: { email: 'k3@example.invalid', name: 'Kunde Drei' }, metadata: { webklarCustomerId: 'c3', productKey: 'wartung', ...metadata }, }, }, }) async function cleanupWebhook(subscriptionId) { await clearEvents('c3') await deleteDocument(config.collections.subscriptions, subscriptionId).catch(() => {}) } test('K01: Fassung 6 ohne Zeitstempel (Betrieb) ohne „ausdrücklich verlangt“, mit Zeitstempel und in Fassung 5 mit dem Satz', async () => { const k01 = async (metadata) => { st.subscriptions.set(SUB_NEW, stripeSubscription({ id: SUB_NEW, customerId: 'c3', productKey: 'wartung', metadata })) mails.sent.length = 0 try { assert.equal((await postWebhook(checkoutCompleted(SUB_NEW))).status, 200) return mails.sent.find((m) => m.subject.includes('Vertragsbestätigung')).text } finally { await cleanupWebhook(SUB_NEW) } } assert.doesNotMatch(await k01({ termsVersion: config.abo.termsVersionNeu, startChoice: 'sofort', earlyStartConsentAt: '' }), /ausdrücklich verlangt/) assert.match(await k01({ termsVersion: config.abo.termsVersionNeu, startChoice: 'sofort', earlyStartConsentAt: new Date().toISOString() }), /ausdrücklich verlangt/) assert.match(await k01({ termsVersion: config.abo.termsVersion, startChoice: 'sofort', earlyStartConsentAt: '' }), /ausdrücklich verlangt/) })