Abo-Ende: Sofortpfad beim Widerruf des Wechsels, Team-Aktivierung nur mit laufendem Abo
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6bd6aed7d5
commit
c5009da570
6 files changed
+726
-10
No files matched your search
@@ -14,7 +14,7 @@ const st = await startFakeStripe()
|
||||
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
||||
const { config } = await import('../server/config.js')
|
||||
const { startTestApp, get, rawGet, post } = await import('./support/testApp.js')
|
||||
const { customerCookie, seedFixtures } = await import('./support/fixtures.js')
|
||||
const { customerCookie, seedFixtures, subscriptionDoc, wartungDoc, hostingDoc } = await import('./support/fixtures.js')
|
||||
const { default: funktionenRoutes } = await import('../server/routes/funktionen.js')
|
||||
const { default: teamRoutes } = await import('../server/routes/team/index.js')
|
||||
const sf = await import('../server/services/siteFeatures.js')
|
||||
@@ -263,3 +263,132 @@ test('Kunde loeschen gelingt auch, wenn das Abschalten scheitert (Fehler im Log)
|
||||
assert.equal(aw.docs('customers').has('kt2'), false)
|
||||
assert.ok(log.mock.calls.some((c) => /Terminplaner von Kunde kt2 nicht abgeschaltet/.test(String(c.arguments[0]))))
|
||||
})
|
||||
|
||||
// ------------------------------------------------ Team-Aktivierung nur mit laufendem Abo (E9) ---
|
||||
// Ein bezahlter Terminplaner (unlockedByPurchase) darf das Team nur einschalten, solange das Wartungs-Abo laeuft: sonst nimmt der
|
||||
// naechste Abgleichslauf die Kulanz zurueck und K22 ginge ein zweites Mal. Pilot und Kulanz ohne Kauf sowie News sind nicht betroffen.
|
||||
const KEIN_ABO = 'Das Wartungs-Abo des Kunden läuft nicht. Der Terminplaner kann erst wieder freigeschaltet werden, wenn es läuft.'
|
||||
let kaNr = 0
|
||||
/** Kunde mit Projekt und Funktionsdokument (key, over); abo: 'wartung' | 'hosting' | null (kein Abo). Liefert { customerId, projectId, docId }. */
|
||||
function legeKunde({ abo = null, aboOver = {}, key = 'termine', over = {} } = {}) {
|
||||
kaNr += 1
|
||||
const customerId = `ka${kaNr}`
|
||||
const projectId = `kap${kaNr}`
|
||||
const docId = `f_${projectId}_${key}`
|
||||
aw.seed('websiteProjects', { $id: projectId, customerId, projectName: `Salon ${kaNr}` })
|
||||
if (abo) aw.seed('subscriptions', subscriptionDoc(`sub_ka${kaNr}`, { ...(abo === 'wartung' ? wartungDoc : hostingDoc), customerId, ...aboOver }))
|
||||
aw.seed('portalFeatures', {
|
||||
$id: docId, customerId, projectId, featureKey: key, siteKey: 'abcdefghijklmnop', state: 'disabled', enabled: false,
|
||||
unlockedByPurchase: true, purchaseStatus: 'paid', disabledReason: 'abo_ende', disabledAt: '2026-10-01T10:00:00.000Z',
|
||||
aboEndeMailAt: '2026-10-01T10:05:00.000Z', adminNote: 'Abgeschaltet: Wartungs-Abo beendet', ...over,
|
||||
})
|
||||
return { customerId, projectId, docId }
|
||||
}
|
||||
const docById = (id) => aw.docs('portalFeatures').get(id)
|
||||
const aktivieren = (k, key = 'termine') => team(k.customerId, key, { projectId: k.projectId, action: 'activate', note: 'Kulanz' })
|
||||
|
||||
test('Team aktiviert einen bezahlten, abgeschalteten Terminplaner ohne laufendes Abo nicht: 409 mit Text, Zustand unveraendert', async () => {
|
||||
for (const abo of [null, 'hosting']) {
|
||||
const k = legeKunde({ abo })
|
||||
const vorher = { ...docById(k.docId) }
|
||||
const res = await aktivieren(k)
|
||||
assert.equal(res.status, 409, `abo ${abo}`)
|
||||
assert.equal(res.body.error, KEIN_ABO)
|
||||
assert.deepEqual(docById(k.docId), vorher, 'kein Schreibzugriff: Zustand, Grund, Frist und Notiz bleiben')
|
||||
}
|
||||
// auch ein bezahlter Terminplaner, den das Team abgeschaltet hat, und ein ausgebliebenes Abo nach Kuendigung (canceled)
|
||||
const team_aus = legeKunde({ over: { disabledReason: 'team', aboEndeMailAt: null } })
|
||||
assert.equal((await aktivieren(team_aus)).status, 409)
|
||||
assert.equal(docById(team_aus.docId).state, 'disabled')
|
||||
const beendet = legeKunde({ abo: 'wartung', aboOver: { status: 'canceled', endedAt: '2026-10-01T10:00:00.000Z' } })
|
||||
assert.equal((await aktivieren(beendet)).status, 409)
|
||||
assert.equal(docById(beendet.docId).state, 'disabled')
|
||||
})
|
||||
|
||||
test('Bezahlter Terminplaner in Einrichtung (setup) ohne Abo: das Team kann ihn auch nicht von Hand freischalten', async () => {
|
||||
const k = legeKunde({ over: { state: 'setup', disabledReason: null, disabledAt: null, aboEndeMailAt: null, adminNote: '' } })
|
||||
const res = await aktivieren(k)
|
||||
assert.equal(res.status, 409)
|
||||
assert.equal(res.body.error, KEIN_ABO)
|
||||
assert.equal(docById(k.docId).state, 'setup')
|
||||
})
|
||||
|
||||
test('Mit laufendem Wartungs-Abo aktiviert das Team den bezahlten Terminplaner: active, Grund und Frist leer, Marker leer', async () => {
|
||||
const k = legeKunde({ abo: 'wartung' })
|
||||
const res = await aktivieren(k)
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
const doc = docById(k.docId)
|
||||
assert.deepEqual([doc.state, doc.enabled, doc.disabledReason, doc.disabledAt, doc.aboEndeMailAt], ['active', true, null, null, null])
|
||||
assert.equal(doc.unlockedByPurchase, true, 'der Kauf bleibt vermerkt')
|
||||
assert.equal(featuresOf(res, k.projectId).termine.state, 'active')
|
||||
// eine vorgemerkte Kuendigung laesst das Abo bis zum Ende laufen
|
||||
const gekuendigt = legeKunde({ abo: 'wartung', aboOver: { cancelAtPeriodEnd: true } })
|
||||
assert.equal((await aktivieren(gekuendigt)).status, 200)
|
||||
assert.equal(docById(gekuendigt.docId).state, 'active')
|
||||
})
|
||||
|
||||
test('Wechsel widerrufen (Spiegel steht noch auf Wartung): 409, denn das Wartungs-Abo laeuft nicht', async () => {
|
||||
const k = legeKunde({ abo: 'wartung' })
|
||||
aw.seed('contractEvents', {
|
||||
$id: `ev_${k.customerId}`, customerId: k.customerId, subscriptionId: `sub_ka${kaNr}`, type: 'wechsel', productKey: 'wartung', details: '{}', status: 'widerrufen',
|
||||
})
|
||||
const res = await aktivieren(k)
|
||||
assert.equal(res.status, 409)
|
||||
assert.equal(res.body.error, KEIN_ABO)
|
||||
assert.equal(docById(k.docId).state, 'disabled')
|
||||
})
|
||||
|
||||
test('Ohne Kauf (unlockedByPurchase false: Pilot, Kulanz) braucht die Aktivierung kein Abo und liest es nicht', async () => {
|
||||
for (const unlockedByPurchase of [false, undefined]) {
|
||||
const k = legeKunde({ over: { unlockedByPurchase, purchaseStatus: '', disabledReason: 'team', aboEndeMailAt: null } })
|
||||
const vorher = aw.listCalls('subscriptions').length
|
||||
const res = await aktivieren(k)
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
assert.equal(docById(k.docId).state, 'active')
|
||||
assert.equal(aw.listCalls('subscriptions').length, vorher, 'keine Abo-Abfrage')
|
||||
}
|
||||
// kein Dokument bisher (ensureFeatureDoc legt es an): ebenfalls frei
|
||||
aw.seed('websiteProjects', { $id: 'kap_neu', customerId: 'ka_neu', projectName: 'Neu' })
|
||||
const neu = await team('ka_neu', 'termine', { projectId: 'kap_neu', action: 'activate' })
|
||||
assert.equal(neu.status, 200)
|
||||
})
|
||||
|
||||
test('News ist nicht betroffen: auch mit unlockedByPurchase und ohne Abo aktiviert das Team', async () => {
|
||||
const k = legeKunde({ key: 'news', over: { disabledReason: 'team', aboEndeMailAt: null } })
|
||||
const res = await aktivieren(k, 'news')
|
||||
assert.equal(res.status, 200, JSON.stringify(res.body))
|
||||
assert.equal(docById(k.docId).state, 'active')
|
||||
})
|
||||
|
||||
test('Abo nicht lesbar: Fehler (5xx), nicht aktiviert; danach geht es mit lesbarem Abo', async (t) => {
|
||||
const log = t.mock.method(console, 'error', () => {})
|
||||
const k = legeKunde({ abo: 'wartung' })
|
||||
const vorher = { ...docById(k.docId) }
|
||||
aw.failNext({ collection: 'subscriptions', method: 'GET', status: 500, message: 'kaputt' })
|
||||
const res = await aktivieren(k)
|
||||
assert.ok(res.status >= 500 && res.status < 600, `Status ${res.status}`)
|
||||
assert.equal(typeof res.body.error, 'string')
|
||||
assert.deepEqual(docById(k.docId), vorher, 'nicht aktiviert')
|
||||
assert.ok(log.mock.calls.length >= 1, 'der Fehler steht im Log')
|
||||
assert.equal((await aktivieren(k)).status, 200)
|
||||
assert.equal(docById(k.docId).state, 'active')
|
||||
})
|
||||
|
||||
test('Ereignisse nicht lesbar (Wechsel-Regel): Fehler, nicht aktiviert', async (t) => {
|
||||
t.mock.method(console, 'error', () => {})
|
||||
const k = legeKunde({ abo: 'wartung' })
|
||||
aw.failNext({ collection: 'contractEvents', method: 'GET', status: 500 })
|
||||
const res = await aktivieren(k)
|
||||
assert.ok(res.status >= 500 && res.status < 600, `Status ${res.status}`)
|
||||
assert.equal(docById(k.docId).state, 'disabled')
|
||||
})
|
||||
|
||||
test('adminActivate (Dienst): bezahlter Terminplaner ohne Abo => Fehler 409, bei laufendem Abo und fuer Dokumente ohne Kauf aktiv', async () => {
|
||||
const k = legeKunde()
|
||||
await assert.rejects(() => sf.adminActivate(k.customerId, k.projectId, 'termine', { by: 'test' }), { status: 409, message: KEIN_ABO })
|
||||
assert.equal(docById(k.docId).state, 'disabled')
|
||||
const mit = legeKunde({ abo: 'wartung' })
|
||||
assert.equal((await sf.adminActivate(mit.customerId, mit.projectId, 'termine', { by: 'test' })).state, 'active')
|
||||
const pilot = legeKunde({ over: { unlockedByPurchase: false } })
|
||||
assert.equal((await sf.adminActivate(pilot.customerId, pilot.projectId, 'termine', { by: 'test' })).state, 'active')
|
||||
})
|
||||
Reference in new issue
Block a user