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:
1 parent
0d2254ca20
commit
19f9c747a2
24 files changed
+407
-100
No files matched your search
+36
-13
@@ -1,6 +1,4 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { Router } from 'express'
|
||||
import { config } from '../config.js'
|
||||
import { getSessionCustomerId, requireSession } from '../middleware/session.js'
|
||||
@@ -20,7 +18,7 @@ import {
|
||||
reconcilePendingSwitch,
|
||||
switchToWartung,
|
||||
} from '../services/planSwitch.js'
|
||||
import { PRODUCT_KEYS, describeItems, productView, termsDocFor } from '../services/products.js'
|
||||
import { PRODUCT_KEYS, contractAttachmentFor, describeItems, productLabel, productView, termsDocFor } from '../services/products.js'
|
||||
import {
|
||||
ACTIVE_STATUSES,
|
||||
cancelAtPeriodEnd,
|
||||
@@ -38,7 +36,6 @@ import {
|
||||
} from '../services/stripeBilling.js'
|
||||
|
||||
const router = Router()
|
||||
const publicDir = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'public')
|
||||
|
||||
const fmtDate = (iso) =>
|
||||
iso
|
||||
@@ -68,6 +65,12 @@ function minTermEndOf(sub) {
|
||||
|
||||
// Alte Dokumente ohne productKey sind Wartungs-Abos (Spec 6)
|
||||
const productKeyOf = (sub) => sub?.productKey || 'wartung'
|
||||
const isWartung = (sub) => Boolean(sub) && productKeyOf(sub) === 'wartung'
|
||||
// {{produkt}} in den Mails: Katalogname ohne "WEBklar " (F33), ohne gefundenes Abo neutral "Abo"
|
||||
const productLabelOf = (sub) => (sub ? productLabel(productKeyOf(sub)) : 'Abo')
|
||||
// {{vertragsende_paragraph}}: Paragraph zum Vertragsende in der Fassung des Kunden (v3 § 9, v4 § 11); ohne
|
||||
// gefundenes Abo die aktuelle Fassung (F08)
|
||||
const endParagraphOf = (sub) => termsDocFor(sub ? sub.termsVersion : config.abo.termsVersion).endParagraph
|
||||
|
||||
/**
|
||||
* Abo fuers Portal. events: Vertragsereignisse des Kunden (listContractEvents); es zaehlen die
|
||||
@@ -411,15 +414,26 @@ async function customerNameOf(sub, fallback = '') {
|
||||
return customer?.name || fallback || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* K01 an den Kunden: Vertragsbestaetigung je Produkt (Katalogname, Preis, Liste "Enthalten" ueber die Bloecke
|
||||
* hosting/wartung) mit den Vertragsunterlagen der Fassung, der der Kunde zugestimmt hat (§ 312f BGB,
|
||||
* contractAttachmentFor: v3 Archiv-PDF, v4 aktuelles PDF). Ohne lesbare Datei geht die Mail ohne Anhang raus.
|
||||
*/
|
||||
async function sendContractConfirmation(subDoc, fallbackName = '') {
|
||||
if (!subDoc || subDoc.confirmationSentAt || !subDoc.email) return
|
||||
const pdfPath = path.join(publicDir, 'WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf')
|
||||
const pdf = await readFile(pdfPath).catch(() => null)
|
||||
const attachment = contractAttachmentFor(subDoc.termsVersion)
|
||||
const pdf = await readFile(attachment.path).catch((err) => {
|
||||
console.error(`[abo] Vertragsunterlagen ${attachment.filename} nicht lesbar:`, err.message)
|
||||
return null
|
||||
})
|
||||
const lateStart = subDoc.startChoice === 'nach_frist'
|
||||
const productKey = productKeyOf(subDoc)
|
||||
await sendCustomerMail(subDoc.email, {
|
||||
template: 'K01',
|
||||
values: {
|
||||
name: await customerNameOf(subDoc, fallbackName),
|
||||
produkt: productLabel(productKey),
|
||||
preis: `${fmtEuro(config.products[productKey].amountCents)} / Monat`,
|
||||
angebot_text: subDoc.offerText || '',
|
||||
vertragsschluss: fmtDate(subDoc.startedAt),
|
||||
leistungsbeginn: fmtDate(lateStart ? subDoc.trialEnd : subDoc.startedAt),
|
||||
@@ -430,12 +444,12 @@ async function sendContractConfirmation(subDoc, fallbackName = '') {
|
||||
},
|
||||
blocks: {
|
||||
angebot: Boolean(subDoc.offerText),
|
||||
hosting: productKey === 'hosting',
|
||||
wartung: productKey === 'wartung',
|
||||
sofortbeginn: !lateStart,
|
||||
beginn_nach_widerrufsfrist: lateStart,
|
||||
},
|
||||
attachments: pdf
|
||||
? [{ filename: 'WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf', content: pdf, contentType: 'application/pdf' }]
|
||||
: [],
|
||||
attachments: pdf ? [{ filename: attachment.filename, content: pdf, contentType: 'application/pdf' }] : [],
|
||||
})
|
||||
await updateDocument(config.collections.subscriptions, subDoc.$id, {
|
||||
confirmationSentAt: new Date().toISOString(),
|
||||
@@ -450,16 +464,19 @@ async function sendPortalCancellationConfirmation(sub, createdUnix) {
|
||||
template: 'K02',
|
||||
values: {
|
||||
name: await customerNameOf(sub),
|
||||
produkt: productLabelOf(sub),
|
||||
eingang_zeitpunkt: fmtDateTime(receivedAt),
|
||||
vertragsende: fmtDate(endsAt),
|
||||
vertragsende_paragraph: endParagraphOf(sub),
|
||||
},
|
||||
blocks: { wartung: isWartung(sub) },
|
||||
})
|
||||
await updateDocument(config.collections.subscriptions, sub.$id, {
|
||||
cancelRequestedAt: receivedAt.toISOString(),
|
||||
}).catch(() => {})
|
||||
await sendAdminNotice({
|
||||
template: 'T05',
|
||||
values: { email: sub.email, stripe_abo_id: sub.stripeSubscriptionId, vertragsende: fmtDate(endsAt) },
|
||||
values: { email: sub.email, produkt: productLabelOf(sub), stripe_abo_id: sub.stripeSubscriptionId, vertragsende: fmtDate(endsAt), vertragsende_paragraph: endParagraphOf(sub) },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -525,6 +542,7 @@ async function handleStripeEvent(event) {
|
||||
await sendAdminNotice({
|
||||
template: 'T02',
|
||||
values: {
|
||||
produkt: productLabel(sub ? productKeyOf(sub) : obj.metadata?.productKey),
|
||||
email: sub?.email || obj.customer_details?.email || '',
|
||||
kunde_name: obj.customer_details?.name || obj.customer_details?.email || '',
|
||||
appwrite_id: sub?.customerId || obj.client_reference_id || '',
|
||||
@@ -599,7 +617,7 @@ async function handleStripeEvent(event) {
|
||||
)
|
||||
}
|
||||
if (reactivated && sub) {
|
||||
await sendAdminNotice({ template: 'T06', values: { email: sub.email, stripe_abo_id: obj.id } })
|
||||
await sendAdminNotice({ template: 'T06', values: { email: sub.email, produkt: productLabelOf(sub), stripe_abo_id: obj.id } })
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -607,7 +625,7 @@ async function handleStripeEvent(event) {
|
||||
const sub = await syncSubscription(obj.id)
|
||||
await sendAdminNotice({
|
||||
template: 'T07',
|
||||
values: { email: sub?.email || '', stripe_abo_id: obj.id, status: obj.status },
|
||||
values: { email: sub?.email || '', produkt: productLabelOf(sub), stripe_abo_id: obj.id, status: obj.status, vertragsende_paragraph: endParagraphOf(sub) },
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -888,6 +906,7 @@ router.post('/public/cancel', async (req, res) => {
|
||||
email: d.email,
|
||||
kundennummer: d.customerNumber,
|
||||
vertrag: contractLabel(d.contract, sub),
|
||||
produkt: productLabelOf(sub),
|
||||
// Eine Verringerung des Speichers wirkt immer zum Ende des Abrechnungsmonats
|
||||
wunschtermin: storage ? 'Ende des Abrechnungsmonats' : d.effective ? fmtDate(`${d.effective}T12:00:00Z`) : 'nächstmöglicher Zeitpunkt',
|
||||
grund: d.reason,
|
||||
@@ -903,11 +922,13 @@ router.post('/public/cancel', async (req, res) => {
|
||||
|
||||
await sendCustomerMail(d.email, {
|
||||
template: 'K03',
|
||||
values: { ...common, pakete_neu: reduced ? result.details.toUnits : '', kuendigungsart: kindLabel, eingang_zeitpunkt: fmtDateTime(receivedAt) },
|
||||
values: { ...common, pakete_neu: reduced ? result.details.toUnits : '', kuendigungsart: kindLabel, eingang_zeitpunkt: fmtDateTime(receivedAt), vertragsende_paragraph: endParagraphOf(sub) },
|
||||
blocks: {
|
||||
kundennummer: Boolean(d.customerNumber),
|
||||
grund: Boolean(d.reason),
|
||||
abo: !storage,
|
||||
// Stundenguthaben nur im Wartungs-Abo (Block innerhalb von abo)
|
||||
wartung: !storage && isWartung(sub),
|
||||
automatisch_gekuendigt: !storage && Boolean(effectiveAt) && d.kind !== 'geschaeftsaufgabe',
|
||||
sonderkuendigung: !storage && Boolean(effectiveAt) && d.kind === 'geschaeftsaufgabe',
|
||||
ausserordentlich: !storage && !effectiveAt && d.kind === 'ausserordentlich',
|
||||
@@ -1070,6 +1091,7 @@ router.post('/public/withdraw', async (req, res) => {
|
||||
values: {
|
||||
name: d.name,
|
||||
bestellung: orderLabel(d.order, sub, { customer: true }),
|
||||
produkt: productLabelOf(sub),
|
||||
eingang_zeitpunkt: fmtDateTime(receivedAt),
|
||||
email: d.email,
|
||||
kundennummer: d.customerNumber,
|
||||
@@ -1091,6 +1113,7 @@ router.post('/public/withdraw', async (req, res) => {
|
||||
name: d.name,
|
||||
kundennummer: d.customerNumber,
|
||||
bestellung: orderLabel(d.order, sub),
|
||||
produkt: productLabelOf(sub),
|
||||
stripe_abo_id: sub?.stripeSubscriptionId || '',
|
||||
abo_beginn: fmtDate(sub?.startedAt),
|
||||
fristende: result.deadline ? `${fmtDate(result.deadline)}, 24 Uhr` : 'keine Frist gefunden',
|
||||
|
||||
Reference in new issue
Block a user