Hosting-Abo: Preis 15 € -> 20 € pro Monat (Vertragsbedingungen v4, Katalog, Leitfaden)
- 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>
This commit is contained in:
1 parent
78b4d7b7fe
commit
e4efdac226
18 files changed
+79
-59
No files matched your search
@@ -81,7 +81,7 @@ async function clearEvents(customerId) {
|
||||
test('GET /api/abo: Katalog, Hosting-Abo mit Speicher, kein Stundenkonto', async () => {
|
||||
aw.seed('subscriptions', { ...hostingDoc, storageUnits: 2 })
|
||||
const r = await get('/api/abo', { cookie: sessionCookie({ customerId: 'c1', appwriteUserId: 'u1' }) })
|
||||
assert.deepEqual(r.products.map((p) => [p.key, p.amountCents]), [['hosting', 1500], ['wartung', 5000]])
|
||||
assert.deepEqual(r.products.map((p) => [p.key, p.amountCents]), [['hosting', 2000], ['wartung', 5000]])
|
||||
assert.equal(r.subscription.productKey, 'hosting'); assert.equal(r.subscription.storage.units, 2)
|
||||
assert.equal(r.subscription.canSwitch, true); assert.equal(r.ledger, null)
|
||||
assert.equal(r.subscription.termsUrl, '/abo-bedingungen.html')
|
||||
@@ -134,10 +134,10 @@ test('Checkout Hosting: Preis, Beschreibung, Hinweistext', async () => {
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
assert.match(res.body.url, /^https:\/\//)
|
||||
assert.equal(call.body['subscription_data[description]'], 'WEBklar Hosting-Abo')
|
||||
assert.match(message(call), /15,00 € monatlich/)
|
||||
assert.match(message(call), /20,00 € monatlich/)
|
||||
assert.equal(call.body['subscription_data[metadata][productKey]'], 'hosting')
|
||||
assert.equal(message(call), `${INTRO_SOFORT} 15,00 € monatlich, ${TAIL}`)
|
||||
assert.equal(call.body['line_items[0][price]'], priceIdFor('webklar_hosting_monatlich_1500'))
|
||||
assert.equal(message(call), `${INTRO_SOFORT} 20,00 € monatlich, ${TAIL}`)
|
||||
assert.equal(call.body['line_items[0][price]'], priceIdFor('webklar_hosting_monatlich_2000'))
|
||||
assert.equal(call.body['discounts[0][coupon]'], undefined)
|
||||
assert.equal(call.body['subscription_data[trial_end]'], undefined)
|
||||
// Nachweis der Zustimmung nur am Abo (F41c): an der Checkout-Sitzung bleiben Zuordnung und Produkt
|
||||
@@ -156,7 +156,7 @@ test('Checkout Hosting nach der Widerrufsfrist: Probezeit bis zum Leistungsbegin
|
||||
const { res, call } = await checkout(orderBody('hosting', { startChoice: 'nach_frist', lateStartAt }))
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
assert.equal(call.body['subscription_data[trial_end]'], String(unix(lateStartAt)))
|
||||
assert.equal(message(call), `${INTRO_FRIST} Danach 15,00 € monatlich, ${TAIL}`)
|
||||
assert.equal(message(call), `${INTRO_FRIST} Danach 20,00 € monatlich, ${TAIL}`)
|
||||
// Veralteter Leistungsbeginn (Datumswechsel seit dem Laden der Seite) -> 409, kein Checkout
|
||||
const stale = await checkout(orderBody('hosting', { startChoice: 'nach_frist', lateStartAt: '2026-01-02T05:00:00.000Z' }))
|
||||
assert.equal(stale.res.status, 409)
|
||||
@@ -421,7 +421,7 @@ test('Webhook checkout.session.completed: K01 je Produkt mit PDF der zugestimmte
|
||||
assert.equal(k01.subject, 'Ihr WEBklar Hosting-Abo – Vertragsbestätigung')
|
||||
assert.deepEqual(k01.attachments.map((a) => [a.filename, a.contentType]), [['WEBklar-Abo-Vertragsunterlagen.pdf', 'application/pdf']])
|
||||
assert.ok(k01.attachments[0].content.length > 1000, 'PDF-Inhalt angehaengt')
|
||||
for (const text of ['15,00 € / Monat', 'gemäß § 19 UStG ohne Umsatzsteuer', '1 E-Mail-Postfach', `(Version ${config.abo.termsVersion})`]) {
|
||||
for (const text of ['20,00 € / Monat', 'gemäß § 19 UStG ohne Umsatzsteuer', '1 E-Mail-Postfach', `(Version ${config.abo.termsVersion})`]) {
|
||||
assert.ok(k01.text.includes(text), `${text}\n---\n${k01.text}`)
|
||||
}
|
||||
assert.ok(!k01.text.includes('Stunde Arbeitszeit') && !k01.text.includes('Stundenkontingent'), k01.text)
|
||||
|
||||
@@ -15,7 +15,7 @@ const { grantCreditForInvoice, loadInvoice, invoiceLines } = await import('../se
|
||||
|
||||
const price = (id, lookup_key, unit_amount) => ({ id, object: 'price', lookup_key, unit_amount, active: true })
|
||||
st.prices.set('price_w', price('price_w', 'webklar_wartung_monatlich_5000', 5000))
|
||||
st.prices.set('price_h', price('price_h', 'webklar_hosting_monatlich_1500', 1500))
|
||||
st.prices.set('price_h', price('price_h', 'webklar_hosting_monatlich_2000', 2000))
|
||||
|
||||
const sub = (id, priceObj) => ({
|
||||
id,
|
||||
@@ -66,14 +66,14 @@ test('Wartungs-Rechnung: 60 Minuten, genau einmal', async () => {
|
||||
})
|
||||
|
||||
test('Hosting-Rechnung: keine Stunde', async () => {
|
||||
st.invoices.set('in_2', inv('in_2', 'sub_h', [regular('price_h', 1500)]))
|
||||
assert.deepEqual(await grantCreditForInvoice(payload('in_2', 'sub_h', 1500)), { skipped: 'keine Wartungs-Leistung' })
|
||||
st.invoices.set('in_2', inv('in_2', 'sub_h', [regular('price_h', 2000)]))
|
||||
assert.deepEqual(await grantCreditForInvoice(payload('in_2', 'sub_h', 2000)), { skipped: 'keine Wartungs-Leistung' })
|
||||
assert.equal(aw.docs('hourCredits').has('in_2'), false)
|
||||
})
|
||||
|
||||
test('Frisch geladene Rechnung zählt, nicht die Webhook-Nutzlast', async () => {
|
||||
// Nutzlast in alter Form (ohne pricing/parent) behauptet Wartung, frisch geladen ist es Hosting
|
||||
st.invoices.set('in_3', inv('in_3', 'sub_h', [regular('price_h', 1500)]))
|
||||
st.invoices.set('in_3', inv('in_3', 'sub_h', [regular('price_h', 2000)]))
|
||||
const oldPayload = {
|
||||
id: 'in_3',
|
||||
subscription: 'sub_h',
|
||||
@@ -94,7 +94,7 @@ test('Preis-Zuordnung wird zwischengespeichert', async () => {
|
||||
})
|
||||
|
||||
test('Unbekannte Preis-ID (404) neben Hosting: keine Stunde, kein Absturz', async () => {
|
||||
st.invoices.set('in_5', inv('in_5', 'sub_h', [regular('price_h', 1500), regular('price_weg', 800)]))
|
||||
st.invoices.set('in_5', inv('in_5', 'sub_h', [regular('price_h', 2000), regular('price_weg', 800)]))
|
||||
assert.deepEqual(await grantCreditForInvoice(payload('in_5', 'sub_h', 2300)), { skipped: 'keine Wartungs-Leistung' })
|
||||
assert.equal(aw.docs('hourCredits').has('in_5'), false)
|
||||
})
|
||||
|
||||
@@ -62,7 +62,7 @@ test('Widerrufsbelehrung und Muster-Widerrufsformular wortgleich mit Fassung 3',
|
||||
|
||||
test('Fassung 4: beide Abos, Postfächer, Zusatzspeicher, Wechsel', () => {
|
||||
const v4html = read(V4), v4 = strip(v4html)
|
||||
for (const s of ['15,00 €', '50,00 €', 'beliebig viele Postfächer', 'zusammen 5 GB', '1,00 €', 'Zusatzspeicher',
|
||||
for (const s of ['20,00 €', '50,00 €', 'beliebig viele Postfächer', 'zusammen 5 GB', '1,00 €', 'Zusatzspeicher',
|
||||
'12 Monate', 'statische', 'Hetzner', '§ 19 UStG', 'Leistungsbeschreibung A', 'Leistungsbeschreibung B',
|
||||
'„Zahlungspflichtig bestellen“', '„Verträge hier kündigen“', '„Nur den E-Mail-Zusatzspeicher“', '„Speichern“',
|
||||
'Vertragsbedingungen für die WEBklar-Abos']) assert.ok(v4.includes(s), s)
|
||||
@@ -412,7 +412,7 @@ test('Übernahmegutschrift und Leitfaden: produktneutral', () => {
|
||||
// Leitfaden: beide Preise mit § 19 UStG, Zusatzspeicher, Wechsel, Gutschrift für beide Abos, Bestellweg „Verträge“
|
||||
const lfText = strip(lf)
|
||||
for (const s of ['zusammen 5 GB', 'Zusatzspeicher', 'Wechsel', 'Übernahmegutschrift', 'beiden Abos', '„Verträge“']) assert.ok(lfText.includes(s), s)
|
||||
for (const preis of ['15,00 €', '50,00 €', '1,00 €']) {
|
||||
for (const preis of ['20,00 €', '50,00 €', '1,00 €']) {
|
||||
const i = lfText.indexOf(preis)
|
||||
assert.ok(i >= 0, preis)
|
||||
assert.ok(lfText.slice(i, i + 80).includes('§ 19 UStG'), `${preis} ohne § 19 UStG`)
|
||||
|
||||
@@ -173,7 +173,7 @@ test('Vorschau Plus: anteiliger Betrag aus der Stripe-Vorschau, Gesamtgröße, n
|
||||
st.invoicePreview = (body) =>
|
||||
previewInvoice(
|
||||
line('price_mailStorage', 133, { quantity: 2, proration: true, periodStart: Number(body.subscription_details.proration_date) }),
|
||||
line('price_hosting', 1500),
|
||||
line('price_hosting', 2000),
|
||||
line('price_mailStorage', 200, { quantity: 2 })
|
||||
)
|
||||
const before = st.calls.length
|
||||
@@ -208,7 +208,7 @@ test('Vorschau Plus mit Posten: Gutschrift und Belastung ab proration_date, ält
|
||||
line('price_mailStorage', 40, { quantity: 1, proration: true, periodStart: at - 60 }),
|
||||
line('price_mailStorage', -67, { quantity: 1, proration: true, periodStart: at - 1 }),
|
||||
line('price_mailStorage', 200, { quantity: 3, proration: true, periodStart: at + 1 }),
|
||||
line('price_hosting', 1500),
|
||||
line('price_hosting', 2000),
|
||||
line('price_mailStorage', 300, { quantity: 3 })
|
||||
)
|
||||
}
|
||||
@@ -257,7 +257,7 @@ test('Vorschau Plus in zwei Schritten, flexibler Modus (Standard): Betrag der St
|
||||
return previewInvoice(
|
||||
line('price_mailStorage', -200, { quantity: 3, proration: true, periodStart: at }),
|
||||
line('price_mailStorage', 333, { quantity: 5, proration: true, periodStart: at }),
|
||||
line('price_hosting', 1500),
|
||||
line('price_hosting', 2000),
|
||||
line('price_mailStorage', 500, { quantity: 5 })
|
||||
)
|
||||
}
|
||||
@@ -287,7 +287,7 @@ test('Vorschau Plus in zwei Schritten, klassischer Modus: anteilig nur für die
|
||||
return previewInvoice(
|
||||
line('price_mailStorage', -67, { quantity: 1, proration: true, periodStart: at }),
|
||||
line('price_mailStorage', 333, { quantity: 5, proration: true, periodStart: at }),
|
||||
line('price_hosting', 1500),
|
||||
line('price_hosting', 2000),
|
||||
line('price_mailStorage', 500, { quantity: 5 })
|
||||
)
|
||||
}
|
||||
@@ -905,7 +905,7 @@ test('invoice.paid: anteilige Speicher-Zeile oder Rechnung ohne Speicher ändert
|
||||
await resetSubH({ units: 2, paidUnits: 2 })
|
||||
const r1 = await recordPaidStorage({ id: 'in_x', lines: { data: [line('price_mailStorage', 90, { quantity: 3, proration: true })] } })
|
||||
assert.ok(r1.skipped)
|
||||
const r2 = await recordPaidStorage({ id: 'in_y', lines: { data: [line('price_hosting', 1500)] } })
|
||||
const r2 = await recordPaidStorage({ id: 'in_y', lines: { data: [line('price_hosting', 2000)] } })
|
||||
assert.ok(r2.skipped)
|
||||
assert.equal(doc().storagePaidUnits, 2)
|
||||
assert.equal(doc().storagePaidPeriodEnd, hostingDoc.currentPeriodEnd)
|
||||
@@ -953,7 +953,7 @@ test('Webhook invoice.paid: Appwrite 4xx → 200 und übersprungen, 5xx → 500'
|
||||
st.invoices.set('in_h2', {
|
||||
id: 'in_h2', object: 'invoice', number: 'WK-h2', created: now, status_transitions: { paid_at: now },
|
||||
parent: { type: 'subscription_details', subscription_details: { subscription: 'sub_h' } },
|
||||
lines: { object: 'list', data: [line('price_hosting', 1500, { periodEnd: PERIOD_END_UNIX + 30 * DAY }), line('price_mailStorage', 300, { quantity: 3, periodEnd: PERIOD_END_UNIX + 30 * DAY })] },
|
||||
lines: { object: 'list', data: [line('price_hosting', 2000, { periodEnd: PERIOD_END_UNIX + 30 * DAY }), line('price_mailStorage', 300, { quantity: 3, periodEnd: PERIOD_END_UNIX + 30 * DAY })] },
|
||||
})
|
||||
const event = { id: 'evt_in_h2', object: 'event', type: 'invoice.paid', created: now, data: { object: { id: 'in_h2', object: 'invoice', parent: { type: 'subscription_details', subscription_details: { subscription: 'sub_h' } } } } }
|
||||
const errors = []
|
||||
|
||||
@@ -325,12 +325,12 @@ test('Neue Vorlagen vorhanden', () => {
|
||||
})
|
||||
|
||||
test('K01 Hosting: 1 Postfach, keine Arbeitsstunde; Wartung: beliebig viele Postfächer', () => {
|
||||
const base = { name: 'K', produkt: 'Hosting-Abo', preis: '15,00 € / Monat', vertragsschluss: '01.10.2026', leistungsbeginn: '01.10.2026',
|
||||
const base = { name: 'K', produkt: 'Hosting-Abo', preis: '20,00 € / Monat', vertragsschluss: '01.10.2026', leistungsbeginn: '01.10.2026',
|
||||
mindestlaufzeit_bis: '01.10.2027', bedingungen_version: '2026-10-05-v4', zustimmung_zeitpunkt: '01.10.2026, 10:00', angebot_text: '' }
|
||||
const h = renderMail('K01', base, { hosting: true, sofortbeginn: true })
|
||||
assert.equal(h.subject, 'Ihr WEBklar Hosting-Abo – Vertragsbestätigung')
|
||||
assert.ok(h.text.includes('1 E-Mail-Postfach') && !h.text.includes('Stunde Arbeitszeit'), h.text)
|
||||
assert.ok(h.text.includes('15,00 € / Monat') && h.text.includes('gemäß § 19 UStG ohne Umsatzsteuer'))
|
||||
assert.ok(h.text.includes('20,00 € / Monat') && h.text.includes('gemäß § 19 UStG ohne Umsatzsteuer'))
|
||||
assert.ok(h.text.includes('Ihr Hosting-Abo ist bestätigt.') && h.text.includes('Abschluss Ihres Hosting-Abos'))
|
||||
assert.doesNotMatch(h.text, /Stundenkontingent/)
|
||||
assert.deepEqual(h.problems, [])
|
||||
@@ -466,7 +466,7 @@ test('K01: Block wartung_v3 mit der Zeile der Fassung 3, wartung mit der Fassung
|
||||
assert.match(v4.text, /• beliebig viele Postfächer/)
|
||||
assert.match(v4.text, /1 Stunde Arbeitszeit je bezahltem Monat/)
|
||||
assert.doesNotMatch(v4.text, /1 E-Mail-Postfach|pro Monat/)
|
||||
const h = renderMail('K01', { ...base, produkt: 'Hosting-Abo', preis: '15,00 € / Monat' }, { hosting: true, sofortbeginn: true })
|
||||
const h = renderMail('K01', { ...base, produkt: 'Hosting-Abo', preis: '20,00 € / Monat' }, { hosting: true, sofortbeginn: true })
|
||||
assert.doesNotMatch(h.text, /Stundenkontingent|Stunde Arbeitszeit/)
|
||||
})
|
||||
|
||||
|
||||
@@ -42,9 +42,9 @@ 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)
|
||||
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)
|
||||
})
|
||||
|
||||
@@ -69,7 +69,7 @@ test('createOffer: offene Angebote fuer Hosting und Wartung koennen nebeneinande
|
||||
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.regularPriceCents, 2000)
|
||||
assert.equal(hosting.doc.status, 'offen')
|
||||
|
||||
await assert.rejects(
|
||||
@@ -118,7 +118,7 @@ test('createOffer: nur bei passendem Produkt wird das Angebot sofort aufs laufen
|
||||
|
||||
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.deepEqual(withoutSub.products, { hosting: { regularPriceCents: 2000 }, wartung: { regularPriceCents: 5000 } })
|
||||
assert.equal(withoutSub.subscription, null)
|
||||
assert.deepEqual(withoutSub.openOffers, { hosting: null, wartung: null })
|
||||
|
||||
|
||||
+10
-10
@@ -25,8 +25,8 @@ const keys = new Map([['price_w', 'wartung'], ['price_h', 'hosting'], ['price_s'
|
||||
|
||||
test('Katalog: Preise, lookup_keys, Minuten, Mindestlaufzeit', () => {
|
||||
assert.equal(getProduct('wartung').amountCents, 5000)
|
||||
assert.equal(getProduct('hosting').amountCents, 1500)
|
||||
assert.equal(getProduct('hosting').lookupKey, 'webklar_hosting_monatlich_1500')
|
||||
assert.equal(getProduct('hosting').amountCents, 2000)
|
||||
assert.equal(getProduct('hosting').lookupKey, 'webklar_hosting_monatlich_2000')
|
||||
assert.equal(getProduct('wartung').minutesPerMonth, 60)
|
||||
assert.equal(getProduct('hosting').minutesPerMonth, 0)
|
||||
assert.equal(getProduct('hosting').minTermMonths, 12)
|
||||
@@ -46,7 +46,7 @@ test('Bisherige Werte bleiben gleich', () => {
|
||||
|
||||
test('lookup_key → Produkt', () => {
|
||||
assert.equal(productKeyForLookupKey('webklar_wartung_monatlich_5000'), 'wartung')
|
||||
assert.equal(productKeyForLookupKey('webklar_hosting_monatlich_1500'), 'hosting')
|
||||
assert.equal(productKeyForLookupKey('webklar_hosting_monatlich_2000'), 'hosting')
|
||||
assert.equal(productKeyForLookupKey('webklar_mailspeicher_5gb_monatlich_100'), 'mailStorage')
|
||||
assert.equal(productKeyForLookupKey('irgendwas'), null)
|
||||
assert.equal(productKeyForLookupKey(undefined), null)
|
||||
@@ -55,14 +55,14 @@ test('lookup_key → Produkt', () => {
|
||||
test('describeItems: Abo-Posten und Zusatzspeicher, Reihenfolge egal', () => {
|
||||
const items = [
|
||||
{ id: 'si_s', quantity: 2, price: { id: 'price_s', lookup_key: 'webklar_mailspeicher_5gb_monatlich_100', unit_amount: 100 } },
|
||||
{ id: 'si_a', quantity: 1, price: { id: 'price_h', lookup_key: 'webklar_hosting_monatlich_1500', unit_amount: 1500 } },
|
||||
{ id: 'si_a', quantity: 1, price: { id: 'price_h', lookup_key: 'webklar_hosting_monatlich_2000', unit_amount: 2000 } },
|
||||
]
|
||||
assert.deepEqual(describeItems(items), {
|
||||
productKey: 'hosting',
|
||||
aboItemId: 'si_a',
|
||||
aboPriceId: 'price_h',
|
||||
lookupKey: 'webklar_hosting_monatlich_1500',
|
||||
priceAmount: 1500,
|
||||
lookupKey: 'webklar_hosting_monatlich_2000',
|
||||
priceAmount: 2000,
|
||||
storageItemId: 'si_s',
|
||||
storageUnits: 2,
|
||||
})
|
||||
@@ -85,11 +85,11 @@ test('describeItems: ohne bekannten Abo-Preis', () => {
|
||||
test('Stunden nur für eine reguläre Wartungs-Zeile über 0 €', () => {
|
||||
const inv = (...lines) => ({ lines: { data: lines } })
|
||||
assert.equal(creditMinutesForInvoice(inv(line('price_w', 5000)), keys), 60)
|
||||
assert.equal(creditMinutesForInvoice(inv(line('price_h', 1500), line('price_s', 200)), keys), 0)
|
||||
assert.equal(creditMinutesForInvoice(inv(line('price_h', 2000), line('price_s', 200)), keys), 0)
|
||||
assert.equal(creditMinutesForInvoice(inv(line('price_w', 2300, true)), keys), 0) // anteilige Verrechnung
|
||||
assert.equal(creditMinutesForInvoice(inv(line('price_w', 0)), keys), 0) // Probezeit
|
||||
// unbekannter Preis neben einem bekannten Abo-Preis: keine Stunde
|
||||
assert.equal(creditMinutesForInvoice(inv(line('price_h', 1500), line('price_x', 800)), keys), 0)
|
||||
assert.equal(creditMinutesForInvoice(inv(line('price_h', 2000), line('price_x', 800)), keys), 0)
|
||||
const invoiceItem = {
|
||||
amount: 1200,
|
||||
pricing: { price_details: { price: 'price_w' } },
|
||||
@@ -156,7 +156,7 @@ test('productView: vollständige Form, Kopie der features', () => {
|
||||
assert.deepEqual(Object.keys(v).sort(), ['amountCents', 'features', 'key', 'minTermMonths', 'minutesPerMonth', 'name'].sort())
|
||||
assert.equal(v.key, 'hosting')
|
||||
assert.equal(v.name, 'WEBklar Hosting-Abo')
|
||||
assert.equal(v.amountCents, 1500)
|
||||
assert.equal(v.amountCents, 2000)
|
||||
assert.equal(v.minTermMonths, 12)
|
||||
assert.equal(v.minutesPerMonth, 0)
|
||||
assert.ok(Array.isArray(v.features) && v.features.length > 0)
|
||||
@@ -201,7 +201,7 @@ test('regularStorageLine: reguläre Zusatzspeicher-Zeile, keine anteilige, kein
|
||||
const storage = { ...line('price_s', 300), quantity: 3 }
|
||||
assert.equal(regularStorageLine(inv(line('price_s', 90, true), line('price_w', 5000), storage), keys), storage)
|
||||
assert.equal(regularStorageLine(inv(line('price_s', 90, true)), keys), null)
|
||||
assert.equal(regularStorageLine(inv(line('price_w', 5000), line('price_h', 1500)), keys), null)
|
||||
assert.equal(regularStorageLine(inv(line('price_w', 5000), line('price_h', 2000)), keys), null)
|
||||
assert.equal(regularStorageLine(inv({ amount: 5 }), keys), null)
|
||||
assert.equal(regularStorageLine({}, keys), null)
|
||||
})
|
||||
|
||||
@@ -51,12 +51,12 @@ test('Setup-Skript legt productKey an', () => {
|
||||
test('Spiegel: Hosting-Abo, Zusatzspeicher steht vorne', async () => {
|
||||
st.subscriptions.set(
|
||||
'sub_h',
|
||||
sub('sub_h', [item('s', 'webklar_mailspeicher_5gb_monatlich_100', 100, 2), item('h', 'webklar_hosting_monatlich_1500', 1500)])
|
||||
sub('sub_h', [item('s', 'webklar_mailspeicher_5gb_monatlich_100', 100, 2), item('h', 'webklar_hosting_monatlich_2000', 2000)])
|
||||
)
|
||||
const doc = await syncSubscription('sub_h')
|
||||
assert.equal(doc.productKey, 'hosting')
|
||||
assert.equal(doc.planKey, 'webklar_hosting_monatlich_1500')
|
||||
assert.equal(doc.priceAmount, 1500)
|
||||
assert.equal(doc.planKey, 'webklar_hosting_monatlich_2000')
|
||||
assert.equal(doc.priceAmount, 2000)
|
||||
})
|
||||
|
||||
test('Spiegel: Wartungs-Abo wie bisher', async () => {
|
||||
@@ -105,8 +105,8 @@ test('ensurePrice legt Hosting einmal an: Preis- und Produktfelder, Bild', async
|
||||
const calls = st.calls.slice(before)
|
||||
const createdPrices = calls.filter((c) => c.method === 'POST' && c.path === '/v1/prices')
|
||||
assert.equal(createdPrices.length, 1)
|
||||
assert.equal(createdPrices[0].body['lookup_key'], 'webklar_hosting_monatlich_1500')
|
||||
assert.equal(createdPrices[0].body['unit_amount'], '1500')
|
||||
assert.equal(createdPrices[0].body['lookup_key'], 'webklar_hosting_monatlich_2000')
|
||||
assert.equal(createdPrices[0].body['unit_amount'], '2000')
|
||||
assert.equal(createdPrices[0].body['tax_behavior'], 'inclusive')
|
||||
assert.equal(createdPrices[0].body['recurring[interval]'], 'month')
|
||||
const createdProducts = calls.filter((c) => c.method === 'POST' && c.path === '/v1/products')
|
||||
@@ -116,6 +116,18 @@ test('ensurePrice legt Hosting einmal an: Preis- und Produktfelder, Bild', async
|
||||
assert.equal(createdProducts[0].body['images[0]'], config.stripe.productImage)
|
||||
})
|
||||
|
||||
// Preisaenderung (Hosting 15 -> 20 EUR): neuer Preis haengt am vorhandenen Produkt, kein zweites Produkt
|
||||
test('ensurePrice: neuer Preis fuer vorhandenes Produkt legt kein zweites Produkt an', async () => {
|
||||
st.products.set('prod_alt_speicher', { id: 'prod_alt_speicher', object: 'product', active: true, name: 'WEBklar E-Mail-Zusatzspeicher 5 GB', images: [], metadata: { webklar: 'mail-speicher' } })
|
||||
const before = st.calls.length
|
||||
await ensurePrice('mailStorage')
|
||||
const calls = st.calls.slice(before)
|
||||
assert.equal(calls.filter((c) => c.method === 'POST' && c.path === '/v1/products').length, 0)
|
||||
const created = calls.filter((c) => c.method === 'POST' && c.path === '/v1/prices')
|
||||
assert.equal(created.length, 1)
|
||||
assert.equal(created[0].body['product'], 'prod_alt_speicher')
|
||||
})
|
||||
|
||||
// Important 4: ein bereits vorhandener aktiver Preis wird gefunden, nichts wird bei Stripe angelegt
|
||||
test('ensurePrice: vorhandener aktiver Preis wird gefunden, nichts wird angelegt', async () => {
|
||||
st.prices.set('price_seeded_wartung', {
|
||||
@@ -157,11 +169,11 @@ test('Stripe-Nachbau: Grenzen bei Preisen und Coupons (400)', async () => {
|
||||
})
|
||||
|
||||
// Important 1: Namen ≤ 40 Zeichen (Stripe-Limit; die alten Namen mit dem vollen Produktnamen waren 49/50)
|
||||
test('Testrabatt Hosting: 14,50 € Nachlass, Name ≤ 40 Zeichen', async () => {
|
||||
test('Testrabatt Hosting: 19,50 € Nachlass, Name ≤ 40 Zeichen', async () => {
|
||||
const before = st.calls.length
|
||||
assert.equal(await ensureTestCoupon('hosting'), 'WEBKLAR-TEST-050-HOSTING')
|
||||
const call = st.calls.slice(before).find((c) => c.path === '/v1/coupons' && c.method === 'POST')
|
||||
assert.equal(call.body['amount_off'], '1450')
|
||||
assert.equal(call.body['amount_off'], '1950')
|
||||
assert.equal(call.body['name'], 'Testzugang Hosting (0,50 €)')
|
||||
assert.ok(call.body['name'].length <= 40)
|
||||
})
|
||||
|
||||
@@ -235,6 +235,10 @@ export async function startFakeStripe() {
|
||||
if (method === 'GET' && (m = path.match(/^\/v1\/products\/([^/]+)$/))) {
|
||||
return products.has(m[1]) ? [200, products.get(m[1])] : noSuch('product', m[1])
|
||||
}
|
||||
if (method === 'GET' && path === '/v1/products') {
|
||||
const active = query.get('active')
|
||||
return [200, listOf([...products.values()].filter((p) => active === null || String(p.active !== false) === active))]
|
||||
}
|
||||
if (method === 'POST' && path === '/v1/products') {
|
||||
const data = unflatten(body)
|
||||
const obj = {
|
||||
|
||||
@@ -5,8 +5,8 @@ import assert from 'node:assert/strict'
|
||||
// Die Pruefung vergleicht den Monatsbetrag beim bisherigen Anbieter mit der Monatsgebuehr des Abos des Kunden.
|
||||
const { runAutoChecks } = await import('../server/services/takeoverCredits.js')
|
||||
|
||||
// 240 € fuer 12 Monate = 20,00 € pro Monat: mehr als das Hosting-Abo (15,00 €), weniger als das Wartungs-Abo (50,00 €)
|
||||
const doc = { $id: 'req1', $createdAt: '2026-10-01T10:00:00.000Z', domain: '', invoiceAmountCents: 24000, billingMonths: 12, files: '[]' }
|
||||
// 300 € fuer 12 Monate = 25,00 € pro Monat: mehr als das Hosting-Abo (20,00 €), weniger als das Wartungs-Abo (50,00 €)
|
||||
const doc = { $id: 'req1', $createdAt: '2026-10-01T10:00:00.000Z', domain: '', invoiceAmountCents: 30000, billingMonths: 12, files: '[]' }
|
||||
const sub = (productKey) => ({
|
||||
status: 'active',
|
||||
startedAt: '2026-10-01T00:00:00.000Z',
|
||||
@@ -16,15 +16,15 @@ const sub = (productKey) => ({
|
||||
const amountCheck = async (subscription) =>
|
||||
(await runAutoChecks(doc, { allRequests: [], subscription, calc: null })).find((c) => c.key === 'amount')
|
||||
|
||||
test('Plausibilität: 20 € pro Monat ist mehr als das Hosting-Abo (15,00 €)', async () => {
|
||||
test('Plausibilität: 25 € pro Monat ist mehr als das Hosting-Abo (20,00 €)', async () => {
|
||||
const hosting = await amountCheck(sub('hosting'))
|
||||
assert.ok(hosting, 'Hinweis „mehr als die Monatsgebühr“ fehlt beim Hosting-Abo')
|
||||
assert.equal(hosting.status, 'info')
|
||||
assert.ok(hosting.label.includes('15,00'), hosting.label)
|
||||
assert.ok(hosting.label.includes('20,00'), hosting.label)
|
||||
assert.ok(hosting.label.includes('25,00'), hosting.label)
|
||||
})
|
||||
|
||||
test('Plausibilität: 20 € pro Monat ist nicht mehr als das Wartungs-Abo (50,00 €)', async () => {
|
||||
test('Plausibilität: 25 € pro Monat ist nicht mehr als das Wartungs-Abo (50,00 €)', async () => {
|
||||
assert.equal(await amountCheck(sub('wartung')), undefined)
|
||||
})
|
||||
|
||||
|
||||
Reference in new issue
Block a user