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:
1 parent
4dc447ed33
commit
b974b3a3ec
7 files changed
+153
-6
No files matched your search
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user