Öffnungszeiten: Fixrunde Server (Schema-Kopplung, K27, Wertersatz, Widerruf, Ticket nachlegen)
- activate() schreibt die Zeiten-Felder (aboEndeAt, behaltenSessionId) nur noch über extra
(nur Abo-Rückkehr und Team-Aktivierung der Zeiten); News und Terminplaner laufen auch vor zeiten-setup.mjs
- Abo-Abgleich: K27 gilt als offen, solange der Marker vor disabledAt liegt (kein Zurücksetzen nach disable() nötig);
K27 geht erst nach dem Ersatz-Ticket raus (scheitert es, wiederholt der nächste Lauf beides);
Löschung setzt auch activatedAt zurück
- Zustimmung "beginn" (bezahlt) mit Wertersatz-Satz (§ 357a Abs. 2 BGB) wie bei News und Terminplaner, Fassung 2026-10-05
- Widerruf: Bestellung "zeiten" in ORDERS, FEATURE_OF_ORDER, orderLabel und widerrufen.html
- Team "Ticket nachlegen" der Zeiten ohne Kauf geht über holeZeitenNach statt über den Zahlungsweg
- Einschalten-Route nimmt nur { zustimmung }, nicht die Altform { consent: true }
- Team-Aktivierung behält kauf/unlockedByPurchase bei einem bisher gekauften Dokument
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c10e50d0b3
commit
ecf67d6978
13 files changed
+337
-48
No files matched your search
@@ -462,11 +462,26 @@ export async function markCheckoutFailed(sessionId) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Oeffnungszeiten live (Spec 5): das erkannte Abo-Ende und eine offene Behalten-Zahlung gehoeren zum alten Lauf und enden mit der
|
||||
* Aktivierung. Als `extra` an activate() (nur der Zeiten-Pfad; die Felder legt scripts/zeiten-setup.mjs an, News und Terminplaner
|
||||
* teilen die Collection).
|
||||
*/
|
||||
export const ZEITEN_AKTIVIERUNG_FELDER = Object.freeze({ aboEndeAt: null, behaltenSessionId: '' })
|
||||
|
||||
function byNote(by, verb) {
|
||||
return by ? `${verb} von ${by}`.slice(0, 300) : undefined
|
||||
}
|
||||
|
||||
export async function activate(docId, { by, adminNote } = {}) {
|
||||
/**
|
||||
* Funktion aktivieren (jede Funktion gleich). `extra` sind zusaetzliche Felder, die der Aufrufer mitschreibt: Oeffnungszeiten live
|
||||
* leert damit sein erkanntes Abo-Ende und eine offene Behalten-Zahlung (ZEITEN_AKTIVIERUNG_FELDER). Diese Felder legt erst
|
||||
* scripts/zeiten-setup.mjs an; darum schreibt activate() sie nie von selbst, sonst scheiterte die Aktivierung von News und
|
||||
* Terminplaner mit "Unknown attribute", solange der Code vor dem Skript laeuft.
|
||||
* @param {string} docId
|
||||
* @param {{ by?: string, adminNote?: string, extra?: Record<string, unknown> }} [options]
|
||||
*/
|
||||
export async function activate(docId, { by, adminNote, extra } = {}) {
|
||||
const note = adminNote ?? byNote(by, 'Freigeschaltet')
|
||||
return updateDocument(config.collections.portalFeatures, docId, {
|
||||
...stateFields('active'),
|
||||
@@ -477,10 +492,7 @@ export async function activate(docId, { by, adminNote } = {}) {
|
||||
disabledAt: null,
|
||||
disabledReason: null,
|
||||
aboEndeMailAt: null,
|
||||
// Oeffnungszeiten live (Spec 5): das erkannte Abo-Ende und eine offene Behalten-Zahlung gehoeren zum alten Lauf und enden mit der
|
||||
// Aktivierung (die Felder legt scripts/zeiten-setup.mjs an; News und Terminplaner teilen die Collection)
|
||||
aboEndeAt: null,
|
||||
behaltenSessionId: '',
|
||||
...(extra ?? {}),
|
||||
...(note ? { adminNote: note } : {}),
|
||||
})
|
||||
}
|
||||
@@ -583,7 +595,7 @@ export async function adminActivate(customerId, projectId, key, { by, adminNote
|
||||
await assertOwnProject(customerId, projectId)
|
||||
const doc = await ensureFeatureDoc(customerId, projectId, key)
|
||||
if (key === 'termine' && doc.unlockedByPurchase === true) await assertAboLaeuftFuerAktivierung(customerId)
|
||||
return activate(doc.$id, { by, adminNote })
|
||||
return activate(doc.$id, { by, adminNote, ...(key === 'zeiten' ? { extra: ZEITEN_AKTIVIERUNG_FELDER } : {}) })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -814,6 +826,26 @@ async function ticketNachZahlung(doc, note) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ticket nachlegen (a) fuer Oeffnungszeiten live ohne Kauf (Abo, Team): es gibt keine Zahlung zu pruefen; das Einrichten holt
|
||||
* holeZeitenNach nach (Neubau, Einbau-Ticket, K24 nur bei Abo), derselbe Weg wie der 5-Minuten-Job und das Einschalten. Dienst
|
||||
* erst beim Aufruf laden (zeiten/* nur per await import()). Sperren: feature-ticket (hier gehalten) -> zeiten-nachholen ->
|
||||
* zeiten-neubau; nichts darin nimmt feature-admin. Wirft, wenn das Ticket nicht angelegt werden kann (der Aufrufer meldet es).
|
||||
*/
|
||||
async function ticketNachHolen(doc, note) {
|
||||
const { holeZeitenNach } = await import('./zeiten/einschalten.js')
|
||||
const nachher = await holeZeitenNach(doc)
|
||||
if (!nachher.ticketId) return { doc: nachher, created: false }
|
||||
if (!note) return { doc: nachher, created: true }
|
||||
try {
|
||||
return { doc: await updateDocument(config.collections.portalFeatures, doc.$id, { adminNote: note }), created: true }
|
||||
} catch (err) {
|
||||
// Ticket und Mail sind raus; die Notiz ist nur ein Vermerk
|
||||
console.error(`[funktionen] Notiz zum nachgelegten Ticket von ${doc.$id} nicht gespeichert:`, err.message)
|
||||
return { doc: nachher, created: true }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ticket nachlegen (b): ticketId gesetzt, das Ticket aber geloescht. Ein neues Ticket ersetzt die ticketId; K15 und T16 gingen beim
|
||||
* ersten Mal raus, darum keine Mail. Laeuft unter der Sperre der Zahlung (feature-paid:<session>), damit kein Stripe-Retry daneben
|
||||
@@ -835,7 +867,8 @@ async function ersetzeGeloeschtesTicket(doc, note) {
|
||||
/**
|
||||
* "Ticket nachlegen": nur fuer eine Funktion in Einrichtung ('setup'; sonst 409, ohne Dokument 409).
|
||||
* (a) ticketId leer: Zahlung bei Stripe pruefen (nicht bezahlt 409, Stripe nicht erreichbar 503) und den Weg des Webhooks gehen,
|
||||
* Ticket + K15 + T16 genau einmal (ticketNachZahlung);
|
||||
* Ticket + K15 + T16 genau einmal (ticketNachZahlung); Oeffnungszeiten live ohne Kauf (Abo, Team) haben keine Zahlung: dort holt
|
||||
* holeZeitenNach Neubau, Ticket und K24 nach (ticketNachHolen);
|
||||
* (b) ticketId gesetzt, Ticket geloescht: neues Ticket, ticketId ersetzt, keine Mails (ersetzeGeloeschtesTicket);
|
||||
* (c) Ticket vorhanden: nichts, created false.
|
||||
* Eine Sperre feature-ticket:<id> je Dokument: gleichzeitige Aufrufe laufen nacheinander und sehen das Ergebnis des ersten
|
||||
@@ -849,7 +882,7 @@ export async function ticketNachlegen({ customerId, projectId, key, by, adminNot
|
||||
return withLocks([`feature-ticket:${vorab.$id}`], async () => {
|
||||
const doc = await frischesDokument(vorab, key)
|
||||
if (doc.state !== 'setup') throw httpError(409, TICKET_NUR_SETUP)
|
||||
if (!doc.ticketId) return ticketNachZahlung(doc, note)
|
||||
if (!doc.ticketId) return key === 'zeiten' && unlockedByVon(doc) !== 'kauf' ? ticketNachHolen(doc, note) : ticketNachZahlung(doc, note)
|
||||
if (await ticketBesteht(doc.ticketId)) return { doc, created: false }
|
||||
return ersetzeGeloeschtesTicket(doc, note)
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user