Abo-Angebote: für Hosting- oder Wartungs-Abo
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7ffe95f24a
commit
e5cc2f3c82
6 files changed
+303
-45
No files matched your search
@@ -0,0 +1,201 @@
|
||||
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')
|
||||
}
|
||||
})
|
||||
Reference in new issue
Block a user