Kundenansicht P6: K38 an den Kunden und T24 ans Team beim Einschalten
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cdc8f5c424
commit
408f7d0cc2
4 files changed
+244
-7
No files matched your search
@@ -0,0 +1,108 @@
|
||||
import { aboProdukt } from './aboFunktionen.js'
|
||||
import { unterlagenFuer } from './aboUnterlagen.js'
|
||||
import { k38Mail, kontostandText, t24Mail } from './aboFunktionenMails.js'
|
||||
import { featureConfig } from './featureRegistry.js'
|
||||
import { getLedger } from './hourLedger.js'
|
||||
import { featureLegalAttachments } from './legalDocs.js'
|
||||
import { sendAdminNotice, sendCustomerMail } from './mailer.js'
|
||||
import { customerOf, projectName } from './siteFeatures.js'
|
||||
|
||||
/**
|
||||
* Mails zum Einschalten einer Funktion im Abo-Modell (Kundenansicht neu, P6; Builder in aboFunktionenMails.js):
|
||||
* K38 an den Kunden (BCC Team wie sendCustomerMail): Die Funktion wird eingerichtet, fuer news, termine, shop und unterkunft;
|
||||
* nicht fuer zeiten, dort sendet holeZeitenNach die K27. Mit den Vertragsunterlagen als PDF (ohne Widerrufsbelehrung), wenn
|
||||
* die Funktion welche hat und sie lesbar sind.
|
||||
* T24 ans Team, nur wenn der Kunde selbst eingeschaltet hat (by 'kunde'), fuer alle fuenf Funktionen.
|
||||
* meldeEingeschaltet wirft nie: jeder Schritt steht in einem eigenen try, ein Fehler ist eine Logzeile mit Art und Status (weder
|
||||
* Name noch Adresse des Kunden) und der Rest laeuft weiter. Aufrufer ist einschalten() (funktionEinschalten.js).
|
||||
*/
|
||||
|
||||
/** Austauschpunkte fuer Tests: die Anhaenge der Vertragsunterlagen (Standard: Leistungsbeschreibung und AVV, ohne Widerrufsbelehrung). */
|
||||
export const meldungDeps = {
|
||||
anhaenge: (key) => featureLegalAttachments(key, { ohne: ['widerruf'] }),
|
||||
}
|
||||
|
||||
/** Fehlerart fuers Log: Name und Status, nie die Meldung (SMTP-Antworten nennen gern die Adresse). */
|
||||
const fehlerArt = (err) => `${err?.name ?? 'Fehler'} ${err?.status ?? err?.responseCode ?? ''}`.trim()
|
||||
|
||||
/**
|
||||
* Meldung nach dem Einschalten: K38 an den Kunden, danach (nur by 'kunde') T24 ans Team.
|
||||
* @param {{ doc: object, ticketWoid?: string|null, by: 'kunde'|'team', abrechnung: 'abo'|'projekt' }} p
|
||||
* doc: das eingeschaltete Funktionsdokument (setup, mit Ticket); ticketWoid: Nummer des Einrichtungs-Tickets.
|
||||
* T24 traegt den Block vorschuss, wenn abrechnung 'abo' ist, das Abo ein Wartungs-Abo ist und das Stundenkonto balanceMinutes <= 0
|
||||
* zeigt oder schon einen Vorschuss hat; anhang_fehlt, wenn die Funktion Rechtsunterlagen hat und der Abruf der PDFs scheiterte.
|
||||
* @returns {Promise<{ k38: boolean, t24: boolean }>} was tatsaechlich versandt wurde
|
||||
*/
|
||||
export async function meldeEingeschaltet({ doc, ticketWoid = null, by, abrechnung }) {
|
||||
const ergebnis = { k38: false, t24: false }
|
||||
try {
|
||||
const key = doc?.featureKey
|
||||
if (!featureConfig(key)) {
|
||||
console.error(`[funktionen-mail] Meldung entfällt: unbekannte Funktion (${String(key)})`)
|
||||
return ergebnis
|
||||
}
|
||||
const customerId = doc.customerId
|
||||
|
||||
// Kunde und Projektname; fehlt etwas, bleibt es leer (Anrede ohne Namen), die Meldung geht trotzdem raus
|
||||
let customer = null
|
||||
let projekt = ''
|
||||
try {
|
||||
;[customer, projekt] = await Promise.all([customerOf(customerId), projectName(doc.projectId)])
|
||||
} catch (err) {
|
||||
console.error(`[funktionen-mail] Kunde oder Projekt nicht lesbar (${key}): ${fehlerArt(err)}`)
|
||||
}
|
||||
const hatUnterlagen = unterlagenFuer(key).avv !== null
|
||||
let anhangFehler = false
|
||||
|
||||
// K38 an den Kunden (nicht fuer zeiten: dort bleibt K27)
|
||||
if (key !== 'zeiten') {
|
||||
try {
|
||||
const to = String(customer?.email ?? '').trim()
|
||||
if (!to) {
|
||||
console.warn(`[funktionen-mail] K38 (${key}) entfällt: Kunde nicht mehr vorhanden oder ohne E-Mail-Adresse`)
|
||||
} else {
|
||||
let anhaenge = []
|
||||
try {
|
||||
const geholt = await meldungDeps.anhaenge(key)
|
||||
anhaenge = Array.isArray(geholt) ? geholt : []
|
||||
} catch (err) {
|
||||
anhangFehler = true
|
||||
console.error(`[funktionen-mail] Vertragsunterlagen (${key}) nicht lesbar, K38 geht ohne Anhang raus: ${fehlerArt(err)}`)
|
||||
}
|
||||
const nachricht = k38Mail({ kunde: customer, projekt, key, abrechnung, ticketNr: ticketWoid, anhang: anhaenge.length > 0 })
|
||||
await sendCustomerMail(to, { ...nachricht, attachments: anhaenge })
|
||||
ergebnis.k38 = true
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[funktionen-mail] K38 (${key}) nicht gesendet: ${fehlerArt(err)}`)
|
||||
}
|
||||
}
|
||||
|
||||
// T24 ans Team, nur wenn der Kunde selbst eingeschaltet hat
|
||||
if (by === 'kunde') {
|
||||
let kontostand = ''
|
||||
if (abrechnung === 'abo') {
|
||||
try {
|
||||
if ((await aboProdukt(customerId)).produkt === 'wartung') {
|
||||
const ledger = await getLedger(customerId)
|
||||
if (ledger.balanceMinutes <= 0 || ledger.vorschussMinutes > 0) kontostand = kontostandText(ledger)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[funktionen-mail] Stundenkonto für T24 (${key}) nicht lesbar: ${fehlerArt(err)}`)
|
||||
}
|
||||
}
|
||||
try {
|
||||
const nachricht = t24Mail({
|
||||
kunde: customer, email: customer?.email, projekt, key, customerId, projectId: doc.projectId, woid: ticketWoid,
|
||||
kontostand, anhangFehlt: anhangFehler && hatUnterlagen,
|
||||
})
|
||||
ergebnis.t24 = (await sendAdminNotice(nachricht))?.sent === true
|
||||
} catch (err) {
|
||||
console.error(`[funktionen-mail] T24 (${key}) nicht gesendet: ${fehlerArt(err)}`)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[funktionen-mail] Meldung nach dem Einschalten fehlgeschlagen: ${fehlerArt(err)}`)
|
||||
}
|
||||
return ergebnis
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { config } from '../config.js'
|
||||
import { updateDocument } from './appwriteAdmin.js'
|
||||
import { aboDeckt } from './aboFunktionen.js'
|
||||
import { meldeEingeschaltet } from './aboFunktionenMeldung.js'
|
||||
import { berlinDay } from './deadlines.js'
|
||||
import { bestellFehler } from './featureOrder.js'
|
||||
import { featureConfig, ticketTexte } from './featureRegistry.js'
|
||||
@@ -11,8 +12,9 @@ import { einschaltenZeiten } from './zeiten/einschalten.js'
|
||||
|
||||
/**
|
||||
* Generisches Einschalten einer Funktion im Abo-Modell (V7, Spec 5.4): ohne Zahlung, mit Einrichtungs-Ticket. Gleiches Muster wie
|
||||
* einschaltenZeiten (Sperre feature-order, Nachweis vor dem Zustand, Ticket danach, Heilung statt Doppelticket). Keine Mails, kein
|
||||
* Stundenkonto, keine Team-Aktivierung (P6). Fuer news, termine, shop und unterkunft laeuft alles hier; zeiten geht ueber
|
||||
* einschaltenZeiten (Sperre feature-order, Nachweis vor dem Zustand, Ticket danach, Heilung statt Doppelticket). Am Ende meldet
|
||||
* meldeEingeschaltet (aboFunktionenMeldung.js, P6) das Einschalten mit K38 an den Kunden und T24 ans Team; die Team-Aktivierung
|
||||
* (adminActivate) steht in siteFeatures.js. Fuer news, termine, shop und unterkunft laeuft alles hier; zeiten geht ueber
|
||||
* einschaltenZeiten (Neubau, Ticket und K27 stehen dort) mit einer Vorpruefung, die dieselben Codes liefert.
|
||||
*/
|
||||
|
||||
@@ -131,10 +133,15 @@ export async function einschalten({ customerId, projectId, key, by, abrechnung,
|
||||
await pruefeAboDeckung(customerId, key, now)
|
||||
pruefeEinschaltZustand(await getFeatureDoc(customerId, projectId, key))
|
||||
}
|
||||
const { doc } = await einschaltenZeiten({ customerId, projectId, by, staff, nachweis, now, abrechnung, vorpruefung })
|
||||
return { doc, ticketWoid: await ticketWoid(doc.ticketId) }
|
||||
const { doc, changed } = await einschaltenZeiten({ customerId, projectId, by, staff, nachweis, now, abrechnung, vorpruefung })
|
||||
const woid = await ticketWoid(doc.ticketId)
|
||||
// nur wenn dieser Aufruf den Zustand gesetzt hat (K27 sendet holeZeitenNach); ein idempotenter Aufruf meldet nichts
|
||||
if (changed === true) await meldeEingeschaltet({ doc, ticketWoid: woid, by, abrechnung })
|
||||
return { doc, ticketWoid: woid }
|
||||
}
|
||||
|
||||
// Abrechnung, mit der dieser Aufruf endet: bei der Heilung die am Dokument gespeicherte
|
||||
let abgerechnet = abrechnung
|
||||
const doc = await withLocks([`feature-order:${customerId}:${projectId}`], async () => {
|
||||
// 1 Abo frisch (nie aus einem Zwischenspeicher); fuer das Team entfaellt es
|
||||
if (by === 'kunde') await pruefeAboDeckung(customerId, key, now)
|
||||
@@ -144,6 +151,7 @@ export async function einschalten({ customerId, projectId, key, by, abrechnung,
|
||||
pruefeEinschaltZustand(bisher)
|
||||
if (heilbar(bisher)) {
|
||||
const gespeichert = Object.hasOwn(ABRECHNUNGS_ZEILEN, bisher.einrichtungAbrechnung) ? bisher.einrichtungAbrechnung : abrechnung
|
||||
abgerechnet = gespeichert
|
||||
return legeTicketAn(bisher, { key, feature, project, customerId, abrechnung: gespeichert })
|
||||
}
|
||||
|
||||
@@ -168,5 +176,8 @@ export async function einschalten({ customerId, projectId, key, by, abrechnung,
|
||||
// 5 Ticket; scheitert es, bleibt setup ohne Ticket und der naechste Aufruf heilt
|
||||
return legeTicketAn(gespeichert, { key, feature, project, customerId, abrechnung })
|
||||
})
|
||||
return { doc, ticketWoid: await ticketWoid(doc.ticketId) }
|
||||
// Der Aufruf hat den Zustand gesetzt oder das Ticket angelegt (Heilung zaehlt); die Meldung wirft nie
|
||||
const woid = await ticketWoid(doc.ticketId)
|
||||
await meldeEingeschaltet({ doc, ticketWoid: woid, by, abrechnung: abgerechnet })
|
||||
return { doc, ticketWoid: woid }
|
||||
}
|
||||
Reference in new issue
Block a user