Zusatzspeicher: Plus sofort anteilig, Minus zum Monatsende, Aufgaben für konsoleH
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
887e1e54a0
commit
379cc26dcd
11 files changed
+1531
-27
No files matched your search
@@ -326,7 +326,8 @@ export function termStartOf(sub) {
|
||||
return new Date(((sub.trial_end || sub.start_date) || 0) * 1000)
|
||||
}
|
||||
|
||||
function subscriptionIdOfInvoice(invoice) {
|
||||
/** Stripe-Abo-ID (oder -Objekt) einer Rechnung: Webhook-Nutzlast (alt/neu) oder frisch geladene Rechnung. */
|
||||
export function subscriptionIdOfInvoice(invoice) {
|
||||
return (
|
||||
invoice.subscription ||
|
||||
invoice.parent?.subscription_details?.subscription ||
|
||||
@@ -356,8 +357,33 @@ export async function resolvePriceKeys(priceIds) {
|
||||
return keys
|
||||
}
|
||||
|
||||
/** Bezahlte Wartungs-Rechnung -> 1 Stunde gutschreiben (idempotent ueber Invoice-ID). */
|
||||
export async function grantCreditForInvoice(invoice) {
|
||||
/**
|
||||
* Alle Zeilen einer Rechnung. invoices.retrieve bettet hoechstens 10 Zeilen ein (lines.has_more); anteilige
|
||||
* Zeilen stehen vor den Abo-Zeilen, mehrere Speicheraenderungen in einem Monat schieben also die Wartungs-
|
||||
* oder Speicher-Zeile hinaus. Bei has_more kommen alle Seiten ueber invoices.listLineItems (F18). Eine
|
||||
* Vorschau-Rechnung hat keine ID und laesst sich nicht nachladen: dort zaehlen die eingebetteten Zeilen.
|
||||
*/
|
||||
export async function invoiceLines(invoice) {
|
||||
const embedded = invoice?.lines?.data || []
|
||||
if (!invoice?.lines?.has_more || !invoice.id) return embedded
|
||||
return getStripe().invoices.listLineItems(invoice.id, { limit: 100 }).autoPagingToArray({ limit: 1000 })
|
||||
}
|
||||
|
||||
/**
|
||||
* Rechnung frisch laden (in der API-Version des SDK, nicht der des Webhook-Endpunkts), mit allen Zeilen.
|
||||
* Der Webhook invoice.paid laedt sie genau einmal und reicht sie an Stundengutschrift und Speicher-Buchung weiter.
|
||||
*/
|
||||
export async function loadInvoice(invoiceId) {
|
||||
const fresh = await getStripe().invoices.retrieve(invoiceId)
|
||||
const data = await invoiceLines(fresh)
|
||||
return { ...fresh, lines: { ...(fresh.lines || {}), data, has_more: false } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Bezahlte Wartungs-Rechnung -> 1 Stunde gutschreiben (idempotent ueber Invoice-ID). invoice ist die
|
||||
* Webhook-Nutzlast (wird frisch geladen) oder, mit fresh: true, eine schon per loadInvoice geladene Rechnung.
|
||||
*/
|
||||
export async function grantCreditForInvoice(invoice, { fresh = false } = {}) {
|
||||
const ref = subscriptionIdOfInvoice(invoice)
|
||||
const subscriptionId = typeof ref === 'string' ? ref : ref?.id
|
||||
if (!subscriptionId) return { skipped: 'keine Abo-Rechnung' }
|
||||
@@ -365,28 +391,28 @@ export async function grantCreditForInvoice(invoice) {
|
||||
// Es zählt die reguläre Wartungs-Zeile vor Rabatt und Guthaben: Auch ein Monat, den ein Angebot
|
||||
// oder eine Übernahmegutschrift ganz deckt, ist ein bezahlter Leistungsmonat. Probezeit (0 €),
|
||||
// anteilige Verrechnungen, Hosting und Zusatzspeicher bringen keine Stunde.
|
||||
const fresh = await getStripe().invoices.retrieve(invoice.id)
|
||||
const minutes = creditMinutesForInvoice(fresh, await resolvePriceKeys(priceIdsOfInvoice(fresh)))
|
||||
const loaded = fresh ? invoice : await loadInvoice(invoice.id)
|
||||
const minutes = creditMinutesForInvoice(loaded, await resolvePriceKeys(priceIdsOfInvoice(loaded)))
|
||||
if (!minutes) return { skipped: 'keine Wartungs-Leistung' }
|
||||
|
||||
const sub = await syncSubscription(subscriptionId)
|
||||
if (!sub) return { skipped: 'Abo ohne Kundenzuordnung' }
|
||||
|
||||
const grantedAt = new Date((fresh.status_transitions?.paid_at || fresh.created) * 1000)
|
||||
const grantedAt = new Date((loaded.status_transitions?.paid_at || loaded.created) * 1000)
|
||||
try {
|
||||
await createDocument(
|
||||
config.collections.hourCredits,
|
||||
{
|
||||
customerId: sub.customerId,
|
||||
stripeSubscriptionId: subscriptionId,
|
||||
stripeInvoiceId: invoice.id,
|
||||
stripeInvoiceId: loaded.id,
|
||||
minutes,
|
||||
grantedAt: grantedAt.toISOString(),
|
||||
expiresAt: addMonths(grantedAt, config.abo.creditValidityMonths).toISOString(),
|
||||
source: 'stripe_invoice',
|
||||
note: fresh.number ? `Rechnung ${fresh.number}` : '',
|
||||
note: loaded.number ? `Rechnung ${loaded.number}` : '',
|
||||
},
|
||||
invoice.id
|
||||
loaded.id
|
||||
)
|
||||
return { granted: true, customerId: sub.customerId, subscription: sub }
|
||||
} catch (err) {
|
||||
|
||||
Reference in new issue
Block a user