/** * 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, } }