import { updateDocument } from './appwriteAdmin.js' import { config } from '../config.js' import { aboProdukt } from './aboFunktionen.js' import { unterlagenFuer } from './aboUnterlagen.js' import { k38Mail, k39Mail, kontostandText, loeschFristTage, t24Mail } from './aboFunktionenMails.js' import { featureConfig } from './featureRegistry.js' import { getLedger } from './hourLedger.js' import { featureLegalAttachments } from './legalDocs.js' import { sendAdminNotice, sendCustomerMail } from './mailer.js' import { customerOf, projectName } from './siteFeatures.js' /** * Mails zum Einschalten einer Funktion im Abo-Modell (Kundenansicht neu, P6; Builder in aboFunktionenMails.js): * K38 an den Kunden (BCC Team wie sendCustomerMail): Die Funktion wird eingerichtet, fuer news, termine, shop und unterkunft; * nicht fuer zeiten, dort sendet holeZeitenNach die K27. Mit den Vertragsunterlagen als PDF (ohne Widerrufsbelehrung), wenn * die Funktion welche hat und sie lesbar sind. * T24 ans Team, nur wenn der Kunde selbst eingeschaltet hat (by 'kunde'), fuer alle fuenf Funktionen. * K39 an den Kunden, wenn der Tagesabgleich (aboFunktionenAbgleich.js) eine Funktion mit dem Abo beendet hat (meldeBeendet). * meldeEingeschaltet wirft nie: jeder Schritt steht in einem eigenen try, ein Fehler ist eine Logzeile mit Art und Status (weder * Name noch Adresse des Kunden) und der Rest laeuft weiter. Aufrufer ist einschalten() (funktionEinschalten.js). */ /** Austauschpunkte fuer Tests: die Anhaenge der Vertragsunterlagen (Standard: Leistungsbeschreibung und AVV, ohne Widerrufsbelehrung). */ export const meldungDeps = { anhaenge: (key) => featureLegalAttachments(key, { ohne: ['widerruf'] }), } /** Fehlerart fuers Log: Name und Status, nie die Meldung (SMTP-Antworten nennen gern die Adresse). */ const fehlerArt = (err) => `${err?.name ?? 'Fehler'} ${err?.status ?? err?.responseCode ?? ''}`.trim() /** * Meldung nach dem Einschalten: K38 an den Kunden, danach (nur by 'kunde') T24 ans Team. * @param {{ doc: object, ticketWoid?: string|null, by: 'kunde'|'team', abrechnung: 'abo'|'projekt' }} p * doc: das eingeschaltete Funktionsdokument (setup, mit Ticket); ticketWoid: Nummer des Einrichtungs-Tickets. * T24 traegt den Block vorschuss, wenn abrechnung 'abo' ist, das Abo ein Wartungs-Abo ist und das Stundenkonto balanceMinutes <= 0 * zeigt oder schon einen Vorschuss hat; anhang_fehlt, wenn die Funktion Rechtsunterlagen hat und der Abruf der PDFs scheiterte. * @returns {Promise<{ k38: boolean, t24: boolean }>} was tatsaechlich versandt wurde */ export async function meldeEingeschaltet({ doc, ticketWoid = null, by, abrechnung }) { const ergebnis = { k38: false, t24: false } try { const key = doc?.featureKey if (!featureConfig(key)) { console.error(`[funktionen-mail] Meldung entfällt: unbekannte Funktion (${String(key)})`) return ergebnis } const customerId = doc.customerId // Kunde und Projektname; fehlt etwas, bleibt es leer (Anrede ohne Namen), die Meldung geht trotzdem raus let customer = null let projekt = '' try { ;[customer, projekt] = await Promise.all([customerOf(customerId), projectName(doc.projectId)]) } catch (err) { console.error(`[funktionen-mail] Kunde oder Projekt nicht lesbar (${key}): ${fehlerArt(err)}`) } const hatUnterlagen = unterlagenFuer(key).avv !== null let anhangFehler = false // K38 an den Kunden (nicht fuer zeiten: dort bleibt K27) if (key !== 'zeiten') { try { const to = String(customer?.email ?? '').trim() if (!to) { console.warn(`[funktionen-mail] K38 (${key}) entfällt: Kunde nicht mehr vorhanden oder ohne E-Mail-Adresse`) } else { let anhaenge = [] try { const geholt = await meldungDeps.anhaenge(key) anhaenge = Array.isArray(geholt) ? geholt : [] } catch (err) { anhangFehler = true console.error(`[funktionen-mail] Vertragsunterlagen (${key}) nicht lesbar, K38 geht ohne Anhang raus: ${fehlerArt(err)}`) } const nachricht = k38Mail({ kunde: customer, projekt, key, abrechnung, ticketNr: ticketWoid, anhang: anhaenge.length > 0 }) await sendCustomerMail(to, { ...nachricht, attachments: anhaenge }) ergebnis.k38 = true } } catch (err) { console.error(`[funktionen-mail] K38 (${key}) nicht gesendet: ${fehlerArt(err)}`) } } // T24 ans Team, nur wenn der Kunde selbst eingeschaltet hat if (by === 'kunde') { let kontostand = '' if (abrechnung === 'abo') { try { if ((await aboProdukt(customerId)).produkt === 'wartung') { const ledger = await getLedger(customerId) if (ledger.balanceMinutes <= 0 || ledger.vorschussMinutes > 0) kontostand = kontostandText(ledger) } } catch (err) { console.error(`[funktionen-mail] Stundenkonto für T24 (${key}) nicht lesbar: ${fehlerArt(err)}`) } } try { const nachricht = t24Mail({ kunde: customer, email: customer?.email, projekt, key, customerId, projectId: doc.projectId, woid: ticketWoid, kontostand, anhangFehlt: anhangFehler && hatUnterlagen, }) ergebnis.t24 = (await sendAdminNotice(nachricht))?.sent === true } catch (err) { console.error(`[funktionen-mail] T24 (${key}) nicht gesendet: ${fehlerArt(err)}`) } } } catch (err) { console.error(`[funktionen-mail] Meldung nach dem Einschalten fehlgeschlagen: ${fehlerArt(err)}`) } return ergebnis } const TAG_MS = 24 * 60 * 60 * 1000 /** * K39 (Deine Funktion wurde beendet) an den Kunden, danach der Marker aboEndeMailAt (erst nach erfolgreichem Versand): die Funktion * wurde mit dem Abo beendet (disabled, Grund abo_ende). loeschdatum ist disabledAt plus die Loeschfrist der Funktion (loeschFristTage; * disabledAt aendert dieser Pfad nie, eine Wiederholung nach einem Mailfehler nennt dasselbe Datum). Kunde nicht mehr vorhanden oder ohne * Adresse: nichts zu senden, der Marker wird gesetzt, damit es keine Wiederholung gibt. Ein Dokument, das nicht abo_ende-abgeschaltet * ist oder den Marker schon traegt, ist erledigt (true, nichts geschrieben). Wirft nie; das Log nennt weder Namen noch Adressen. * @param {object} doc portalFeatures-Dokument (frisch gelesen, disabled) * @param {{ art: string, now?: Date, onGesendet?: (() => void) | null }} options art 'hosting' (das Abo ist ein Hosting-Abo), sonst Abo beendet; * onGesendet wird nach dem versendeten K39 gerufen (Zaehler des Abgleichs) * @returns {Promise} true = nichts mehr offen (gesendet, nichts zu senden oder Marker gesetzt), false = Versand oder Marker gescheitert * (Marker leer: der naechste Lauf wiederholt) */ export async function meldeBeendet(doc, { art, now = new Date(), onGesendet = null } = {}) { let gesendet = false try { if (doc?.state !== 'disabled' || doc.disabledReason !== 'abo_ende' || doc.aboEndeMailAt) return true const zeitpunkt = now instanceof Date ? now : new Date(now) const setzeMarker = () => updateDocument(config.collections.portalFeatures, doc.$id, { aboEndeMailAt: zeitpunkt.toISOString() }) const customer = await customerOf(doc.customerId) const to = String(customer?.email ?? '').trim() if (!to) { console.warn(`[funktionen-mail] K39 (${doc.featureKey}) entfällt: Kunde nicht mehr vorhanden oder ohne E-Mail-Adresse, Marker wird gesetzt`) await setzeMarker() return true } const projekt = (await projectName(doc.projectId)) || 'Dein Projekt' const beginn = Number.isFinite(Date.parse(doc.disabledAt)) ? Date.parse(doc.disabledAt) : zeitpunkt.getTime() // disabledAt fehlt nie, ausser bei Altdaten const loeschdatum = new Date(beginn + loeschFristTage(doc.featureKey) * TAG_MS) await sendCustomerMail(to, k39Mail({ kunde: customer, projekt, key: doc.featureKey, loeschdatum, art })) gesendet = true onGesendet?.() await setzeMarker() return true } catch (err) { console.error(`[funktionen-mail] K39 (${doc?.featureKey}) ${gesendet ? 'Marker nicht gesetzt' : 'nicht gesendet'}, der nächste Lauf wiederholt: ${fehlerArt(err)}`) return false } }