Retention: abhaengige Daten vor dem Abo loeschen, Fehlschlag haelt den Kunden fuer den naechsten Lauf
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bade4edb74
commit
9a2a70b491
2 files changed
+172
-29
No files matched your search
@@ -169,18 +169,23 @@ async function deleteQuietly(collection, id) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Faellige Daten loeschen. Reihenfolge je Kunde: erst Stripe-Metadaten (Abo-ID steht noch im Plan), dann die
|
||||
* Appwrite-Dokumente. Lehnt Stripe ab (beendete Abos lassen sich oft nicht mehr aendern), steht das Abo in manual;
|
||||
* Faellige Daten loeschen. Reihenfolge je Kunde: erst die abhaengigen Daten (News-Beitraege samt Fotos, Funktionen abschalten,
|
||||
* Zustimmungsnachweise der Funktionsbestellungen), dann die Stripe-Metadaten (Abo-ID steht noch im Plan) und zuletzt die
|
||||
* Appwrite-Dokumente der Abos (Vertragsereignisse, dann subscriptions). Grund: findDueDeletions leitet faellige Kunden allein aus den
|
||||
* Abo-Dokumenten ab. Schlaegt ein Loeschen abhaengiger Daten fehl (oder das eines Vertragsereignisses), bleiben die Abo-Dokumente des
|
||||
* Kunden stehen (result.held), der naechste Lauf findet den Kunden wieder und versucht es erneut; sonst blieben die uebrig gebliebenen
|
||||
* 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 },
|
||||
* manual: { customerId: string, subscriptionId: string, reason: string }[],
|
||||
* failed: { what: string, id: 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 manual = []
|
||||
const failed = []
|
||||
const held = []
|
||||
const clear = Object.fromEntries(CONSENT_METADATA_KEYS.map((key) => [key, '']))
|
||||
const attempt = async (what, id, fn) => {
|
||||
try {
|
||||
@@ -191,30 +196,10 @@ export async function applyDeletions(plan) {
|
||||
}
|
||||
}
|
||||
|
||||
for (const customer of plan.customers || []) {
|
||||
for (const id of customer.subscriptionIds) {
|
||||
try {
|
||||
await getStripe().subscriptions.update(id, { metadata: clear })
|
||||
deleted.stripeCleared += 1
|
||||
} catch (err) {
|
||||
manual.push({
|
||||
customerId: customer.customerId,
|
||||
subscriptionId: id,
|
||||
reason: `Stripe lehnt die Änderung ab (${err.message}); Zustimmungsangaben in den Metadaten von Hand entfernen`,
|
||||
})
|
||||
}
|
||||
}
|
||||
for (const id of customer.eventIds) {
|
||||
if (await attempt('contractEvents', id, () => deleteQuietly(EVENTS(), id))) deleted.contractEvents += 1
|
||||
}
|
||||
for (const id of customer.subscriptionIds) {
|
||||
if (await attempt('subscriptions', id, () => deleteQuietly(SUBSCRIPTIONS(), id))) deleted.subscriptions += 1
|
||||
}
|
||||
}
|
||||
|
||||
for (const entry of plan.news || []) {
|
||||
// News-Beitraege, Funktionen und Zustimmungsnachweise eines Kunden (Eintrag aus plan.news)
|
||||
const deleteDependents = async (entry) => {
|
||||
for (const post of entry.posts) {
|
||||
// erst das Bild (Dokument bleibt bei Fehler und wird beim naechsten Lauf erneut gefunden), dann der Beitrag
|
||||
// erst das Bild (Dokument bleibt bei Fehler), dann der Beitrag
|
||||
if (post.imageId) {
|
||||
const removed = await attempt('newsImages', post.imageId, async () => {
|
||||
await deleteNewsImage(post.projectId, post.imageId)
|
||||
@@ -233,12 +218,51 @@ export async function applyDeletions(plan) {
|
||||
})
|
||||
if (off) deleted.features += 1
|
||||
}
|
||||
// Zustimmungsnachweise der Funktionsbestellungen zuletzt: ein Fehler stoppt den Rest nicht, der naechste Lauf findet den Eintrag erneut
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
const newsOf = new Map((plan.news || []).map((entry) => [entry.customerId, entry]))
|
||||
for (const customer of plan.customers || []) {
|
||||
const failedBefore = failed.length
|
||||
const entry = newsOf.get(customer.customerId)
|
||||
if (entry) {
|
||||
newsOf.delete(customer.customerId)
|
||||
await deleteDependents(entry)
|
||||
}
|
||||
if (failed.length > failedBefore) {
|
||||
held.push(customer.customerId)
|
||||
continue
|
||||
}
|
||||
for (const id of customer.subscriptionIds) {
|
||||
try {
|
||||
await getStripe().subscriptions.update(id, { metadata: clear })
|
||||
deleted.stripeCleared += 1
|
||||
} catch (err) {
|
||||
manual.push({
|
||||
customerId: customer.customerId,
|
||||
subscriptionId: id,
|
||||
reason: `Stripe lehnt die Änderung ab (${err.message}); Zustimmungsangaben in den Metadaten von Hand entfernen`,
|
||||
})
|
||||
}
|
||||
}
|
||||
for (const id of customer.eventIds) {
|
||||
if (await attempt('contractEvents', id, () => deleteQuietly(EVENTS(), id))) deleted.contractEvents += 1
|
||||
}
|
||||
if (failed.length > failedBefore) {
|
||||
// ein Vertragsereignis ist stehen geblieben: das Abo-Dokument bleibt, sonst wuerde das Ereignis zur Waise
|
||||
held.push(customer.customerId)
|
||||
continue
|
||||
}
|
||||
for (const id of customer.subscriptionIds) {
|
||||
if (await attempt('subscriptions', id, () => deleteQuietly(SUBSCRIPTIONS(), id))) deleted.subscriptions += 1
|
||||
}
|
||||
}
|
||||
// Eintraege ohne Kunden-Eintrag im Plan (kein Abo-Dokument, das zu halten waere)
|
||||
for (const entry of newsOf.values()) await deleteDependents(entry)
|
||||
|
||||
for (const item of (plan.takeover || []).filter((t) => t.action === 'loeschen')) {
|
||||
const removed = await attempt('takeoverCredits', item.id, async () => {
|
||||
await rm(path.join(config.takeover.uploadDir, item.id), { recursive: true, force: true })
|
||||
@@ -247,7 +271,7 @@ export async function applyDeletions(plan) {
|
||||
if (!removed) continue // Dokument bleibt, solange die Nachweise nicht weg sind
|
||||
if (await attempt('takeoverCredits', item.id, () => deleteQuietly(TAKEOVER(), item.id))) deleted.takeover += 1
|
||||
}
|
||||
return { deleted, manual, failed }
|
||||
return { deleted, manual, failed, held }
|
||||
}
|
||||
|
||||
const fmtDate = (iso) => new Date(iso).toLocaleDateString('de-DE', { timeZone: 'Europe/Berlin' })
|
||||
@@ -289,6 +313,9 @@ export function formatReport(plan, result = null) {
|
||||
if (result.failed?.length) {
|
||||
lines.push('', 'Fehlgeschlagen (Lauf erneut starten):', ...result.failed.map((f) => ` ${f.what} ${f.id}: ${f.reason}`))
|
||||
}
|
||||
if (result.held?.length) {
|
||||
lines.push('', 'Abo-Daten bleiben stehen, bis die abhängigen Daten gelöscht sind (der nächste Lauf findet den Kunden wieder):', ...result.held.map((id) => ` Kunde ${id}`))
|
||||
}
|
||||
if (result.manual.length) {
|
||||
lines.push('', 'Von Hand erledigen:', ...result.manual.map((m) => ` Abo ${m.subscriptionId} (Kunde ${m.customerId}): ${m.reason}`))
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user