Buchungskalender: Abschluss – S5 Hinweis-Mail K35 vor der Loeschung, Rechnungen im Zustand abgeschaltet lesbar
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e2fc4eb676
commit
ab838d07bc
6 files changed
+225
-4
No files matched your search
@@ -0,0 +1,80 @@
|
||||
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
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { laufeFristenAb } from './ablauf.js'
|
||||
import { config } from '../../config.js'
|
||||
import { listDocuments, Query } from '../appwriteAdmin.js'
|
||||
import { sendeHinweiseVorLoeschung } from './abschaltHinweis.js'
|
||||
import { anonymisiere, loescheAbgeschaltete } from './aufbewahrung.js'
|
||||
import { gleicheAlleAb } from './icalAbgleich.js'
|
||||
import { unterkunftModus } from './schalter.js'
|
||||
@@ -55,7 +56,7 @@ async function gibtDaten() {
|
||||
|
||||
/**
|
||||
* Tagesjob: Aufbewahrung (ipHash, Verfall von `neu`, Anonymisierung, Rechnungen nach 8 Jahren) und Loeschung abgeschalteter Funktionen
|
||||
* (90 Tage). Gemeinsamer Deckel config.unterkunft.aufbewahrungDeckel (500) je Lauf (die Loeschung bekommt, was die Aufbewahrung uebrig laesst). Bei UNTERKUNFT_ENABLED=off nur, wenn es noch
|
||||
* (90 Tage; davor einmal die Hinweis-Mail K35). Gemeinsamer Deckel config.unterkunft.aufbewahrungDeckel (500) je Lauf (die Loeschung bekommt, was die Aufbewahrung uebrig laesst). Bei UNTERKUNFT_ENABLED=off nur, wenn es noch
|
||||
* Buchungsdaten gibt (die Fristen gelten auch dann). Ein Teil, der scheitert, stoppt den anderen nicht.
|
||||
*/
|
||||
export async function taeglicherJob(now = new Date()) {
|
||||
@@ -71,6 +72,12 @@ export async function taeglicherJob(now = new Date()) {
|
||||
} catch (err) {
|
||||
console.error(`[unterkunft-jobs] taeglicherJob: Aufbewahrung fehlgeschlagen: ${err?.name} ${err?.status ?? ''}`)
|
||||
}
|
||||
// Vor der Loeschung: einmaliger Hinweis K35 (Rechnungen sichern); ein Fehler stoppt die Loeschung nicht (Marker fehlt, naechster Lauf wiederholt)
|
||||
try {
|
||||
await sendeHinweiseVorLoeschung(now)
|
||||
} catch (err) {
|
||||
console.error(`[unterkunft-jobs] taeglicherJob: Hinweis vor der Löschung fehlgeschlagen: ${err?.name} ${err?.status ?? ''}`)
|
||||
}
|
||||
try {
|
||||
weg = await loescheAbgeschaltete(now, { deckel: rest })
|
||||
} catch (err) {
|
||||
|
||||
Reference in new issue
Block a user