TP2 Abschlussprüfung: Zahlung/Team (Wiederzustellung, K16-Warnung, Sperrschlüssel)

- adoptPaidSession übernimmt ein erneut zugestelltes checkout.session.completed nicht mehr, wenn das Dokument nach 30 Tagen zurückgesetzt wurde
  (off, purchaseStatus zurueckgesetzt, gleiche Sitzung): kein Setup, kein Ticket, keine K15/T16
- meldeFreigeschaltet liefert true/false; die Staff-Aktivierung in Textform vermerkt eine nicht gesendete K16 mit Unterlagen in adminNote und
  antwortet mit warnung: true
- disable() nutzt die Sperre feature-activate:<id> statt feature-disable:<id> (Freischaltung und Abschalten überholen sich nicht)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-05 06:18:34 +00:00
1 parent 4dc447ed33
commit b974b3a3ec
7 files changed
+153 -6

No files matched your search

+29 -4
View File
@@ -2,7 +2,7 @@ import { Router } from 'express'
import { config } from '../../config.js'
import { aboMemo } from '../../services/aboStatus.js'
import { validateTextform } from '../../services/analyse.js'
import { getDocument } from '../../services/appwriteAdmin.js'
import { getDocument, updateDocument } from '../../services/appwriteAdmin.js'
import { featureConfig } from '../../services/featureRegistry.js'
import { withLocks } from '../../services/keyedLock.js'
import { featureLegalAttachments, featureLegalPaths } from '../../services/legalDocs.js'
@@ -67,6 +67,26 @@ async function customerName(customerId) {
}
}
const K16_FEHLT = 'K16 mit Unterlagen NICHT gesendet – von Hand nachsenden'
/**
* Vermerkt in der Notiz des Funktionsdokuments, dass die K16 mit den Unterlagen nicht rausging (Staff-Aktivierung in Textform, die
* Unterlagen sind der dauerhafte Datentraeger). Best effort unter der Sperre der Verwaltung: scheitert das Schreiben, steht der Fehler im
* Log, die Antwort traegt die Warnung trotzdem.
*/
async function notiereK16Fehlt(projectId, doc) {
try {
await withLocks([`feature-admin:${projectId}`], async () => {
const frisch = await getDocument(config.collections.portalFeatures, doc.$id)
await updateDocument(config.collections.portalFeatures, doc.$id, {
adminNote: composeAdminNote(frisch.adminNote, { by: 'System', action: K16_FEHLT }),
})
})
} catch (err) {
console.error('[funktionen-admin] Notiz zur nicht gesendeten K16 nicht geschrieben:', err.message)
}
}
const ticketExists = async (ticketId) => (await readTicket(ticketId)) !== null
/**
@@ -231,9 +251,14 @@ funktionenAdminRouter.post('/:customerId/:key', async (req, res) => {
await disable(doc.$id, { by, adminNote })
}
})
// Der Kunde bekommt die Unterlagen auf einem dauerhaften Datentraeger (Anhang); ein Versandfehler wird dort geloggt, nie geworfen
if (freigeschaltet) await meldeFreigeschaltet(freigeschaltet.doc, { attachments: freigeschaltet.attachments })
res.json({ projects: (await adminView(customerId)).filter((p) => p.projectId === projectId) })
// Der Kunde bekommt die Unterlagen auf einem dauerhaften Datentraeger (Anhang); ein Versandfehler wird dort geloggt, nie geworfen.
// Ging K16 nicht raus (Versand, Kunde ohne Adresse), steht es in der Notiz und die Antwort traegt warnung: true - das Team sendet von Hand nach
let warnung = false
if (freigeschaltet && !(await meldeFreigeschaltet(freigeschaltet.doc, { attachments: freigeschaltet.attachments }))) {
warnung = true
await notiereK16Fehlt(projectId, freigeschaltet.doc)
}
res.json({ projects: (await adminView(customerId)).filter((p) => p.projectId === projectId), ...(warnung ? { warnung } : {}) })
} catch (err) {
sendError(res, err)
}
+11 -1
View File
@@ -438,7 +438,10 @@ function assertReason(reason) {
*/
export async function disable(docId, { by, adminNote, reason = 'team' } = {}) {
assertReason(reason)
const doc = await withLocks([`feature-disable:${docId}`], async () => {
// Dieselbe Sperre wie die Freischaltung (activateClosedTickets, freischaltenNachAbnahme): beide schreiben den Zustand desselben Dokuments und
// duerfen sich nicht ueberholen. Kein Halter von feature-activate:<id> ruft disable() auf; die Reihenfolge feature-admin -> feature-activate
// besteht schon. Keine Mehrfach-Sperre (withLocks mit mehreren Schluesseln kann sich verklemmen).
const doc = await withLocks([`feature-activate:${docId}`], async () => {
const current = await getDocument(config.collections.portalFeatures, docId)
if (current.state === 'disabled') {
if (current.disabledAt) return current
@@ -1080,6 +1083,13 @@ async function unmatchedNotice(session) {
async function adoptPaidSession(session) {
const { customerId, projectId, featureKey } = session.metadata || {}
const doc = customerId && projectId && featureKey ? await getFeatureDoc(customerId, projectId, featureKey) : null
// Erneut zugestelltes Ereignis ("Resend event" in Stripe) zu einer Bestellung, die laengst eingerichtet, abgeschaltet und nach 30 Tagen
// geloescht wurde (off, purchaseStatus 'zurueckgesetzt', dieselbe Sitzung): sie ist ausgeliefert, nichts wird neu eingerichtet und
// niemand benachrichtigt (kein Ticket, keine K15, kein T16).
if (doc?.state === 'off' && doc.purchaseStatus === 'zurueckgesetzt' && doc.stripeSessionId === session.id) {
console.warn(`[funktionen] Sitzung ${session.id} gehoert zu einer Funktion, die nach dem Abo-Ende schon geloescht und zurueckgesetzt wurde - nicht uebernommen, nichts gesendet`)
return false
}
if (!doc || (doc.state !== 'ordered' && doc.state !== 'off')) {
console.error(
`[funktionen] BEZAHLTE Sitzung ${session.id} ohne passende Bestellung (Kunde ${customerId}, Projekt ${projectId}, Zustand ${doc?.state || 'kein Dokument'}) - bitte von Hand pruefen`
+6 -1
View File
@@ -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
}
}