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:
ClaudeandClaude Fable 5.1 committed 2026-09-28 10:48:29 +00:00
1 parent 887e1e54a0
commit 379cc26dcd
11 files changed
+1531 -27

No files matched your search

+62 -1
View File
@@ -9,6 +9,7 @@ import { eventIdFor, listContractEvents, openWithdrawals, recordContractEvent }
import { lateStartAt, withdrawalDeadline } from '../services/deadlines.js'
import { getLedger } from '../services/hourLedger.js'
import { sendAdminNotice, sendCustomerMail } from '../services/mailer.js'
import { changeStorage, previewStorageChange, recordPaidStorage } from '../services/mailStorage.js'
import { activateOfferFromCheckout, getOpenOffer, offerText, offerView } from '../services/offers.js'
import { PRODUCT_KEYS, productView, termsDocFor } from '../services/products.js'
import {
@@ -20,6 +21,7 @@ import {
createBillingPortalUrl,
getCurrentSubscription,
grantCreditForInvoice,
loadInvoice,
minTermMonthsFor,
isStripeConfigured,
syncSubscription,
@@ -256,6 +258,59 @@ router.post('/portal', requireSession, async (req, res) => {
}
})
// ---------------------------------------------------------------------------
// E-Mail-Zusatzspeicher (Spec 3.4, 4.3): Vorschau und Aenderung (mailStorage.js)
// ---------------------------------------------------------------------------
/** Vorschau fuer { units }: Richtung, Gesamtgroesse, Mehrpreis, anteiliger Betrag, naechste Abbuchung, Wirksamkeit. */
router.post('/storage/preview', requireSession, async (req, res) => {
try {
if (req.session.viewAs) {
return res.status(403).json({ error: 'Im "Als Kunde ansehen"-Modus nicht möglich.' })
}
return res.json(await previewStorageChange(getSessionCustomerId(req), req.body?.units))
} catch (err) {
return res.status(err.status || 500).json({ error: err.message || 'Vorschau fehlgeschlagen' })
}
})
/**
* Aenderung { units, acceptTerms, termsVersion }: ein Plus ist eine Bestellung ("Zahlungspflichtig bestellen")
* und braucht acceptTerms zur aktuellen Fassung - der Nachweis (Fassung, Zeitpunkt, IP, Browser) landet im
* Ereignis `speicher`; ein Minus ("Speichern") braucht kein Haekchen und speichert keine Zustimmungsdaten.
* Antwort: die Abo-Ansicht wie in GET /api/abo (mit Speicherstand) und die Aenderung selbst.
*/
router.post('/storage', requireSession, async (req, res) => {
try {
if (req.session.viewAs) {
return res.status(403).json({ error: 'Im "Als Kunde ansehen"-Modus kann der Zusatzspeicher nicht geändert werden.' })
}
const customerId = getSessionCustomerId(req)
const { units, acceptTerms, termsVersion } = req.body || {}
let consent = null
if (acceptTerms === true) {
if (termsVersion !== config.abo.termsVersion) {
return res.status(409).json({ error: 'Die Bedingungen wurden aktualisiert. Bitte Seite neu laden.' })
}
consent = {
termsVersion,
consentAt: new Date().toISOString(),
consentIp: req.ip,
consentUserAgent: req.get('user-agent') || '',
}
}
const { subscription, change } = await changeStorage(customerId, units, consent)
const events = await listContractEvents(customerId).catch((e) => {
console.error('[abo] Vertragsereignisse nicht lesbar:', e.message)
return []
})
return res.json({ subscription: publicSubscription(subscription, events), change })
} catch (err) {
console.error('[abo] Zusatzspeicher-Änderung fehlgeschlagen:', err.message)
return res.status(err.status || 500).json({ error: err.message || 'Änderung fehlgeschlagen' })
}
})
// ---------------------------------------------------------------------------
// Stripe-Webhook
// ---------------------------------------------------------------------------
@@ -396,10 +451,16 @@ async function handleStripeEvent(event) {
return
}
case 'invoice.paid': {
const result = await grantCreditForInvoice(obj)
// Einmal frisch laden, mit allen Zeilen (F18): Stundengutschrift (Wartung) und bezahlte Speichermenge
const fresh = await loadInvoice(obj.id)
const result = await grantCreditForInvoice(fresh, { fresh: true })
if (result.granted) {
console.log(`[abo] 60 min gutgeschrieben: Kunde ${result.customerId}, Rechnung ${obj.id}`)
}
const storage = await recordPaidStorage(fresh)
if (storage.recorded) {
console.log(`[abo] Zusatzspeicher bezahlt: Abo ${storage.subscriptionId}, ${storage.units} Pakete bis ${storage.periodEnd}`)
}
return
}
case 'invoice.payment_failed': {