Finanzen: Liquiditaetsvorschau, Abo-Kuendigung: cancelAt allein massgeblich, cancelAtPeriodEnd ohne cancelAt keine Verlaengerung
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9361000a70
commit
34dfe181e4
2 files changed
+30
-6
No files matched your search
@@ -146,12 +146,14 @@ export function buildForecast({
|
|||||||
if (!isCents(subscription.priceAmount) || subscription.priceAmount <= 0) continue
|
if (!isCents(subscription.priceAmount) || subscription.priceAmount <= 0) continue
|
||||||
const periodEnd = berlinDayOf(subscription.currentPeriodEnd)
|
const periodEnd = berlinDayOf(subscription.currentPeriodEnd)
|
||||||
if (!periodEnd) continue
|
if (!periodEnd) continue
|
||||||
|
// Kuendigung: Ein gesetztes cancelAt gilt allein (es kann mehrere Perioden voraus liegen, z.B. Mindestlaufzeit);
|
||||||
|
// cancelAtPeriodEnd allein heisst: Stripe bucht zum Periodenende nichts mehr ab, also gar keine Verlaengerung.
|
||||||
const cancelAt = berlinDayOf(subscription.cancelAt)
|
const cancelAt = berlinDayOf(subscription.cancelAt)
|
||||||
|
if (!cancelAt && subscription.cancelAtPeriodEnd) continue
|
||||||
for (const renewal of recurring(periodEnd, 1)) {
|
for (const renewal of recurring(periodEnd, 1)) {
|
||||||
const payoutDate = addDays(renewal, PAYOUT_DELAY_DAYS)
|
const payoutDate = addDays(renewal, PAYOUT_DELAY_DAYS)
|
||||||
if (payoutDate > end) break
|
if (payoutDate > end) break
|
||||||
if (cancelAt && renewal >= cancelAt) break // gekuendigt: ab dem Kuendigungstag keine Verlaengerung mehr
|
if (cancelAt && renewal >= cancelAt) break // nur Verlaengerungen vor dem Kuendigungstag
|
||||||
if (subscription.cancelAtPeriodEnd && renewal > periodEnd) break // zum Periodenende gekuendigt: nur die laufende Periode
|
|
||||||
if (payoutDate >= today) add(payoutDate, 'Abo-Verlängerung', subscription.priceAmount, ERWARTET)
|
if (payoutDate >= today) add(payoutDate, 'Abo-Verlängerung', subscription.priceAmount, ERWARTET)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,10 +135,15 @@ test('Abo: Periodenende wird als Berliner Tag gelesen (Mitternachtsrand), Klamme
|
|||||||
assert.deepEqual(dates(r), ['2026-11-04', '2026-12-04', '2027-01-04'])
|
assert.deepEqual(dates(r), ['2026-11-04', '2026-12-04', '2027-01-04'])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Abo mit cancelAtPeriodEnd: kein Posten am 09.11.', () => {
|
test('Abo mit cancelAtPeriodEnd ohne cancelAt: gar keine Verlängerung (Stripe bucht zum Periodenende nichts mehr ab)', () => {
|
||||||
const r = base({ horizonDays: 70, subscriptions: [sub({ cancelAtPeriodEnd: true })] })
|
const r = base({ horizonDays: 70, subscriptions: [sub({ cancelAtPeriodEnd: true, cancelAt: null })] })
|
||||||
assert.ok(!dates(r).includes('2026-11-09'))
|
assert.deepEqual(r.posten, [])
|
||||||
assert.deepEqual(dates(r), ['2026-10-09'])
|
// gilt auch ohne das Feld cancelAt
|
||||||
|
const { cancelAt, ...ohneCancelAt } = sub({ cancelAtPeriodEnd: true })
|
||||||
|
assert.deepEqual(base({ horizonDays: 70, subscriptions: [ohneCancelAt] }).posten, [])
|
||||||
|
// andere Abos daneben laufen weiter
|
||||||
|
const mixed = base({ horizonDays: 40, subscriptions: [sub({ cancelAtPeriodEnd: true }), sub({ customerId: 'c2' })] })
|
||||||
|
assert.deepEqual(mixed.posten.map((p) => [p.date, p.amountCents]), [['2026-10-09', 2000], ['2026-11-09', 2000]])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Abo mit cancelAt: keine Termine ab cancelAt', () => {
|
test('Abo mit cancelAt: keine Termine ab cancelAt', () => {
|
||||||
@@ -153,6 +158,23 @@ test('Abo mit cancelAt: keine Termine ab cancelAt', () => {
|
|||||||
assert.deepEqual(r3.posten, [])
|
assert.deepEqual(r3.posten, [])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Abo mit cancelAt mehrere Perioden voraus (Mindestlaufzeit): Verlängerungen bis vor cancelAt, cancelAtPeriodEnd ändert daran nichts', () => {
|
||||||
|
// Produktionsform: Stripe liefert cancelAtPeriodEnd = true, sobald cancel_at gesetzt ist
|
||||||
|
const cancelled = (cancelAt, extra = {}) => sub({ cancelAt, cancelAtPeriodEnd: true, ...extra })
|
||||||
|
// cancelAt 01.01.2027, Horizont 120 (bis 29.01.2027): 05.10., 05.11., 05.12. -> Auszahlung 09.10., 09.11., 09.12.; die vom 05.01. nicht
|
||||||
|
const r = base({ horizonDays: 120, subscriptions: [cancelled('2027-01-01T10:00:00.000+00:00')] })
|
||||||
|
assert.deepEqual(dates(r), ['2026-10-09', '2026-11-09', '2026-12-09'])
|
||||||
|
assert.ok(r.posten.every((p) => p.amountCents === 2000 && p.sicherheit === 'erwartet'))
|
||||||
|
// cancelAt am Verlängerungstag selbst: diese Verlängerung entfällt, die davor bleiben
|
||||||
|
assert.deepEqual(dates(base({ horizonDays: 120, subscriptions: [cancelled('2027-01-05T10:00:00.000+00:00')] })), ['2026-10-09', '2026-11-09', '2026-12-09'])
|
||||||
|
// einen Tag später: die Verlängerung am 05.01. läuft noch (Auszahlung 09.01.)
|
||||||
|
assert.deepEqual(dates(base({ horizonDays: 120, subscriptions: [cancelled('2027-01-06T10:00:00.000+00:00')] })), ['2026-10-09', '2026-11-09', '2026-12-09', '2027-01-09'])
|
||||||
|
// cancelAt wird als Berliner Tag gelesen: 04.01. 23:30 UTC ist in Berlin bereits der 05.01.
|
||||||
|
assert.deepEqual(dates(base({ horizonDays: 120, subscriptions: [cancelled('2027-01-04T23:30:00.000+00:00')] })), ['2026-10-09', '2026-11-09', '2026-12-09'])
|
||||||
|
// ohne cancelAtPeriodEnd (nur cancelAt) dasselbe
|
||||||
|
assert.deepEqual(dates(base({ horizonDays: 120, subscriptions: [sub({ cancelAt: '2027-01-01T10:00:00.000+00:00' })] })), ['2026-10-09', '2026-11-09', '2026-12-09'])
|
||||||
|
})
|
||||||
|
|
||||||
test('Abo: nur aktive zählen, Periodenende in der Vergangenheit wird vorgespult', () => {
|
test('Abo: nur aktive zählen, Periodenende in der Vergangenheit wird vorgespult', () => {
|
||||||
const r = base({
|
const r = base({
|
||||||
subscriptions: [
|
subscriptions: [
|
||||||
|
|||||||
Reference in new issue
Block a user