Abo-Angebote: für Hosting- oder Wartungs-Abo

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5 committed 2026-09-29 10:08:59 +00:00
1 parent 7ffe95f24a
commit e5cc2f3c82
6 files changed
+303 -45

No files matched your search

+19 -14
View File
@@ -178,13 +178,16 @@ router.get('/', requireSession, async (req, res) => {
events = await readEvents()
}
const active = Boolean(sub && ACTIVE_STATUSES.has(sub.status))
const [ledger, offer] = await Promise.all([
const [ledger, hostingOffer, wartungOffer] = await Promise.all([
// Arbeitszeit gibt es nur im Wartungs-Abo
sub && productKeyOf(sub) === 'wartung' ? getLedger(customerId, { active }) : null,
active ? null : getOpenOffer(customerId).catch(() => null),
// Ein offenes Angebot zeigt das Portal nur vor der Bestellung (mit Abo ist es entweder schon
// eingeloest oder betrifft ein anderes Produkt, das ohnehin nicht bestellbar ist)
active ? null : getOpenOffer(customerId, 'hosting').catch(() => null),
active ? null : getOpenOffer(customerId, 'wartung').catch(() => null),
])
return res.json({
available: isStripeConfigured() && isPilotAllowed(customer, offer),
available: isStripeConfigured() && isPilotAllowed(customer, hostingOffer || wartungOffer),
testAccount: config.abo.testEmails.includes(String(customer?.email || '').toLowerCase()),
termsVersion: config.abo.termsVersion,
// Leistungsbeginn bei "Nach Ablauf der Widerrufsfrist" für die Bestellseite
@@ -196,8 +199,7 @@ router.get('/', requireSession, async (req, res) => {
unitPriceCents: config.mailStorage.amountCents,
maxUnits: config.mailStorage.maxUnits,
},
// Angebote gibt es bis Aufgabe 11 nur fuer das Wartungs-Abo
offers: { hosting: null, wartung: offer ? offerView(offer) : null },
offers: { hosting: hostingOffer ? offerView(hostingOffer) : null, wartung: wartungOffer ? offerView(wartungOffer) : null },
ledger,
subscription: publicSubscription(sub, events),
})
@@ -226,16 +228,19 @@ router.post('/checkout', requireSession, async (req, res) => {
return res.status(409).json({ error: 'Die Bedingungen wurden aktualisiert. Bitte Seite neu laden.' })
}
const customer = await getDocument(config.collections.customers, getSessionCustomerId(req))
// Bestellt wird genau der Preis, den die Bestelluebersicht gezeigt hat. Angebote gibt es bis
// Aufgabe 11 nur fuer das Wartungs-Abo; ein Angebot bei einer Hosting-Bestellung passt nie.
let offer = null
if (productKey === 'wartung') {
offer = await getOpenOffer(customer.$id)
if ((offer?.$id || '') !== String(offerId || '')) {
return res.status(409).json({ error: 'Ihr Angebot hat sich geändert. Bitte laden Sie die Seite neu.' })
// Bestellt wird genau der Preis, den die Bestelluebersicht gezeigt hat: das offene Angebot fuer
// genau dieses Produkt (F32). Gehoert die mitgeschickte Angebots-ID zum jeweils anderen Produkt,
// ist die Meldung eindeutiger als das allgemeine "hat sich geändert".
const offer = await getOpenOffer(customer.$id, productKey)
if ((offer?.$id || '') !== String(offerId || '')) {
if (offerId) {
const otherKey = productKey === 'hosting' ? 'wartung' : 'hosting'
const other = await getOpenOffer(customer.$id, otherKey)
if (other?.$id === offerId) {
return res.status(409).json({ error: 'Ihr Angebot gilt für ein anderes Abo. Bitte laden Sie die Seite neu.' })
}
}
} else if (offerId) {
return res.status(409).json({ error: 'Ihr Angebot gilt für ein anderes Abo. Bitte laden Sie die Seite neu.' })
return res.status(409).json({ error: 'Ihr Angebot hat sich geändert. Bitte laden Sie die Seite neu.' })
}
if (!isStripeConfigured() || !isPilotAllowed(customer, offer)) {
return res.status(403).json({ error: 'Die Online-Bestellung ist noch nicht freigeschaltet.' })