#!/usr/bin/env node /** * Vor dem Umschalten der Kundenansicht (P10): Käufe lesen. Nur lesend, schreibt nie. * * Nach dem Rückbau der Kaufwege (Teil C, Task 20) gibt es keine neuen Käufe mehr (dann 404); bis dahin bleiben sie offen. Bestehendes muss * weiter laufen: die Zahlung einer * vorher gestarteten Bestellung (SEPA braucht Tage) und der Widerruf gekaufter Funktionen innerhalb der Frist. Das Skript zählt beides: * offene Bestellungen state 'ordered' (die Zahlung steht noch aus); Exit 1, solange es welche gibt. Erst abwarten, dann umschalten. * Fristen laufen gekaufte Funktionen (state setup | active | disabled und unlockedByPurchase), deren Widerrufsfrist noch läuft. * Nur zur Information, kein Hindernis: der Widerruf funktioniert nach dem Umschalten weiter. * Die Frist ist eine Näherung: Anker ist consentAt am Dokument, sonst orderedAt (der Nachweis in featureConsents wird nicht gelesen). * Ohne Datum gilt die Frist vorsichtshalber als laufend. * Behalten-Sitzung Öffnungszeiten "Behalten": starteBehalten ändert den Zustand nicht, es trägt nur behaltenSessionId ein. Eine * solche Zahlung ist also weder 'ordered' noch gekauft. Eigene Zeile mit den Dokument-IDs, kein Exit 1: eine * abgelaufene Sitzungs-ID bleibt stehen, bis aktiviert oder gebucht wird. Vor dem Umschalten von Hand klären. * * Ausgabe: nur Zahlen und Dokument-IDs, nie Namen, Adressen oder Beträge. * node scripts/kauf-bestand-pruefen.mjs * Gegen Produktion nur mit Freigabe des Inhabers (liest alle portalFeatures-Dokumente). Tests laufen nur gegen den Appwrite-Nachbau. */ import 'dotenv/config' import { pathToFileURL } from 'node:url' import { config } from '../server/config.js' import { listAllDocuments } from '../server/services/appwriteAdmin.js' import { withdrawalDeadline } from '../server/services/deadlines.js' const GEKAUFT_STAENDE = ['setup', 'active', 'disabled'] const istGekauft = (doc) => GEKAUFT_STAENDE.includes(doc.state) && Boolean(doc.unlockedByPurchase) /** Zeitpunkt des Vertragsschlusses (Näherung): Zustimmung am Dokument, sonst Bestellzeit; leer oder ungültig => null. */ function vertragAnker(doc) { const wert = doc.consentAt || doc.orderedAt if (!wert) return null return Number.isNaN(new Date(wert).getTime()) ? null : new Date(wert) } /** @returns {{ laeuft: boolean, ohneDatum: boolean }} Läuft die Widerrufsfrist dieser gekauften Funktion noch? */ function fristStand(doc, jetzt) { const anker = vertragAnker(doc) if (!anker) return { laeuft: true, ohneDatum: true } return { laeuft: withdrawalDeadline(anker).endsAt > jetzt, ohneDatum: false } } /** Wie unlockedByVon (siteFeatures.js), hier nachgebaut, damit das Skript nicht Stripe und Mailer lädt. */ const freigeschaltetVon = (doc) => doc.unlockedBy || (doc.unlockedByPurchase ? 'kauf' : 'team') /** Behalten-Sitzung eingetragen und noch nicht gebucht (gebucht = stripeSessionId ist diese Sitzung und das Dokument ein Kauf). */ const behaltenEingetragen = (doc) => Boolean(doc.behaltenSessionId) && !(doc.stripeSessionId === doc.behaltenSessionId && freigeschaltetVon(doc) === 'kauf') const liste = (ids) => (ids.length ? ` (${ids.join(', ')})` : '') /** * @param {{ log?: (line: string) => void, jetzt?: Date }} [options] * @returns {Promise<{ offeneBestellungen: string[], fristLaeuft: string[], gekauftGesamt: number }>} nur Dokument-IDs */ export async function pruefeKaeufe({ log = console.log, jetzt = new Date() } = {}) { const docs = await listAllDocuments(config.collections.portalFeatures) const offeneBestellungen = docs.filter((d) => d.state === 'ordered').map((d) => d.$id) const behalten = docs.filter(behaltenEingetragen).map((d) => d.$id) const gekauft = docs.filter(istGekauft) const fristLaeuft = [] const ohneDatum = [] for (const doc of gekauft) { const stand = fristStand(doc, jetzt) if (!stand.laeuft) continue fristLaeuft.push(doc.$id) if (stand.ohneDatum) ohneDatum.push(doc.$id) } log(`Funktionsdokumente: ${docs.length}, davon gekauft: ${gekauft.length}`) log(`Offene Bestellungen: ${offeneBestellungen.length}${liste(offeneBestellungen)}`) log(`Fristen laufen: ${fristLaeuft.length}${liste(fristLaeuft)}${ohneDatum.length ? ` - davon ohne Datum: ${ohneDatum.length}` : ''}`) log(`Behalten-Sitzung eingetragen: ${behalten.length}${liste(behalten)}`) if (behalten.length) log('Hinweis: bei einer Behalten-Sitzung kann die Zahlung noch laufen oder die Sitzung abgelaufen sein; kein Hindernis, vor dem Umschalten aber klären.') if (offeneBestellungen.length) log('Offene Bestellungen vorhanden: erst die Zahlung abwarten (oder klären), dann umschalten.') return { offeneBestellungen, fristLaeuft, gekauftGesamt: gekauft.length } } if (import.meta.url === pathToFileURL(process.argv[1] || '').href) { try { const result = await pruefeKaeufe() if (result.offeneBestellungen.length) process.exitCode = 1 } catch (err) { console.error('Fehler:', err.message) process.exit(1) } }