- Bestellung auf der Portal-Startseite: Zusammenfassung, AGB- und Vorab-Leistungs-Zustimmung, Button 'Zahlungspflichtig bestellen', danach Stripe Checkout (Abo, Kleinunternehmer-Fusszeile) - Stripe-Webhook /api/abo/webhook spiegelt Abos nach Appwrite (subscriptions) und schreibt je bezahlter Rechnung 60 min gut (hourCredits, idempotent ueber Invoice-ID, 12 Monate gueltig) - Stundenkonto: FIFO-Verbrauch aus Worksheets mit countsToPlan, 15-Minuten-Takt, Mehraufwand separat ausgewiesen - Stripe-Kundenportal fuer Zahlungsart/Rechnungen/Kuendigung - Oeffentlicher Kuendigungsbutton (§ 312k) und Widerrufsbutton (§ 356a) mit Eingangsbestaetigung per Mail - Abo-Bedingungen inkl. Leistungsbeschreibung und Widerrufsbelehrung, als Anhang der Vertragsbestaetigung Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
96 lines
3.3 KiB
JavaScript
96 lines
3.3 KiB
JavaScript
import nodemailer from 'nodemailer'
|
|
import { config } from '../config.js'
|
|
|
|
let transporter = null
|
|
|
|
function getTransporter() {
|
|
if (!config.smtp.host || !config.smtp.user) return null
|
|
if (!transporter) {
|
|
transporter = nodemailer.createTransport({
|
|
host: config.smtp.host,
|
|
port: config.smtp.port,
|
|
secure: config.smtp.secure,
|
|
auth: { user: config.smtp.user, pass: config.smtp.pass },
|
|
})
|
|
}
|
|
return transporter
|
|
}
|
|
|
|
// Throttle: pro Kunde maximal eine Benachrichtigung je Zeitfenster,
|
|
// damit ein aktiver Chat nicht pro Nachricht eine Mail ausloest.
|
|
const lastNotifyByCustomer = new Map() // customerId -> timestamp
|
|
|
|
/**
|
|
* Benachrichtigt die Admins per E-Mail ueber eine neue Portal-Chatnachricht.
|
|
* Fehler werden nur geloggt - der Chat darf am Mailversand nie scheitern.
|
|
*/
|
|
export async function sendChatNotification(customer, text) {
|
|
try {
|
|
const mailer = getTransporter()
|
|
if (!mailer || !config.chatNotify.emails.length) return { sent: false, reason: 'nicht konfiguriert' }
|
|
|
|
const now = Date.now()
|
|
const last = lastNotifyByCustomer.get(customer.$id) || 0
|
|
const throttleMs = config.chatNotify.throttleMin * 60 * 1000
|
|
if (now - last < throttleMs) return { sent: false, reason: 'throttled' }
|
|
lastNotifyByCustomer.set(customer.$id, now)
|
|
|
|
const name = customer.name || customer.email || 'Kunde'
|
|
await mailer.sendMail({
|
|
from: `"Webklar Kundenportal" <${config.smtp.from}>`,
|
|
to: config.chatNotify.emails.join(', '),
|
|
subject: `Neue Portalnachricht von ${name}`,
|
|
text: [
|
|
`${name} hat im Kundenportal geschrieben:`,
|
|
'',
|
|
text,
|
|
'',
|
|
'Antworten: https://project.webklar.com/admin.html (Tab Chat)',
|
|
].join('\n'),
|
|
})
|
|
return { sent: true }
|
|
} catch (err) {
|
|
console.error('[mailer] Chat-Benachrichtigung fehlgeschlagen:', err.message)
|
|
return { sent: false, reason: err.message }
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Wartungs-Abo: Vertrags-/Kuendigungs-/Widerrufsbestaetigungen
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** Mail an die Webklar-Admins (Abo-Ereignisse). Fehler werden nur geloggt. */
|
|
export async function sendAdminNotice(subject, lines) {
|
|
try {
|
|
const mailer = getTransporter()
|
|
if (!mailer || !config.chatNotify.emails.length) return { sent: false, reason: 'nicht konfiguriert' }
|
|
await mailer.sendMail({
|
|
from: `"Webklar Kundenportal" <${config.smtp.from}>`,
|
|
to: config.chatNotify.emails.join(', '),
|
|
subject: `[Abo] ${subject}`,
|
|
text: lines.join('\n'),
|
|
})
|
|
return { sent: true }
|
|
} catch (err) {
|
|
console.error('[mailer] Admin-Hinweis fehlgeschlagen:', err.message)
|
|
return { sent: false, reason: err.message }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mail an Kunden. `attachments` fuer die Vertragsbestaetigung auf dauerhaftem
|
|
* Datentraeger (§ 312f BGB): Bedingungen als HTML-Anhang.
|
|
*/
|
|
export async function sendCustomerMail(to, subject, lines, attachments = []) {
|
|
const mailer = getTransporter()
|
|
if (!mailer) throw new Error('SMTP nicht konfiguriert')
|
|
await mailer.sendMail({
|
|
from: `"Webklar" <${config.smtp.from}>`,
|
|
to,
|
|
bcc: config.chatNotify.emails.join(', ') || undefined,
|
|
subject,
|
|
text: lines.join('\n'),
|
|
attachments,
|
|
})
|
|
}
|