Abo-Ende: Sofortpfad beim Widerruf des Wechsels, Team-Aktivierung nur mit laufendem Abo
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6bd6aed7d5
commit
c5009da570
6 files changed
+726
-10
No files matched your search
@@ -6,7 +6,7 @@ import { sendAdminNotice, sendCustomerMail } from './mailer.js'
|
||||
import { getStripe, retrieveCheckoutSession } from './stripeBilling.js'
|
||||
import { anleitungsLink, featureConfig, featureHaken, featureKeys, ticketFreischaltungKeys, ticketTexte } from './featureRegistry.js'
|
||||
import { zustimmungFuerSitzung } from './featureConsents.js'
|
||||
import { laeuft } from './aboStatus.js'
|
||||
import { aboLage, laeuft } from './aboStatus.js'
|
||||
import { loeschTag } from './termine/frist.js'
|
||||
import { createFeatureTicket } from './ticketAdmin.js'
|
||||
|
||||
@@ -477,11 +477,35 @@ async function stopAnalyseCollecting(doc) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Freischaltung ohne Zahlung und ohne Ticket (unlockedByPurchase bleibt false). */
|
||||
const KEIN_ABO_AKTIVIERUNG =
|
||||
'Das Wartungs-Abo des Kunden läuft nicht. Der Terminplaner kann erst wieder freigeschaltet werden, wenn es läuft.'
|
||||
|
||||
/**
|
||||
* Schutz der Team-Aktivierung (E9): ein vom Kauf freigeschalteter Terminplaner gehoert zum Wartungs-Abo. Laeuft es nicht, wuerde der
|
||||
* naechste Abgleichslauf die Kulanz zuruecknehmen und K22 ein zweites Mal senden; darum 409 ohne Schreibzugriff. Frisch gelesen
|
||||
* (aboLage direkt, nie der Memo), mit der Wechsel-Regel des Abgleichs. Ein Lesefehler ist ein Fehler (503, geloggt), nie "laeuft".
|
||||
*/
|
||||
async function assertAboLaeuftFuerAktivierung(customerId) {
|
||||
let laeuftJetzt
|
||||
try {
|
||||
laeuftJetzt = (await aboLage(customerId)).laeuft
|
||||
} catch (err) {
|
||||
console.error(`[funktionen] Wartungs-Abo von Kunde ${customerId} für die Aktivierung nicht lesbar:`, err.message)
|
||||
throw httpError(503, 'Das Wartungs-Abo des Kunden konnte nicht geprüft werden. Bitte später erneut versuchen.')
|
||||
}
|
||||
if (!laeuftJetzt) throw httpError(409, KEIN_ABO_AKTIVIERUNG)
|
||||
}
|
||||
|
||||
/**
|
||||
* Freischaltung ohne Zahlung und ohne Ticket (unlockedByPurchase bleibt false). Ausnahme: ein Terminplaner, den ein Kauf
|
||||
* freigeschaltet hat (unlockedByPurchase true), wird nur bei laufendem Wartungs-Abo aktiviert (409 sonst, Lesefehler => Fehler).
|
||||
* News und Dokumente ohne Kauf (Pilot, Kulanz) sind nicht betroffen.
|
||||
*/
|
||||
export async function adminActivate(customerId, projectId, key, { by, adminNote } = {}) {
|
||||
assertKey(key)
|
||||
await assertOwnProject(customerId, projectId)
|
||||
const doc = await ensureFeatureDoc(customerId, projectId, key)
|
||||
if (key === 'termine' && doc.unlockedByPurchase === true) await assertAboLaeuftFuerAktivierung(customerId)
|
||||
return activate(doc.$id, { by, adminNote })
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 { activate, customerOf, disable, meldeErstattung, projectName } from '../siteFeatures.js'
|
||||
import { activate, customerOf, disable, listCustomerFeatureDocs, meldeErstattung, projectName } from '../siteFeatures.js'
|
||||
import { loeschzeit } from './frist.js'
|
||||
import { ladeEinstellung } from './kontext.js'
|
||||
import { pruefeNow, zeitMs } from './util.js'
|
||||
@@ -33,6 +33,11 @@ import { datumKurz } from './zeit.js'
|
||||
* 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.
|
||||
*
|
||||
* Sofortpfad (E9): endeTermineFuerKunde(customerId) beendet die Terminplaner eines Kunden sofort, wenn dessen Wechsel ins Wartungs-
|
||||
* Abo in der Frist widerrufen wird (routes/abo.js, withdrawOrder). Dieselben Aktionen wie Schritt 1 und 1b, aber OHNE Abo-Pruefung
|
||||
* (der Spiegel steht noch auf Wartung, das Team setzt ihn von Hand zurueck); die Meldung (meldeAboEnde) ist dieselbe wie im
|
||||
* Tagesabgleich. Danach laesst der Tagesabgleich diese Dokumente in Ruhe (aboLage kennt die Wechsel-Regel: kein Flattern).
|
||||
*
|
||||
* Regeln (Block E):
|
||||
* - Eine Listenabfrage fuer alle Kunden (Entwuerfe `off` bleiben ungelesen), ein aboMemo() je Lauf: die Abo-Lage jedes Kunden wird
|
||||
* fuer die Vorauswahl nur einmal gelesen. Aktive Dokumente, die nicht vom Kauf freigeschaltet wurden (Pilot, Kulanz:
|
||||
@@ -56,6 +61,9 @@ 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 NOTIZ_WECHSEL = 'Abgeschaltet: Wechsel widerrufen'
|
||||
// Lage fuer den Sofortpfad: das Abo bleibt (oder wird wieder) ein Hosting-Abo, K22 nimmt den Block hosting
|
||||
const LAGE_WECHSEL_WIDERRUFEN = { laeuft: false, art: 'wechsel_widerrufen', endedAt: null }
|
||||
|
||||
const ADRESSE = /[^\s<>"',;()]+@[^\s<>"',;()]+/g
|
||||
|
||||
@@ -226,6 +234,58 @@ async function holeErstattungNach(doc, ctx) {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Sofortpfad, ein Dokument: unter der Sperre feature-admin:<projectId> frisch lesen (das Team kann inzwischen aktiviert oder
|
||||
* abgeschaltet, der Webhook die Zahlung gebucht haben) und nach dem FRISCHEN Zustand abschalten: aktiv und vom Kauf freigeschaltet =>
|
||||
* 'abo_ende', ordered/setup => 'abo_ende_vor_einrichtung' (aktionFuer, wie im Tagesabgleich). Das Abo wird nicht gelesen.
|
||||
* @returns {Promise<object|null>} das abgeschaltete Dokument (mit Kauf- und Zahlungsfeldern fuer meldeAboEnde) oder null, wenn nichts zu tun war
|
||||
*/
|
||||
async function beendeSofort(doc) {
|
||||
return withLocks([`feature-admin:${doc.projectId}`], async () => {
|
||||
const frisch = await getDocument(portalFeatures(), doc.$id)
|
||||
const aktion = aktionFuer(frisch)
|
||||
const grund = aktion === 'abschalten' ? GRUND : aktion === 'einrichtung' ? GRUND_VOR : null
|
||||
if (!grund) return null
|
||||
const abgeschaltet = await disable(frisch.$id, { reason: grund, adminNote: NOTIZ_WECHSEL })
|
||||
return abgeschaltet.state === 'disabled' && abgeschaltet.disabledReason === grund ? abgeschaltet : null // sonst war jemand schneller
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sofortpfad beim Widerruf des Wechsels ins Wartungs-Abo (E9): alle Terminplaner des Kunden, die der Wechsel getragen hat, werden
|
||||
* sofort abgeschaltet, ohne Abo-Pruefung (der Spiegel ist veraltet). Aktiv und vom Kauf freigeschaltet => 'abo_ende'; ordered/setup =>
|
||||
* 'abo_ende_vor_einrichtung'. Aktive Dokumente ohne Kauf (Pilot, Kulanz), News, 'off' und schon Abgeschaltete bleiben unberuehrt.
|
||||
* Danach je Dokument NACH der Sperre meldeAboEnde: K22 (Block hosting bzw. vor_einrichtung); eine bezahlte Einrichtung meldet zuerst
|
||||
* T19 (genau einmal), ein unbezahltes ordered bekommt weder T19 noch K22 noch Marker. Scheitert eine Meldung, holt der Tagesabgleich
|
||||
* sie nach. Ein Fehler bei einem Dokument (Lesen, Abschalten) stoppt die anderen nicht; Fehler stehen im Log (ohne Namen und Adressen).
|
||||
* Wirft nie, wie endAnalyseForCustomer.
|
||||
* @param {string} customerId
|
||||
* @param {{ now?: Date }} [options] Zeitpunkt des Widerrufs (Marker aboEndeMailAt)
|
||||
* @returns {Promise<{ beendet: string[] }>} die IDs der abgeschalteten Funktionsdokumente
|
||||
*/
|
||||
export async function endeTermineFuerKunde(customerId, { now = new Date() } = {}) {
|
||||
const beendet = []
|
||||
if (!customerId) return { beendet }
|
||||
try {
|
||||
const kandidaten = (await listCustomerFeatureDocs(customerId)).filter(
|
||||
(doc) => doc.featureKey === 'termine' && doc.projectId && ['abschalten', 'einrichtung'].includes(aktionFuer(doc))
|
||||
)
|
||||
for (const doc of kandidaten) {
|
||||
try {
|
||||
const abgeschaltet = await beendeSofort(doc)
|
||||
if (!abgeschaltet) continue
|
||||
beendet.push(abgeschaltet.$id)
|
||||
await meldeAboEnde(abgeschaltet, { lage: LAGE_WECHSEL_WIDERRUFEN, now }) // nach der Sperre; bei Misserfolg wiederholt der Tagesabgleich
|
||||
} catch (err) {
|
||||
console.error(`[termine-abo] Sofortpfad: Funktion ${doc.$id} nicht vollständig beendet, der Tagesabgleich holt nach: ${fehlerText(err)}`)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[termine-abo] Sofortpfad: Funktionen eines Kunden nicht lesbar, nichts beendet: ${fehlerText(err)}`)
|
||||
}
|
||||
return { beendet }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
Reference in new issue
Block a user