Mails: Produktname statt fest Wartungs-Abo, neue Mails für Wechsel und Zusatzspeicher, UptimeRobot-Hinweis

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Fable 5.1 committed 2026-09-29 10:38:43 +00:00
1 parent 0d2254ca20
commit 19f9c747a2
24 files changed
+407 -100

No files matched your search

+21 -7
View File
@@ -197,6 +197,7 @@ async function stripeContext(doc, customer) {
stripeSub,
wartungPriceId,
aboItemId: items.aboItemId,
storageUnits: items.storageUnits,
// Beginn des laufenden Abrechnungsmonats (seit API 2025-03 am Posten)
periodStart: aboItem?.current_period_start ?? stripeSub.current_period_start ?? null,
paymentMethod: paymentMethodTypeOf(stripeSub),
@@ -297,7 +298,7 @@ async function sendSwitchConfirmation(contact, consent, facts, blocks) {
name: contact.name,
wechseltag: fmtDate(facts.switchedAt),
betrag_heute: fmtEuro(facts.amountDueCents),
monatspreis: fmtEuro(config.products.wartung.amountCents),
monatspreis: fmtEuro(monthlyCentsOf(facts)),
mindestlaufzeit_bis: fmtDate(addMonths(new Date(facts.switchedAt), 12)),
bedingungen_version: consent.termsVersion,
zustimmung_zeitpunkt: fmtDateTime(consent.consentAt),
@@ -317,13 +318,19 @@ async function sendSwitchNotice(contact, subscriptionId, facts, blocks) {
stripe_abo_id: subscriptionId,
wechseltag: fmtDate(facts.switchedAt),
betrag_heute: fmtEuro(facts.amountDueCents),
monatspreis: fmtEuro(config.products.wartung.amountCents),
monatspreis: fmtEuro(monthlyCentsOf(facts)),
},
blocks,
})
}
const factsOf = (event) => ({ switchedAt: event.details?.switchedAt || event.consentAt, amountDueCents: event.details?.amountDueCents })
const factsOf = (event) => ({
switchedAt: event.details?.switchedAt || event.consentAt,
amountDueCents: event.details?.amountDueCents,
monthlyCents: event.details?.monthlyCents,
})
// Monatsgebuehr fuer {{monatspreis}}; Ereignisse von vor Task 12 kennen nur den Katalogpreis des Wartungs-Abos
const monthlyCentsOf = (facts) => facts.monthlyCents ?? config.products.wartung.amountCents
// Ein Wechsel je Abo zur selben Zeit (Doppelklick): der zweite wartet nicht, sondern bekommt 409
const switchesInFlight = new Set()
@@ -370,6 +377,8 @@ async function performSwitch(customerId, current, consent, prorationDate, expect
}
const sepa = ctx.paymentMethod === 'sepa_debit'
const switchedAt = new Date().toISOString()
// Spec 3.5: Monatsgebuehr des Wartungs-Abos mit gebuchtem Zusatzspeicher ({{monatspreis}} in K12 und T12)
const monthlyCents = config.products.wartung.amountCents + ctx.storageUnits * config.mailStorage.amountCents
// Stripe-Metadaten am Abo (max. 500 Zeichen je Wert): Produkt, neue Mindestlaufzeit, Nachweis der Zustimmung
const metadata = {
productKey: 'wartung',
@@ -421,6 +430,7 @@ async function performSwitch(customerId, current, consent, prorationDate, expect
details: {
from: 'hosting',
amountDueCents,
monthlyCents,
prorationDate,
switchedAt,
paymentMethod: ctx.paymentMethod,
@@ -440,10 +450,10 @@ async function performSwitch(customerId, current, consent, prorationDate, expect
})
const contact = { name: customer?.name || '', email: customer?.email || doc.email || '' }
const facts = { switchedAt, amountDueCents }
const facts = { switchedAt, amountDueCents, monthlyCents }
const lastschrift = sepa && effective && !paid
// K12 sofort; zahlung_offen nur bei zurueckgehaltener Aenderung (Karte), lastschrift bei SEPA (F37)
await sendSwitchConfirmation(contact, consent, facts, { zahlung_offen: pending, lastschrift })
await sendSwitchConfirmation(contact, consent, facts, { zahlung_offen: pending, lastschrift, zusatzspeicher: ctx.storageUnits > 0 })
// T12 erst, wenn der Wechsel wirksam ist - ohne Nachweis sofort, damit das Team ihn von Hand verfolgt
if (effective || !event) {
await sendSwitchNotice(contact, doc.$id, facts, { wirksam: effective, lastschrift, nachweis_fehlt: !event })
@@ -623,7 +633,7 @@ export async function handleSwitchPaymentFailed(invoice) {
if (d.invoiceId && d.invoiceId !== invoice.id) return { skipped: 'nicht die Wechselrechnung' }
if (d.lastschriftFailedAt) return { skipped: 'schon gemeldet' }
await updateContractEvent(event.$id, { details: { lastschriftFailedAt: new Date().toISOString(), invoiceId: invoice.id } })
await sendSwitchNotice(contact, subscriptionId, { switchedAt: d.switchedAt, amountDueCents: invoice.amount_due }, { lastschrift_gescheitert: true })
await sendSwitchNotice(contact, subscriptionId, { switchedAt: d.switchedAt, amountDueCents: invoice.amount_due, monthlyCents: d.monthlyCents }, { lastschrift_gescheitert: true })
return { notified: true, eventId: event.$id }
})
}
@@ -636,7 +646,11 @@ export async function handleSwitchPaymentFailed(invoice) {
await sendSwitchNotice(
contact,
subscriptionId,
{ switchedAt: new Date(switchedAt).toISOString(), amountDueCents: invoice.amount_due },
{
switchedAt: new Date(switchedAt).toISOString(),
amountDueCents: invoice.amount_due,
monthlyCents: config.products.wartung.amountCents + describeItems(stripeSub.items?.data).storageUnits * config.mailStorage.amountCents,
},
{ lastschrift_gescheitert: true, nachweis_fehlt: true }
)
return { notified: true, eventId: null }