Übernahmegutschrift: Verrechnung nach Wechsel, Mail K09, Leitfaden, Admin zeigt das Abo, Tests
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
0c2e50fc20
commit
e456c3de69
9 files changed
+80
-23
No files matched your search
Binary file not shown.
@@ -4,22 +4,22 @@
|
||||
<meta charset="utf-8">
|
||||
<title>Empfehlen für WEBklar – Leitfaden</title>
|
||||
<style>
|
||||
@page { size: A4; margin: 11mm 14mm 10mm; }
|
||||
@page { size: A4; margin: 10mm 13mm 9mm; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; color: #1d1d1f; font: 9.5pt/1.38 "DejaVu Sans", system-ui, sans-serif; }
|
||||
.brand { font-weight: 800; letter-spacing: 0.12em; font-size: 10pt; color: #6d1fd1; }
|
||||
h1 { font-size: 19pt; line-height: 1.15; margin: 2.5mm 0 1.2mm; }
|
||||
.lead { font-size: 10.5pt; color: #444; margin: 0 0 3mm; }
|
||||
h2 { font-size: 12pt; margin: 3.5mm 0 1.5mm; }
|
||||
ol.steps { list-style: none; padding: 0; margin: 0; display: grid; gap: 2mm; }
|
||||
.lead { font-size: 10.5pt; color: #444; margin: 0 0 2.5mm; }
|
||||
h2 { font-size: 12pt; margin: 3mm 0 1.2mm; }
|
||||
ol.steps { list-style: none; padding: 0; margin: 0; display: grid; gap: 1.5mm; }
|
||||
ol.steps li { display: grid; grid-template-columns: 9mm 1fr; gap: 3mm; align-items: start; }
|
||||
.num { width: 8mm; height: 8mm; border-radius: 50%; background: #6d1fd1; color: #fff; font-weight: 700; display: flex; align-items: center; justify-content: center; }
|
||||
ol.steps strong { display: block; }
|
||||
.box { border: 1px solid #e0dccf; border-radius: 3mm; padding: 2mm 4mm; margin: 2.5mm 0; background: #faf9f6; }
|
||||
.rules { margin: 0; padding-left: 5mm; }
|
||||
.rules li { margin: 1mm 0; }
|
||||
.rules li { margin: 0.8mm 0; }
|
||||
.abos { margin: 1mm 0; padding-left: 4.5mm; }
|
||||
.abos li { margin: 0.8mm 0; }
|
||||
.abos li { margin: 0.6mm 0; }
|
||||
.grid2 { display: grid; grid-template-columns: 1fr 1.2fr; gap: 4mm; }
|
||||
.muted { color: #666; font-size: 9pt; }
|
||||
.contact { font-size: 10pt; }
|
||||
@@ -48,12 +48,14 @@
|
||||
<div class="box">
|
||||
<strong>Unsere Abos auf einen Blick</strong> · beide mit Mindestlaufzeit 12 Monate, danach monatlich kündbar
|
||||
<ul class="abos">
|
||||
<li><b>Hosting-Abo, 15,00 € pro Monat</b> (Endpreis, gemäß § 19 UStG ohne Umsatzsteuer): nur statische Websites; 1 Domain,
|
||||
1 E-Mail-Postfach mit 5 GB; keine Pflege der Website und keine Arbeitszeit.</li>
|
||||
<li><b>Hosting-Abo, 15,00 € pro Monat</b> (Endpreis, gemäß § 19 UStG ohne Umsatzsteuer): nur statische Websites (ohne CMS, Login,
|
||||
Datenbank oder Formulare); 1 Domain, 1 E-Mail-Postfach mit 5 GB; keine Pflege der Website und kein Stundenkontingent
|
||||
(Änderungen nur nach Freigabe, gesondert berechnet).</li>
|
||||
<li><b>Wartungs-Abo, 50,00 € pro Monat</b> (Endpreis, gemäß § 19 UStG ohne Umsatzsteuer): alle Websites und Web-Anwendungen; Betrieb und Pflege,
|
||||
1 Domain, beliebig viele Postfächer mit zusammen 5 GB, 1 Stunde Arbeitszeit pro Monat (sammelt sich an, 12 Monate gültig).</li>
|
||||
<li><b>In beiden Abos:</b> E-Mail-Zusatzspeicher je 5 GB für 1,00 € pro Monat (Endpreis, gemäß § 19 UStG ohne Umsatzsteuer) und die
|
||||
Übernahmegutschrift, wenn der Kunde Domain, Hosting oder E-Mail beim bisherigen Anbieter im Voraus bezahlt hat (nach Prüfung, anteilig).</li>
|
||||
1 Domain, beliebig viele Postfächer mit zusammen 5 GB, 1 Stunde Arbeitszeit pro Monat (nicht genutzte Zeit bleibt 12 Monate verfügbar).</li>
|
||||
<li><b>In beiden Abos:</b> E-Mail-Zusatzspeicher je 5 GB für 1,00 € pro Monat (Endpreis, gemäß § 19 UStG ohne Umsatzsteuer).
|
||||
Außerdem kann der Kunde die freiwillige Übernahmegutschrift beantragen, wenn er Domain, Hosting oder E-Mail beim bisherigen Anbieter
|
||||
im Voraus bezahlt hat: Nach unserer Prüfung verrechnen wir den doppelt bezahlten Anteil mit seinen Monatsgebühren (keine Auszahlung).</li>
|
||||
<li><b>Wechsel</b> vom Hosting- ins Wartungs-Abo im Kundenportal unter „Verträge“ (Hosting-Abo läuft, nicht gekündigt, keine offene Zahlung);
|
||||
es beginnt eine neue Mindestlaufzeit von 12 Monaten.</li>
|
||||
</ul>
|
||||
|
||||
+1
-1
@@ -634,7 +634,7 @@ function renderCreditDetail(panel, d, message) {
|
||||
<dl class="kv">
|
||||
<dt>Kunde</dt><dd>${escapeHtml(c.name || c.companyName)}${c.code ? ` · Kd.-Nr. ${escapeHtml(c.code)}` : ''}</dd>
|
||||
<dt>E-Mail</dt><dd>${escapeHtml(c.email || '–')}</dd>
|
||||
<dt>Wartungs-Abo</dt><dd>${aboPill}</dd>
|
||||
<dt>${escapeHtml(s?.productName?.replace(/^WEBklar /, '') || 'Abo')}</dt><dd>${aboPill}</dd>
|
||||
${s ? `
|
||||
<dt>Leistungsbeginn</dt><dd>${fmtDate(s.serviceStart)}</dd>
|
||||
<dt>Nächste Abbuchung</dt><dd>${s.active ? fmtDate(s.currentPeriodEnd) : '–'}</dd>
|
||||
|
||||
+2
-2
@@ -1046,7 +1046,7 @@ const TAKEOVER_CONDITIONS = [
|
||||
'Die Übernahmegutschrift gibt es für Leistungen, die Ihr Abo ersetzt (Domain, Hosting bzw. Betrieb der Website, E-Mail-Postfächer) und die Sie bei Ihrem bisherigen Anbieter bereits im Voraus bezahlt haben.',
|
||||
'Wir schreiben den Anteil der Rechnung gut, der auf die Zeit ab Leistungsbeginn Ihres Abos bis zum Ende des bezahlten Zeitraums entfällt, tagesgenau berechnet.',
|
||||
'Nachweis ist die Rechnung des bisherigen Anbieters, ausgestellt auf Sie, mit Leistung, Zeitraum und Betrag. Wir können weitere Nachweise anfordern, etwa die Kündigungsbestätigung. Erstattet Ihr bisheriger Anbieter Beträge für diesen Zeitraum, teilen Sie uns das bitte mit.',
|
||||
'Die Gutschrift wird mit Ihren nächsten Monatsgebühren verrechnet. Sie wird nicht ausgezahlt und ist nicht übertragbar; ein bei Vertragsende noch nicht verrechneter Rest entfällt.',
|
||||
'Die Gutschrift wird mit Ihren nächsten Monatsgebühren verrechnet, auch nach einem Wechsel in ein anderes Abo. Sie wird nicht ausgezahlt und ist nicht übertragbar; endet Ihr Abo ohne einen solchen Wechsel, entfällt ein noch nicht verrechneter Rest.',
|
||||
'Die Übernahmegutschrift ist eine freiwillige Leistung von WEBklar. Über Ihren Antrag entscheiden wir nach Prüfung und antworten Ihnen per E-Mail.',
|
||||
]
|
||||
|
||||
@@ -1098,7 +1098,7 @@ function takeoverFormHtml(limits) {
|
||||
<div class="tk-services">
|
||||
<label><input type="checkbox" name="services" value="domain"> Domain</label>
|
||||
<label><input type="checkbox" name="services" value="hosting"> Hosting / Website-Betrieb</label>
|
||||
<label><input type="checkbox" name="services" value="email"> E-Mail-Postfach</label>
|
||||
<label><input type="checkbox" name="services" value="email"> E-Mail-Postfächer</label>
|
||||
<label><input type="checkbox" name="services" value="sonstiges"> Sonstiges</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@@ -42,8 +42,8 @@ a{color:#111111;}
|
||||
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="ink" style="padding:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-size:16px;line-height:25px;color:#111111;">{{anmerkung}}</div></td></tr></table></td></tr>
|
||||
<!-- ENDE anmerkung -->
|
||||
|
||||
<tr><td class="px mut" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Die Gutschrift verrechnen wir automatisch mit den nächsten Monatsgebühren Ihres Wartungs-Abos. Auf Ihren Rechnungen erscheint sie als angerechnetes Guthaben.</td></tr>
|
||||
<tr><td class="px mut" style="padding:12px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:13px;line-height:20px;mso-line-height-rule:exactly;color:#6b6964;">Eine Auszahlung ist nicht möglich; ein bei Vertragsende noch nicht verrechneter Rest entfällt.</td></tr>
|
||||
<tr><td class="px mut" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Die Gutschrift verrechnen wir automatisch mit den nächsten Monatsgebühren Ihres Abos. Auf Ihren Rechnungen erscheint sie als angerechnetes Guthaben.</td></tr>
|
||||
<tr><td class="px mut" style="padding:12px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:13px;line-height:20px;mso-line-height-rule:exactly;color:#6b6964;">Eine Auszahlung ist nicht möglich. Wechseln Sie in ein anderes Abo, verrechnen wir die Gutschrift weiter; endet Ihr Abo ohne einen solchen Wechsel, entfällt ein noch nicht verrechneter Rest.</td></tr>
|
||||
<tr><td class="px ink" style="padding:28px 40px 44px 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:24px;color:#111111;">Viele Grüße<br><strong>Ihr WEBklar-Team</strong></td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { domainToASCII, domainToUnicode } from 'node:url'
|
||||
import { config } from '../config.js'
|
||||
import { createDocument, getDocument, listDocuments, Query, updateDocument } from './appwriteAdmin.js'
|
||||
import { sendAdminNotice, sendCustomerMail } from './mailer.js'
|
||||
import { getProduct } from './products.js'
|
||||
import { getProduct, PRODUCT_KEYS } from './products.js'
|
||||
import { ACTIVE_STATUSES, getCurrentSubscription, getStripe, isStripeConfigured } from './stripeBilling.js'
|
||||
|
||||
/**
|
||||
@@ -35,7 +35,7 @@ export const STATUS = {
|
||||
export const SERVICE_LABELS = {
|
||||
domain: 'Domain',
|
||||
hosting: 'Hosting / Website-Betrieb',
|
||||
email: 'E-Mail-Postfach',
|
||||
email: 'E-Mail-Postfächer',
|
||||
sonstiges: 'Sonstiges',
|
||||
}
|
||||
|
||||
@@ -349,6 +349,8 @@ function withHistory(doc, entry) {
|
||||
|
||||
const serviceStartOf = (sub) => sub?.trialEnd || sub?.startedAt || null
|
||||
const isActiveSub = (sub) => Boolean(sub && ACTIVE_STATUSES.has(sub.status))
|
||||
// Produkt des Abos: Altbestand ohne productKey und unbekannte Schluessel gelten als Wartungs-Abo
|
||||
const productKeyOf = (sub) => (PRODUCT_KEYS.includes(sub?.productKey) ? sub.productKey : 'wartung')
|
||||
|
||||
export async function getTakeoverRequest(id) {
|
||||
return getDocument(COLLECTION(), id)
|
||||
@@ -638,9 +640,9 @@ export async function runAutoChecks(doc, { allRequests, subscription, calc }) {
|
||||
)
|
||||
}
|
||||
|
||||
// Plausibilitaet gegen die Monatsgebuehr des Abos des Kunden; Abos ohne productKey (Altbestand) gelten als Wartungs-Abo
|
||||
// Plausibilitaet gegen die Monatsgebuehr des Abos des Kunden (productKeyOf: ohne bzw. mit unbekanntem Schluessel = Wartungs-Abo)
|
||||
const monthly = doc.invoiceAmountCents / Math.max(1, doc.billingMonths)
|
||||
const aboCents = getProduct(subscription?.productKey || 'wartung').amountCents
|
||||
const aboCents = getProduct(productKeyOf(subscription)).amountCents
|
||||
if (monthly > aboCents) {
|
||||
checks.push({
|
||||
key: 'amount',
|
||||
@@ -735,6 +737,8 @@ export async function getAdminDetail(id) {
|
||||
? {
|
||||
status: subscription.status,
|
||||
active: isActiveSub(subscription),
|
||||
productKey: productKeyOf(subscription),
|
||||
productName: getProduct(productKeyOf(subscription)).name,
|
||||
startedAt: subscription.startedAt,
|
||||
serviceStart: serviceStartOf(subscription),
|
||||
currentPeriodEnd: subscription.currentPeriodEnd,
|
||||
|
||||
@@ -9,6 +9,11 @@ const { config } = await import('../server/config.js')
|
||||
// Fest eingetragen, weil die Tests auch ohne git laufen muessen (node:20-alpine).
|
||||
const V3_SHA256 = '1c9e4b002dcd5d7896bbda24c573e56544b7123e3a88d154e5db77bd2855bc47'
|
||||
|
||||
// = sha256 des Blocks ab `const TAKEOVER_CONDITIONS` bis vor das schliessende ] in public/app.js, je conditionsVersion
|
||||
// (Nachweis der Zustimmung). Aendert sich der Text der Bedingungen, muss config.takeover.conditionsVersion hochgezogen
|
||||
// und der neue Hash hier eingetragen werden; die Fehlermeldung des Tests nennt ihn.
|
||||
const TAKEOVER_CONDITIONS_SHA256 = { '2026-09-27': '2b7a745cb599c0ab28f20615a745bd86da6faa8d120391ca56beac35a0473e62' }
|
||||
|
||||
const file = (rel) => new URL(`../${rel}`, import.meta.url)
|
||||
const read = (rel) => readFileSync(file(rel), 'utf8')
|
||||
const sha256 = (rel) => createHash('sha256').update(readFileSync(file(rel))).digest('hex')
|
||||
@@ -335,16 +340,17 @@ test('Übernahmegutschrift und Leitfaden: produktneutral', () => {
|
||||
assert.notEqual(config.takeover.conditionsVersion, '2026-09-25', 'conditionsVersion hochgezogen')
|
||||
const tc = read('server/services/takeoverCredits.js')
|
||||
assert.ok(!/Wartungs-Abo (läuft|ist beendet)|kein (laufendes )?Wartungs-Abo/.test(tc), 'Prüf-Labels und Fehlermeldungen sagen „Abo“')
|
||||
assert.ok(!tc.includes('config.stripe.priceAmountCents'), 'Plausibilität gegen den Produktpreis, nicht gegen den Wartungspreis')
|
||||
const lf = read('docs/leitfaden-empfehlungsgeber.html')
|
||||
assert.ok(lf.includes('Hosting-Abo') && lf.includes('beliebig viele Postfächer'), 'Leitfaden nennt beide Abos')
|
||||
// Festlegungen zur Umsetzung: Postfächer im Plural (Wartungs-Abo: beliebig viele), Entwurfsdatum als Version,
|
||||
// Karte im Portal und Checkliste ohne Produktnamen, Preisvergleich je Produkt
|
||||
// Karte im Portal und Checkliste ohne Produktnamen (Preisvergleich je Produkt: test/takeoverCredits.test.js)
|
||||
assert.ok(cond.includes('E-Mail-Postfächer'))
|
||||
assert.match(config.takeover.conditionsVersion, /^\d{4}-\d{2}-\d{2}$/)
|
||||
const condHash = createHash('sha256').update(cond).digest('hex')
|
||||
assert.equal(condHash, TAKEOVER_CONDITIONS_SHA256[config.takeover.conditionsVersion],
|
||||
`Bedingungen der Übernahmegutschrift geändert: conditionsVersion hochziehen und Hash eintragen (${condHash})`)
|
||||
assert.ok(app.includes('sobald Ihr Abo läuft') && !app.includes('sobald Ihr Wartungs-Abo läuft'))
|
||||
assert.ok(!tc.includes('durch das Wartungs-Abo ersetzt'))
|
||||
assert.ok(tc.includes("getProduct(subscription?.productKey || 'wartung')"))
|
||||
// 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)
|
||||
|
||||
@@ -129,3 +129,11 @@ test('htmlToText: Links, Buttons und Leerzeilen', () => {
|
||||
test('Vorlagen-Ordner enthält nur Vorlagen', () => {
|
||||
assert.deepEqual(readdirSync(DIR).filter((f) => !/^[KT]\d{2} .*\.html$/.test(f)), [])
|
||||
})
|
||||
|
||||
test('K09: Gutschrift wird mit dem Abo verrechnet, auch nach einem Wechsel', () => {
|
||||
const tpl = loadTemplate('K09')
|
||||
const mail = renderMail('K09', sampleValues(tpl), allBlocks(tpl, true))
|
||||
assert.match(mail.text, /mit den nächsten Monatsgebühren Ihres Abos\. Auf Ihren Rechnungen erscheint sie als angerechnetes Guthaben\./)
|
||||
assert.match(mail.text, /Eine Auszahlung ist nicht möglich\. Wechseln Sie in ein anderes Abo, verrechnen wir die Gutschrift weiter; endet Ihr Abo ohne einen solchen Wechsel, entfällt ein noch nicht verrechneter Rest\./)
|
||||
assert.doesNotMatch(mail.html, /Wartungs-Abo|Vertragsende/)
|
||||
})
|
||||
@@ -0,0 +1,37 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
// runAutoChecks arbeitet ohne Domain komplett offline (kein DNS-Abgleich, kein Appwrite, kein Stripe):
|
||||
// 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: '[]' }
|
||||
const sub = (productKey) => ({
|
||||
status: 'active',
|
||||
startedAt: '2026-10-01T00:00:00.000Z',
|
||||
currentPeriodEnd: '2026-11-01T00:00:00.000Z',
|
||||
...(productKey === undefined ? {} : { 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 () => {
|
||||
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)
|
||||
})
|
||||
|
||||
test('Plausibilität: 20 € pro Monat ist nicht mehr als das Wartungs-Abo (50,00 €)', async () => {
|
||||
assert.equal(await amountCheck(sub('wartung')), undefined)
|
||||
})
|
||||
|
||||
test('Abos ohne oder mit unbekanntem productKey gelten als Wartungs-Abo, ohne Abo ebenso', async () => {
|
||||
assert.equal(await amountCheck(sub(undefined)), undefined, 'Altbestand ohne productKey')
|
||||
assert.equal(await amountCheck(sub('gibtsnicht')), undefined, 'unbekannter productKey darf die Prüfung nicht abbrechen')
|
||||
const ohneAbo = await runAutoChecks(doc, { allRequests: [], subscription: null, calc: null })
|
||||
assert.equal(ohneAbo.find((c) => c.key === 'amount'), undefined)
|
||||
assert.equal(ohneAbo.find((c) => c.key === 'abo')?.status, 'warn')
|
||||
})
|
||||
Reference in new issue
Block a user