frontend_nyla/src/hooks/useBilling.ts
ValueOn AG 9b99020686 feat(billing): Nutzerhinweise bei leerem Budget + Mandats-Mail (402/SSE)
Gateway
- InsufficientBalanceException: billingModel, userAction (TOP_UP_SELF /
  CONTACT_MANDATE_ADMIN), DE/EN-Texte, toClientDict(), fromBalanceCheck()
- HTTP 402 + JSON detail für globale API-Fehlerbehandlung
- AI/Chatbot: vor Raise ggf. E-Mail an BillingSettings.notifyEmails
  (PREPAY_MANDATE, Throttle 1h/Mandat) via billingExhaustedNotify
- Agent-Loop & Workspace-Route: SSE-ERROR mit strukturiertem Billing-Payload
- datamodelBilling: notifyEmails-Doku für Pool-Alerts
frontend_nyla
- useWorkspace: SSE onError für INSUFFICIENT_BALANCE mit messageDe/En
  und Hinweis auf Billing-Pfad bei TOP_UP_SELF
2026-03-21 01:34:47 +01:00

319 lines
8.6 KiB
TypeScript

/**
* useBilling Hook
*
* Hook für die Verwaltung von Billing-Daten.
* Bietet Zugriff auf Guthaben, Transaktionen, Statistiken und Admin-Funktionen.
*/
import { useState, useEffect, useCallback } from 'react';
import { useApiRequest } from './useApi';
import {
fetchBalances,
fetchBalanceForMandate,
fetchTransactions,
fetchStatistics,
fetchAllowedProviders,
fetchSettingsAdmin,
updateSettingsAdmin,
addCreditAdmin,
createCheckoutSession as createCheckoutSessionApi,
fetchAccountsAdmin,
fetchTransactionsAdmin,
fetchUsersForMandateAdmin,
type BillingBalance,
type BillingTransaction,
type BillingSettings,
type BillingSettingsUpdate,
type UsageReport,
type AccountSummary,
type CreditAddRequest,
type CheckoutCreateRequest,
type MandateUserSummary,
} from '../api/billingApi';
// Re-export types
export type {
BillingBalance,
BillingTransaction,
BillingSettings,
BillingSettingsUpdate,
UsageReport,
AccountSummary,
CreditAddRequest,
MandateUserSummary,
};
export type { BillingModel, TransactionType, ReferenceType } from '../api/billingApi';
/**
* Hook for user billing operations
*/
export function useBilling() {
const [balances, setBalances] = useState<BillingBalance[]>([]);
const [transactions, setTransactions] = useState<BillingTransaction[]>([]);
const [statistics, setStatistics] = useState<UsageReport | null>(null);
const [allowedProviders, setAllowedProviders] = useState<string[]>([]);
const { request, isLoading: loading, error } = useApiRequest();
// Fetch all balances for the user
const loadBalances = useCallback(async () => {
try {
const data = await fetchBalances(request);
setBalances(Array.isArray(data) ? data : []);
return data;
} catch (err) {
console.error('Error loading balances:', err);
setBalances([]);
return [];
}
}, [request]);
// Fetch balance for a specific mandate
const loadBalanceForMandate = useCallback(async (mandateId: string) => {
try {
return await fetchBalanceForMandate(request, mandateId);
} catch (err) {
console.error('Error loading balance for mandate:', err);
return null;
}
}, [request]);
// Fetch transactions
const loadTransactions = useCallback(async (limit: number = 50, offset: number = 0) => {
try {
const data = await fetchTransactions(request, limit, offset);
setTransactions(Array.isArray(data) ? data : []);
return data;
} catch (err) {
console.error('Error loading transactions:', err);
setTransactions([]);
return [];
}
}, [request]);
// Fetch statistics
const loadStatistics = useCallback(async (
period: 'day' | 'month' | 'year',
year: number,
month?: number
) => {
try {
const data = await fetchStatistics(request, period, year, month);
setStatistics(data);
return data;
} catch (err) {
console.error('Error loading statistics:', err);
setStatistics(null);
return null;
}
}, [request]);
// Fetch allowed providers
const loadAllowedProviders = useCallback(async () => {
try {
const data = await fetchAllowedProviders(request);
setAllowedProviders(Array.isArray(data) ? data : []);
return data;
} catch (err) {
console.error('Error loading allowed providers:', err);
setAllowedProviders([]);
return [];
}
}, [request]);
// Initial load
useEffect(() => {
loadBalances();
loadAllowedProviders();
}, []);
return {
balances,
transactions,
statistics,
allowedProviders,
loading,
error,
loadBalances,
loadBalanceForMandate,
loadTransactions,
loadStatistics,
loadAllowedProviders,
refetch: loadBalances,
};
}
/**
* Hook for admin billing operations
*/
export function useBillingAdmin(mandateId?: string) {
const [settings, setSettings] = useState<BillingSettings | null>(null);
const [accounts, setAccounts] = useState<AccountSummary[]>([]);
const [transactions, setTransactions] = useState<BillingTransaction[]>([]);
const [users, setUsers] = useState<MandateUserSummary[]>([]);
const { request, isLoading: loading, error } = useApiRequest();
// Fetch settings for a mandate
const loadSettings = useCallback(async (targetMandateId?: string) => {
const mId = targetMandateId || mandateId;
if (!mId) return null;
try {
const data = await fetchSettingsAdmin(request, mId);
setSettings(data);
return data;
} catch (err) {
console.error('Error loading billing settings:', err);
setSettings(null);
return null;
}
}, [request, mandateId]);
// Fetch accounts for a mandate
const loadAccounts = useCallback(async (targetMandateId?: string) => {
const mId = targetMandateId || mandateId;
if (!mId) return [];
try {
const data = await fetchAccountsAdmin(request, mId);
setAccounts(Array.isArray(data) ? data : []);
return data;
} catch (err) {
console.error('Error loading accounts:', err);
setAccounts([]);
return [];
}
}, [request, mandateId]);
// Fetch transactions for a mandate
const loadTransactions = useCallback(async (targetMandateId?: string, limit: number = 100) => {
const mId = targetMandateId || mandateId;
if (!mId) return [];
try {
const data = await fetchTransactionsAdmin(request, mId, limit);
setTransactions(Array.isArray(data) ? data : []);
return data;
} catch (err) {
console.error('Error loading transactions:', err);
setTransactions([]);
return [];
}
}, [request, mandateId]);
// Fetch users for a mandate
const loadUsers = useCallback(async (targetMandateId?: string) => {
const mId = targetMandateId || mandateId;
if (!mId) return [];
try {
const data = await fetchUsersForMandateAdmin(request, mId);
setUsers(Array.isArray(data) ? data : []);
return data;
} catch (err) {
console.error('Error loading users:', err);
setUsers([]);
return [];
}
}, [request, mandateId]);
// Update settings — after billing model change, reload dependent data (accounts / users / tx)
const saveSettings = useCallback(
async (settingsUpdate: BillingSettingsUpdate, targetMandateId?: string) => {
const mId = targetMandateId || mandateId;
if (!mId) return null;
const previousModel = settings?.billingModel;
try {
const data = await updateSettingsAdmin(request, mId, settingsUpdate);
setSettings(data);
const newModel = settingsUpdate.billingModel;
const modelChanged =
newModel !== undefined && newModel !== null && newModel !== previousModel;
if (modelChanged) {
await Promise.all([
loadAccounts(mId),
loadTransactions(mId, 100),
loadUsers(mId),
]);
}
return data;
} catch (err) {
console.error('Error saving billing settings:', err);
throw err;
}
},
[request, mandateId, settings?.billingModel, loadAccounts, loadTransactions, loadUsers]
);
// Add credit (manual, admin)
const addCredit = useCallback(
async (creditRequest: CreditAddRequest, targetMandateId?: string) => {
const mId = targetMandateId || mandateId;
if (!mId) return null;
try {
const result = await addCreditAdmin(request, mId, creditRequest);
await loadAccounts(mId);
return result;
} catch (err) {
console.error('Error adding credit:', err);
throw err;
}
},
[request, mandateId, loadAccounts]
);
// Create Stripe Checkout Session (returns redirect URL)
const createCheckout = useCallback(
async (checkoutRequest: CheckoutCreateRequest, targetMandateId?: string) => {
const mId = targetMandateId || mandateId;
if (!mId) return null;
try {
return await createCheckoutSessionApi(request, mId, checkoutRequest);
} catch (err) {
console.error('Error creating checkout session:', err);
throw err;
}
},
[request, mandateId]
);
// Load data when mandateId changes
useEffect(() => {
if (mandateId) {
loadSettings();
loadAccounts();
loadTransactions();
loadUsers();
}
}, [mandateId]);
return {
settings,
accounts,
transactions,
users,
loading,
error,
loadSettings,
saveSettings,
addCredit,
createCheckout,
loadAccounts,
loadTransactions,
loadUsers,
refetch: () => {
if (mandateId) {
loadSettings();
loadAccounts();
loadTransactions();
loadUsers();
}
},
};
}
export default useBilling;