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:
1 parent
c600118374
commit
054b04d5b6
5 files changed
+227
-12
No files matched your search
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Rabattzeitraum eines Abo-Angebots (reine Datumsrechnung, ohne Stripe- oder Appwrite-Aufruf).
|
||||
*
|
||||
* Abgebucht wird am Leistungsbeginn (Anker: Ende der Probezeit bzw. Abo-Start) plus k Monate, am
|
||||
* Monatsende wie bei Stripe. Der Angebots-Coupon (duration "repeating", N Monate) gilt fuer die
|
||||
* Rechnungen der N Monate ab dem Anwenden, bei monatlicher Abrechnung also fuer N Abbuchungen:
|
||||
* - Angebot beim Abschluss (Checkout): ab Abo-Start, also die Abbuchungen 1..N
|
||||
* - laufendes Abo und Testkonto (Coupon kommt erst nach dem Abschluss per Webhook): ab dem Anhaengen
|
||||
* Der Rabatt endet mit der Abbuchung danach, ab dann gilt wieder der regulaere Preis.
|
||||
*/
|
||||
|
||||
// Wie addMonths in stripeBilling.js: 31.01. + 1 Monat = 28./29.02., immer vom Anker aus gerechnet
|
||||
function addMonths(date, months) {
|
||||
const d = new Date(date)
|
||||
const day = d.getUTCDate()
|
||||
d.setUTCDate(1)
|
||||
d.setUTCMonth(d.getUTCMonth() + months)
|
||||
const last = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 0)).getUTCDate()
|
||||
d.setUTCDate(Math.min(day, last))
|
||||
return d
|
||||
}
|
||||
|
||||
/** Die naechsten `count` Abbuchungen eines Abos, beginnend mit der ersten am oder nach `from` (ISO-Strings). */
|
||||
export function chargesFrom(anchor, from, count) {
|
||||
// new Date(null) waere der 01.01.1970, deshalb fehlende Werte vorab abfangen
|
||||
if (!anchor || !from) return []
|
||||
const start = new Date(anchor)
|
||||
const since = new Date(from)
|
||||
if (Number.isNaN(start.getTime()) || Number.isNaN(since.getTime())) return []
|
||||
let k = 0
|
||||
while (addMonths(start, k) < since) k += 1
|
||||
return Array.from({ length: count }, (_, i) => addMonths(start, k + i).toISOString())
|
||||
}
|
||||
|
||||
/**
|
||||
* Rabatt eines eingeloesten Angebots. `attachedLater`: Coupon wurde erst nach dem Abo-Start angehaengt
|
||||
* (laufendes Abo, Testkonto), dann zaehlt `appliedAt` statt des Abo-Starts.
|
||||
* Ergebnis: erste rabattierte Abbuchung, Rabattende (= erste Abbuchung zum regulaeren Preis) und ein
|
||||
* frueheres Abo-Ende (gekuendigt/beendet), oder { known: false }, wenn das Abo fehlt.
|
||||
*/
|
||||
export function redeemedDiscount({ months, appliedAt, attachedLater }, sub) {
|
||||
const anchor = sub?.trialEnd || sub?.startedAt
|
||||
const from = attachedLater ? appliedAt : sub?.startedAt
|
||||
const charges = anchor && from ? chargesFrom(anchor, from, months + 1) : []
|
||||
if (!charges.length) return { known: false }
|
||||
const endsAt = charges[months]
|
||||
const subscriptionEndsAt = sub.endedAt || sub.cancelAt || null
|
||||
return {
|
||||
known: true,
|
||||
firstChargeAt: charges[0],
|
||||
endsAt,
|
||||
// Abo endet vor dem Rabatt: danach wird ohnehin nichts mehr abgebucht
|
||||
subscriptionEndsAt: subscriptionEndsAt && new Date(subscriptionEndsAt) < new Date(endsAt) ? subscriptionEndsAt : null,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user