GET /api/team/customers/:id/stundenkonto kannte nur Produkt und Konto; WOMS (stundenkontoView.js) kann bei beendetem Abo nicht "verfällt, wird nicht berechnet" (V8, E17) statt "wird mit den nächsten Gutschriften verrechnet" zeigen, weil das Feld active fehlte. Jetzt trägt jede Antwort active (Abo läuft: Status active, trialing oder past_due, auch mit vorgemerkter Kündigung; ohne Abo false). WOMS liest antwort.active schon, nur ein ausdrückliches false ändert den Satz. Test in teamStundenkonto.test.js. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
99 lines
4.7 KiB
JavaScript
99 lines
4.7 KiB
JavaScript
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'
|
|
import { serviceStartOf } from '../../services/takeoverCredits.js'
|
|
import { createStaffPreviewToken } from '../../services/staffPreviewTokens.js'
|
|
|
|
// Kunden im Team-Eingang (/api/team/customers), requireStaffJwt sitzt in routes/team/index.js
|
|
const router = Router()
|
|
|
|
// Portal-Zugang und letzter Login (Karte "Portal-Zugang" im Ticketsystem)
|
|
router.get('/:id/portal', async (req, res) => {
|
|
try {
|
|
const { customer, hasAccess, lastLoginAt } = await getPortalAccessStatus(req.params.id)
|
|
if (!customer) return res.status(404).json({ error: 'Kunde nicht gefunden' })
|
|
return res.json({ hasAccess, lastLoginAt })
|
|
} catch (err) {
|
|
return res.status(500).json({ error: err.message || 'Portal-Zugang konnte nicht geladen werden' })
|
|
}
|
|
})
|
|
|
|
// Protokoll der Kundenansicht (Reiter "Protokoll"), neueste zuerst
|
|
router.get('/:id/protokoll', async (req, res) => {
|
|
try {
|
|
return res.json({ entries: await listViewLog(req.params.id) })
|
|
} catch (err) {
|
|
return res.status(500).json({ error: err.message || 'Protokoll konnte nicht geladen werden' })
|
|
}
|
|
})
|
|
|
|
// Abo-Status (Kunden-Reiter "Abo & Angebote", Stunden-Pruefung am Ticket): productKey/active wie die Admin-Route
|
|
// /api/admin/customers/:id/abo, dazu Laufzeit und Speicher; nur der Appwrite-Spiegel, kein Stripe
|
|
router.get('/:id/abo', async (req, res) => {
|
|
try {
|
|
const sub = await getCurrentSubscription(req.params.id)
|
|
if (!sub) {
|
|
return res.json({ productKey: null, active: false, status: null, storageUnits: 0, startedAt: null, serviceStart: null, currentPeriodEnd: null, cancelAt: null })
|
|
}
|
|
return res.json({
|
|
productKey: PRODUCT_KEYS.includes(sub.productKey) ? sub.productKey : 'wartung',
|
|
active: ACTIVE_STATUSES.has(sub.status),
|
|
status: sub.status || null,
|
|
storageUnits: Number(sub.storageUnits) || 0,
|
|
startedAt: sub.startedAt || null,
|
|
serviceStart: serviceStartOf(sub),
|
|
currentPeriodEnd: sub.currentPeriodEnd || null,
|
|
cancelAt: sub.cancelAt || null,
|
|
})
|
|
} catch (err) {
|
|
return res.status(err.status || 500).json({ error: err.message || 'Abo konnte nicht geladen werden' })
|
|
}
|
|
})
|
|
|
|
// 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.
|
|
// active (E-Fix W1): das Abo laeuft (Status active, trialing oder past_due, auch mit vorgemerkter Kuendigung); ohne Abo false. WOMS zeigt
|
|
// bei active false zum Vorschuss "verfaellt, wird nicht berechnet" (V8, E17) statt "wird mit den naechsten Gutschriften verrechnet".
|
|
router.get('/:id/stundenkonto', async (req, res) => {
|
|
try {
|
|
const sub = await getCurrentSubscription(req.params.id)
|
|
if (!sub) return res.json({ produkt: null, active: false, ledger: null })
|
|
const active = ACTIVE_STATUSES.has(sub.status)
|
|
const produkt = PRODUCT_KEYS.includes(sub.productKey) ? sub.productKey : 'wartung'
|
|
if (produkt !== 'wartung') return res.json({ produkt, active, ledger: null })
|
|
const l = await getLedger(req.params.id, { active })
|
|
return res.json({
|
|
produkt,
|
|
active,
|
|
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 {
|
|
const { customer, hasAccess } = await getPortalAccessStatus(req.params.id)
|
|
if (!customer) return res.status(404).json({ error: 'Kunde nicht gefunden' })
|
|
if (!hasAccess) return res.status(409).json({ code: 'no_portal_access', error: 'Kein Portal-Zugang' })
|
|
const token = createStaffPreviewToken({ purpose: 'kundenansicht', customerId: customer.$id, staff: req.staff })
|
|
return res.json({ openUrl: `${config.publicBaseUrl}/api/auth/kundenansicht?token=${encodeURIComponent(token)}` })
|
|
} catch (err) {
|
|
return res.status(500).json({ error: err.message || 'Kundenansicht konnte nicht geöffnet werden' })
|
|
}
|
|
})
|
|
|
|
export default router
|