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' // Nachbauten starten und die Umgebung setzen, BEVOR config.js (ueber testApp/fixtures/Routen) geladen wird const aw = await startAppwriteMock({ schema: { subscriptions: schemaKeys('subscriptions'), aboOffers: schemaKeys('aboOffers'), contractEvents: schemaKeys('contractEvents'), hourCredits: schemaKeys('hourCredits'), }, }) const st = await startFakeStripe() useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url }) process.env.ABO_TEST_EMAILS = 'test@example.invalid' const { config } = await import('../server/config.js') const { startTestApp, get } = await import('./support/testApp.js') const { hostingDoc, customerCookie, seedFixtures, stripeSubscription } = await import('./support/fixtures.js') const { default: aboRoutes } = await import('../server/routes/abo.js') const { deleteDocument } = await import('../server/services/appwriteAdmin.js') const { setMailTransportForTests } = await import('../server/services/mailer.js') const { validateOfferInput, getOpenOffer, createOffer, offerContext, activateOfferFromCheckout } = await import('../server/services/offers.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 ADMIN = { name: 'Admin', email: 'admin@example.invalid' } // --------------------------------------------------------------------------- // Step 1 (Brief): validateOfferInput + getOpenOffer // --------------------------------------------------------------------------- test('Angebot Hosting: Normalpreis 20 €, Preis darunter', () => { assert.equal(validateOfferInput({ productKey: 'hosting', months: 2, monthlyPriceCents: 1000 }).regularPriceCents, 2000) assert.throws(() => validateOfferInput({ productKey: 'hosting', months: 2, monthlyPriceCents: 2000 }), (e) => e.status === 400) assert.throws(() => validateOfferInput({ months: 2, monthlyPriceCents: 1000 }), (e) => e.status === 400) }) test('Offenes Angebot nur fürs passende Produkt; alte Angebote gelten als Wartung', async () => { aw.seed('aboOffers', { $id: 'o_alt', customerId: 'c1', status: 'offen', validUntil: '2099-01-01T00:00:00.000Z', months: 2, monthlyPriceCents: 2500 }) aw.seed('aboOffers', { $id: 'o_h', customerId: 'c1', status: 'offen', productKey: 'hosting', validUntil: '2099-01-01T00:00:00.000Z', months: 2, monthlyPriceCents: 1000 }) try { assert.equal((await getOpenOffer('c1', 'hosting')).$id, 'o_h') assert.equal((await getOpenOffer('c1', 'wartung')).$id, 'o_alt') } finally { await deleteDocument(config.collections.aboOffers, 'o_alt') await deleteDocument(config.collections.aboOffers, 'o_h') } }) // --------------------------------------------------------------------------- // createOffer: getrennte offene Angebote je Produkt (F32) // --------------------------------------------------------------------------- test('createOffer: offene Angebote fuer Hosting und Wartung koennen nebeneinander bestehen, Konflikt nur im selben Produkt', async () => { // c3 hat kein Abo (Fixtures) const hosting = await createOffer({ customerId: 'c3', productKey: 'hosting', months: 2, monthlyPriceCents: 1000, notifyCustomer: false }, ADMIN) try { assert.equal(hosting.doc.productKey, 'hosting') assert.equal(hosting.doc.regularPriceCents, 2000) assert.equal(hosting.doc.status, 'offen') await assert.rejects( createOffer({ customerId: 'c3', productKey: 'hosting', months: 1, monthlyPriceCents: 500, notifyCustomer: false }, ADMIN), (e) => e.status === 409 ) const wartung = await createOffer({ customerId: 'c3', productKey: 'wartung', months: 1, monthlyPriceCents: 2000, notifyCustomer: false }, ADMIN) try { assert.equal(wartung.doc.productKey, 'wartung') assert.equal((await getOpenOffer('c3', 'hosting')).$id, hosting.doc.$id) assert.equal((await getOpenOffer('c3', 'wartung')).$id, wartung.doc.$id) } finally { await deleteDocument(config.collections.aboOffers, wartung.doc.$id) } } finally { await deleteDocument(config.collections.aboOffers, hosting.doc.$id) } }) test('createOffer: nur bei passendem Produkt wird das Angebot sofort aufs laufende Abo angewendet', async () => { // c1 hat ein laufendes Hosting-Abo (sub_h) st.subscriptions.set('sub_h', stripeSubscription({ id: 'sub_h', customerId: 'c1', productKey: 'hosting' })) const matching = await createOffer({ customerId: 'c1', productKey: 'hosting', months: 2, monthlyPriceCents: 1000, notifyCustomer: false }, ADMIN) try { assert.equal(matching.doc.scope, 'laufend') assert.equal(matching.doc.status, 'aktiv') assert.equal(matching.doc.stripeSubscriptionId, 'sub_h') } finally { await deleteDocument(config.collections.aboOffers, matching.doc.$id) } const other = await createOffer({ customerId: 'c1', productKey: 'wartung', months: 2, monthlyPriceCents: 2000, notifyCustomer: false }, ADMIN) try { // c1 hat kein Wartungs-Abo: bleibt offen, wird nicht angewendet assert.equal(other.doc.scope, 'neu') assert.equal(other.doc.status, 'offen') } finally { await deleteDocument(config.collections.aboOffers, other.doc.$id) } }) // --------------------------------------------------------------------------- // offerContext: Produkte mit Normalpreisen, productKey am Abo, offene Angebote je Produkt // --------------------------------------------------------------------------- test('offerContext: products mit Normalpreisen, subscription.productKey, offene Angebote je Produkt', async () => { const withoutSub = await offerContext('c3') assert.deepEqual(withoutSub.products, { hosting: { regularPriceCents: 2000 }, wartung: { regularPriceCents: 5000 } }) assert.equal(withoutSub.subscription, null) assert.deepEqual(withoutSub.openOffers, { hosting: null, wartung: null }) const withSub = await offerContext('c1') assert.equal(withSub.subscription.productKey, 'hosting') assert.equal(withSub.subscription.active, true) aw.seed('aboOffers', { $id: 'ctx_h', customerId: 'c1', status: 'offen', productKey: 'hosting', validUntil: '2099-01-01T00:00:00.000Z', months: 1, monthlyPriceCents: 500 }) aw.seed('aboOffers', { $id: 'ctx_w', customerId: 'c1', status: 'offen', productKey: 'wartung', validUntil: '2099-01-01T00:00:00.000Z', months: 1, monthlyPriceCents: 1000 }) try { const ctx = await offerContext('c1') assert.equal(ctx.openOffers.hosting.id, 'ctx_h') assert.equal(ctx.openOffers.wartung.id, 'ctx_w') } finally { await deleteDocument(config.collections.aboOffers, 'ctx_h') await deleteDocument(config.collections.aboOffers, 'ctx_w') } }) // --------------------------------------------------------------------------- // activateOfferFromCheckout: Produkt passt nicht -> nichts angewendet, T03 mit Grund (F33) // --------------------------------------------------------------------------- test('activateOfferFromCheckout: Produkt weicht ab -> nichts angewendet, T03 mit Grund in angebot_status', async () => { mails.sent.length = 0 const offer = await createOffer({ customerId: 'c3', productKey: 'wartung', months: 2, monthlyPriceCents: 2000, notifyCustomer: false }, ADMIN) try { const session = { subscription: 'sub_mismatch', metadata: { offerId: offer.doc.$id, productKey: 'hosting' }, } const result = await activateOfferFromCheckout(session) assert.equal(result, null) const fresh = (await import('../server/services/appwriteAdmin.js')).getDocument const doc = await fresh(config.collections.aboOffers, offer.doc.$id) assert.equal(doc.status, 'offen') const t03 = mails.sent.find((m) => m.subject.includes('nicht mehr offen')) assert.ok(t03, 'T03 wurde nicht verschickt') assert.match(t03.html, /Wartungs-Abo/) assert.match(t03.html, /Hosting-Abo/) } finally { await deleteDocument(config.collections.aboOffers, offer.doc.$id) } }) // --------------------------------------------------------------------------- // K05/K06 nennen das Produkt ({{produkt}}, F33) // --------------------------------------------------------------------------- test('K05 nennt das bestellte Produkt', async () => { mails.sent.length = 0 const offer = await createOffer( { customerId: 'c3', productKey: 'hosting', months: 2, monthlyPriceCents: 1000, notifyCustomer: true }, ADMIN ) try { const k05 = mails.sent.find((m) => m.to?.[0]?.address === 'k3@example.invalid') assert.ok(k05, 'K05 wurde nicht verschickt') assert.match(k05.html, /Hosting-Abo/) assert.doesNotMatch(k05.html, /Wartungs-Abo/) } finally { await deleteDocument(config.collections.aboOffers, offer.doc.$id) } }) // --------------------------------------------------------------------------- // GET /api/abo: offers.hosting und offers.wartung unabhaengig voneinander // --------------------------------------------------------------------------- test('GET /api/abo: offers.hosting und offers.wartung getrennt', async () => { aw.seed('aboOffers', { $id: 'g_h', customerId: 'c3', status: 'offen', productKey: 'hosting', validUntil: '2099-01-01T00:00:00.000Z', months: 1, monthlyPriceCents: 500 }) try { const r = await get('/api/abo', { cookie: customerCookie('c3') }) assert.equal(r.offers.hosting.id, 'g_h') assert.equal(r.offers.wartung, null) } finally { await deleteDocument(config.collections.aboOffers, 'g_h') } })