Kundenansicht neu E-Fix M8: Wechselangebot beim Einschalten nur, wenn der Wechsel möglich ist
aboDeckt nennt falsches_abo vor gekuendigt, beginnt_spaeter und nicht_aktiv (die Reihenfolge bleibt gepinnt). Die 409-Antwort setzte wechsel: true allein aus grund === 'falsches_abo': ein Hosting-Abo in der Probezeit, mit offener Zahlung oder vorgemerkter Kündigung bekam das Angebot, landete auf #wechsel bei "Ein Wechsel ist derzeit nicht möglich" und verlor den eigentlichen Grund. Jetzt prüft pruefeAboDeckung bei falsches_abo zusätzlich wechselMoeglich (aboFunktionen.js, dieselben Bedingungen wie canSwitch und assertCanSwitch: Hosting, aktiv, nicht gekündigt, kein offener Wechsel); ein Lesefehler dabei nimmt nur das Angebot weg. Test in funktionEinschalten.test.js. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
29ca12a409
commit
3e32ddfe86
3 files changed
+46
-5
No files matched your search
@@ -45,6 +45,7 @@ async function lies(customerId, now) {
|
||||
// Ereignisse dieses Abos: Wechsel-Regel und offene Widerrufsfristen
|
||||
const ereignisse = (await listContractEvents(customerId)).filter((e) => e.subscriptionId === abo.$id)
|
||||
const wechselWiderrufen = ereignisse.find((e) => e.type === 'wechsel')?.status === 'widerrufen' // neueste zuerst
|
||||
const wechselOffen = ereignisse.some((e) => e.type === 'wechsel' && e.status === 'offen')
|
||||
const produkt = spiegel === 'wartung' && wechselWiderrufen ? 'hosting' : spiegel
|
||||
|
||||
const enden = openWithdrawals(ereignisse.filter((e) => FRIST_TYPEN.has(e.type)), now).map((f) => zeitpunkt(f.until))
|
||||
@@ -56,6 +57,7 @@ async function lies(customerId, now) {
|
||||
widerrufsfristBis: spaetestes === null ? null : new Date(spaetestes).toISOString(),
|
||||
status: abo.status,
|
||||
trialEnd: abo.trialEnd,
|
||||
wechselOffen,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +67,15 @@ export async function aboProdukt(customerId, { now = new Date() } = {}) {
|
||||
return { produkt, laeuft, gekuendigt, widerrufsfristBis }
|
||||
}
|
||||
|
||||
/**
|
||||
* Kann der Kunde jetzt ins Wartungs-Abo wechseln (E-Fix M8, wie canSwitch in GET /api/abo und assertCanSwitch in planSwitch.js)? Hosting-Abo, Status active,
|
||||
* nicht gekuendigt (cancelAtPeriodEnd oder cancelAt), kein Wechsel, der auf die Zahlung wartet. Lesefehler werden weitergereicht.
|
||||
*/
|
||||
export async function wechselMoeglich(customerId, { now = new Date() } = {}) {
|
||||
const l = await lies(customerId, now)
|
||||
return l.laeuft && l.produkt === 'hosting' && !l.gekuendigt && l.status === 'active' && !l.wechselOffen
|
||||
}
|
||||
|
||||
/**
|
||||
* Deckt das Abo des Kunden diese Funktion? { ok: true } | { ok: false, grund } mit grund in der Reihenfolge
|
||||
* kein_abo, falsches_abo, gekuendigt, beginnt_spaeter (mit ab = Ende der Probezeit als ISO), nicht_aktiv.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { config } from '../config.js'
|
||||
import { updateDocument } from './appwriteAdmin.js'
|
||||
import { aboDeckt } from './aboFunktionen.js'
|
||||
import { aboDeckt, wechselMoeglich } from './aboFunktionen.js'
|
||||
import { meldeEingeschaltet } from './aboFunktionenMeldung.js'
|
||||
import { berlinDay } from './deadlines.js'
|
||||
import { bestellFehler } from './featureOrder.js'
|
||||
@@ -54,9 +54,14 @@ export function aboFehlerText(grund, ab = null) {
|
||||
return grund === 'beginnt_spaeter' ? text.replace('{datum}', datumDe(ab)) : text
|
||||
}
|
||||
|
||||
/** 409 abo_fehlt: body.grund, body.wechsel (Hosting-Kunde: Wechsel ins Wartungs-Abo anbieten) und bei beginnt_spaeter body.ab. */
|
||||
const aboFehlt = (grund, ab) =>
|
||||
bestellFehler(409, aboFehlerText(grund, ab), 'abo_fehlt', { grund, wechsel: grund === 'falsches_abo', ...(grund === 'beginnt_spaeter' ? { ab } : {}) })
|
||||
/**
|
||||
* 409 abo_fehlt: body.grund, body.wechsel (Hosting-Kunde, der wirklich wechseln kann: Wechsel ins Wartungs-Abo anbieten) und bei beginnt_spaeter body.ab.
|
||||
* wechsel ist nur bei falsches_abo und moeglichem Wechsel true (E-Fix M8): aboDeckt nennt falsches_abo vor gekuendigt, beginnt_spaeter und nicht_aktiv
|
||||
* (die Reihenfolge der Gruende bleibt), ein Hosting-Abo in der Probezeit, bei offener Zahlung oder mit vorgemerkter Kuendigung bekaeme sonst ein
|
||||
* Angebot, das auf #wechsel in "Ein Wechsel ist derzeit nicht möglich" endet.
|
||||
*/
|
||||
const aboFehlt = (grund, ab, wechsel = false) =>
|
||||
bestellFehler(409, aboFehlerText(grund, ab), 'abo_fehlt', { grund, wechsel: grund === 'falsches_abo' && wechsel === true, ...(grund === 'beginnt_spaeter' ? { ab } : {}) })
|
||||
|
||||
const konflikt = (code, text) => bestellFehler(409, text, code)
|
||||
|
||||
@@ -72,7 +77,13 @@ export async function pruefeAboDeckung(customerId, key, now = new Date()) {
|
||||
console.error(`[funktionen] Abo für das Einschalten nicht lesbar: ${err?.name ?? 'Fehler'} ${err?.status ?? ''}`.trim())
|
||||
throw bestellFehler(503, EINSCHALTEN_TEXTE.nichtPruefbar)
|
||||
}
|
||||
if (ergebnis.ok !== true) throw aboFehlt(ergebnis.grund, ergebnis.ab)
|
||||
if (ergebnis.ok === true) return
|
||||
let wechsel = false
|
||||
if (ergebnis.grund === 'falsches_abo') {
|
||||
// Ein Lesefehler hier nimmt nur das Angebot weg (fail-closed fuer das Angebot, nicht fuer die Pruefung)
|
||||
wechsel = await wechselMoeglich(customerId, { now }).catch(() => false)
|
||||
}
|
||||
throw aboFehlt(ergebnis.grund, ergebnis.ab, wechsel)
|
||||
}
|
||||
|
||||
/** setup ohne Ticket und durch Abo oder Projekt eingeschaltet: der Dienst heilt (nur das Ticket), es entsteht nichts Neues. */
|
||||
|
||||
@@ -160,6 +160,25 @@ test('gleichzeitig: ein Erfolg, ein 409, ein Ticket, ein Nachweis', async () =>
|
||||
assert.deepEqual([aw.docs('workorders').size, aufrufe.length], [1, 1])
|
||||
})
|
||||
|
||||
test('M8: das Wechselangebot (wechsel true) gibt es nur, wenn der Wechsel auch möglich ist (Hosting, aktiv, nicht gekündigt, kein offener Wechsel)', async () => {
|
||||
const ohneWechsel = async (doc, name) => {
|
||||
await loescheAbos(); aw.seed('subscriptions', subscriptionDoc('sub_h', doc))
|
||||
await assert.rejects(E(), (e) => e.status === 409 && e.body.grund === 'falsches_abo' && e.body.wechsel === false && e.message === EINSCHALTEN_TEXTE.abo.falsches_abo, name)
|
||||
}
|
||||
await ohneWechsel({ ...hostingDoc, cancelAtPeriodEnd: true }, 'Hosting zum Periodenende gekündigt')
|
||||
await ohneWechsel({ ...hostingDoc, cancelAt: '2026-12-01T00:00:00.000Z' }, 'Hosting mit cancelAt')
|
||||
await ohneWechsel({ ...hostingDoc, status: 'past_due' }, 'Hosting past_due')
|
||||
await ohneWechsel({ ...hostingDoc, status: 'trialing', startChoice: 'nach_frist', trialEnd: '2026-11-03T05:00:00.000Z' }, 'Hosting in der Probezeit')
|
||||
// offener Wechsel (Zahlung steht aus): das Angebot käme zu #wechsel, dort ist nichts mehr zu bestellen
|
||||
await loescheAbos(); aw.seed('subscriptions', subscriptionDoc('sub_h', hostingDoc))
|
||||
aw.seed('contractEvents', { $id: 'ev_wechsel_offen', customerId: 'c1', subscriptionId: 'sub_h', type: 'wechsel', productKey: 'wartung', status: 'offen', details: '{}', termsVersion: 'x', consentAt: '2026-10-16T08:00:00.000Z' })
|
||||
await assert.rejects(E(), (e) => e.body.grund === 'falsches_abo' && e.body.wechsel === false, 'offener Wechsel')
|
||||
await deleteDocument('contractEvents', 'ev_wechsel_offen')
|
||||
// Hosting aktiv und nicht gekündigt: das Angebot bleibt
|
||||
await assert.rejects(E(), (e) => e.body.grund === 'falsches_abo' && e.body.wechsel === true, 'Hosting aktiv')
|
||||
assert.deepEqual(spuren(), { docs: 0, tickets: 0 })
|
||||
})
|
||||
|
||||
test('Nachweis scheitert: kein Zustand, kein Ticket', async () => {
|
||||
await assert.rejects(E({ nachweis: async () => { throw Object.assign(new Error('Nachweis weg'), { status: 500 }) } }), /Nachweis weg/)
|
||||
assert.deepEqual(spuren(), { docs: 0, tickets: 0 })
|
||||
|
||||
Reference in new issue
Block a user