Webshop E4: Mails (G09-G14 an Käufer, K25/K26 an Händler), shop/mails.js, Mailversand der Warteschlange auch bei Terminplaner off
Käufermails laufen über die Warteschlange des Terminplaners (G09/G12 mit Vorrang), G09 wird bei Überlänge um den AGB-Text gekürzt (Link statt Text). Der Mailteil des Minutenjobs und die Aufräumschritte des Tagesjobs laufen auch bei TERMINE_ENABLED=off, solange SHOP_ENABLED nicht off ist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
38bd56fe8b
commit
0ebd2e6663
15 files changed
+1570
-26
No files matched your search
@@ -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,
|
||||
@@ -792,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
|
||||
@@ -864,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
|
||||
@@ -880,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), [])
|
||||
@@ -926,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
|
||||
|
||||
@@ -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) 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,8 +71,12 @@ const ID_BLOCK = 50
|
||||
|
||||
const iso = (wertMs) => new Date(wertMs).toISOString()
|
||||
|
||||
/** Mails, die vor allen anderen rausgehen duerfen (Bestaetigung, Termin, Eingangsbestaetigung des Widerrufs). */
|
||||
export const MAIL_VORRANG = Object.freeze(['G01', 'G02', 'G08'])
|
||||
/**
|
||||
* Mails, die vor allen anderen rausgehen duerfen (Bestaetigung, Termin, Eingangsbestaetigung des Widerrufs). Dazu die beiden
|
||||
* Pflichtmails des Webshops: G09 Bestellbestaetigung (dauerhafter Datentraeger) und G12 Eingangsbestaetigung des Widerrufs. Die
|
||||
* uebrigen Shop-Mails G10, G11, G13, G14 sind 'normal'.
|
||||
*/
|
||||
export const MAIL_VORRANG = Object.freeze(['G01', 'G02', 'G08', 'G09', 'G12'])
|
||||
/** 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' ... 'G14')
|
||||
* @returns {null|'pause'|'budget'|'reserve'|'kontingent'}
|
||||
*/
|
||||
export function pruefePlatz(nowMs, featureId, art) {
|
||||
@@ -438,7 +442,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) 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.
|
||||
|
||||
Reference in new issue
Block a user