Abo-Angebote: Admin sieht, bis wann der Rabatt läuft

"Gültig bis" ist nur die Frist zum Annehmen. Jetzt steht auch das
Rabattende da:
- Formular "Neues Angebot", Kunde ohne Abo: Schätzung "Rabatt läuft
  N Monate ab Abschluss und endet voraussichtlich zwischen <heute + N>
  und <gültig bis + N>", mit Hinweis auf den späteren Start nach der
  Widerrufsfrist (gut 2 Wochen)
- Formular, laufendes Abo: genaues Ende aus den nächsten Abbuchungen
  (Anker = Leistungsbeginn, Monatsende wie bei Stripe), auch in der
  Erfolgsmeldung
- Angebotsliste: offen -> Schätzung, aktiv -> "Rabatt endet am ..."
  aus dem Abo-Dokument (bzw. "endete", "endet mit dem Abo am ...",
  "unbekannt (Abo nicht gefunden)")

Rechnung in server/services/offerDiscount.js: Der Coupon deckt die
N Abbuchungen ab dem Anwenden. Beim Checkout ist das der Abo-Start, bei
laufendem Abo und Testkonto (Coupon per Webhook) der Zeitpunkt des
Anhängens; Testkonten laufen deshalb einen Monat länger.

Tests: npm test (37 Fälle, davon 8 neu).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-09-26 23:01:45 +00:00
1 parent c600118374
commit 054b04d5b6
5 files changed
+227 -12

No files matched your search

+81
View File
@@ -0,0 +1,81 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
// Vor dem Import von config.js setzen (jede Testdatei laeuft in einem eigenen Prozess)
process.env.ABO_TEST_EMAILS = 'test@example.invalid'
const { chargesFrom, redeemedDiscount } = await import('../server/services/offerDiscount.js')
const { adminOfferView } = await import('../server/services/offers.js')
test('Abbuchungen: vom Anker aus, Monatsende wie bei Stripe', () => {
assert.deepEqual(chargesFrom('2026-09-26T08:00:00.000Z', '2026-09-26T08:00:00.000Z', 3), [
'2026-09-26T08:00:00.000Z', '2026-10-26T08:00:00.000Z', '2026-11-26T08:00:00.000Z',
])
// 31. bleibt Anker: Februar kuerzer, danach wieder der 31.
assert.deepEqual(chargesFrom('2026-01-31T10:00:00.000Z', '2026-01-31T10:00:00.000Z', 4), [
'2026-01-31T10:00:00.000Z', '2026-02-28T10:00:00.000Z', '2026-03-31T10:00:00.000Z', '2026-04-30T10:00:00.000Z',
])
// Ab einem Zeitpunkt mitten im Abrechnungsmonat: naechste Abbuchung
assert.deepEqual(chargesFrom('2026-01-15T10:00:00.000Z', '2026-03-20T12:00:00.000Z', 2), [
'2026-04-15T10:00:00.000Z', '2026-05-15T10:00:00.000Z',
])
assert.deepEqual(chargesFrom(null, '2026-03-20T12:00:00.000Z', 2), [])
})
test('Angebot beim Abschluss, Leistung sofort: Abbuchungen 1..N rabattiert', () => {
const sub = { startedAt: '2026-09-26T08:00:00.000Z', trialEnd: null }
const d = redeemedDiscount({ months: 2, appliedAt: '2026-09-26T08:00:05.000Z', attachedLater: false }, sub)
assert.deepEqual(d, { known: true, firstChargeAt: '2026-09-26T08:00:00.000Z', endsAt: '2026-11-26T08:00:00.000Z', subscriptionEndsAt: null })
})
test('Angebot beim Abschluss, Beginn nach Widerrufsfrist: Rabatt ab Leistungsbeginn', () => {
const sub = { startedAt: '2026-09-26T08:00:00.000Z', trialEnd: '2026-10-13T04:00:00.000Z' }
const d = redeemedDiscount({ months: 2, appliedAt: '2026-09-26T08:00:05.000Z', attachedLater: false }, sub)
assert.equal(d.firstChargeAt, '2026-10-13T04:00:00.000Z')
assert.equal(d.endsAt, '2026-12-13T04:00:00.000Z')
})
test('Testkonto (Coupon nach der ersten Abbuchung): Abbuchungen 2..N+1', () => {
const sub = { startedAt: '2026-09-26T08:00:00.000Z', trialEnd: null }
const d = redeemedDiscount({ months: 2, appliedAt: '2026-09-26T08:00:05.000Z', attachedLater: true }, sub)
assert.equal(d.firstChargeAt, '2026-10-26T08:00:00.000Z')
assert.equal(d.endsAt, '2026-12-26T08:00:00.000Z')
})
test('Laufendes Abo: ab der naechsten Abbuchung nach dem Anhaengen', () => {
const sub = { startedAt: '2026-03-14T09:00:00.000Z', trialEnd: null }
const d = redeemedDiscount({ months: 2, appliedAt: '2026-09-26T20:00:00.000Z', attachedLater: true }, sub)
assert.equal(d.firstChargeAt, '2026-10-14T09:00:00.000Z')
assert.equal(d.endsAt, '2026-12-14T09:00:00.000Z')
})
test('Abo endet vor dem Rabatt: Abo-Ende wird gemeldet, sonst nicht', () => {
const base = { startedAt: '2026-09-26T08:00:00.000Z', trialEnd: null }
const offer = { months: 3, appliedAt: '2026-09-26T08:00:05.000Z', attachedLater: false }
assert.equal(redeemedDiscount(offer, { ...base, cancelAt: '2026-10-26T08:00:00.000Z' }).subscriptionEndsAt, '2026-10-26T08:00:00.000Z')
assert.equal(redeemedDiscount(offer, { ...base, endedAt: '2026-10-01T08:00:00.000Z' }).subscriptionEndsAt, '2026-10-01T08:00:00.000Z')
assert.equal(redeemedDiscount(offer, { ...base, cancelAt: '2027-09-26T08:00:00.000Z' }).subscriptionEndsAt, null)
})
test('Ohne Abo-Dokument ist das Rabattende unbekannt', () => {
assert.deepEqual(redeemedDiscount({ months: 2, appliedAt: '2026-09-26T08:00:05.000Z', attachedLater: false }, null), { known: false })
assert.deepEqual(redeemedDiscount({ months: 2, appliedAt: null, attachedLater: true }, { startedAt: '2026-09-26T08:00:00.000Z' }), { known: false })
})
test('Admin-Ansicht: Rabatt nur bei eingeloesten Angeboten, Testkonto erkannt', () => {
const doc = {
$id: 'offer-1', $createdAt: '2026-09-26T07:00:00.000Z', status: 'aktiv', scope: 'neu', months: 2,
monthlyPriceCents: 2500, regularPriceCents: 5000, validUntil: '2026-10-27T00:00:00.000Z',
appliedAt: '2026-09-26T08:00:05.000Z', stripeSubscriptionId: 'sub_1', customerId: 'c1',
}
const sub = { $id: 'sub_1', startedAt: '2026-09-26T08:00:00.000Z', trialEnd: null }
const normal = adminOfferView(doc, { name: 'Kunde', email: 'kunde@example.invalid' }, sub)
assert.equal(normal.testAccount, false)
assert.equal(normal.discount.endsAt, '2026-11-26T08:00:00.000Z')
const testView = adminOfferView(doc, { name: 'Test', email: 'TEST@example.invalid' }, sub)
assert.equal(testView.testAccount, true)
assert.equal(testView.discount.endsAt, '2026-12-26T08:00:00.000Z')
const running = adminOfferView({ ...doc, scope: 'laufend', appliedAt: '2026-10-01T12:00:00.000Z' }, { email: 'kunde@example.invalid' }, sub)
assert.equal(running.discount.firstChargeAt, '2026-10-26T08:00:00.000Z')
assert.equal(adminOfferView({ ...doc, status: 'offen', appliedAt: null }, { email: 'kunde@example.invalid' }, null).discount, null)
assert.deepEqual(adminOfferView(doc, { email: 'kunde@example.invalid' }, undefined).discount, { known: false })
})