Stundenkonto: Arbeitszeit nur für bezahlte Wartungs-Monate, nicht für andere Abo-Rechnungen
Die Rechnung wird frisch geladen; eine Stunde bringt nur eine reguläre (nicht anteilige) Wartungs-Zeile über 0 €. Preis-IDs werden einmal bei Stripe nachgeschlagen und zwischengespeichert. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d097704d49
commit
51fc2434e5
2 files changed
+133
-9
No files matched your search
@@ -8,7 +8,7 @@ import {
|
||||
updateDocument,
|
||||
} from './appwriteAdmin.js'
|
||||
import { lateStartAt } from './deadlines.js'
|
||||
import { describeItems } from './products.js'
|
||||
import { creditMinutesForInvoice, describeItems, priceIdsOfInvoice, productKeyForLookupKey } from './products.js'
|
||||
|
||||
/**
|
||||
* Wartungs-Abo ueber Stripe Billing.
|
||||
@@ -255,20 +255,44 @@ function subscriptionIdOfInvoice(invoice) {
|
||||
)
|
||||
}
|
||||
|
||||
/** Bezahlte Abo-Rechnung -> 1 Stunde gutschreiben (idempotent ueber Invoice-ID). */
|
||||
// Preis-ID -> Produkt ('wartung' | 'hosting' | 'mailStorage' | null); Preise ändern ihren lookup_key nicht
|
||||
const priceKeyCache = new Map()
|
||||
|
||||
/** Produkte zu Preis-IDs; unbekannte IDs einmal bei Stripe nachschlagen (gelöscht/404 -> null). */
|
||||
export async function resolvePriceKeys(priceIds) {
|
||||
const keys = new Map()
|
||||
for (const id of priceIds) {
|
||||
if (!priceKeyCache.has(id)) {
|
||||
const price = await getStripe()
|
||||
.prices.retrieve(id)
|
||||
.catch((err) => {
|
||||
if (err?.statusCode === 404) return null
|
||||
throw err
|
||||
})
|
||||
priceKeyCache.set(id, productKeyForLookupKey(price?.lookup_key))
|
||||
}
|
||||
keys.set(id, priceKeyCache.get(id))
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
/** Bezahlte Wartungs-Rechnung -> 1 Stunde gutschreiben (idempotent ueber Invoice-ID). */
|
||||
export async function grantCreditForInvoice(invoice) {
|
||||
const ref = subscriptionIdOfInvoice(invoice)
|
||||
const subscriptionId = typeof ref === 'string' ? ref : ref?.id
|
||||
if (!subscriptionId) return { skipped: 'keine Abo-Rechnung' }
|
||||
// subtotal (vor Rabatt und Guthaben) statt amount_paid: Auch ein Monat, den ein Angebot oder eine
|
||||
// Uebernahmegutschrift ganz deckt, ist ein bezahlter Leistungsmonat. Nur Probezeit-Rechnungen
|
||||
// haben eine Zwischensumme von 0 € und bringen keine Stunde.
|
||||
if (!(invoice.subtotal > 0)) return { skipped: 'Betrag 0' }
|
||||
// Frisch laden: Die Webhook-Nutzlast hat die API-Version des Webhook-Endpunkts, nicht die des SDK.
|
||||
// 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)))
|
||||
if (!minutes) return { skipped: 'keine Wartungs-Leistung' }
|
||||
|
||||
const sub = await syncSubscription(subscriptionId)
|
||||
if (!sub) return { skipped: 'Abo ohne Kundenzuordnung' }
|
||||
|
||||
const grantedAt = new Date((invoice.status_transitions?.paid_at || invoice.created) * 1000)
|
||||
const grantedAt = new Date((fresh.status_transitions?.paid_at || fresh.created) * 1000)
|
||||
try {
|
||||
await createDocument(
|
||||
config.collections.hourCredits,
|
||||
@@ -276,11 +300,11 @@ export async function grantCreditForInvoice(invoice) {
|
||||
customerId: sub.customerId,
|
||||
stripeSubscriptionId: subscriptionId,
|
||||
stripeInvoiceId: invoice.id,
|
||||
minutes: config.abo.minutesPerMonth,
|
||||
minutes,
|
||||
grantedAt: grantedAt.toISOString(),
|
||||
expiresAt: addMonths(grantedAt, config.abo.creditValidityMonths).toISOString(),
|
||||
source: 'stripe_invoice',
|
||||
note: invoice.number ? `Rechnung ${invoice.number}` : '',
|
||||
note: fresh.number ? `Rechnung ${fresh.number}` : '',
|
||||
},
|
||||
invoice.id
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user