Wechsel Hosting → Wartung: neuer Abbuchungstag, Gutschrift, 12 neue Monate

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-09-28 11:30:21 +00:00
1 parent 379cc26dcd
commit 188db4b3c1
15 files changed
+2035 -12

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env node
/**
* Wechsel ins Wartungs-Abo als gescheitert abschließen, wenn die SEPA-Lastschrift der Wechselrechnung nicht
* eingelöst wurde (Vertragsbedingungen § 9 Abs. 3, Rechtsrunde 28.09.2026). Das Team bekommt dann T12 mit den
* Handgriffen: Wechselrechnung stornieren, Preis ohne Verrechnung zurück auf Hosting, Metadaten zurücksetzen.
* Danach setzt dieses Skript das Ereignis `wechsel` auf `gescheitert` und schickt K14 an den Kunden und T12
* (gescheitert) ans Team (markSwitchFailed in server/services/planSwitch.js, wie beim Webhook
* customer.subscription.pending_update_expired für Karten).
*
* Usage (im Container, als Datei):
* node scripts/wechsel-gescheitert.mjs <Stripe-Abo-ID> --pruefen (nur anzeigen: offener Wechsel,
* Metadaten vor dem Wechsel)
* node scripts/wechsel-gescheitert.mjs <Stripe-Abo-ID> --ausfuehren [Grund] (Ereignis gescheitert, K14, T12)
* Ohne Grund nennt K14: "Die SEPA-Lastschrift für die Wechselrechnung wurde nicht eingelöst."
*/
import 'dotenv/config'
import { config } from '../server/config.js'
import { getDocument } from '../server/services/appwriteAdmin.js'
import { findOpenSwitchEvent, markSwitchFailed, SWITCH_FAILED_REASONS } from '../server/services/planSwitch.js'
const USAGE = 'Usage: node scripts/wechsel-gescheitert.mjs <Stripe-Abo-ID> --pruefen | --ausfuehren [Grund]'
/** { subscriptionId, mode: 'pruefen' | 'ausfuehren', grund? } oder null bei falschem Aufruf. */
export function parseArgs(argv) {
const [subscriptionId, mode, ...rest] = argv.slice(2)
if (!/^sub_\w+$/.test(subscriptionId || '')) return null
if (mode === '--pruefen' && rest.length === 0) return { subscriptionId, mode: 'pruefen' }
if (mode === '--ausfuehren') return { subscriptionId, mode: 'ausfuehren', grund: rest.join(' ').trim() || SWITCH_FAILED_REASONS.lastschrift }
return null
}
/** Offenen Wechsel des Abos zeigen und (ausfuehren) als gescheitert abschließen. Liefert { ok, event? }. */
export async function run({ subscriptionId, mode, grund }, log = console.log) {
const sub = await getDocument(config.collections.subscriptions, subscriptionId).catch(() => null)
if (!sub) {
log(`❌ Abo ${subscriptionId} ist im Kundenportal nicht bekannt.`)
return { ok: false }
}
const event = await findOpenSwitchEvent(sub.customerId, subscriptionId)
if (!event) {
log(`❌ Kein offener Wechsel für Abo ${subscriptionId}.`)
return { ok: false }
}
const details = event.details || {}
const euro = `${(Number(details.amountDueCents || 0) / 100).toFixed(2).replace('.', ',')} €`
log(`Offener Wechsel ${event.$id} (Kunde ${event.customerId}, ${sub.email || 'ohne E-Mail'})`)
log(` Wechsel am ${details.switchedAt || '–'}, Zahlart ${details.paymentMethod || 'unbekannt'}, Rechnung ${details.invoiceId || '–'} über ${euro}`)
log(` Mindestlaufzeit des Hosting-Abos vor dem Wechsel ab ${details.previousTermStartAt || '–'}`)
log(`Metadaten am Stripe-Abo ${subscriptionId} auf den Stand vor dem Wechsel zurücksetzen:`)
for (const [key, value] of Object.entries(details.previousMetadata || {})) log(` ${key} = ${value ?? '(löschen)'}`)
if (mode === 'pruefen') {
log('')
log(`Nur geprüft, nichts geändert. Abschließen: node scripts/wechsel-gescheitert.mjs ${subscriptionId} --ausfuehren`)
return { ok: true, event }
}
const result = await markSwitchFailed(event.$id, grund, { doneBy: 'skript' })
log(`✅ Wechsel ${event.$id} ist gescheitert: K14 an den Kunden und T12 ans Team verschickt (Fehler stehen im Log).`)
return { ok: true, event: result.event }
}
async function main() {
const args = parseArgs(process.argv)
if (!args) {
console.error(`❌ ${USAGE}`)
process.exit(1)
}
const result = await run(args)
if (!result.ok) process.exitCode = 1
}
// Nur beim direkten Aufruf laufen lassen, nicht wenn ein Test das Modul importiert
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((err) => {
console.error('❌', err.message)
process.exit(1)
})
}