Kundenansicht P6: Team-Endpunkt Stundenkonto mit Vorschuss
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f016d7ae6c
commit
3d4d49de82
2 files changed
+61
No files matched your search
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
Reference in new issue
Block a user