TP2 Abschlussprüfung: Löschung (IP/Browser auf portalFeatures, Erstattungsspur, K22-Datum)
- markOrdered speichert IP und Browser der Bestellung nicht mehr am Funktionsdokument (nur im Nachweis featureConsents, dort mit Löschfrist); Altwerte werden bei einer neuen Bestellung geleert - Aufbewahrungslauf (retention): consentIp, consentUserAgent, consentText, consentAt werden an allen Funktionsdokumenten eines fälligen Kunden geleert, auch an abgeschalteten; Fehlschlag hält den Kunden wie die übrigen abhängigen Daten - Einmal-Skript scripts/feature-consent-felder-leeren.mjs (Standard Probelauf, --apply leert nur, wo featureConsents die Angabe trägt) - 30-Tage-Reset (jobs.js): auch eine bezahlte Einrichtung mit Grund abo_ende_vor_einrichtung und noch nicht gemeldetem T19 wird nicht zurückgesetzt, solange zahlungMeldung nicht gemeldet ist (die Spur der Erstattung bleibt) - K22: enddatum ist das Abo-Ende (lage.endedAt), sonst der Tag des Abschaltens; loeschdatum bleibt 30 Tage nach dem Abschalten Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b974b3a3ec
commit
2fc48df915
13 files changed
+425
-22
No files matched your search
@@ -323,7 +323,7 @@ router.post('/:key/checkout', requireSession, async (req, res) => {
|
||||
projectId,
|
||||
key,
|
||||
sessionId: created.id,
|
||||
consent: { text: hakenTexte(feature), at: klickAt, ip: req.ip, userAgent: req.get('user-agent') },
|
||||
consent: { text: hakenTexte(feature), at: klickAt },
|
||||
})
|
||||
return created
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { rm } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { config } from '../config.js'
|
||||
import { deleteDocument, getDocument, listDocuments, Query } from './appwriteAdmin.js'
|
||||
import { deleteDocument, getDocument, listDocuments, Query, updateDocument } from './appwriteAdmin.js'
|
||||
import { deleteNewsImage } from './newsImages.js'
|
||||
import { disable } from './siteFeatures.js'
|
||||
import { getStripe } from './stripeBilling.js'
|
||||
@@ -25,6 +25,9 @@ import { filesOf, STATUS } from './takeoverCredits.js'
|
||||
* "Diese Angaben loeschen wir drei Jahre nach Ablauf des Jahres, in dem Ihr letztes Abo endet") loescht derselbe Lauf fuer faellige
|
||||
* Kunden mit (plan.news[].consentIds). Kunden ohne Abo, zum Beispiel nur mit News, sind nie faellig; ihre Nachweise bleiben, wie ihre
|
||||
* Beitraege, ausserhalb dieser Frist.
|
||||
* - Bestellangaben am Funktionsdokument: Fruehere Bestellungen speicherten IP, Browser, Zustimmungstext und -zeit auch am Dokument
|
||||
* (portalFeatures.consentIp, consentUserAgent, consentText, consentAt). Fuer faellige Kunden werden sie an ALLEN ihren
|
||||
* Funktionsdokumenten geleert, auch an abgeschalteten (plan.news[].featureFieldIds); die Dokumente selbst bleiben (siteKey).
|
||||
* Zeitrechnung in deutscher Zeit: Silvester 23:30 in Berlin ist noch das alte Jahr.
|
||||
* Nicht abgedeckt: Vertragsereignisse ohne customerId bzw. ohne zugehoeriges Abo-Dokument (Waisen) werden
|
||||
* weder gefunden noch geloescht; sie muessen von Hand geprueft werden. Listen werden nicht paginiert: wird eine
|
||||
@@ -38,6 +41,11 @@ const NEWS_POSTS = () => config.collections.newsPosts
|
||||
const FEATURES = () => config.collections.portalFeatures
|
||||
const CONSENTS = () => config.collections.featureConsents
|
||||
|
||||
/** Felder am Funktionsdokument, die die Zustimmung zur Bestellung festhalten (leer => nichts zu loeschen). */
|
||||
const FEATURE_CONSENT_FIELDS = ['consentIp', 'consentUserAgent', 'consentText', 'consentAt']
|
||||
const BLANK_FEATURE_FIELDS = { consentIp: '', consentUserAgent: '', consentText: '', consentAt: null }
|
||||
const hasConsentFields = (doc) => FEATURE_CONSENT_FIELDS.some((key) => doc[key])
|
||||
|
||||
export const RETENTION_YEARS = 3
|
||||
export const RECEIPT_YEARS = 8
|
||||
const LIST_LIMIT = 5000
|
||||
@@ -73,7 +81,8 @@ function endOf(doc) {
|
||||
/**
|
||||
* @param {Date} [now]
|
||||
* @returns {Promise<{ customers: { customerId: string, lastEndAt: string, subscriptionIds: string[], eventIds: string[] }[],
|
||||
* news: { customerId: string, posts: { id: string, projectId: string, imageId: string | null }[], featureIds: string[], consentIds: string[] }[],
|
||||
* news: { customerId: string, posts: { id: string, projectId: string, imageId: string | null }[], featureIds: string[], consentIds: string[],
|
||||
* featureFieldIds: string[] }[],
|
||||
* takeover: { id: string, reason: string, action: 'loeschen' | 'melden' }[],
|
||||
* pending: { customerId: string, subscriptionIds: string[], reason: string }[], skipped: { id: string, reason: string }[] }>}
|
||||
*/
|
||||
@@ -117,14 +126,18 @@ export async function findDueDeletions(now = new Date(), { limit = LIST_LIMIT }
|
||||
const news = []
|
||||
for (const { customerId } of customers) {
|
||||
const posts = await list(NEWS_POSTS(), [Query.equal('customerId', customerId)])
|
||||
const features = (await list(FEATURES(), [Query.equal('customerId', customerId)])).filter((f) => f.state !== 'disabled')
|
||||
const allFeatures = await list(FEATURES(), [Query.equal('customerId', customerId)])
|
||||
const features = allFeatures.filter((f) => f.state !== 'disabled')
|
||||
// Bestellangaben (IP, Browser, Text, Zeit) leeren wir an jedem Dokument des Kunden, auch am schon abgeschalteten
|
||||
const withFields = allFeatures.filter(hasConsentFields)
|
||||
const consents = await list(CONSENTS(), [Query.equal('customerId', customerId)])
|
||||
if (!posts.length && !features.length && !consents.length) continue
|
||||
if (!posts.length && !features.length && !consents.length && !withFields.length) continue
|
||||
news.push({
|
||||
customerId,
|
||||
posts: posts.map((p) => ({ id: p.$id, projectId: p.projectId, imageId: p.imageId || null })),
|
||||
featureIds: features.map((f) => f.$id),
|
||||
consentIds: consents.map((c) => c.$id),
|
||||
featureFieldIds: withFields.map((f) => f.$id),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -177,12 +190,12 @@ async function deleteQuietly(collection, id) {
|
||||
* Daten fuer immer liegen. Lehnt Stripe die Aenderung ab (beendete Abos lassen sich oft nicht mehr aendern), steht das Abo in manual;
|
||||
* die Appwrite-Dokumente werden trotzdem geloescht, denn die Frist ist abgelaufen.
|
||||
* @returns {Promise<{ deleted: { subscriptions: number, contractEvents: number, takeover: number, stripeCleared: number,
|
||||
* newsPosts: number, newsImages: number, features: number, featureConsents: number },
|
||||
* newsPosts: number, newsImages: number, features: number, featureConsents: number, featureFields: number },
|
||||
* manual: { customerId: string, subscriptionId: string, reason: string }[],
|
||||
* failed: { what: string, id: string, reason: string }[], held: string[] }>}
|
||||
*/
|
||||
export async function applyDeletions(plan) {
|
||||
const deleted = { subscriptions: 0, contractEvents: 0, takeover: 0, stripeCleared: 0, newsPosts: 0, newsImages: 0, features: 0, featureConsents: 0 }
|
||||
const deleted = { subscriptions: 0, contractEvents: 0, takeover: 0, stripeCleared: 0, newsPosts: 0, newsImages: 0, features: 0, featureConsents: 0, featureFields: 0 }
|
||||
const manual = []
|
||||
const failed = []
|
||||
const held = []
|
||||
@@ -218,6 +231,19 @@ export async function applyDeletions(plan) {
|
||||
})
|
||||
if (off) deleted.features += 1
|
||||
}
|
||||
// IP, Browser, Text und Zeit der Bestellung am Funktionsdokument (auch am abgeschalteten): das Dokument bleibt, die Angaben gehen
|
||||
for (const id of entry.featureFieldIds || []) {
|
||||
const blanked = await attempt('portalFeatures', id, async () => {
|
||||
try {
|
||||
await updateDocument(FEATURES(), id, BLANK_FEATURE_FIELDS)
|
||||
return true
|
||||
} catch (err) {
|
||||
if (isGone(err)) return false
|
||||
throw err
|
||||
}
|
||||
})
|
||||
if (blanked) deleted.featureFields += 1
|
||||
}
|
||||
// Zustimmungsnachweise der Funktionsbestellungen zuletzt: ein Fehler stoppt den Rest nicht
|
||||
for (const id of entry.consentIds || []) {
|
||||
if (await attempt('featureConsents', id, () => deleteQuietly(CONSENTS(), id))) deleted.featureConsents += 1
|
||||
@@ -288,8 +314,9 @@ export function formatReport(plan, result = null) {
|
||||
for (const n of plan.news || []) {
|
||||
const images = n.posts.filter((p) => p.imageId).length
|
||||
const consents = n.consentIds?.length || 0
|
||||
const fields = n.featureFieldIds?.length || 0
|
||||
lines.push(
|
||||
`Kunde ${n.customerId}: ${n.posts.length} News-Beitrag/-Beiträge mit ${images} Foto(s) zu löschen, ${n.featureIds.length} Funktion(en) abzuschalten, ${consents} Zustimmungsnachweis(e) zu löschen`
|
||||
`Kunde ${n.customerId}: ${n.posts.length} News-Beitrag/-Beiträge mit ${images} Foto(s) zu löschen, ${n.featureIds.length} Funktion(en) abzuschalten, ${consents} Zustimmungsnachweis(e) zu löschen, ${fields} Funktionsdokument(e) mit Zustimmungsangaben zu leeren`
|
||||
)
|
||||
}
|
||||
const del = plan.takeover.filter((t) => t.action === 'loeschen')
|
||||
@@ -308,7 +335,8 @@ export function formatReport(plan, result = null) {
|
||||
'',
|
||||
`Gelöscht: ${d.subscriptions} Abo(s), ${d.contractEvents} Vertragsereignis(se), ${d.takeover} Übernahme-Antrag/-Anträge; Stripe-Metadaten geleert bei ${d.stripeCleared} Abo(s).`,
|
||||
`News: ${d.newsPosts} Beitrag/Beiträge und ${d.newsImages} Foto(s) gelöscht, ${d.features} Funktion(en) abgeschaltet.`,
|
||||
`Zustimmungsnachweise zu Funktionsbestellungen: ${d.featureConsents} gelöscht.`
|
||||
`Zustimmungsnachweise zu Funktionsbestellungen: ${d.featureConsents} gelöscht.`,
|
||||
`Zustimmungsangaben an Funktionsdokumenten (IP, Browser, Text, Zeit): ${d.featureFields} geleert.`
|
||||
)
|
||||
if (result.failed?.length) {
|
||||
lines.push('', 'Fehlgeschlagen (Lauf erneut starten):', ...result.failed.map((f) => ` ${f.what} ${f.id}: ${f.reason}`))
|
||||
|
||||
@@ -255,7 +255,12 @@ export function hakenTexte(feature) {
|
||||
.join('\n\n')
|
||||
}
|
||||
|
||||
/** Bestellung eintragen (Zustand ordered); die Meldungen bei setup/active/disabled stehen im Register der Funktion. */
|
||||
/**
|
||||
* Bestellung eintragen (Zustand ordered); die Meldungen bei setup/active/disabled stehen im Register der Funktion.
|
||||
* consent: { text, at } - Wortlaut (Kopie fuer das Team) und Zeit des Klicks. IP und Browser der Bestellung stehen NUR im Nachweis
|
||||
* (featureConsents, Loeschfrist wie dort, Datenschutzerklaerung Abschnitt 7) und werden am Funktionsdokument nie gespeichert: die Felder
|
||||
* consentIp und consentUserAgent werden leer geschrieben, das leert auch einen Wert aus einer frueheren Bestellung desselben Dokuments.
|
||||
*/
|
||||
export async function markOrdered({ customerId, projectId, key, sessionId, consent = {} }) {
|
||||
assertKey(key)
|
||||
const feature = featureConfig(key)
|
||||
@@ -270,8 +275,8 @@ export async function markOrdered({ customerId, projectId, key, sessionId, conse
|
||||
orderedAt: new Date().toISOString(),
|
||||
consentText: String(consent.text || '').slice(0, 1000),
|
||||
consentAt: consent.at || null,
|
||||
consentIp: String(consent.ip || '').slice(0, 64),
|
||||
consentUserAgent: String(consent.userAgent || '').slice(0, 300),
|
||||
consentIp: '',
|
||||
consentUserAgent: '',
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ const istBewertet = (doc) =>
|
||||
* Schritt 2b: bezahlte, wegen des Abo-Endes vor der Einrichtung abgeschaltete Einrichtung, deren Erstattung (T19) noch nicht beim Team ist.
|
||||
* Das Abo spielt keine Rolle: die Erstattung bleibt geschuldet, auch wenn es zurueckkommt.
|
||||
*/
|
||||
const istErstattungOffen = (doc) =>
|
||||
export const istErstattungOffen = (doc) =>
|
||||
doc.state === 'disabled' && doc.disabledReason === GRUND_VOR && doc.unlockedByPurchase === true && doc.zahlungMeldung !== 'gemeldet'
|
||||
|
||||
/** Schritt 3: abgeschaltet wegen des Abo-Endes (nur 'abo_ende', nicht 'abo_ende_vor_einrichtung'). */
|
||||
@@ -93,7 +93,8 @@ const portalFeatures = () => config.collections.portalFeatures
|
||||
* Der gemeinsame Melder fuer das Ende des Terminplaners (Schritt 2, Schritt 1b und der Sofortpfad E9 rufen nur ihn, nie T19 oder
|
||||
* K22 einzeln). Reihenfolge: (T19,) K22 an die E-Mail des Kundenkontos, danach der Marker aboEndeMailAt = now.
|
||||
* - Grund 'abo_ende': K22 mit Block `abo_ende` (Wartungs-Abo beendet) bzw. `hosting` (lage.art 'hosting' oder 'wechsel_widerrufen':
|
||||
* das Abo ist ein Hosting-Abo), dazu der Kasten `daten`. enddatum = Berliner Tag von disabledAt, loeschdatum = 30 Tage danach.
|
||||
* das Abo ist ein Hosting-Abo), dazu der Kasten `daten`. enddatum = Berliner Tag des Abo-Endes (lage.endedAt), sonst von disabledAt;
|
||||
* loeschdatum = 30 Tage nach disabledAt.
|
||||
* - Grund 'abo_ende_vor_einrichtung': ein unbezahltes Dokument (unlockedByPurchase nicht true, die Bestellung wartet noch auf die
|
||||
* Zahlung) hat nichts zu erstatten: weder T19 noch K22 noch Marker, kein Schreibzugriff (kommt die Zahlung spaeter, meldet
|
||||
* siteFeatures das als T19 und der Abgleich sendet dann K22). Ein bezahltes Dokument meldet ZUERST die Erstattung an das Team
|
||||
@@ -136,6 +137,10 @@ export async function meldeAboEnde(doc, { lage = null, now, onGesendet = null })
|
||||
}
|
||||
const projekt = (await projectName(doc.projectId)) || 'Dein Projekt'
|
||||
const beginn = new Date(Number.isFinite(zeitMs(doc.disabledAt)) ? zeitMs(doc.disabledAt) : nowMs) // disabledAt fehlt nie, ausser bei Altdaten
|
||||
// enddatum ist der Tag des Abo-Endes (lage.endedAt: endedAt || canceledAt des Abos), wenn bekannt - das Abschalten kann Tage danach liegen;
|
||||
// sonst (Sofortpfad, Nachlauf ohne Abo-Lesen, kein lesbares Datum) der Tag des Abschaltens. loeschdatum bleibt 30 Tage nach dem Abschalten.
|
||||
const aboEnde = zeitMs(lage?.endedAt)
|
||||
const ende = Number.isFinite(aboEnde) ? new Date(aboEnde) : beginn
|
||||
const grundBlock = vorEinrichtung
|
||||
? { vor_einrichtung: true }
|
||||
: { [lage?.art === 'hosting' || lage?.art === 'wechsel_widerrufen' ? 'hosting' : 'abo_ende']: true, daten: true }
|
||||
@@ -144,7 +149,7 @@ export async function meldeAboEnde(doc, { lage = null, now, onGesendet = null })
|
||||
values: {
|
||||
kunde_name: customer.name || '',
|
||||
projekt,
|
||||
enddatum: datumKurz(beginn),
|
||||
enddatum: datumKurz(ende),
|
||||
loeschdatum: datumKurz(loeschzeit(beginn)),
|
||||
preis: config.features.termine.priceLabel,
|
||||
portal_url: `${config.publicBaseUrl}/dashboard.html#termine`,
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Query, deleteDocument, getDocument, listAllDocuments, listDocuments, up
|
||||
import { withLocks } from '../keyedLock.js'
|
||||
import { stateFields } from '../siteFeatures.js'
|
||||
import { gibFreiSicher, iso, vertragAm } from './ablauf.js'
|
||||
import { gleicheAboAb } from './aboAbgleich.js'
|
||||
import { gleicheAboAb, istErstattungOffen } from './aboAbgleich.js'
|
||||
import { meldeTagesjob, pruefeBetrieb, zaehleAktiveBetriebe } from './betrieb.js'
|
||||
import {
|
||||
abgelaufeneHaltungen,
|
||||
@@ -603,7 +603,8 @@ function loescheEinstellung(lauf, funktionId, grenze) {
|
||||
* Das Funktionsdokument nach der Loeschung zuruecksetzen, damit der Betrieb neu bestellen kann: unter den Sperren
|
||||
* feature-admin:<projectId> (wie Aktivieren und Abschalten im WOMS) und termine-cfg:<id> frisch lesen und nochmals pruefen
|
||||
* (leseAbgeschaltet ohne Abo: es gibt nichts Unumkehrbares mehr), dann schreiben. Hat das Team es inzwischen aktiviert, bleibt es.
|
||||
* R108: steht am frisch gelesenen Dokument ein offener Zahlungsfall (zahlungMeldung nicht leer und nicht 'gemeldet'), wird NICHT
|
||||
* R108: steht am frisch gelesenen Dokument ein offener Zahlungsfall (zahlungMeldung nicht leer und nicht 'gemeldet', oder C2: bezahlte
|
||||
* Einrichtung mit Grund abo_ende_vor_einrichtung und T19 nicht gemeldet, auch bei leerer zahlungMeldung), wird NICHT
|
||||
* zurueckgesetzt (Warnung ohne Kennungen, +1 in lauf.fehler, kein Wurf): das Dokument bleibt disabled samt unlockedByPurchase, ticketId
|
||||
* und zahlungMeldung, der taegliche Abo-Abgleich (aboAbgleich, Nachholen der Erstattungsmeldung T19) bleibt zustaendig und ein
|
||||
* spaeterer Lauf setzt zurueck, sobald 'gemeldet' steht. Buchungen und Einstellung sind dann schon geloescht (bleibt so).
|
||||
@@ -619,7 +620,10 @@ function setzeFunktionZurueck(lauf, funktion, grenze) {
|
||||
// R108: ein offener Zahlungsfall (zahlungMeldung gesetzt, nicht 'gemeldet') bleibt sichtbar. Das Dokument bleibt disabled samt Markern
|
||||
// (unlockedByPurchase, ticketId, zahlungMeldung), damit der Abo-Abgleich die Meldung (T19) weiter nachholt; ein spaeterer Lauf setzt zurueck,
|
||||
// sobald 'gemeldet' steht. Ohne PII im Log, zaehlt in lauf.fehler (Tagesbericht), kein Wurf.
|
||||
if (frisch.zahlungMeldung && frisch.zahlungMeldung !== 'gemeldet') {
|
||||
// C2: dazu die bezahlte, wegen des Abo-Endes vor der Einrichtung abgeschaltete Einrichtung, deren Erstattung (T19) noch nicht beim Team ist, auch
|
||||
// wenn zahlungMeldung noch leer ist (T19 scheiterte seit dem Abschalten): das Zuruecksetzen wuerde unlockedByPurchase, ticketId und
|
||||
// zahlungMeldung loeschen, und mit ihnen die Spur der geschuldeten Erstattung. Der Abo-Abgleich (Schritt 2b) meldet T19 weiter.
|
||||
if ((frisch.zahlungMeldung && frisch.zahlungMeldung !== 'gemeldet') || istErstattungOffen(frisch)) {
|
||||
lauf.fehler += 1
|
||||
console.warn(`[termine-jobs] ${lauf.job}: eine Funktion hat einen offenen Zahlungsfall, sie wird nicht zurückgesetzt (der Abo-Abgleich meldet ihn weiter)`)
|
||||
return false
|
||||
|
||||
Reference in new issue
Block a user