Abo-Abgleich: Rückkehr innerhalb der Frist schaltet wieder ein (K23)

Schritt 3 von gleicheAboAb: Ein abgeschaltetes Dokument mit Grund abo_ende,
dessen Wartungs-Abo wieder läuft, wird unter feature-admin:<projectId> nach
frischem Lesen von Dokument und Abo und mit vorhandener Einstellung über
activate wieder eingeschaltet (Daten und Einstellungen bleiben). K23 geht
nur, wenn K22 ging (aboEndeMailAt vor dem activate gelesen); scheitert K23,
bleibt die Reaktivierung bestehen. Nicht automatisch: off nach der Löschung,
abo_ende_vor_einrichtung, team, kunde_geloescht, Altbestand ohne Grund.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-04 14:44:47 +00:00
1 parent addb88d5fd
commit 13523ca5e2
2 files changed
+457 -12

No files matched your search

+74 -7
View File
@@ -3,8 +3,9 @@ import { aboLage, aboMemo } from '../aboStatus.js'
import { Query, getDocument, listAllDocuments, updateDocument } from '../appwriteAdmin.js'
import { withLocks } from '../keyedLock.js'
import { sendCustomerMail } from '../mailer.js'
import { customerOf, disable, meldeErstattung, projectName } from '../siteFeatures.js'
import { activate, customerOf, disable, meldeErstattung, projectName } from '../siteFeatures.js'
import { loeschzeit } from './frist.js'
import { ladeEinstellung } from './kontext.js'
import { pruefeNow, zeitMs } from './util.js'
import { datumKurz } from './zeit.js'
@@ -20,6 +21,12 @@ import { datumKurz } from './zeit.js'
* Schritt 2 fuer jedes abgeschaltete Dokument mit Grund 'abo_ende' oder (bezahlt) 'abo_ende_vor_einrichtung' ohne aboEndeMailAt
* die Meldung nachholen (auch fuer die eben abgeschalteten); der Marker aboEndeMailAt wird erst nach erfolgreichem
* Versand gesetzt
* Schritt 3 Rueckkehr: ein abgeschaltetes Dokument mit Grund 'abo_ende', dessen Wartungs-Abo wieder laeuft, wird wieder eingeschaltet
* (activate: Daten und Einstellungen bleiben, Grund, Marker und disabledAt enden), solange die Einstellung noch da ist, auch
* nach Tag 30 (der Abgleich laeuft im Tagesjob vor der Loeschung; sonst blockierten sich Loeschung und Rueckkehr).
* K23 geht nur an Kunden, die K22 bekommen haben (Marker aboEndeMailAt, vor dem activate gelesen). Nach der Loeschung ist
* das Dokument `off` und wird nie automatisch aktiv. Nicht automatisch: 'abo_ende_vor_einrichtung' (die Erstattung laeuft,
* das Team entscheidet per WOMS), 'team', 'kunde_geloescht' und Altbestand ohne Grund.
*
* Regeln (Block E):
* - Eine Listenabfrage fuer alle Kunden (Entwuerfe `off` bleiben ungelesen), ein aboMemo() je Lauf: die Abo-Lage jedes Kunden wird
@@ -41,6 +48,7 @@ const GRUND = 'abo_ende'
const NOTIZ = 'Abgeschaltet: Wartungs-Abo beendet'
const GRUND_VOR = 'abo_ende_vor_einrichtung'
const NOTIZ_VOR = 'Abgeschaltet: Wartungs-Abo endete vor der Einrichtung'
const NOTIZ_WIEDER = 'Wieder eingeschaltet: Wartungs-Abo läuft wieder'
const ADRESSE = /[^\s<>"',;()]+@[^\s<>"',;()]+/g
@@ -54,6 +62,9 @@ const istBewertet = (doc) =>
doc.state === 'setup' ||
(doc.state === 'disabled' && typeof doc.disabledReason === 'string' && doc.disabledReason.startsWith('abo_ende'))
/** Schritt 3: abgeschaltet wegen des Abo-Endes (nur 'abo_ende', nicht 'abo_ende_vor_einrichtung'). */
const istRueckkehr = (doc) => doc.state === 'disabled' && doc.disabledReason === GRUND
const portalFeatures = () => config.collections.portalFeatures
/**
@@ -160,7 +171,7 @@ async function bearbeiteDokument(doc, ctx) {
return null
}
const lage = await aboLage(doc.customerId) // wirft bei Lesefehler: dann bleibt alles, wie es ist
if (lage.laeuft) return null // das Abo ist zurueck (Schritt 2 und die Wiedereinschaltung gehoeren E6)
if (lage.laeuft) return null // das Abo ist zurueck: nichts mehr abzuschalten oder nachzuholen (die Rueckkehr ist Schritt 3, schalteWiederEin)
let zuMelden = frisch
if (aktion !== 'nachholen') {
@@ -175,6 +186,59 @@ async function bearbeiteDokument(doc, ctx) {
if (!fertig) erg.fehler += 1
}
/**
* K23 an die E-Mail des Kundenkontos. Nur nach geschriebenem `active` gerufen, nie innerhalb einer Sperre. Scheitert der Versand,
* bleibt die Reaktivierung bestehen: das Log (ohne Adresse) und `fehler` halten es fest; der Marker aboEndeMailAt ist mit dem
* Aktivieren geleert, es gibt also keine Wiederholung am naechsten Tag (K23 geht hoechstens einmal je Rueckkehr). Ohne Kunde oder
* ohne E-Mail-Adresse entfaellt die Mail (Hinweis im Log, kein Fehler). Wirft nie.
*/
async function sendeWiederEin(doc, ctx) {
const { erg } = ctx
try {
const customer = await customerOf(doc.customerId)
if (!customer?.email) {
console.warn('[termine-abo] K23 entfällt: Kunde nicht mehr vorhanden oder ohne E-Mail-Adresse')
return
}
await sendCustomerMail(customer.email, {
template: 'K23',
values: {
kunde_name: customer.name || '',
projekt: (await projectName(doc.projectId)) || 'Dein Projekt',
portal_url: `${config.publicBaseUrl}/dashboard.html#termine`,
},
})
erg.mails += 1
} catch (err) {
erg.fehler += 1
console.error(`[termine-abo] K23 für Funktion ${doc.$id} nicht gesendet, der Terminplaner bleibt eingeschaltet: ${fehlerText(err)}`)
}
}
/**
* Schritt 3, ein Dokument: in der Liste abgeschaltet mit Grund 'abo_ende' und das Abo laeuft. Unter der Sperre feature-admin:<projectId>
* frisch lesen (noch disabled/abo_ende: das Team kann inzwischen aktiviert oder mit anderem Grund abgeschaltet haben), frisch das
* Abo lesen (laeuft es wieder nicht, bleibt alles), die Einstellung lesen (fehlt sie, ist sie geloescht oder nie gespeichert worden:
* nie automatisch aktiv), dann activate. Ob K22 ging, zeigt der Marker aboEndeMailAt, der VOR dem activate gelesen wird (activate
* leert ihn). K23 folgt nach der Sperre und nur, wenn K22 ging; wer nie von dem Ende erfuhr, hoert auch nicht von der Rueckkehr.
* Lesefehler werfen (der Aufrufer zaehlt fehler): dann bleibt das Dokument abgeschaltet.
*/
async function schalteWiederEin(doc, ctx) {
const { erg } = ctx
const eingeschaltet = await withLocks([`feature-admin:${doc.projectId}`], async () => {
const frisch = await getDocument(portalFeatures(), doc.$id)
if (!istRueckkehr(frisch)) return null
const lage = await aboLage(doc.customerId)
if (!lage.laeuft) return null // das Abo endete zwischen Vorauswahl und Sperre wieder
if ((await ladeEinstellung(frisch.$id)) === null) return null // Daten geloescht oder nie da: Rueckkehr nur, solange sie existieren
const k22Ging = Boolean(frisch.aboEndeMailAt)
await activate(frisch.$id, { adminNote: NOTIZ_WIEDER })
erg.wiederEin += 1
return { k22Ging }
})
if (eingeschaltet?.k22Ging) await sendeWiederEin(doc, ctx)
}
async function bearbeiteKunde(customerId, dokumente, ctx) {
const { erg } = ctx
erg.ausgenommen += dokumente.filter(istAusgenommen).length
@@ -190,12 +254,13 @@ async function bearbeiteKunde(customerId, dokumente, ctx) {
return
}
erg.geprueft += bewertet.length
if (lage.laeuft) return // laeuft weiter (E6 schaltet abgeschaltete wieder ein)
for (const doc of bewertet) {
if (!aktionFuer(doc)) continue
// laeuft das Abo, gibt es nur die Rueckkehr (Schritt 3); sonst abschalten, einrichten, nachholen (Schritte 1, 1b, 2)
const bearbeite = lage.laeuft ? istRueckkehr(doc) && schalteWiederEin : aktionFuer(doc) && bearbeiteDokument
if (!bearbeite) continue
try {
await bearbeiteDokument(doc, ctx)
await bearbeite(doc, ctx)
} catch (err) {
erg.fehler += 1
console.error(`[termine-abo] Funktion ${doc.$id} nicht bearbeitet: ${fehlerText(err)}`)
@@ -209,8 +274,10 @@ async function bearbeiteKunde(customerId, dokumente, ctx) {
* @param {Date} now
* @returns {Promise<{ geprueft: number, abgeschaltet: number, wiederEin: number, mails: number, ausgenommen: number, fehler: number }>}
* geprueft = bewertete Dokumente (aktiv mit unlockedByPurchase true, ordered/setup, disabled mit Grund abo_ende*, deren Kunde
* lesbar war), abgeschaltet = in diesem Lauf abgeschaltet, wiederEin = wieder eingeschaltet (E6; hier immer 0), mails = versendete
* K22, ausgenommen = aktive Dokumente ohne Kauf (Pilot, Kulanz), fehler = Kunden und Dokumente, die nicht bearbeitet werden konnten
* lesbar war), abgeschaltet = in diesem Lauf abgeschaltet, wiederEin = in diesem Lauf wieder eingeschaltet (Schritt 3), mails =
* versendete Kundenmails K22 und K23 (T19 geht ans Team und zaehlt nicht), ausgenommen = aktive Dokumente ohne Kauf (Pilot, Kulanz),
* fehler = Kunden und Dokumente, die nicht bearbeitet werden konnten, sowie ein gescheiterter K23-Versand (das Dokument ist dann
* trotzdem wieder aktiv)
*/
export async function gleicheAboAb(now) {
pruefeNow(now, 'gleicheAboAb')