import { config } from '../config.js' import { listDocuments, Query } from './appwriteAdmin.js' /** * Abo-Stundenkonto eines Kunden. * * Gutschriften: je bezahlter Abo-Rechnung 60 min (Collection hourCredits), gueltig 12 Monate. * Verbrauch: Worksheets mit countsToPlan=true auf Tickets des Kunden, je Eintrag * auf 15 min aufgerundet. * Verrechnung: FIFO - die aelteste noch gueltige Gutschrift wird zuerst verbraucht. * Was kein Guthaben mehr findet, ist Mehraufwand (separat abzurechnen). * Vertragsende: Restguthaben verfaellt (siehe Abo-Bedingungen Ziff. 4). */ function parseSheetDate(ws) { const raw = ws.startDate || ws.endDate || '' const m = /^(\d{2})\.(\d{2})\.(\d{4})$/.exec(raw) if (m) return new Date(`${m[3]}-${m[2]}-${m[1]}T12:00:00Z`) const d = new Date(raw || ws.createdAt || ws.$createdAt) return Number.isNaN(d.getTime()) ? new Date(ws.$createdAt) : d } function roundUsage(minutes) { const step = config.abo.usageRoundingMinutes return Math.ceil(Math.max(0, Number(minutes) || 0) / step) * step } export async function listCredits(customerId) { return listDocuments(config.collections.hourCredits, [ Query.equal('customerId', customerId), Query.orderAsc('grantedAt'), Query.limit(1000), ]) } export async function listPlanUsage(customerId) { const workorders = await listDocuments(config.collections.workorders, [ Query.equal('customerId', customerId), Query.limit(5000), ]) if (!workorders.length) return [] const byId = new Map(workorders.map((w) => [w.$id, w])) const ids = [...byId.keys()] const sheets = [] // Appwrite begrenzt die Werte-Liste pro equal-Query -> in Bloecken abfragen for (let i = 0; i < ids.length; i += 100) { const chunk = await listDocuments(config.collections.worksheets, [ Query.equal('workorderId', ids.slice(i, i + 100)), Query.equal('countsToPlan', true), Query.limit(5000), ]) sheets.push(...chunk) } return sheets .filter((ws) => Number(ws.totalTime) > 0) .map((ws) => { const wo = byId.get(ws.workorderId) || {} return { id: ws.$id, wsid: ws.wsid || '', woid: ws.woid || wo.woid || '', topic: wo.topic || wo.title || '', date: parseSheetDate(ws).toISOString(), rawMinutes: Number(ws.totalTime) || 0, minutes: roundUsage(ws.totalTime), details: String(ws.details || '').slice(0, 300), employee: ws.employeeName || ws.employeeShort || '', } }) .sort((a, b) => a.date.localeCompare(b.date)) } /** Reine Berechnung (testbar ohne DB). */ export function computeLedger(credits, usage, { now = new Date(), active = true } = {}) { const pool = credits .map((c) => ({ id: c.$id, minutes: Number(c.minutes) || 0, remaining: Number(c.minutes) || 0, grantedAt: c.grantedAt, expiresAt: c.expiresAt, })) .sort((a, b) => String(a.grantedAt).localeCompare(String(b.grantedAt))) let overageMinutes = 0 const entries = usage.map((u) => { let need = u.minutes const at = new Date(u.date) for (const c of pool) { if (!need) break // Nur Gutschriften, die zum Arbeitszeitpunkt noch gueltig waren if (c.remaining <= 0 || new Date(c.expiresAt) <= at) continue const take = Math.min(need, c.remaining) c.remaining -= take need -= take } overageMinutes += need return { ...u, coveredMinutes: u.minutes - need, overageMinutes: need } }) const valid = pool.filter((c) => c.remaining > 0 && new Date(c.expiresAt) > now) const expiredMinutes = pool .filter((c) => c.remaining > 0 && new Date(c.expiresAt) <= now) .reduce((s, c) => s + c.remaining, 0) const balanceMinutes = active ? valid.reduce((s, c) => s + c.remaining, 0) : 0 const nextExpiry = active && valid.length ? valid.reduce((a, c) => (new Date(c.expiresAt) < new Date(a.expiresAt) ? c : a)) : null return { balanceMinutes, grantedMinutes: pool.reduce((s, c) => s + c.minutes, 0), usedMinutes: entries.reduce((s, e) => s + e.coveredMinutes, 0), overageMinutes, expiredMinutes, nextExpiry: nextExpiry ? { minutes: nextExpiry.remaining, at: nextExpiry.expiresAt } : null, credits: pool.map((c) => ({ grantedAt: c.grantedAt, expiresAt: c.expiresAt, minutes: c.minutes, remaining: active && new Date(c.expiresAt) > now ? c.remaining : 0, expired: new Date(c.expiresAt) <= now, })), usage: entries.reverse(), } } export async function getLedger(customerId, { active = true } = {}) { const [credits, usage] = await Promise.all([listCredits(customerId), listPlanUsage(customerId)]) return computeLedger(credits, usage, { active }) }