WOMS fragt nach einer Team-Sperre die Abrechnung nicht mehr ab und sendet keine; der Server setzte dann pauschal 'abo', pruefeAboDeckung warf
409 abo_fehlt, und eine in der Aufbauphase ('projekt', noch kein Abo) eingebaute Funktion liess sich nicht wieder einschalten.
Jetzt gilt bei disabled/team die am Dokument gespeicherte einrichtungAbrechnung, sonst wie bisher 'abo'; eine ausdrücklich gesendete
Abrechnung geht vor. Test in funktionenAdminAbo.test.js (Aufbauphase: ein, aus, ohne Angabe wieder ein = 200 mit warnung; mit abo hart).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
136 lines
12 KiB
JavaScript
136 lines
12 KiB
JavaScript
import { test, after, beforeEach, afterEach, mock } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import { startAppwriteMock } from './support/appwriteMock.js'
|
|
import { startFakeStripe } from './support/fakeStripe.js'
|
|
import { mailSink } from './support/mailSink.js'
|
|
import { useTestEnv } from './support/testEnv.js'
|
|
import { schemaKeys } from './support/schema.js'
|
|
|
|
// Team-Aktivierung im Abo-Modell (P6 Task 7, V10): POST /api/team/funktionen/:customerId/:key mit abrechnung. Pilotkunde c1 (Wartung),
|
|
// c2 außerhalb der neuen Ansicht. Aufbauphase (E9, N28): projekt ohne jedes bisherige Abo ergibt 200 mit warnung abo_fehlt.
|
|
const aw = await startAppwriteMock({
|
|
schema: {
|
|
portalFeatures: schemaKeys('portalFeatures'),
|
|
subscriptions: schemaKeys('subscriptions'),
|
|
contractEvents: schemaKeys('contractEvents'),
|
|
featureConsents: schemaKeys('featureConsents'),
|
|
},
|
|
})
|
|
const st = await startFakeStripe()
|
|
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
|
const { config } = await import('../server/config.js')
|
|
const { startTestApp, post } = await import('./support/testApp.js')
|
|
const { hostingDoc, seedFixtures, subscriptionDoc, wartungDoc } = await import('./support/fixtures.js')
|
|
const { default: teamRoutes } = await import('../server/routes/team/index.js')
|
|
const { deleteDocument, updateDocument } = await import('../server/services/appwriteAdmin.js')
|
|
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
|
const { ABRECHNUNGS_ZEILEN } = await import('../server/services/ticketAdmin.js')
|
|
const app = await startTestApp({ '/api/team': teamRoutes })
|
|
const mails = mailSink()
|
|
setMailTransportForTests(mails)
|
|
const MODUS_SHOP = { mode: config.shop.mode, pilotEmails: [...config.shop.pilotEmails] }
|
|
const MODUS_NEU = { mode: config.kundenansichtNeu.mode, pilotEmails: [...config.kundenansichtNeu.pilotEmails] }
|
|
after(() => {
|
|
config.shop.mode = MODUS_SHOP.mode
|
|
config.shop.pilotEmails = MODUS_SHOP.pilotEmails
|
|
config.kundenansichtNeu.mode = MODUS_NEU.mode
|
|
config.kundenansichtNeu.pilotEmails = MODUS_NEU.pilotEmails
|
|
setMailTransportForTests(null)
|
|
return Promise.all([app.close(), aw.close(), st.close()])
|
|
})
|
|
|
|
const auth = { headers: { authorization: 'Bearer jwt-staff' } }
|
|
const go = (cid, key, body = {}, pid = 'p1') => post(`/api/team/funktionen/${cid}/${key}`, { projectId: pid, action: 'activate', ...body }, auth)
|
|
const feat = (key, pid = 'p1') => aw.docs('portalFeatures').get(`f_${pid}_${key}`)
|
|
const nichts = () => assert.deepEqual([aw.docs('portalFeatures').size, aw.docs('workorders').size, aw.docs('featureConsents').size, mails.sent.length], [0, 0, 0, 0])
|
|
// aw.docs() liefert eine Kopie: Dokumente werden ueber die API geloescht
|
|
const leere = async (...collections) => {
|
|
for (const col of collections) for (const id of aw.docs(col).keys()) await deleteDocument(col, id)
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
await leere('portalFeatures', 'workorders', 'featureConsents', 'subscriptions', 'websiteProjects')
|
|
mails.sent.length = 0
|
|
seedFixtures(aw)
|
|
aw.seed('employees', { $id: 'e_staff2', userId: 'u_staff2', displayName: 'Andrej', email: 'andrej@webklar.com' })
|
|
aw.seed('subscriptions', subscriptionDoc('sub_h', wartungDoc))
|
|
for (const [id, customerId] of [['p1', 'c1'], ['p2', 'c1'], ['p3', 'c2']]) {
|
|
aw.seed('websiteProjects', { $id: id, customerId, projectName: `Projekt ${id}`, subdomain: id, repoFullName: `JUSN/${id}`, previewMode: 'auto', previewDetected: 'static' })
|
|
}
|
|
config.kundenansichtNeu.mode = 'pilot'
|
|
config.kundenansichtNeu.pilotEmails = ['k@example.invalid']
|
|
mock.method(console, 'error', () => {})
|
|
mock.method(console, 'warn', () => {})
|
|
mock.method(console, 'log', () => {})
|
|
})
|
|
afterEach(() => mock.restoreAll())
|
|
|
|
test('Pilotkunde mit Wartung: setup, unlockedBy abo, Ticket mit Abo-Zeile, K38, kein T24; im Projekt: projekt', async () => {
|
|
const r = await go('c1', 'news'); assert.equal(r.status, 200, JSON.stringify(r.body)); assert.equal(r.body.warnung, undefined) // Abo deckt: keine Warnung
|
|
const d = feat('news'); assert.deepEqual([d.state, d.unlockedBy, d.einrichtungAbrechnung, d.unlockedByPurchase], ['setup', 'abo', 'abo', false])
|
|
const t = aw.docs('workorders').get(d.ticketId); assert.deepEqual([t.einrichtungAbrechnung, t.details.endsWith(ABRECHNUNGS_ZEILEN.abo)], ['abo', true])
|
|
assert.deepEqual([mails.sent.filter((m) => /^Deine Funktion wird eingerichtet/.test(m.subject)).length, mails.sent.filter((m) => m.subject.startsWith('[Abo]')).length], [1, 0])
|
|
assert.equal((await go('c1', 'shop', { abrechnung: 'projekt' })).status, 200); const s = feat('shop'); assert.deepEqual([s.unlockedBy, s.einrichtungAbrechnung], ['projekt', 'projekt'])
|
|
assert.ok(aw.docs('workorders').get(s.ticketId).details.endsWith(ABRECHNUNGS_ZEILEN.projekt))
|
|
})
|
|
test('kein deckendes Abo: 409 abo_fehlt mit Grund, nichts geschrieben (abo immer; projekt, sobald der Kunde ein Abo hat oder hatte); ungültige Abrechnung 400', async () => {
|
|
for (const [abo, grund, wechsel] of [[hostingDoc, 'falsches_abo', true], [{ ...wartungDoc, cancelAtPeriodEnd: true }, 'gekuendigt', false], [{ ...wartungDoc, status: 'canceled' }, 'kein_abo', false]]) {
|
|
await leere('subscriptions'); aw.seed('subscriptions', subscriptionDoc('sub_h', abo))
|
|
for (const abrechnung of ['abo', 'projekt']) { const r = await go('c1', 'termine', { abrechnung }); assert.deepEqual([r.status, r.body.code, r.body.grund, r.body.wechsel, typeof r.body.error], [409, 'abo_fehlt', grund, wechsel, 'string'], `${grund} ${abrechnung}`) }
|
|
nichts()
|
|
}
|
|
await leere('subscriptions') // noch nie ein Abo: aus dem Abo ist das Stundenkonto nötig, also hart
|
|
const ohne = await go('c1', 'termine'); assert.deepEqual([ohne.status, ohne.body.code, ohne.body.grund], [409, 'abo_fehlt', 'kein_abo']); nichts()
|
|
assert.equal((await go('c1', 'news', { abrechnung: 'frei' })).status, 400); nichts()
|
|
})
|
|
test('Projektstart ohne jedes Abo (E9, Aufbauphase): 200 mit warnung abo_fehlt, Funktion in setup mit Ticket; abgebrochene Bestellung zählt nicht; mit deckendem Abo keine Warnung', async () => {
|
|
await leere('subscriptions'); aw.seed('subscriptions', subscriptionDoc('sub_i', { ...wartungDoc, status: 'incomplete' }))
|
|
const r = await go('c1', 'shop', { abrechnung: 'projekt' }); assert.equal(r.status, 200, JSON.stringify(r.body)); assert.deepEqual([r.body.warnung, r.body.grund], ['abo_fehlt', 'kein_abo'])
|
|
const d = feat('shop'); assert.deepEqual([d.state, d.unlockedBy, d.einrichtungAbrechnung], ['setup', 'projekt', 'projekt']); assert.ok(aw.docs('workorders').get(d.ticketId))
|
|
assert.equal(mails.sent.filter((m) => /^Deine Funktion wird eingerichtet/.test(m.subject)).length, 1) // K38 mit dem Satz zur Bedingung (Task 3)
|
|
await leere('subscriptions'); const z = await go('c1', 'news', { abrechnung: 'projekt' }, 'p2'); assert.deepEqual([z.status, z.body.warnung], [200, 'abo_fehlt'])
|
|
aw.seed('subscriptions', subscriptionDoc('sub_h', wartungDoc)); const ok = await go('c1', 'news', { abrechnung: 'projekt' }); assert.deepEqual([ok.status, ok.body.warnung, ok.body.grund], [200, undefined, undefined])
|
|
})
|
|
test('Kunde außerhalb der neuen Ansicht: wie heute direkt aktiv, abrechnung im Body wird ignoriert', async () => {
|
|
const r = await go('c2', 'news', { abrechnung: 'projekt' }, 'p3'); assert.equal(r.status, 200)
|
|
const d = feat('news', 'p3'); assert.deepEqual([d.state, d.enabled, d.ticketId, d.unlockedBy, d.einrichtungAbrechnung], ['active', true, undefined, undefined, undefined]); assert.equal(aw.docs('workorders').size, 0)
|
|
config.kundenansichtNeu.mode = 'off'; await leere('portalFeatures'); assert.equal(feat('news'), undefined)
|
|
assert.equal((await go('c1', 'news')).status, 200); assert.equal(feat('news').state, 'active'); config.kundenansichtNeu.mode = 'pilot'
|
|
})
|
|
test('Bestand und vom Team abgeschaltet: kauf wie heute, disabled/team direkt aktiv (mit Abo), sonst 409', async () => {
|
|
aw.seed('portalFeatures', { $id: 'f_p2_termine', customerId: 'c1', projectId: 'p2', featureKey: 'termine', state: 'disabled', disabledReason: 'abo_ende', unlockedBy: 'kauf', unlockedByPurchase: true })
|
|
await leere('subscriptions'); const alt = await go('c1', 'termine', {}, 'p2'); assert.deepEqual([alt.status, alt.body.code, /Wartungs-Abo des Kunden läuft nicht/.test(alt.body.error)], [409, undefined, true])
|
|
aw.seed('portalFeatures', { $id: 'f_p1_shop', customerId: 'c1', projectId: 'p1', featureKey: 'shop', state: 'disabled', disabledReason: 'team', unlockedBy: 'abo', einrichtungAbrechnung: 'abo', activatedAt: '2026-10-01T09:00:00.000Z' })
|
|
assert.equal((await go('c1', 'shop')).body.code, 'abo_fehlt'); aw.seed('subscriptions', subscriptionDoc('sub_h', wartungDoc))
|
|
assert.equal((await go('c1', 'shop')).status, 200); assert.deepEqual([feat('shop').state, aw.docs('workorders').size], ['active', 0])
|
|
})
|
|
test('Wiedereinschalten nach Team-Sperre ohne gesendete abrechnung: die am Dokument gespeicherte Abrechnung gilt, nicht pauschal abo (F4)', async () => {
|
|
// Aufbauphase: der Kunde hatte nie ein Abo, das Team schaltet den Webshop 'im Projekt enthalten' ein, ab und wieder ein; WOMS fragt nach einer
|
|
// Team-Sperre die Abrechnung nicht mehr ab (die Abrechnung steht schon) und sendet keine abrechnung
|
|
await leere('subscriptions')
|
|
const an = await go('c1', 'shop', { abrechnung: 'projekt' }); assert.deepEqual([an.status, an.body.warnung], [200, 'abo_fehlt'], JSON.stringify(an.body))
|
|
const aus = await go('c1', 'shop', { action: 'disable' }); assert.equal(aus.status, 200, JSON.stringify(aus.body))
|
|
assert.deepEqual([feat('shop').state, feat('shop').disabledReason, feat('shop').einrichtungAbrechnung], ['disabled', 'team', 'projekt'])
|
|
const wieder = await go('c1', 'shop'); assert.equal(wieder.status, 200, JSON.stringify(wieder.body))
|
|
assert.deepEqual([wieder.body.warnung, wieder.body.grund, feat('shop').state], ['abo_fehlt', 'kein_abo', 'active'])
|
|
// wieder aus; eine ausdrücklich gesendete Abrechnung gilt vor der gespeicherten: abo ohne Abo bleibt hart
|
|
assert.equal((await go('c1', 'shop', { action: 'disable' })).status, 200)
|
|
const hart = await go('c1', 'shop', { abrechnung: 'abo' }); assert.deepEqual([hart.status, hart.body.code, hart.body.grund], [409, 'abo_fehlt', 'kein_abo']); assert.equal(feat('shop').state, 'disabled')
|
|
// gespeichert abo (oder ohne Angabe): ohne gesendete Abrechnung gilt abo wie bisher
|
|
await updateDocument('portalFeatures', 'f_p1_shop', { einrichtungAbrechnung: 'abo', unlockedBy: 'abo' })
|
|
const abo = await go('c1', 'shop'); assert.deepEqual([abo.status, abo.body.code, abo.body.grund], [409, 'abo_fehlt', 'kein_abo'])
|
|
await updateDocument('portalFeatures', 'f_p1_shop', { einrichtungAbrechnung: '' })
|
|
assert.equal((await go('c1', 'shop')).body.code, 'abo_fehlt')
|
|
})
|
|
test('Textform: Nachweis vor dem Zustand, K38 mit PDF-Anhängen, kein K16; Aktion ticket legt das Ticket ohne Stripe nach', async () => {
|
|
config.shop.mode = 'on'; config.shop.pilotEmails = []
|
|
const text = { textformDate: '2026-10-10', textformChannel: 'whatsapp' }
|
|
aw.failNext({ collection: 'featureConsents', method: 'POST', status: 500 }) // Nachweis scheitert: Funktion bleibt aus, kein Ticket, keine Mail
|
|
assert.ok((await go('c1', 'shop', text)).status >= 500); assert.deepEqual([feat('shop')?.state ?? 'off', aw.docs('workorders').size, mails.sent.length], ['off', 0, 0])
|
|
const r = await go('c1', 'shop', text); assert.equal(r.status, 200, JSON.stringify(r.body))
|
|
const n = [...aw.docs('featureConsents').values()].find((c) => c.featureKey === 'shop'); assert.match(n.konto, /^Mitarbeiter: /)
|
|
assert.ok(mails.sent.find((m) => /^Deine Funktion wird eingerichtet/.test(m.subject)).attachments.length >= 2); assert.equal(mails.sent.filter((m) => /ist bereit/.test(m.subject)).length, 0)
|
|
await deleteDocument('workorders', feat('shop').ticketId); await updateDocument('portalFeatures', 'f_p1_shop', { ticketId: '' }) // Ticket weg: nachlegen heilt über einschalten
|
|
const calls = st.calls.length; const t = await go('c1', 'shop', { action: 'ticket' }); assert.equal(t.status, 200, JSON.stringify(t.body)); assert.ok(feat('shop').ticketId); assert.equal(st.calls.length, calls)
|
|
})
|