- server/config.js: amountCents 2000, lookup_key webklar_hosting_monatlich_2000 - ensurePrice haengt einen neuen Preis an das vorhandene Stripe-Produkt (metadata.webklar), statt ein zweites "WEBklar Hosting-Abo" anzulegen - abo-bedingungen.html (Uebersicht, Preise, Leistungsbeschreibung) + PDF, Leitfaden + PDF - Tests auf 20 € umgestellt (399/399) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
202 lines
9.6 KiB
JavaScript
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 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')
|
|
}
|
|
})
|