Kundenansicht P6: alte Abgleiche lassen Abo-Modell-Dokumente dem neuen Abgleich, Zeiten ohne Behalten
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d8c952ac2a
commit
7a03f073bc
5 files changed
+125
-11
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { config } from '../../config.js'
|
||||
import { aboLage, aboMemo } from '../aboStatus.js'
|
||||
import { istAboModell } from '../aboModell.js'
|
||||
import { Query, getDocument, listAllDocuments, updateDocument } from '../appwriteAdmin.js'
|
||||
import { withLocks } from '../keyedLock.js'
|
||||
import { sendCustomerMail } from '../mailer.js'
|
||||
@@ -33,6 +34,10 @@ 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.
|
||||
*
|
||||
* Abo-Modell der Kundenansicht neu (P6): Dokumente mit Marker (istAboModell: unlockedBy UND einrichtungAbrechnung aus abo oder projekt) gehoeren
|
||||
* dem Abgleich der Funktionen des Abo-Modells (aboFunktionenAbgleich.js). Dieser Abgleich und der Sofortpfad ueberspringen sie in jedem Zustand;
|
||||
* sie zaehlen weder in `geprueft` noch in `ausgenommen` und kosten keine Abo-Abfrage.
|
||||
*
|
||||
* 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
|
||||
@@ -274,7 +279,7 @@ export async function endeTermineFuerKunde(customerId, { now = new Date() } = {}
|
||||
if (!customerId) return { beendet }
|
||||
try {
|
||||
const kandidaten = (await listCustomerFeatureDocs(customerId)).filter(
|
||||
(doc) => doc.featureKey === 'termine' && doc.projectId && ['abschalten', 'einrichtung'].includes(aktionFuer(doc))
|
||||
(doc) => doc.featureKey === 'termine' && doc.projectId && !istAboModell(doc) && ['abschalten', 'einrichtung'].includes(aktionFuer(doc))
|
||||
)
|
||||
for (const doc of kandidaten) {
|
||||
try {
|
||||
@@ -407,7 +412,7 @@ export async function gleicheAboAb(now) {
|
||||
return erg
|
||||
}
|
||||
const nachKunde = new Map()
|
||||
for (const doc of dokumente) {
|
||||
for (const doc of dokumente.filter((d) => !istAboModell(d))) {
|
||||
if (!doc.customerId || !doc.projectId) {
|
||||
// ohne Kunde gaelte "kein Abo": nie abschalten, was sich nicht zuordnen laesst
|
||||
erg.fehler += 1
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { config } from '../../config.js'
|
||||
import { ohneAboBisher } from '../aboAufbauphase.js'
|
||||
import { ABRECHNUNGEN, istAboModell } from '../aboModell.js'
|
||||
import { aboLage, aboMemo } from '../aboStatus.js'
|
||||
import { getDocument, listAllDocuments, Query, updateDocument } from '../appwriteAdmin.js'
|
||||
import { withLocks } from '../keyedLock.js'
|
||||
@@ -17,8 +19,12 @@ import { ladeZeiten, loescheZeiten } from './speicher.js'
|
||||
* ist sie abgeschaltet und die Eingaben bleiben noch 30 Tage. gleicheZeitenAboAb(now) ist der Baustein des Tagesjobs (angeschlossen in
|
||||
* Task 22); er kennt weder Schalter noch Überlappungssperre: der Aufrufer lässt nie zwei Läufe gleichzeitig laufen.
|
||||
*
|
||||
* Nur Dokumente mit unlockedBy 'abo' (unlockedByVon) sind ans Abo gekoppelt; 'kauf' und 'team' fasst der Abgleich nie an, und das Abo
|
||||
* wird dann nicht einmal gelesen. Je Dokument gilt (Abo gelesen mit aboLage, grosszügige Regel inklusive Wechsel-Regel):
|
||||
* Nur Dokumente mit unlockedBy 'abo' oder 'projekt' (unlockedByVon) sind ans Abo gekoppelt; 'kauf' und 'team' fasst der Abgleich nie an, und
|
||||
* das Abo wird dann nicht einmal gelesen. 'projekt' (Kundenansicht neu, P6) gilt wie 'abo', ausser in der Aufbauphase (E9): ein projekt-Dokument
|
||||
* im Zustand setup oder active bleibt unberuehrt, solange das Abo nicht laeuft und der Kunde noch nie ein Abo hatte (ohneAboBisher): die
|
||||
* Website ist noch im Bau. Mit dem ersten Abo gilt die Abo-Regel auch fuer projekt. Dokumente des Abo-Modells (istAboModell: Marker
|
||||
* einrichtungAbrechnung) bekommen K40 und K41 statt K29 und K30 (ohne Preis) und kein Behalten (behalten.js); Gratis-Zeiten ohne Marker
|
||||
* behalten K29, K30 und den Behalten-Weg. Je Dokument gilt (Abo gelesen mit aboLage, grosszügige Regel inklusive Wechsel-Regel):
|
||||
*
|
||||
* setup/active, Abo läuft nicht, aboEndeAt leer Schritt 1: aboEndeAt = now, K29 (Variante nach art; Frist = Berliner Tag von
|
||||
* aboEndeAt + 30 Tage)
|
||||
@@ -79,8 +85,10 @@ const portalFeatures = () => config.collections.portalFeatures
|
||||
|
||||
const istLaufend = (doc) => doc.state === 'setup' || doc.state === 'active'
|
||||
const istAbgeschaltet = (doc) => doc.state === 'disabled' && doc.disabledReason === GRUND
|
||||
/** Ans Wartungs-Abo gekoppelt: von ihm freigeschaltet und im laufenden Betrieb oder wegen des Abo-Endes abgeschaltet. */
|
||||
const istGekoppelt = (doc) => unlockedByVon(doc) === 'abo' && (istLaufend(doc) || istAbgeschaltet(doc))
|
||||
/** Ans Wartungs-Abo gekoppelt: von ihm freigeschaltet (abo oder, im Abo-Modell, projekt) und im laufenden Betrieb oder wegen des Abo-Endes abgeschaltet. */
|
||||
const istGekoppelt = (doc) => ABRECHNUNGEN.includes(unlockedByVon(doc)) && (istLaufend(doc) || istAbgeschaltet(doc))
|
||||
/** Aufbauphasen-Kandidat (E9): im Projekt eingebaut und noch im laufenden Betrieb; ob der Kunde je ein Abo hatte, entscheidet ohneAboBisher. */
|
||||
const istProjektImBetrieb = (doc) => unlockedByVon(doc) === 'projekt' && istLaufend(doc)
|
||||
|
||||
/** Berliner Kalendertag eines Datumsfeldes; unlesbar ist ein Fehler (das Dokument wird übersprungen), nie "heute". */
|
||||
function tagVon(wert, feld) {
|
||||
@@ -150,6 +158,8 @@ async function unterSperre(doc, ctx) {
|
||||
const frisch = await getDocument(portalFeatures(), doc.$id)
|
||||
if (!istGekoppelt(frisch)) return null // das Team war schneller (anderer Grund, anderer Zustand, nicht mehr im Abo)
|
||||
const lage = await aboLage(frisch.customerId) // wirft bei Lesefehler: dann bleibt alles, wie es ist
|
||||
// Aufbauphase (E9): ein im Projekt eingebautes Dokument bleibt, solange es nie ein Abo gab (frisch gelesen; ein Lesefehler wirft: nichts geschrieben)
|
||||
if (istProjektImBetrieb(frisch) && !lage.laeuft && (await ohneAboBisher(frisch.customerId))) return null
|
||||
const aktion = entscheide(frisch, lage, now)
|
||||
const hatteMail = Boolean(frisch.aboEndeMailAt) // vor dem Leeren gelesen: wer nie vom Ende erfuhr, hört nicht von der Rückkehr
|
||||
|
||||
@@ -255,7 +265,7 @@ async function sendeWiederMail(doc, einbau, ctx) {
|
||||
async function danach({ aktion, doc, lage, hatteMail }, ctx) {
|
||||
switch (aktion) {
|
||||
case 'k29':
|
||||
await sendeMitMarker(doc, ({ kunde, projekt }) => k29Mail({ kunde, projekt, art: lage.art, frist: mittags(fristTag(doc)), preis: preis() }), ctx)
|
||||
await sendeMitMarker(doc, ({ kunde, projekt }) => k29Mail({ kunde, projekt, art: lage.art, frist: mittags(fristTag(doc)), preis: preis(), modell: istAboModell(doc) }), ctx)
|
||||
return
|
||||
case 'k30':
|
||||
// Erst das Ersatz-Ticket, dann K30 mit Marker. Scheitert das Ticket, gibt es weder K30 noch Marker: der nächste Lauf wiederholt beides
|
||||
@@ -264,7 +274,7 @@ async function danach({ aktion, doc, lage, hatteMail }, ctx) {
|
||||
ctx.erg.fehler += 1
|
||||
return
|
||||
}
|
||||
await sendeMitMarker(doc, ({ kunde, projekt }) => k30Mail({ kunde, projekt, loeschdatum: mittags(loeschTag(doc)), preis: preis() }), ctx, doc.disabledAt)
|
||||
await sendeMitMarker(doc, ({ kunde, projekt }) => k30Mail({ kunde, projekt, loeschdatum: mittags(loeschTag(doc)), preis: preis(), modell: istAboModell(doc) }), ctx, doc.disabledAt)
|
||||
return
|
||||
case 'zurueck': {
|
||||
const einbau = await stosseNeubauAn(await ladeProjekt(doc.projectId))
|
||||
@@ -302,9 +312,21 @@ async function bearbeiteKunde(customerId, dokumente, ctx) {
|
||||
console.error(`[zeiten-abo] Abo-Lage eines Kunden nicht lesbar, ${gekoppelt.length} Funktion(en) übersprungen: ${fehlerText(err)}`)
|
||||
return
|
||||
}
|
||||
erg.geprueft += gekoppelt.length
|
||||
// Aufbauphase (E9): im Projekt eingebaute Dokumente bleiben in Ruhe, solange das Abo nicht laeuft und der Kunde noch nie ein Abo hatte;
|
||||
// gelesen wird das nur, wenn es ein solches Dokument gibt. Ein Lesefehler ueberspringt den Kunden wie beim Lesen der Abo-Lage.
|
||||
let zuBewerten = gekoppelt
|
||||
if (!lage.laeuft && gekoppelt.some(istProjektImBetrieb)) {
|
||||
try {
|
||||
if (await ohneAboBisher(customerId)) zuBewerten = gekoppelt.filter((doc) => !istProjektImBetrieb(doc))
|
||||
} catch (err) {
|
||||
erg.fehler += 1
|
||||
console.error(`[zeiten-abo] Abo-Verlauf eines Kunden nicht lesbar, ${gekoppelt.length} Funktion(en) übersprungen: ${fehlerText(err)}`)
|
||||
return
|
||||
}
|
||||
}
|
||||
erg.geprueft += zuBewerten.length
|
||||
|
||||
for (const doc of gekoppelt) {
|
||||
for (const doc of zuBewerten) {
|
||||
// je Dokument gefangen (auch Entscheidung, Builder und Versand): ein kaputtes Datum stoppt weder den Kunden noch den Lauf
|
||||
try {
|
||||
if (!entscheide(doc, lage, now)) continue // Vorauswahl mit dem Memo: nichts zu tun kostet weder Sperre noch Abo-Abfrage
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { config } from '../../config.js'
|
||||
import { aboLage } from '../aboStatus.js'
|
||||
import { istAboModell } from '../aboModell.js'
|
||||
import { getDocument, updateDocument } from '../appwriteAdmin.js'
|
||||
import { featureConfig } from '../featureRegistry.js'
|
||||
import { withLocks } from '../keyedLock.js'
|
||||
@@ -43,9 +44,10 @@ async function datenDa(featureId) {
|
||||
/**
|
||||
* Darf der Kunde die Funktion jetzt behalten? In Einrichtung oder aktiv: sobald das Ende des Abos erkannt ist (aboEndeAt). Abgeschaltet:
|
||||
* nur nach dem Abo-Ende (disabledReason abo_ende, nicht: Team, vor der Einrichtung, Kunde geloescht) und solange die Eingaben noch da sind.
|
||||
* Nie fuer ein Dokument des Abo-Modells (istAboModell).
|
||||
*/
|
||||
async function behaltenMoeglich(doc) {
|
||||
if (!doc) return false
|
||||
if (!doc || istAboModell(doc)) return false // Abo-Modell (P6): kein Behalten gegen Zahlung, die Funktion endet mit dem Abo und kehrt mit einem neuen zurueck
|
||||
if (doc.state === 'setup' || doc.state === 'active') return Boolean(doc.aboEndeAt)
|
||||
if (doc.state === 'disabled' && doc.disabledReason === 'abo_ende') return datenDa(doc.$id)
|
||||
return false
|
||||
|
||||
Reference in new issue
Block a user