diff --git a/server/routes/team/customers.js b/server/routes/team/customers.js index 0871c13..7be8a0c 100644 --- a/server/routes/team/customers.js +++ b/server/routes/team/customers.js @@ -1,6 +1,7 @@ import { Router } from 'express' import { config } from '../../config.js' import { listViewLog } from '../../services/customerViewLog.js' +import { getLedger } from '../../services/hourLedger.js' import { getPortalAccessStatus } from '../../services/portalAccessStatus.js' import { PRODUCT_KEYS } from '../../services/products.js' import { ACTIVE_STATUSES, getCurrentSubscription } from '../../services/stripeBilling.js' @@ -53,6 +54,30 @@ router.get('/:id/abo', async (req, res) => { } }) +// Stundenkonto fuers Ticketsystem (Abo-Modell der Funktionen): Produkt wie /:id/abo, dazu Guthaben, Vorschuss +// (Einrichtung ohne Deckung) und Mehraufwand. Nur das Wartungs-Abo hat ein Konto, auch nach dem Ende +// (dann Guthaben 0, der Vorschuss bleibt stehen); Hosting und kein Abo ergeben ledger null +router.get('/:id/stundenkonto', async (req, res) => { + try { + const sub = await getCurrentSubscription(req.params.id) + if (!sub) return res.json({ produkt: null, ledger: null }) + const produkt = PRODUCT_KEYS.includes(sub.productKey) ? sub.productKey : 'wartung' + if (produkt !== 'wartung') return res.json({ produkt, ledger: null }) + const l = await getLedger(req.params.id, { active: ACTIVE_STATUSES.has(sub.status) }) + return res.json({ + produkt, + ledger: { + balanceMinutes: l.balanceMinutes, + vorschussMinutes: l.vorschussMinutes, + overageMinutes: l.overageMinutes, + nextExpiry: l.nextExpiry, + }, + }) + } catch (err) { + return res.status(500).json({ error: err.message || 'Stundenkonto konnte nicht geladen werden' }) + } +}) + // Kundenansicht oeffnen: Einmal-Link (60 s), den das Ticketsystem in einem neuen Tab aufruft router.post('/:id/kundenansicht', async (req, res) => { try { diff --git a/test/teamStundenkonto.test.js b/test/teamStundenkonto.test.js new file mode 100644 index 0000000..19b701a --- /dev/null +++ b/test/teamStundenkonto.test.js @@ -0,0 +1,36 @@ +import { test, after } from 'node:test' +import assert from 'node:assert/strict' +import { startAppwriteMock } from './support/appwriteMock.js' +import { startFakeStripe } from './support/fakeStripe.js' +import { useTestEnv } from './support/testEnv.js' + +// Team-Endpunkt Stundenkonto fuers Ticketsystem: Produkt, Guthaben, Vorschuss, Mehraufwand +const aw = await startAppwriteMock() +const st = await startFakeStripe() +useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url }) +const { startTestApp, rawGet } = await import('./support/testApp.js') +const { seedFixtures } = await import('./support/fixtures.js') +const { default: teamRoutes } = await import('../server/routes/team/index.js') +const { updateDocument } = await import('../server/services/appwriteAdmin.js') +const app = await startTestApp({ '/api/team': teamRoutes }) +after(() => Promise.all([app.close(), aw.close(), st.close()])) + +seedFixtures(aw) +const auth = { headers: { authorization: 'Bearer jwt-staff' } } + +test('Wartung: Guthaben, Vorschuss und Mehraufwand aus dem Stundenkonto', async () => { + aw.seed('subscriptions', { $id: 'sub_t', customerId: 'c3', status: 'active', productKey: 'wartung', storageUnits: 0, startedAt: '2026-09-01T10:00:00.000Z' }) + aw.seed('hourCredits', { $id: 'h1', customerId: 'c3', minutes: 60, grantedAt: '2026-09-01T00:00:00.000Z', expiresAt: '2099-01-01T00:00:00.000Z' }) + aw.seed('workorders', { $id: 'wo_t', customerId: 'c3', woid: '100401', topic: 'Einrichtung', einrichtungAbrechnung: 'abo' }) + aw.seed('worksheets', { $id: 'ws_t', workorderId: 'wo_t', wsid: 'ws_t', countsToPlan: true, totalTime: 90, startDate: '10.09.2026' }) + const res = await rawGet('/api/team/customers/c3/stundenkonto', auth) + assert.deepEqual([res.status, res.body], [200, { produkt: 'wartung', ledger: { balanceMinutes: 0, vorschussMinutes: 30, overageMinutes: 0, nextExpiry: null } }]) +}) +test('Hosting: kein Konto; ohne Abo null; beendetes Wartungs-Abo behält den Vorschuss, Guthaben 0; ohne JWT 401', async () => { + assert.deepEqual((await rawGet('/api/team/customers/c1/stundenkonto', auth)).body, { produkt: 'hosting', ledger: null }) // c1: Hosting aus seedFixtures + assert.deepEqual((await rawGet('/api/team/customers/c4/stundenkonto', auth)).body, { produkt: null, ledger: null }) + await updateDocument('subscriptions', 'sub_t', { status: 'canceled' }) // aw.docs() liefert Kopien: ueber die API aendern + const ende = (await rawGet('/api/team/customers/c3/stundenkonto', auth)).body + assert.deepEqual([ende.produkt, ende.ledger.vorschussMinutes, ende.ledger.balanceMinutes], ['wartung', 30, 0]) + assert.equal((await rawGet('/api/team/customers/c3/stundenkonto')).status, 401) +})