Files
Webklar-Kundenbereich/test/offerProduct.test.js
T
2026-09-29 10:08:59 +00:00

202 lines
9.6 KiB
JavaScript

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 15 €, Preis darunter', () => {
assert.equal(validateOfferInput({ productKey: 'hosting', months: 2, monthlyPriceCents: 1000 }).regularPriceCents, 1500)
assert.throws(() => validateOfferInput({ productKey: 'hosting', months: 2, monthlyPriceCents: 1500 }), (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, 1500)
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: 1500 }, 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')
}
})