import { config } from '../../config.js' import { getDocument, listAllDocuments, Query, updateDocument } from '../appwriteAdmin.js' import { sendCustomerMail } from '../mailer.js' import { customerOf } from '../siteFeatures.js' import { datumKurz, heute } from '../termine/zeit.js' import { hatUkDaten } from './aufbewahrung.js' /** * Hinweis-Mail K35 vor der Loeschung: Sobald der Tagesjob einen abgeschalteten Buchungskalender mit Daten erstmals sieht, geht EINMAL * K35 an die E-Mail des Kundenkontos (Loeschdatum = disabledAt + loeschTageNachAbschalten Tage, Rechnungen bis dahin im Portal abrufbar). * Marker in portalFeatures.adminNote: "K35 am YYYY-MM-DD" (gilt nur, wenn nicht vor disabledAt; ein spaeteres Abschalten setzt disabledAt neu * und loest den Hinweis erneut aus). Scheitert der Versand, bleibt der Marker aus und der naechste Lauf wiederholt ihn. Ein Kunde ohne Adresse * (z. B. Kontoloeschung) bekommt den Marker ohne Mail. Ist das Loeschdatum schon erreicht, geht keine Mail mehr raus (die Loeschung laeuft im * selben Lauf). Logs ohne Personendaten. */ const TAG_MS = 24 * 60 * 60 * 1000 const MARKER_RE = /K35 am (\d{4}-\d{2}-\d{2})/ const zeitMs = (v) => { const t = Date.parse(v) return Number.isNaN(t) ? NaN : t } function markiere(notiz, tag) { const marker = `K35 am ${tag}` const alt = String(notiz ?? '').replace(/\s*(?:[-–|]\s*)?K35 am \d{4}-\d{2}-\d{2}/g, '').trim() if (!alt) return marker return `${alt.slice(0, Math.max(0, 300 - marker.length - 3))} – ${marker}` } /** @returns {Promise<{ geprueft: number, gesendet: number, ohneAdresse: number, fehler: number }>} */ export async function sendeHinweiseVorLoeschung(now = new Date()) { const nowMs = now instanceof Date ? now.getTime() : Number(now) const tage = config.unterkunft.loeschTageNachAbschalten ?? 90 const FEATURES = config.collections.portalFeatures const erg = { geprueft: 0, gesendet: 0, ohneAdresse: 0, fehler: 0 } const funktionen = await listAllDocuments(FEATURES, [Query.equal('featureKey', 'unterkunft'), Query.equal('state', 'disabled')]) for (const f of funktionen) { const abgeschaltet = zeitMs(f.disabledAt) if (!Number.isFinite(abgeschaltet)) continue const m = MARKER_RE.exec(String(f.adminNote ?? '')) if (m && m[1] >= String(f.disabledAt).slice(0, 10)) continue const loeschAm = abgeschaltet + tage * TAG_MS if (loeschAm <= nowMs) continue erg.geprueft += 1 try { if (!(await hatUkDaten(f.$id))) continue const kunde = f.customerId ? await customerOf(f.customerId).catch(() => null) : null const an = typeof kunde?.email === 'string' ? kunde.email.trim() : '' if (an) { let projekt = '' try { const p = await getDocument(config.collections.websiteProjects, f.projectId) projekt = p?.projectName || p?.subdomain || '' } catch { // ohne Projektname: die Vorlage nennt dann keinen } await sendCustomerMail(an, { template: 'K35', values: { kunde_name: typeof kunde.name === 'string' ? kunde.name.trim() : '', projekt, loeschdatum: datumKurz(new Date(loeschAm)), portal_url: `${config.publicBaseUrl}/dashboard.html#buchungen?projekt=${encodeURIComponent(f.projectId ?? '')}`, }, blocks: {}, bccTeam: true, }) erg.gesendet += 1 } else { erg.ohneAdresse += 1 } await updateDocument(FEATURES, f.$id, { adminNote: markiere(f.adminNote, heute(new Date(nowMs))) }) } catch (err) { erg.fehler += 1 console.warn(`[unterkunft-aufbewahrung] K35 für Funktion ${f.$id} nicht gesendet: ${[err?.name, err?.status].filter(Boolean).join(' ')}`) } } return erg }