Öffnungszeiten: nach Abo-Ende für 30 € behalten

Neuer Modus neben Bestellen und Einschalten (Spec 3.3): POST /api/funktionen/zeiten/behalten
startet einen Checkout mit zweck 'behalten', ohne den Zustand zu ändern; der Webhook-Zweig
bucht vor markPaid/adoptPaidSession unter feature-admin zum Kauf (Frist weg, stripeSessionId =
Sitzung), schaltet eine abgeschaltete Funktion wieder ein (Neubau nach der Sperre), ohne neues
Ticket, mit K15 im Behalten-Wortlaut. Passt die Sitzung nicht, geht T19 nach der Sperre raus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 02:54:39 +00:00
1 parent 97e6971276
commit 53253f0451
6 files changed
+1116 -12

No files matched your search

+31 -8
View File
@@ -324,7 +324,7 @@ function offenerFall(doc) {
}
/** Neuester Eintrag zuerst, hoechstens 300 Zeichen (wie die Notiz im Ticketsystem). */
function notizVorn(previous, entry) {
export function notizVorn(previous, entry) {
const neu = entry.slice(0, NOTE_MAX)
const aelter = String(previous || '')
return aelter ? `${neu} | ${aelter}`.slice(0, NOTE_MAX) : neu
@@ -757,7 +757,7 @@ export async function customerOf(customerId) {
}
/** Link zur Stripe-Rechnung; fehlt sie noch oder ist sie nicht lesbar, geht die Mail ohne Link raus. */
async function invoiceOf(invoiceRef) {
export async function invoiceOf(invoiceRef) {
const id = typeof invoiceRef === 'string' ? invoiceRef : invoiceRef?.id
if (!id) return { invoiceId: '', invoiceUrl: '' }
try {
@@ -818,7 +818,7 @@ async function teamNotice(doc, customer, { failed = false, keinAbo = false } = {
}
/** Bezahlte Sitzung ohne passende Bestellung: das Team muss von Hand klaeren (T16, Block ungeklaert). */
async function unmatchedNotice(session) {
export async function unmatchedNotice(session) {
const { customerId, projectId, featureKey } = session.metadata || {}
const customer = customerId ? await customerOf(customerId).catch(() => null) : null
await sendT16(
@@ -920,14 +920,18 @@ export function meldeZahlungFall(doc, fall, { customer = null } = {}) {
* am (frisch gelesenen) Dokument der offene Fall 'abo_ende' steht - so auch, wenn der Abgleich vor dem Webhook-Versand zuerst meldet;
* ticket_offen, wenn das Dokument ein Ticket hat (kein Ticket und keine spaete Zahlung: keiner von beiden); rechnung mit Rechnungslink.
* Genau einmal je Dokument (siehe meldeEinmal), wirft nie.
* ticketOffen und rechnungUrl ueberschreiben, was sonst am Dokument steht (Ticket vorhanden, invoiceUrl): bei einer Zahlung, die nicht zu
* dessen Ticket und Rechnung gehoert (Behalten, zeiten/behalten.js), sagt T19 sonst "Ticket offen" und zeigt die Rechnung einer anderen Zahlung.
* rechnungUrl '' heisst "keine Rechnung"; null (Vorgabe) nimmt die des Dokuments.
* @param {object} doc portalFeatures-Dokument
* @param {{ customer?: object|null, sitzung?: string, nachAboEnde?: boolean }} [options]
* @param {{ customer?: object|null, sitzung?: string, nachAboEnde?: boolean, ticketOffen?: boolean|null, rechnungUrl?: string|null }} [options]
* @returns {Promise<{ sent: boolean, schonGemeldet?: boolean }>}
*/
export async function meldeErstattung(doc, { customer = null, sitzung = doc.stripeSessionId, nachAboEnde = false } = {}) {
export async function meldeErstattung(doc, { customer = null, sitzung = doc.stripeSessionId, nachAboEnde = false, ticketOffen = null, rechnungUrl = null } = {}) {
return meldeEinmal(doc, 'T19 Erstattung faellig', async (frisch) => {
const kopf = await meldeKopf(frisch, customer)
const woid = frisch.ticketId ? await ticketWoidOf(frisch).catch(() => frisch.ticketId) : ''
const rechnung = rechnungUrl ?? frisch.invoiceUrl ?? ''
return sendAdminNotice({
template: 'T19',
values: {
@@ -936,12 +940,12 @@ export async function meldeErstattung(doc, { customer = null, sitzung = doc.stri
betrag: featureConfig(frisch.featureKey)?.priceLabel || '',
sitzung: sitzung || '',
woid,
rechnung_url: frisch.invoiceUrl || '',
rechnung_url: rechnung,
},
blocks: {
zahlung_nach_ende: nachAboEnde || frisch.zahlungMeldung === 'abo_ende',
ticket_offen: Boolean(frisch.ticketId),
rechnung: Boolean(frisch.invoiceUrl),
ticket_offen: ticketOffen ?? Boolean(frisch.ticketId),
rechnung: Boolean(rechnung),
},
})
})
@@ -968,7 +972,18 @@ async function meldeFallZurZahlung(doc, fall, session) {
if (!ergebnis.sent && !ergebnis.schonGemeldet) throw new Error(`Meldung zur Zahlung ${session.id} (${fall}) nicht gesendet`)
}
/**
* Zahlung "fuer 30 EUR behalten" (Oeffnungszeiten live nach dem Abo-Ende, Spec 3.3): eigener Weg in zeiten/behalten.js, nie ueber
* markPaid und adoptPaidSession (die schalten nur aus 'ordered' ein und meldeten die Zahlung einer laufenden Funktion als ungeklaert).
* Dynamischer Import: zeiten/* importiert dieses Modul.
*/
async function onBehaltenPaid(session) {
const { bucheBehalten } = await import('./zeiten/behalten.js')
await bucheBehalten(session)
}
async function onFeaturePaid(session) {
if (session.metadata?.zweck === 'behalten') return onBehaltenPaid(session)
const { invoiceId, invoiceUrl } = await invoiceOf(session.invoice)
const paid = { sessionId: session.id, invoiceId, invoiceUrl }
let { doc, created, fall } = await markPaid(paid)
@@ -1027,16 +1042,24 @@ export async function handleFeatureCheckoutEvent(event) {
await onFeaturePaid(session)
return
case 'checkout.session.async_payment_failed': {
if (session.metadata?.zweck === 'behalten') return verwerfeBehaltenSitzung(session)
const doc = await markCheckoutFailed(session.id)
if (doc && doc.state === 'off') await teamNotice(doc, await customerOf(doc.customerId), { failed: true })
return
}
case 'checkout.session.expired':
if (session.metadata?.zweck === 'behalten') return verwerfeBehaltenSitzung(session)
await markCheckoutFailed(session.id)
return
}
}
/** Behalten-Zahlung abgelaufen oder fehlgeschlagen: die offene Sitzung am Dokument wird frei (Dynamischer Import wie onBehaltenPaid). */
async function verwerfeBehaltenSitzung(session) {
const { verwerfeBehalten } = await import('./zeiten/behalten.js')
await verwerfeBehalten(session)
}
/**
* Selbstheilung der Oeffnungszeiten (Spec 3.2): fuer jedes Dokument der Zeiten in 'setup' holt holeZeitenNach fehlende Schritte
* nach (Neubau, Einbau-Ticket, K24; es sperrt je Dokument selbst und fasst Kauf, aktiv und aus nicht an). Hierher und nicht in