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
319 lines
8.6 KiB
TypeScript
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;
|