Wechsel Hosting → Wartung: neuer Abbuchungstag, Gutschrift, 12 neue Monate
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
379cc26dcd
commit
188db4b3c1
15 files changed
+2035
-12
No files matched your search
@@ -11,6 +11,14 @@ 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 {
|
||||
handlePendingUpdateApplied,
|
||||
handlePendingUpdateExpired,
|
||||
handleSwitchInvoicePaid,
|
||||
handleSwitchPaymentFailed,
|
||||
previewSwitch,
|
||||
switchToWartung,
|
||||
} from '../services/planSwitch.js'
|
||||
import { PRODUCT_KEYS, productView, termsDocFor } from '../services/products.js'
|
||||
import {
|
||||
ACTIVE_STATUSES,
|
||||
@@ -311,6 +319,64 @@ router.post('/storage', requireSession, async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Wechsel vom Hosting- ins Wartungs-Abo (Spec 3.5, Bedingungen § 9): Vorschau und Bestellung (planSwitch.js)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Vorschau fuer die Bestelluebersicht: Gutschrift, heute faellig, Monatspreis, neuer Abbuchungstag, Mindestlaufzeit. */
|
||||
router.post('/switch/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 previewSwitch(getSessionCustomerId(req)))
|
||||
} catch (err) {
|
||||
return res.status(err.status || 500).json({ error: err.message || 'Vorschau fehlgeschlagen' })
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Bestellung { acceptTerms: true, earlyStart: true, termsVersion, prorationDate } ("Zahlungspflichtig bestellen").
|
||||
* Nachweis: Fassung, Zeitpunkt, IP, Browser und die Zustimmung zum Sofortbeginn (§ 357a Abs. 2 BGB) landen am
|
||||
* Abo und im Ereignis `wechsel`. Antwort: die Abo-Ansicht wie in GET /api/abo und paymentUrl (Karte: Zahlung
|
||||
* noch zu bestaetigen, das Portal leitet zur Stripe-Zahlungsseite weiter; sonst null).
|
||||
*/
|
||||
router.post('/switch', requireSession, async (req, res) => {
|
||||
try {
|
||||
if (req.session.viewAs) {
|
||||
return res.status(403).json({ error: 'Im "Als Kunde ansehen"-Modus kann nicht gewechselt werden.' })
|
||||
}
|
||||
const { acceptTerms, earlyStart, termsVersion, prorationDate } = req.body || {}
|
||||
if (acceptTerms !== true) {
|
||||
return res.status(400).json({ error: 'Bitte stimmen Sie den Vertragsbedingungen zu, um zu wechseln.' })
|
||||
}
|
||||
if (earlyStart !== true) {
|
||||
return res.status(400).json({ error: 'Bitte bestätigen Sie, dass wir sofort mit dem Wartungs-Abo beginnen sollen.' })
|
||||
}
|
||||
if (!termsVersion || !Number.isInteger(prorationDate)) {
|
||||
return res.status(400).json({ error: 'Die Bestellung ist unvollständig. Bitte laden Sie die Seite neu.' })
|
||||
}
|
||||
if (termsVersion !== config.abo.termsVersion) {
|
||||
return res.status(409).json({ error: 'Die Bedingungen wurden aktualisiert. Bitte Seite neu laden.' })
|
||||
}
|
||||
const customerId = getSessionCustomerId(req)
|
||||
const now = new Date().toISOString()
|
||||
const { subscription, paymentUrl } = await switchToWartung(
|
||||
customerId,
|
||||
{ termsVersion, consentAt: now, earlyStartConsentAt: now, consentIp: req.ip, consentUserAgent: req.get('user-agent') || '' },
|
||||
prorationDate
|
||||
)
|
||||
const events = await listContractEvents(customerId).catch((e) => {
|
||||
console.error('[abo] Vertragsereignisse nicht lesbar:', e.message)
|
||||
return []
|
||||
})
|
||||
return res.json({ subscription: publicSubscription(subscription, events), paymentUrl })
|
||||
} catch (err) {
|
||||
console.error('[abo] Wechsel fehlgeschlagen:', err.message)
|
||||
return res.status(err.status || 500).json({ error: err.message || 'Wechsel fehlgeschlagen' })
|
||||
}
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stripe-Webhook
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -461,11 +527,17 @@ async function handleStripeEvent(event) {
|
||||
if (storage.recorded) {
|
||||
console.log(`[abo] Zusatzspeicher bezahlt: Abo ${storage.subscriptionId}, ${storage.units} Pakete bis ${storage.periodEnd}`)
|
||||
}
|
||||
// Wechselrechnung bezahlt (F26): SEPA-Wechsel erledigt; Karte, falls vor pending_update_applied
|
||||
const switched = await handleSwitchInvoicePaid(fresh)
|
||||
if (switched.completed) console.log(`[abo] Wechsel ins Wartungs-Abo bezahlt: Ereignis ${switched.eventId}`)
|
||||
return
|
||||
}
|
||||
case 'invoice.payment_failed': {
|
||||
const ref = obj.parent?.subscription_details?.subscription || obj.subscription
|
||||
const sub = ref ? await syncSubscription(typeof ref === 'string' ? ref : ref.id) : null
|
||||
// Wechselrechnung per SEPA-Lastschrift nicht eingeloest: T12 mit den Handgriffen (vor T04, damit eine
|
||||
// Wiederholung nach einem Fehler hier beide Hinweise nachholt)
|
||||
await handleSwitchPaymentFailed(obj)
|
||||
await sendAdminNotice({
|
||||
template: 'T04',
|
||||
values: {
|
||||
@@ -479,6 +551,15 @@ async function handleStripeEvent(event) {
|
||||
case 'customer.subscription.created':
|
||||
await syncSubscription(obj.id)
|
||||
return
|
||||
// Wechsel ins Wartungs-Abo mit Karte (Rechtsrunde 28.09.2026): Stripe hat die zurueckgehaltene Aenderung
|
||||
// mit der Zahlung uebernommen (erledigt, T12) oder verworfen (gescheitert, K14, T12). Am Stripe-Endpunkt
|
||||
// freischalten (Task 20).
|
||||
case 'customer.subscription.pending_update_applied':
|
||||
await handlePendingUpdateApplied(obj.id)
|
||||
return
|
||||
case 'customer.subscription.pending_update_expired':
|
||||
await handlePendingUpdateExpired(obj.id)
|
||||
return
|
||||
case 'customer.subscription.updated': {
|
||||
const sub = await syncSubscription(obj.id)
|
||||
const prev = event.data.previous_attributes || {}
|
||||
|
||||
Reference in new issue
Block a user