Merge remote-tracking branch 'origin/main' into feature/oeffnungszeiten
Zusammenführung von TP2-Abschluss und Webshop (origin/main cab9c33) mit Öffnungszeiten live. Beide
Funktionen bleiben vollständig: jeder Konflikt ist als Vereinigung gelöst, Reihenfolge der Funktionsschlüssel
überall news, termine, shop, zeiten.
Konfliktdateien (28): public/app.js, dashboard.html, funktionen.js, widerrufen.html; scripts/stripe-products.mjs;
server/config.js, index.js, middleware/kundenansichtGuard.js, routes/abo.js, routes/admin/funktionenAdmin.js,
routes/funktionen.js, services/customerAdmin.js, customerViewLog.js, featureRegistry.js, retention.js,
siteFeatures.js, stripeBilling.js; test/customerViewLog, featureFreischaltung, featureRegistry, funktionenRoutes,
funktionenTermine, kundenansichtGuard, mailTemplates, publicForms, siteFeatures, stripeProductsScript (je
.test.js) und test/support/schema.js.
Entscheidungen: Mailvorlagen G01–G15, K01–K30, T01–T21 lückenlos (K24–K26 Webshop, K27–K30 Öffnungszeiten);
Stripe-Katalog mit shopSetup und zeitenSetup; Widerruf kennt shop und zeiten; ABO_FUNKTIONEN bleibt analyse
und termine (kein zeiten); activate() schreibt die Zeiten-Felder nur über extra; kein Halter von
feature-activate:<id> ruft disable() auf; die Aufbewahrungs-Löschung löscht Zeiten-Daten vor dem Abschalten und
hält den Kunden bei einem Fehler (held); die Admin-Antwort trägt ersatzTicket und warnung; Hash-Routen webshop
und zeiten, Ansichten view-webshop und view-zeiten; assertOwnProject behält die Meldung von main.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
e87140e5ba
245 files changed
+38799
-1050
No files matched your search
@@ -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,11 @@ 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.
|
||||
// Nur bei art 'beendet' ist lage.endedAt das Ende des Wartungs-Abos; bei 'hosting' gehoert es zum Hosting-Abo (etwa eine vorgemerkte Kuendigung).
|
||||
const aboEnde = lage?.art === 'beendet' ? zeitMs(lage.endedAt) : NaN
|
||||
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 +150,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`,
|
||||
|
||||
@@ -40,18 +40,23 @@ export async function freischaltHindernis(doc, { abo = aboMemo() } = {}) {
|
||||
* Fehler (Kunde, Register, Versand) werden geloggt und nie geworfen: die Freischaltung ist dann schon geschehen, und eine Mail,
|
||||
* die nicht rausgeht, darf die uebrigen Dokumente des Laufs nicht aufhalten. Kunde ohne Adresse: keine Mail, kein Fehler.
|
||||
* attachments: Anhaenge der Mail, z. B. die PDFs der Staff-Aktivierung (F13a).
|
||||
* Liefert true, wenn K16 rausging; false, wenn nicht (Fehler, Kunde ohne Adresse oder unbekannt). Nur die Staff-Aktivierung mit
|
||||
* Unterlagen wertet das aus (sie muss die Unterlagen dann von Hand nachsenden); alle anderen Aufrufer ignorieren den Wert wie bisher.
|
||||
* @param {{ customerId: string, projectId: string, featureKey: string }} doc portalFeatures-Dokument
|
||||
* @param {{ attachments?: object[] }} [optionen]
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
export async function meldeFreigeschaltet(doc, { attachments = [] } = {}) {
|
||||
try {
|
||||
const customer = await customerOf(doc.customerId)
|
||||
if (!customer?.email) return
|
||||
if (!customer?.email) return false
|
||||
const feature = featureConfig(doc.featureKey)
|
||||
if (!feature) throw new Error(`Funktion ${doc.featureKey} nicht im Register`)
|
||||
const mail = bereitMail(feature, { kundeName: customer.name || '', projekt: await projectName(doc.projectId) })
|
||||
await sendCustomerMail(customer.email, { ...mail, attachments })
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error(`[funktionen] K16 an Kunde ${doc.customerId} fehlgeschlagen:`, err.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
@@ -23,6 +23,7 @@ import { LOESCHFRIST_TAGE } from './frist.js'
|
||||
import { kontextPerFeature, ladeEinstellung } from './kontext.js'
|
||||
import { anGast } from './mails.js'
|
||||
import { beendeFestgefahrene, raeumeAuf, verarbeite } from './outbox.js'
|
||||
import { shopModus } from '../shop/schalter.js'
|
||||
import { termineAus } from './schalter.js'
|
||||
import { buchungsNummer } from './token.js'
|
||||
import { pruefeNow, zeitMs } from './util.js'
|
||||
@@ -57,6 +58,12 @@ import { gibZellenFrei, raeumeVerwaisteZellen } from './zellen.js'
|
||||
* (`eingeschraenkt: true`): nur der Abo-Abgleich und die Loeschung abgeschalteter Funktionen, damit ein beendetes Abo auch bei
|
||||
* TERMINE_ENABLED=off zum Abschalten und zur Loeschung nach 30 Tagen fuehrt. Loeschfristen der Buchungen, Warteschlange und Zellen
|
||||
* bleiben aus.
|
||||
* - Webshop (SHOP_ENABLED != off): die Mail-Warteschlange gehoert auch den Kaeufermails des Shops (G09-G14, shop/mails.js). Deshalb
|
||||
* laufen bei Modus off des Terminplaners, aber aktivem Shop, der Mailversand-Teil des Minutenjobs (Warteschlange senden und
|
||||
* Betriebspruefung) und im Tagesjob das Beenden festgefahrener und das Loeschen alter Warteschlangen-Eintraege weiter; alles
|
||||
* andere bleibt an den Terminplaner-Modus gebunden. Der Tagesjob ist dann eingeschraenkt (`eingeschraenkt: true`, mit
|
||||
* `festgefahren` und `aufgeraeumt` im Ergebnis); gibt es keine bezahlten Terminplaner, laufen Abo-Abgleich und Loeschung
|
||||
* abgeschalteter Funktionen nicht.
|
||||
* - Keine ueberlappenden Laeufe: laeuft ein Job noch, wird der naechste Takt uebersprungen ({ uebersprungen: true }, einmal im
|
||||
* Log je Ueberlappung). Die Sperre gilt je `job:teil` (fuehreAus mit { teil }): haengt beim Minutenjob der Verfall, versendet der
|
||||
* naechste Takt trotzdem die Warteschlange und umgekehrt; uebersprungen gilt der Takt erst, wenn beide Teile uebersprungen sind
|
||||
@@ -127,18 +134,21 @@ async function schritt(lauf, was, fn, ersatz) {
|
||||
* Rahmen jedes Jobs: Schalter, Ueberlappung, Laufdaten. => Ergebnis von arbeit(lauf) | { aus: true } | { uebersprungen: true }
|
||||
* `teil` (Name eines Teiljobs, z. B. „Verfall“) gibt dem Teil eine eigene Ueberlappungssperre `job:teil` und nennt ihn in der
|
||||
* Warnung (`job (teil)`); `lauf.job` bleibt der Jobname (Fehlerlog, Warnung bei fehlender Collection).
|
||||
* `trotzAus` (async, => wahr/falsch): im Modus off laeuft der Job nur, wenn es wahr liefert; dann steht `lauf.eingeschraenkt` auf true
|
||||
* und der Job beschraenkt sich auf das, was auch bei abgeschaltetem Terminplaner gelten muss. Wirft es, wird gewarnt (ohne Kennungen)
|
||||
* `trotzAus` (async, => falsch/wahr/Objekt): im Modus off laeuft der Job nur, wenn es etwas Wahres liefert; dann steht
|
||||
* `lauf.eingeschraenkt` auf true, `lauf.freigabe` traegt die Antwort (Tagesjob: { termine, mails }) und der Job beschraenkt sich auf
|
||||
* das, was auch bei abgeschaltetem Terminplaner gelten muss. Wirft es, wird gewarnt (ohne Kennungen)
|
||||
* und { aus: true } geliefert; ohne `trotzAus` gilt Modus off wie bisher (sofort { aus: true }, ohne Abfrage). Die Ueberlappungssperre
|
||||
* gilt unveraendert je Job (bzw. `job:teil`), der eingeschraenkte Lauf teilt sie mit dem vollen.
|
||||
*/
|
||||
async function fuehreAus(job, now, arbeit, { teil = null, trotzAus = null } = {}) {
|
||||
const nowMs = pruefeNow(now, job)
|
||||
let eingeschraenkt = false
|
||||
let freigabe = null
|
||||
if (termineAus()) {
|
||||
if (trotzAus === null) return { aus: true }
|
||||
try {
|
||||
if (!(await trotzAus())) return { aus: true }
|
||||
freigabe = await trotzAus()
|
||||
if (!freigabe) return { aus: true }
|
||||
} catch (err) {
|
||||
console.warn(`[termine-jobs] ${job}: Modus off, die Prüfung auf bezahlte Terminplaner ist fehlgeschlagen, der Job bleibt aus: ${fehlerText(err)}`)
|
||||
return { aus: true }
|
||||
@@ -155,13 +165,16 @@ async function fuehreAus(job, now, arbeit, { teil = null, trotzAus = null } = {}
|
||||
}
|
||||
laufend.add(sperre)
|
||||
try {
|
||||
return await arbeit({ job, nowMs, fehler: 0, gedeckelt: 0, eingeschraenkt })
|
||||
return await arbeit({ job, nowMs, fehler: 0, gedeckelt: 0, eingeschraenkt, freigabe })
|
||||
} finally {
|
||||
laufend.delete(sperre)
|
||||
ueberlappGemeldet.delete(sperre)
|
||||
}
|
||||
}
|
||||
|
||||
/** Guard des Mailversand-Teils (fuehreAus: trotzAus): bei Modus off des Terminplaners laeuft er, solange der Webshop aktiv ist. */
|
||||
const shopMailsLaufen = async () => shopModus() !== 'off'
|
||||
|
||||
/** Laufdaten fuer einen Baustein, der ohne Job-Rahmen aufgerufen wird (Tests). */
|
||||
const einzelLauf = (job, now) => ({ job, nowMs: pruefeNow(now, job), fehler: 0, gedeckelt: 0 })
|
||||
|
||||
@@ -285,10 +298,15 @@ export async function minutenJob(now) {
|
||||
await schritt(lauf, 'Betriebsprüfung', () => pruefeBetrieb(now), [])
|
||||
return { faellig, gesendet, fehler: lauf.fehler }
|
||||
},
|
||||
{ teil: 'Mailversand' },
|
||||
{ teil: 'Mailversand', trotzAus: shopMailsLaufen },
|
||||
),
|
||||
])
|
||||
if (verfall.aus || mail.aus) return { aus: true }
|
||||
if (verfall.aus && mail.aus) return { aus: true }
|
||||
// Terminplaner aus, Webshop aktiv: nur der Mailversand-Teil lief
|
||||
if (verfall.aus) {
|
||||
if (mail.uebersprungen) return { uebersprungen: true }
|
||||
return { eingeschraenkt: true, verfallen: 0, faellig: mail.faellig ?? 0, gesendet: mail.gesendet ?? 0, fehler: mail.fehler ?? 0 }
|
||||
}
|
||||
if (verfall.uebersprungen && mail.uebersprungen) return { uebersprungen: true }
|
||||
return {
|
||||
verfallen: verfall.verfallen ?? 0,
|
||||
@@ -603,7 +621,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 +638,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
|
||||
@@ -788,6 +810,23 @@ async function gibtBezahlteTerminplaner() {
|
||||
return treffer.length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Freigabe des Tagesjobs im Modus off (fuehreAus: trotzAus): false (aus) oder { termine, mails }. termine = es gibt ein bezahltes
|
||||
* Termine-Dokument (Abo-Abgleich und Loeschung abgeschalteter Funktionen), mails = der Webshop ist aktiv (Warteschlange aufraeumen).
|
||||
* Eine einzige Abfrage; scheitert sie bei aktivem Shop, laeuft nur der Mail-Teil (Warnung), sonst wirft sie wie bisher.
|
||||
*/
|
||||
async function tagesjobFreigabe() {
|
||||
const mails = shopModus() !== 'off'
|
||||
let termine = false
|
||||
try {
|
||||
termine = await gibtBezahlteTerminplaner()
|
||||
} catch (err) {
|
||||
if (!mails) throw err
|
||||
console.warn(`[termine-jobs] taeglicherJob: die Prüfung auf bezahlte Terminplaner ist fehlgeschlagen, es läuft nur die Mail-Warteschlange: ${fehlerText(err)}`)
|
||||
}
|
||||
return termine || mails ? { termine, mails } : false
|
||||
}
|
||||
|
||||
/**
|
||||
* Abo-Abgleich (aboAbgleich.js) als Schritt des Tagesjobs: ein Fehler stoppt weder die Loeschung noch den Rest. Die Fehler des
|
||||
* Abgleichs (Lese- und Versandfehler, schon im Log) kommen in lauf.fehler. => Zaehler des Abgleichs
|
||||
@@ -860,7 +899,7 @@ export async function taeglicherJob(now) {
|
||||
await schritt(lauf, 'Tagesjob-Meldung', () => meldeTagesjob(ergebnis, now), [])
|
||||
return ergebnis
|
||||
},
|
||||
{ trotzAus: gibtBezahlteTerminplaner },
|
||||
{ trotzAus: tagesjobFreigabe },
|
||||
)
|
||||
} catch (err) {
|
||||
// Abbruch ausserhalb der Schritte: das Team erfaehrt es; der Fehler (Text im Log des Aufrufers) geht unveraendert weiter
|
||||
@@ -876,19 +915,28 @@ export async function taeglicherJob(now) {
|
||||
* und wird wie im vollen Lauf gemeldet (Fehler gehen als Vorfall ans Team).
|
||||
*/
|
||||
async function eingeschraenkterTagesjob(lauf, now) {
|
||||
const aboAbgleich = await aboAbgleichSchritt(lauf, now)
|
||||
const { gedeckelt: abgeschaltetGedeckelt, ...abgeschaltet } = await schritt(lauf, 'Abgeschaltete Funktionen', () => loescheAbgeschaltete(lauf), {
|
||||
funktionen: 0,
|
||||
buchungen: 0,
|
||||
gedeckelt: 0,
|
||||
})
|
||||
// Ohne Freigabe-Angabe (Aufruf ohne tagesjobFreigabe) gilt das bisherige Verhalten: nur Terminplaner-Teile
|
||||
const termine = lauf.freigabe?.termine !== false
|
||||
const mails = lauf.freigabe?.mails === true
|
||||
const aboAbgleich = termine ? await aboAbgleichSchritt(lauf, now) : aboErsatz()
|
||||
const { gedeckelt: abgeschaltetGedeckelt, ...abgeschaltet } = termine
|
||||
? await schritt(lauf, 'Abgeschaltete Funktionen', () => loescheAbgeschaltete(lauf), { funktionen: 0, buchungen: 0, gedeckelt: 0 })
|
||||
: { funktionen: 0, buchungen: 0, gedeckelt: 0 }
|
||||
// Webshop aktiv: festgefahrene Mails beenden (sie bleiben 48 Stunden als Zustellfehler sichtbar), dann alte Eintraege loeschen
|
||||
const mailErgebnis = mails
|
||||
? {
|
||||
festgefahren: await schritt(lauf, 'Festgefahrene Mails', () => beendeFestgefahrene(now), 0),
|
||||
aufgeraeumt: await schritt(lauf, 'Warteschlange aufräumen', () => raeumeAuf(now), 0),
|
||||
}
|
||||
: {}
|
||||
const ergebnis = {
|
||||
eingeschraenkt: true,
|
||||
aboAbgleich,
|
||||
abgeschaltet,
|
||||
gedeckelt: { loeschfristen: 0, abgeschaltet: abgeschaltetGedeckelt, zellen: 0 },
|
||||
gedeckelt: { loeschfristen: 0, abgeschaltet: abgeschaltetGedeckelt ?? 0, zellen: 0 },
|
||||
geloescht: abgeschaltet.buchungen,
|
||||
aktiveBetriebe: null,
|
||||
...mailErgebnis,
|
||||
fehler: lauf.fehler,
|
||||
}
|
||||
await schritt(lauf, 'Tagesjob-Meldung', () => meldeTagesjob(ergebnis, now), [])
|
||||
@@ -922,9 +970,12 @@ export function tagesbericht(ergebnis) {
|
||||
? ''
|
||||
: `Abo-Abgleich ${zahl(abo.geprueft)} geprüft / ${zahl(abo.abgeschaltet)} abgeschaltet / ${zahl(abo.wiederEin)} wieder ein / ${zahl(abo.mails)} Mails / ${zahl(abo.ausgenommen)} ausgenommen, `
|
||||
if (ergebnis.eingeschraenkt) {
|
||||
// Webshop aktiv: dazu die Warteschlange (festgefahrene Mails beenden, alte Eintraege loeschen)
|
||||
const mitMails = Number.isInteger(ergebnis.aufgeraeumt)
|
||||
const mailSegment = mitMails ? `${ergebnis.aufgeraeumt} Outbox, ${zahl(ergebnis.festgefahren)} Mails nicht zugestellt, ` : ''
|
||||
return (
|
||||
`[termine] Tagesjob (Modus off, nur Abo-Abgleich und Löschung): ${a.buchungen} Buchungen gelöscht (abgeschaltete Funktionen ${a.buchungen}), ` +
|
||||
`${a.funktionen} Funktionen bereinigt, ${deckel}${aboSegment}${ergebnis.fehler} Fehler`
|
||||
`[termine] Tagesjob (Modus off, nur Abo-Abgleich und Löschung${mitMails ? ' sowie Mail-Warteschlange' : ''}): ${a.buchungen} Buchungen gelöscht (abgeschaltete Funktionen ${a.buchungen}), ` +
|
||||
`${a.funktionen} Funktionen bereinigt, ${mailSegment}${deckel}${aboSegment}${ergebnis.fehler} Fehler`
|
||||
)
|
||||
}
|
||||
const l = ergebnis.loeschfristen
|
||||
|
||||
@@ -214,7 +214,7 @@ export async function kontextFuerPortal(feature) {
|
||||
return baueKontext(feature)
|
||||
}
|
||||
|
||||
const veraltet = () =>
|
||||
export const veraltet = () =>
|
||||
Object.assign(new Error('Die Einstellungen wurden inzwischen geändert. Bitte neu laden.'), {
|
||||
status: 409,
|
||||
code: 'EINSTELLUNG_VERALTET',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { config } from '../../config.js'
|
||||
import { Query, countDocuments, createDocument, deleteDocument, getDocument, listAllDocuments, listDocuments, updateDocument } from '../appwriteAdmin.js'
|
||||
import { Query, countDocuments, createDocument, deleteDocument, getDocument, keinerVon, listAllDocuments, listDocuments, updateDocument } from '../appwriteAdmin.js'
|
||||
import { withLocks } from '../keyedLock.js'
|
||||
import { sendGuestMail } from '../mailer.js'
|
||||
import { renderMail } from '../mailTemplates.js'
|
||||
@@ -39,7 +39,7 @@ import { pruefeNow, pruefeText, zeitMs } from './util.js'
|
||||
* pause Anmeldepause laeuft
|
||||
* budget Plaetze im Fenster >= mailsProStunde
|
||||
* reserve nachrangige Mails (G05, K20, K21) im Fenster >= max(1, floor(mailsProStunde x (1 - mailsReserve))): so bleibt
|
||||
* ein Teil der Stunde den Vorrang-Mails (G01, G02, G08) und den normalen Mails vorbehalten
|
||||
* ein Teil der Stunde den Vorrang-Mails (G01, G02, G08, Webshop G09, G12, G15) und den normalen Mails vorbehalten
|
||||
* kontingent Mails dieser Funktion im Fenster >= mailsProFunktion: eine Funktion kann die Stunde nicht allein fuellen
|
||||
*
|
||||
* Minutenjob (verarbeite): liest nie alle wartenden Eintraege. Bei Pause oder leerem Gesamtbudget endet er ohne Abfrage.
|
||||
@@ -71,7 +71,11 @@ const ID_BLOCK = 50
|
||||
|
||||
const iso = (wertMs) => new Date(wertMs).toISOString()
|
||||
|
||||
/** Mails, die vor allen anderen rausgehen duerfen (Bestaetigung, Termin, Eingangsbestaetigung des Widerrufs). */
|
||||
/**
|
||||
* Mails, die vor allen anderen rausgehen duerfen (Bestaetigung, Termin, Eingangsbestaetigung des Widerrufs). Die Shop-Mails
|
||||
* G09 bis G15 sind bewusst 'normal' (jede Minute bedient): jede Art in VORRANG/NACHRANGIG kostet in der Abfrage der Klasse
|
||||
* 'normal' ein notEqual mehr, und die URL stoesst live bei nginx an (414, Messung G5 im TP2-Livegang).
|
||||
*/
|
||||
export const MAIL_VORRANG = Object.freeze(['G01', 'G02', 'G08'])
|
||||
/** Mails, die zuerst warten (Erinnerung, Hinweise an den Betrieb): sie nehmen nie die ganze Stunde. */
|
||||
export const MAIL_NACHRANGIG = Object.freeze(['G05', 'K20', 'K21'])
|
||||
@@ -115,7 +119,7 @@ function gesamtBremse(nowMs) {
|
||||
* 'pause' | 'budget' | 'reserve' | 'kontingent'. Nur lesen: belegt keinen Platz.
|
||||
* @param {number} nowMs
|
||||
* @param {string} featureId
|
||||
* @param {string} art Vorlagen-ID der Mail ('G01' ... 'K21')
|
||||
* @param {string} art Vorlagen-ID der Mail ('G01' ... 'K21', Shop: 'G09' ... 'G15')
|
||||
* @returns {null|'pause'|'budget'|'reserve'|'kontingent'}
|
||||
*/
|
||||
export function pruefePlatz(nowMs, featureId, art) {
|
||||
@@ -372,16 +376,24 @@ async function speichereFehlschlag(doc, err, nowMs) {
|
||||
const KLASSEN = ['vorrang', 'normal', 'nachrangig']
|
||||
/** Hoechstens so viele Abfragen je Klasse und Lauf: die erste und bis zu vier weitere Runden mit neuer Ausschlussliste. */
|
||||
const RUNDEN_MAX = 5
|
||||
/** Hoechstens so viele Funktionen in der Ausschlussliste einer Abfrage (Appwrite erlaubt hoechstens 100 Werte je Abfrage). */
|
||||
const AUSSCHLUSS_MAX = 90
|
||||
/**
|
||||
* Hoechstens so viele Funktionen in der Ausschlussliste einer Abfrage. Jede ist ein eigenes notEqual in der URL (Appwrite 1.8.1
|
||||
* nimmt je notEqual nur einen Wert, M1). Gemessen am Produktionsserver (M4, 2026-10-05): in dieser Abfrageform antwortet der nginx
|
||||
* ab 46 Funktions-IDs mit 36 Zeichen mit 414 (URL zu lang); 30 lassen Luft fuer laengere IDs. Eine blockierte Funktion ausserhalb
|
||||
* der Liste wird gelesen und uebersprungen (verarbeiteKlasse), das kostet nur Platz in der Liste.
|
||||
*/
|
||||
export const AUSSCHLUSS_MAX = 30
|
||||
/** Die Felder der Liste: genug fuer pruefePlatz und sendeEintrag, nie ein Mailtext. */
|
||||
const LISTENFELDER = ['$id', 'featureId', 'art', 'naechsterVersuch']
|
||||
|
||||
/** Filter auf die Art der Mail: die Arten der Klasse (equal) bzw. alle anderen (notEqual, die Klasse normal). */
|
||||
/**
|
||||
* Filter auf die Art der Mail, immer eine Liste von Abfragen: die Arten der Klasse (equal mit Liste, das erlaubt Appwrite) bzw.
|
||||
* alle anderen (die Klasse normal: je Art ein notEqual, Appwrite schneidet sie; art ist an jedem Eintrag gesetzt, einreihen prueft es).
|
||||
*/
|
||||
function klassenFilter(klasse) {
|
||||
if (klasse === 'vorrang') return Query.equal('art', [...MAIL_VORRANG])
|
||||
if (klasse === 'nachrangig') return Query.equal('art', [...MAIL_NACHRANGIG])
|
||||
return Query.notEqual('art', [...MAIL_VORRANG, ...MAIL_NACHRANGIG])
|
||||
if (klasse === 'vorrang') return [Query.equal('art', [...MAIL_VORRANG])]
|
||||
if (klasse === 'nachrangig') return [Query.equal('art', [...MAIL_NACHRANGIG])]
|
||||
return keinerVon('art', [...MAIL_VORRANG, ...MAIL_NACHRANGIG])
|
||||
}
|
||||
|
||||
/** Eine Runde der Klasse: faellige wartende Eintraege, fuer die Funktionen ohne Kontingent nicht mitgelesen werden. */
|
||||
@@ -389,8 +401,8 @@ function listenAbfrage(klasse, nowMs, blockiert) {
|
||||
return [
|
||||
Query.equal('status', 'wartet'),
|
||||
Query.lessThanEqual('naechsterVersuch', iso(nowMs)),
|
||||
klassenFilter(klasse),
|
||||
...(blockiert.size > 0 ? [Query.notEqual('featureId', [...blockiert].slice(0, AUSSCHLUSS_MAX))] : []),
|
||||
...klassenFilter(klasse),
|
||||
...keinerVon('featureId', [...blockiert].slice(0, AUSSCHLUSS_MAX)),
|
||||
Query.orderAsc('naechsterVersuch'),
|
||||
Query.limit(config.termine.mailListe),
|
||||
Query.select(LISTENFELDER),
|
||||
@@ -438,7 +450,7 @@ async function verarbeiteKlasse(klasse, nowMs, now, lauf) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Minutenjob: sendet faellige wartende Eintraege, Vorrang-Mails (G01, G02, G08) vor normalen vor nachrangigen (G05, K20, K21),
|
||||
* Minutenjob: sendet faellige wartende Eintraege, Vorrang-Mails (G01, G02, G08, G09, G12, G15) vor normalen vor nachrangigen (G05, K20, K21),
|
||||
* in jeder Klasse die faelligsten zuerst, bis Pause oder Stundenbudget bremsen. Er liest begrenzte Listen statt aller Eintraege
|
||||
* (siehe Kopfkommentar); Eintraege, die Vorrang-Reserve oder Kontingent ihrer Funktion bremsen, bleiben `wartet`, und die uebrigen
|
||||
* kommen trotzdem dran. Fehler bei einzelnen Eintraegen (z. B. Appwrite) stehen im Log und stoppen den Lauf nicht.
|
||||
|
||||
@@ -386,7 +386,7 @@ export const ZUSTIMMUNG_VORZEITIG = (name) =>
|
||||
+ 'Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung der Leistung erlischt (§ 356 Abs. 5 Nr. 2 BGB). '
|
||||
+ 'Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).'
|
||||
|
||||
/** Pflicht-Haken, wenn der Betrieb Stornobedingungen hat. */
|
||||
/** Pflicht-Haken, wenn der Betrieb Stornobedingungen hat und die gebuchte Leistung einen Preis hat (bei kostenlosen Leistungen gibt es ihn nicht). */
|
||||
export const ZUSTIMMUNG_STORNO = 'Ich habe die Stornobedingungen gelesen.'
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,3 +28,15 @@ export function termineFuerKunde(email) {
|
||||
const adresse = String(email || '').trim().toLowerCase()
|
||||
return adresse !== '' && erlaubt.includes(adresse)
|
||||
}
|
||||
|
||||
/**
|
||||
* Verlangt die Aktivierung durch das Team (WOMS, ohne Bestellung) die Zustimmung des Kunden in Textform? Nur im Modus on und nur,
|
||||
* wenn die Adresse des Kunden nicht in TERMINE_PILOT_EMAILS steht. '*' ist keine Pilotadresse und befreit niemanden. In off und
|
||||
* pilot bleibt die Aktivierung wie in TP1 (Pilotbetrieb, Mitarbeiter und Kunde sprechen direkt miteinander).
|
||||
*/
|
||||
export function termineTextformNoetig(email) {
|
||||
if (termineModus() !== 'on') return false
|
||||
const adresse = String(email || '').trim().toLowerCase()
|
||||
if (adresse === '' || adresse === '*') return true
|
||||
return !config.termine.pilotEmails.includes(adresse)
|
||||
}
|
||||
Reference in new issue
Block a user