Kundenansicht P6: Team-Endpunkt Stundenkonto mit Vorschuss

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-16 16:24:31 +00:00
1 parent f016d7ae6c
commit 3d4d49de82
2 files changed
+61

No files matched your search

+25
View File
@@ -1,6 +1,7 @@
import { Router } from 'express' import { Router } from 'express'
import { config } from '../../config.js' import { config } from '../../config.js'
import { listViewLog } from '../../services/customerViewLog.js' import { listViewLog } from '../../services/customerViewLog.js'
import { getLedger } from '../../services/hourLedger.js'
import { getPortalAccessStatus } from '../../services/portalAccessStatus.js' import { getPortalAccessStatus } from '../../services/portalAccessStatus.js'
import { PRODUCT_KEYS } from '../../services/products.js' import { PRODUCT_KEYS } from '../../services/products.js'
import { ACTIVE_STATUSES, getCurrentSubscription } from '../../services/stripeBilling.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 // Kundenansicht oeffnen: Einmal-Link (60 s), den das Ticketsystem in einem neuen Tab aufruft
router.post('/:id/kundenansicht', async (req, res) => { router.post('/:id/kundenansicht', async (req, res) => {
try { try {
+36
View File
@@ -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)
})