| Anbieter | WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein (jeder einzeln vertretungsberechtigt) · Telefon 0176 23726355 · E-Mail support@webklar.com |
- | Hosting-Abo | 15,00 € pro Monat · Bereitstellung Ihrer statischen Website auf unserem Server in Deutschland · 1 Domain · 1 E-Mail-Postfach mit 5 GB · Änderungen an der Website nur nach Ihrer Freigabe, 100 € pro Stunde (Leistungsbeschreibung A) |
+ | Hosting-Abo | 20,00 € pro Monat · Bereitstellung Ihrer statischen Website auf unserem Server in Deutschland · 1 Domain · 1 E-Mail-Postfach mit 5 GB · Änderungen an der Website nur nach Ihrer Freigabe, 100 € pro Stunde (Leistungsbeschreibung A) |
| Wartungs-Abo | 50,00 € pro Monat · Betrieb und Pflege Ihrer Website bzw. Web-Anwendung auf unserem Server in Deutschland · 1 Domain · beliebig viele E-Mail-Postfächer mit zusammen 5 GB · 1 Stunde Arbeitszeit pro Monat, nicht genutzte Zeit bleibt 12 Monate verfügbar; Mehraufwand nur nach Ihrer Freigabe, 100 € pro Stunde (Leistungsbeschreibung B) |
| E-Mail-Zusatzspeicher | In beiden Abos: je Paket von 5 GB 1,00 € pro Monat, bis zu 10 Pakete. Im Kundenportal bestellbar und reduzierbar nach Leistungsbeginn, solange das Abo nicht gekündigt ist und keine Zahlung offen ist; ganz oder teilweise kündbar zum Ende des Abrechnungsmonats. |
| Preise | Alle Preise sind Endpreise, zahlbar monatlich im Voraus. Gemäß § 19 UStG wird keine Umsatzsteuer berechnet. |
@@ -206,7 +206,7 @@
§ 7 Preise und Zahlung
- - Die Monatsgebühr beträgt im Hosting-Abo 15,00 € und im Wartungs-Abo 50,00 €. E-Mail-Zusatzspeicher kostet je Paket von 5 GB 1,00 € pro Monat (§ 10). Alle Preise sind Endpreise. Als Kleinunternehmer im Sinne von § 19 UStG berechnen wir keine Umsatzsteuer.
+ - Die Monatsgebühr beträgt im Hosting-Abo 20,00 € und im Wartungs-Abo 50,00 €. E-Mail-Zusatzspeicher kostet je Paket von 5 GB 1,00 € pro Monat (§ 10). Alle Preise sind Endpreise. Als Kleinunternehmer im Sinne von § 19 UStG berechnen wir keine Umsatzsteuer.
- Die Monatsgebühr ist monatlich im Voraus fällig, erstmals mit Beginn der Leistung (bei sofortigem Beginn mit Vertragsschluss, sonst nach Ablauf der Widerrufsfrist), danach jeweils am selben Kalendertag der Folgemonate (Abrechnungsmonat). Gebuchter Zusatzspeicher wird zusammen mit der Monatsgebühr abgebucht. Die Gebühren werden per Kreditkarte oder SEPA-Lastschrift über die Stripe Payments Europe, Limited (Irland) eingezogen. Rechnungen erhält der Kunde per E-Mail und im Kundenportal über „Abo verwalten & Rechnungen“.
- Bestellt der Kunde Zusatzspeicher, berechnen wir für den Rest des laufenden Abrechnungsmonats einen anteiligen Betrag. Er wird in der Bestellübersicht angezeigt und mit der nächsten Abbuchung eingezogen. Ab dem folgenden Abrechnungsmonat gilt der volle Monatspreis. Pakete, die für den laufenden Abrechnungsmonat schon bezahlt sind, berechnen wir nicht noch einmal. Verringert der Kunde den Zusatzspeicher, wirkt das zum Ende des laufenden Abrechnungsmonats; die Gebühr für den laufenden Monat wird nicht anteilig erstattet.
- Beim Wechsel vom Hosting- ins Wartungs-Abo (§ 9) wird die Monatsgebühr des Wartungs-Abos am Wechseltag fällig, zusammen mit gebuchtem Zusatzspeicher. Die im Voraus bezahlten Gebühren für die nicht genutzten Tage des laufenden Abrechnungsmonats schreiben wir auf dieser Rechnung gut. Die folgenden Abrechnungsmonate beginnen jeweils am Kalendertag des Wechsels. Den fälligen Betrag und den neuen Abbuchungstag zeigt die Bestellübersicht vor der Bestellung. Wird die Wechselrechnung nicht bezahlt, gilt § 9 Abs. 3.
@@ -283,7 +283,7 @@
Leistungsbeschreibung A
Hosting-Abo
- 15,00 € pro Monat · Endpreis, gemäß § 19 UStG ohne Umsatzsteuer · Mindestlaufzeit 12 Monate · für statische Websites
+ 20,00 € pro Monat · Endpreis, gemäß § 19 UStG ohne Umsatzsteuer · Mindestlaufzeit 12 Monate · für statische Websites
1. Umfang
- Bereitstellung der Website auf unserem Server in Deutschland mit SSL-Zertifikat, Überwachung der Erreichbarkeit und Updates von Betriebssystem und Server-Software (§ 3 Abs. 2)
diff --git a/public/app.js b/public/app.js
index 2757b66..2c2a482 100644
--- a/public/app.js
+++ b/public/app.js
@@ -806,7 +806,7 @@ function aboTermText(sub) {
/* Seitenleiste: noch kein Abo (Auswahl steht in der Ansicht "Verträge") */
function renderAboSideOffer(card, data, { ended }) {
- const cheapest = Math.min(...(data.products || []).map((p) => p.amountCents).filter(Number.isFinite), 1500)
+ const cheapest = Math.min(...(data.products || []).map((p) => p.amountCents).filter(Number.isFinite), 2000)
card.classList.add('kb-abo-offer')
card.innerHTML = `
Ihr Vertrag
diff --git a/server/config.js b/server/config.js
index ea2a070..ecb682c 100644
--- a/server/config.js
+++ b/server/config.js
@@ -52,8 +52,8 @@ const products = {
hosting: {
key: 'hosting',
name: 'WEBklar Hosting-Abo',
- lookupKey: 'webklar_hosting_monatlich_1500',
- amountCents: 1500,
+ lookupKey: 'webklar_hosting_monatlich_2000',
+ amountCents: 2000,
description:
'Bereitstellung Ihrer statischen Website auf unserem Server in Deutschland (SSL, tägliche Datensicherung, Überwachung der Erreichbarkeit, Updates von Betriebssystem und Server-Software), 1 Domain inklusive Verwaltung und 1 E-Mail-Postfach (5 GB). Mindestlaufzeit 12 Monate, danach monatlich kündbar. Gemäß § 19 UStG ohne Umsatzsteuer.',
statementDescriptor: 'WEBKLAR HOSTING',
diff --git a/server/mail-templates/K01 Vertragsbestaetigung.html b/server/mail-templates/K01 Vertragsbestaetigung.html
index e69fe8e..a57d551 100644
--- a/server/mail-templates/K01 Vertragsbestaetigung.html
+++ b/server/mail-templates/K01 Vertragsbestaetigung.html
@@ -9,7 +9,7 @@
Empfänger: Kunde (E-Mail aus der Bestellung) · BCC intern · Anhang: Vertragsunterlagen der zugestimmten Fassung (PDF, contractAttachmentFor)
Platzhalter: {{name}}, {{produkt}}, {{preis}}, {{angebot_text}}, {{vertragsschluss}}, {{leistungsbeginn}}, {{mindestlaufzeit_bis}}, {{bedingungen_version}}, {{zustimmung_zeitpunkt}}
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): angebot, hosting, wartung, wartung_v3, sofortbeginn, beginn_nach_widerrufsfrist
- produkt = Katalogname ohne „WEBklar “ („Hosting-Abo“, „Wartungs-Abo“), preis z. B. „15,00 € / Monat“; genau einer der Blöcke hosting/wartung/wartung_v3 bringt die Liste „Enthalten“:
+ produkt = Katalogname ohne „WEBklar “ („Hosting-Abo“, „Wartungs-Abo“), preis z. B. „20,00 € / Monat“; genau einer der Blöcke hosting/wartung/wartung_v3 bringt die Liste „Enthalten“:
hosting und wartung nach Fassung 4 (Spec 3.1), wartung_v3 für ein Wartungs-Abo nach Fassung 3 (termsDocFor nicht v4: „1 E-Mail-Postfach · 1 Stunde Arbeitszeit pro Monat“ wie in den Bedingungen v3 § 3).
bedingungen_version und das PDF im Anhang kommen aus derselben Quelle (termsDocFor(subDoc.termsVersion).version): ohne gespeicherte Fassung beides Fassung 3.
-->
diff --git a/server/services/stripeBilling.js b/server/services/stripeBilling.js
index a76aaab..cdb63e5 100644
--- a/server/services/stripeBilling.js
+++ b/server/services/stripeBilling.js
@@ -77,7 +77,11 @@ export async function ensurePrice(key) {
priceIdCache.set(key, data[0].id)
return data[0].id
}
- const product = await stripe.products.create({
+ // Neuer Preis fuer ein bestehendes Produkt (z. B. Hosting 15 -> 20 EUR, 29.09.2026): an das vorhandene
+ // Produkt haengen statt ein zweites gleichnamiges Produkt bei Stripe anzulegen
+ const products = await stripe.products.list({ active: true, limit: 100 })
+ const existing = products.data.find((p) => p.metadata?.webklar === PRODUCT_METADATA[key])
+ const product = existing || await stripe.products.create({
name: catalog.name,
...(catalog.description ? { description: catalog.description } : {}),
...(catalog.statementDescriptor ? { statement_descriptor: catalog.statementDescriptor } : {}),
@@ -484,7 +488,7 @@ export async function ensureTestCoupon(productKey) {
return created.id
}
-// Preis wie in den Bedingungen: "15,00 €" (normales Leerzeichen, kein geschuetztes wie bei toLocaleString)
+// Preis wie in den Bedingungen: "20,00 €" (normales Leerzeichen, kein geschuetztes wie bei toLocaleString)
const fmtEuro = (cents) => `${(Number(cents || 0) / 100).toFixed(2).replace('.', ',')} €`
/**
diff --git a/test/aboRoutes.test.js b/test/aboRoutes.test.js
index 083fe6f..be9986c 100644
--- a/test/aboRoutes.test.js
+++ b/test/aboRoutes.test.js
@@ -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)
diff --git a/test/hourCredits.test.js b/test/hourCredits.test.js
index 3001ae5..de72275 100644
--- a/test/hourCredits.test.js
+++ b/test/hourCredits.test.js
@@ -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)
})
diff --git a/test/legalTexts.test.js b/test/legalTexts.test.js
index 4edcab8..55acbcc 100644
--- a/test/legalTexts.test.js
+++ b/test/legalTexts.test.js
@@ -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`)
diff --git a/test/mailStorage.test.js b/test/mailStorage.test.js
index 725e4aa..92b2999 100644
--- a/test/mailStorage.test.js
+++ b/test/mailStorage.test.js
@@ -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 = []
diff --git a/test/mailTemplates.test.js b/test/mailTemplates.test.js
index 4127d37..4ca9cbd 100644
--- a/test/mailTemplates.test.js
+++ b/test/mailTemplates.test.js
@@ -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/)
})
diff --git a/test/offerProduct.test.js b/test/offerProduct.test.js
index e0787c7..90d23e9 100644
--- a/test/offerProduct.test.js
+++ b/test/offerProduct.test.js
@@ -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 })
diff --git a/test/products.test.js b/test/products.test.js
index 5e9ced6..9ab400c 100644
--- a/test/products.test.js
+++ b/test/products.test.js
@@ -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)
})
diff --git a/test/stripeBilling.test.js b/test/stripeBilling.test.js
index 8ef5012..d808f01 100644
--- a/test/stripeBilling.test.js
+++ b/test/stripeBilling.test.js
@@ -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)
})
diff --git a/test/support/fakeStripe.js b/test/support/fakeStripe.js
index 919d5ac..bfdd172 100644
--- a/test/support/fakeStripe.js
+++ b/test/support/fakeStripe.js
@@ -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 = {
diff --git a/test/takeoverCredits.test.js b/test/takeoverCredits.test.js
index abb0fda..4ca79a1 100644
--- a/test/takeoverCredits.test.js
+++ b/test/takeoverCredits.test.js
@@ -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)
})