Team-API, Sidebar-Zaehler und Format-Hilfen fuer Gutschriften, Angebote und E-Mail-Speicher

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-02 02:55:48 +00:00
1 parent 813c6a7e18
commit 91d526e909
13 files changed
+487 -47

No files matched your search

+117
View File
@@ -0,0 +1,117 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
// Etappe 3: Texte und Rechnungen aus dem Admin-Panel (public/admin.js), wortgleich ins Ticketsystem uebernommen
const fmt = await loadModule('src/lib/format.js')
const credit = await loadModule('src/lib/creditFormat.js')
const offer = await loadModule('src/lib/offerFormat.js')
const storage = await loadModule('src/lib/storageFormat.js')
const NB = ' '
test('format: Euro aus Cent, Eingabe in Cent, deutsche Daten', () => {
assert.equal(fmt.formatCents(123456), `1.234,56${NB}€`)
assert.equal(fmt.formatCents(0), `0,00${NB}€`)
assert.equal(fmt.parseEuroToCents('12,5'), 1250)
assert.equal(fmt.parseEuroToCents('12.50'), 1250)
assert.equal(fmt.parseEuroToCents(''), null)
assert.equal(fmt.parseEuroToCents('abc'), null)
assert.equal(fmt.fmtDate('2026-09-30T22:30:00.000Z'), '01.10.2026')
assert.equal(fmt.fmtDate(null), '–')
assert.equal(fmt.fmtDateTime('2026-09-30T10:05:00.000Z'), '30.09.26, 12:05')
assert.equal(fmt.fmtYmd('2026-10-27'), '27.10.2026')
})
test('Gutschrift: Status, Verlauf, Rechenzeile', () => {
assert.deepEqual(credit.CREDIT_STATUS.offen, { tone: 'warn', label: 'Zu prüfen' })
assert.deepEqual(credit.CREDIT_STATUS.rueckfrage, { tone: 'muted', label: 'Rückfrage beim Kunden' })
assert.equal(credit.historyLabel('verrechnet'), 'In Stripe gutgeschrieben')
assert.equal(credit.historyLabel('nachweise-geloescht'), 'Nachweise gelöscht')
assert.equal(credit.historyLabel('neu'), 'neu')
assert.deepEqual(credit.BILLING_MONTHS, [1, 3, 6, 12, 24, 36])
assert.equal(
credit.calcLine({ periodStart: '2026-01-01', periodEnd: '2026-12-31', periodDays: 365, overlapStart: '2026-10-01', overlapDays: 92, suggestedCents: 3025 }),
`Rechnungszeitraum 01.01.2026 – 31.12.2026 (365 Tage). Doppelt bezahlt ab 01.10.2026: 92 Tage → Vorschlag 30,25${NB}€`
)
})
test('Gutschrift: Pruefung, Rueckfragen und Meldungen wie im Admin-Panel', () => {
assert.equal(credit.decisionError('question', 'kurz'), 'Bitte die Rückfrage an den Kunden ins Nachrichtenfeld schreiben.')
assert.equal(credit.decisionError('reject', ''), 'Bitte die Begründung für den Kunden ins Nachrichtenfeld schreiben.')
assert.equal(credit.decisionError('question', 'Bitte Rechnung'), null)
assert.equal(credit.decisionError('approve', ''), null)
assert.equal(credit.decisionConfirm('approve', 3025, 'Berta'), `Gutschrift über 30,25${NB}€ genehmigen, in Stripe gutschreiben und Berta per E-Mail informieren?`)
assert.equal(credit.decisionConfirm('question', 0, ''), 'Rückfrage per E-Mail an den Kunden senden?')
assert.equal(credit.decisionConfirm('reject', 0, 'Berta'), 'Antrag ablehnen und Berta die Begründung per E-Mail senden?')
assert.equal(credit.decisionMessage('approve', { status: 'genehmigt', applied: true, mailSent: true }), 'Genehmigt. Das Guthaben ist in Stripe gebucht. Die E-Mail an den Kunden ist raus.')
assert.equal(
credit.decisionMessage('approve', { status: 'genehmigt', applied: false, reason: 'Stripe: kaputt', mailSent: false, mailError: 'SMTP weg' }),
'Genehmigt. Noch nicht in Stripe gebucht: Stripe: kaputt E-Mail an den Kunden fehlgeschlagen (SMTP weg) – bitte selbst informieren.'
)
assert.equal(credit.decisionMessage('question', { status: 'rueckfrage', mailSent: false }), 'Rückfrage gesendet. E-Mail an den Kunden fehlgeschlagen (unbekannt) – bitte selbst informieren.')
assert.equal(credit.applyMessage({ applied: true }), 'Das Guthaben ist in Stripe gebucht.')
assert.equal(credit.applyMessage({ applied: false, reason: 'Der Kunde hat kein laufendes Abo.' }), 'Nicht gebucht: Der Kunde hat kein laufendes Abo.')
})
test('Angebot: Vorschautext, Monate, Rabattende', () => {
assert.equal(offer.offerPreviewText(2500, 2, 5000, false), `Erste 2 Monate je 25,00${NB}€ statt 50,00${NB}€, danach 50,00${NB}€ pro Monat`)
assert.equal(offer.offerPreviewText(0, 1, 2000, true), `Nächster Monat kostenlos, danach 20,00${NB}€ pro Monat`)
assert.equal(offer.addMonthsYmd('2026-01-31', 1), '2026-02-28')
assert.equal(offer.addMonthsYmd('2026-11-15', 2), '2027-01-15')
const est = offer.offerDiscountEstimate(2, '2026-10-31', false, '2026-10-02')
assert.deepEqual(est, { earliest: '2026-12-02', latest: '2026-12-31' })
assert.deepEqual(offer.offerDiscountEstimate(2, '2026-10-31', true, '2026-10-02'), { earliest: '2027-01-02', latest: '2027-01-31' })
assert.equal(offer.discountLine({ status: 'offen', validUntil: '2026-10-02T00:00:00.000Z', months: 2 }, '2026-10-02'), 'Rabatt endet voraussichtlich am 02.12.2026')
assert.equal(offer.discountLine({ status: 'aktiv', discount: { known: false } }), 'Rabattende unbekannt (Abo nicht gefunden)')
assert.equal(offer.discountLine({ status: 'aktiv', discount: { known: true, endsAt: '2099-01-01T12:00:00.000Z' } }), 'Rabatt endet am 01.01.2099')
assert.deepEqual(offer.OFFER_STATUS.zurueckgezogen, { tone: 'muted', label: 'zurückgezogen' })
})
const ctx = (over = {}) => ({
customer: { id: 'c1', name: 'Berta', portalAccessEnabled: true, testAccount: false },
subscription: null,
openOffers: { hosting: null, wartung: null },
products: { hosting: { regularPriceCents: 2000 }, wartung: { regularPriceCents: 5000 } },
defaultValidUntil: '2026-11-01',
...over,
})
const form = (over = {}) => ({ customerId: 'c1', productKey: 'wartung', monthlyPriceCents: 2500, months: 2, validUntil: '2026-10-31', ...over })
test('Angebotsformular: Sperren, laufendes Abo, Knopftext', () => {
const ok = offer.evaluateOfferForm(ctx(), form(), '2026-10-02')
assert.deepEqual(ok.errors, [])
assert.equal(ok.running, false)
assert.equal(ok.saveLabel, 'Angebot anlegen')
assert.equal(ok.previewLines[0], `Erste 2 Monate je 25,00${NB}€ statt 50,00${NB}€, danach 50,00${NB}€ pro Monat · Ersparnis 50,00${NB}€`)
assert.equal(ok.previewLines.at(-1), 'Noch kein laufendes Abo: Der Coupon wird angelegt und beim Abschluss im Kundenportal angewendet.')
const blocked = offer.evaluateOfferForm(ctx({ openOffers: { hosting: null, wartung: { ref: 'A1', text: 'x' } } }), form({ monthlyPriceCents: 5000 }), '2026-10-02')
assert.deepEqual(blocked.errors, [
'Es gibt schon ein offenes Wartungs-Abo-Angebot (A1: x) – bitte zuerst zurückziehen.',
`Angebotspreis bitte zwischen 0,00 € und 49,99${NB}€ angeben.`,
])
const running = offer.evaluateOfferForm(ctx({ subscription: { active: true, productKey: 'wartung', currentPeriodEnd: '2026-11-01T00:00:00.000Z', upcomingCharges: ['2026-11-01T00:00:00.000Z', '2026-12-01T00:00:00.000Z', '2027-01-01T00:00:00.000Z'] } }), form({ validUntil: '' }), '2026-10-02')
assert.equal(running.running, true)
assert.equal(running.saveLabel, 'Angebot anlegen und aufs laufende Abo anwenden')
assert.ok(running.previewLines.includes('Rabatt endet am 01.01.2027 (nach 2 Abbuchungen).'))
assert.deepEqual(offer.evaluateOfferForm(ctx(), form({ validUntil: '' }), '2026-10-02').errors, ['Bitte „Gültig bis“ angeben.'])
assert.equal(
offer.offerConfirm(ctx(), form(), false),
`Angebot für Berta anlegen?\n\nErste 2 Monate je 25,00${NB}€ statt 50,00${NB}€, danach 50,00${NB}€ pro Monat\nGültig bis 31.10.2026\n\nDer Coupon wird sofort in Stripe angelegt.`
)
})
test('E-Mail-Speicher: Pakete, Zusammenfassung, Aufgaben', () => {
assert.equal(storage.storageLabel(0), 'kein Paket (5 GB)')
assert.equal(storage.storageLabel(1), '1 Paket (10 GB)')
assert.equal(storage.storageLabel(3), '3 Pakete (20 GB)')
assert.deepEqual(storage.summaryText({ allocatedGb: 20, packageGb: 50, warnGb: 40, packageName: 'Webhosting M' }), { warn: false, text: '20 von 50 GB vergeben (Paket Webhosting M) · Warnung ab 40 GB' })
assert.deepEqual(storage.summaryText({ allocatedGb: 41, packageGb: 50, warnGb: 40, packageName: 'Webhosting M' }), { warn: true, text: '41 von 50 GB vergeben (Paket Webhosting M) – Warnschwelle 40 GB erreicht: größeres Hetzner-Paket buchen.' })
assert.equal(storage.taskText({ kind: 'einrichten', targetGb: 15 }), 'Postfachgrenze in konsoleH jetzt auf 15 GB setzen')
assert.equal(storage.taskText({ kind: 'periodenende', targetGb: 10, dueAt: '2026-11-01T00:00:00.000Z' }), 'Postfachgrenze in konsoleH am 01.11.2026 auf 10 GB senken')
assert.equal(storage.scopeText({ targetGb: 15, contactDomain: 'kunde.de' }), 'Gesamtspeicher des Kunden: 15 GB, auf seine Postfächer verteilt nach seiner Vorgabe – § 3 Abs. 4 · Kontakt: kunde.de')
assert.equal(storage.changeText({ fromUnits: 0, toUnits: 2, viaCancellation: true }), 'kein Paket (5 GB) → 2 Pakete (15 GB) · über die Kündigungsseite')
assert.equal(storage.partialText({ partial: true, toUnits: 1, error: 'x' }), 'Teilweise: Stripe steht noch auf der bezahlten Menge, nicht auf 1 Paket(en) – Stückzahl in Stripe von Hand nachziehen. Fehler: x')
assert.equal(storage.orderedKind({ fromUnits: 0, toUnits: 2 }), 'Bestellung (Plus)')
assert.equal(storage.orderedKind({ fromUnits: 3, toUnits: 1 }), 'Verringerung (Minus)')
assert.equal(storage.failedText({ fromUnits: 0, toUnits: 1, error: '' }), 'kein Paket (5 GB) → 1 Paket (10 GB) – Stripe hat die Änderung abgelehnt, nichts berechnet, keine Aufgabe.')
})