From d82bb4467c52c56f3bb061e5141518e76b017649 Mon Sep 17 00:00:00 2001 From: Ida Dittrich Date: Wed, 25 Feb 2026 08:45:12 +0100 Subject: [PATCH] feat: integrated stripe billing --- src/api/billingApi.ts | 25 +++++++++ src/hooks/useBilling.ts | 21 ++++++- src/pages/billing/BillingAdmin.tsx | 88 +++++++++++++++++++----------- 3 files changed, 100 insertions(+), 34 deletions(-) diff --git a/src/api/billingApi.ts b/src/api/billingApi.ts index 7c78496..5e25261 100644 --- a/src/api/billingApi.ts +++ b/src/api/billingApi.ts @@ -96,6 +96,15 @@ export interface CreditAddRequest { description?: string; } +export interface CheckoutCreateRequest { + userId?: string; + amount: number; +} + +export interface CheckoutCreateResponse { + redirectUrl: string; +} + // Type for the request function passed to API functions export type ApiRequestFunction = (options: ApiRequestOptions) => Promise; @@ -231,6 +240,22 @@ export async function addCreditAdmin( }); } +/** + * Create Stripe Checkout Session for credit top-up + * Endpoint: POST /api/billing/checkout/create/{mandateId} + */ +export async function createCheckoutSession( + request: ApiRequestFunction, + mandateId: string, + checkoutRequest: CheckoutCreateRequest +): Promise { + return await request({ + url: `/api/billing/checkout/create/${mandateId}`, + method: 'post', + data: checkoutRequest + }); +} + /** * Fetch all accounts for a mandate (Admin) * Endpoint: GET /api/billing/admin/accounts/{mandateId} diff --git a/src/hooks/useBilling.ts b/src/hooks/useBilling.ts index 8b79f77..15f20aa 100644 --- a/src/hooks/useBilling.ts +++ b/src/hooks/useBilling.ts @@ -16,6 +16,7 @@ import { fetchSettingsAdmin, updateSettingsAdmin, addCreditAdmin, + createCheckoutSession as createCheckoutSessionApi, fetchAccountsAdmin, fetchTransactionsAdmin, fetchUsersForMandateAdmin, @@ -26,6 +27,7 @@ import { type UsageReport, type AccountSummary, type CreditAddRequest, + type CheckoutCreateRequest, type MandateUserSummary, } from '../api/billingApi'; @@ -185,7 +187,7 @@ export function useBillingAdmin(mandateId?: string) { } }, [request, mandateId]); - // Add credit + // Add credit (manual, admin) const addCredit = useCallback(async ( creditRequest: CreditAddRequest, targetMandateId?: string @@ -204,6 +206,22 @@ export function useBillingAdmin(mandateId?: string) { } }, [request, mandateId]); + // 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]); + // Fetch accounts for a mandate const loadAccounts = useCallback(async (targetMandateId?: string) => { const mId = targetMandateId || mandateId; @@ -272,6 +290,7 @@ export function useBillingAdmin(mandateId?: string) { loadSettings, saveSettings, addCredit, + createCheckout, loadAccounts, loadTransactions, loadUsers, diff --git a/src/pages/billing/BillingAdmin.tsx b/src/pages/billing/BillingAdmin.tsx index 92a5825..7624f24 100644 --- a/src/pages/billing/BillingAdmin.tsx +++ b/src/pages/billing/BillingAdmin.tsx @@ -8,10 +8,13 @@ */ import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import { useSearchParams } from 'react-router-dom'; import { useBillingAdmin, type BillingSettings, type AccountSummary, type MandateUserSummary } from '../../hooks/useBilling'; import { useAdminMandates } from '../../hooks/useMandates'; import styles from './Billing.module.css'; +const STRIPE_AMOUNT_PRESETS = [10, 25, 50, 100, 250, 500]; + // ============================================================================ // MANDATE SELECTOR // ============================================================================ @@ -192,13 +195,12 @@ interface CreditAdderProps { settings: BillingSettings | null; accounts: AccountSummary[]; users: MandateUserSummary[]; - onAddCredit: (userId: string | undefined, amount: number, description: string) => Promise; + onCreateCheckout: (userId: string | undefined, amount: number) => Promise<{ redirectUrl: string }>; } -const CreditAdder: React.FC = ({ settings, accounts, users, onAddCredit }) => { +const CreditAdder: React.FC = ({ settings, accounts, users, onCreateCheckout }) => { const [selectedUserId, setSelectedUserId] = useState(''); const [amount, setAmount] = useState(10); - const [description, setDescription] = useState('Manuelles Aufladen'); const [saving, setSaving] = useState(false); const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); @@ -223,13 +225,10 @@ const CreditAdder: React.FC = ({ settings, accounts, users, on setMessage(null); try { - await onAddCredit(isPrepayUser ? selectedUserId : undefined, amount, description); - setMessage({ type: 'success', text: `${amount} CHF erfolgreich gutgeschrieben!` }); - setAmount(10); - setDescription('Manuelles Aufladen'); + const { redirectUrl } = await onCreateCheckout(isPrepayUser ? selectedUserId : undefined, amount); + window.location.href = redirectUrl; } catch (err: any) { setMessage({ type: 'error', text: err.message || 'Fehler beim Aufladen' }); - } finally { setSaving(false); } }; @@ -280,26 +279,18 @@ const CreditAdder: React.FC = ({ settings, accounts, users, on
- setAmount(Number(e.target.value))} - min="0.01" - step="0.01" required - /> -
- -
- - setDescription(e.target.value)} - placeholder="Grund für Gutschrift" - /> + > + {STRIPE_AMOUNT_PRESETS.map((preset) => ( + + ))} +
@@ -308,7 +299,7 @@ const CreditAdder: React.FC = ({ settings, accounts, users, on className={`${styles.button} ${styles.buttonPrimary}`} disabled={saving || (isPrepayUser && !selectedUserId)} > - {saving ? 'Aufladen...' : 'Guthaben aufladen'} + {saving ? 'Weiterleitung zu Stripe...' : 'Mit Stripe aufladen'} @@ -379,8 +370,18 @@ const AccountsOverview: React.FC = ({ accounts, users, lo // ============================================================================ export const BillingAdmin: React.FC = () => { + const [searchParams, setSearchParams] = useSearchParams(); const [selectedMandateId, setSelectedMandateId] = useState(null); - const { settings, accounts, users, loading, saveSettings, addCredit, loadAccounts } = useBillingAdmin(selectedMandateId || undefined); + const { settings, accounts, users, loading, saveSettings, createCheckout, loadAccounts } = useBillingAdmin(selectedMandateId || undefined); + + const successParam = searchParams.get('success'); + const canceledParam = searchParams.get('canceled'); + + useEffect(() => { + if (successParam === 'true' && selectedMandateId) { + loadAccounts(); + } + }, [successParam, selectedMandateId, loadAccounts]); const handleMandateSelect = (mandateId: string) => { setSelectedMandateId(mandateId || null); @@ -391,12 +392,20 @@ export const BillingAdmin: React.FC = () => { await saveSettings(settingsUpdate); }, [selectedMandateId, saveSettings]); - const handleAddCredit = useCallback(async (userId: string | undefined, amount: number, description: string) => { - if (!selectedMandateId) return; - await addCredit({ userId, amount, description }); - await loadAccounts(); - }, [selectedMandateId, addCredit, loadAccounts]); + const handleCreateCheckout = useCallback(async (userId: string | undefined, amount: number) => { + if (!selectedMandateId) throw new Error('Mandant nicht ausgewählt'); + const result = await createCheckout({ userId, amount }); + if (!result) throw new Error('Checkout konnte nicht erstellt werden'); + return result; + }, [selectedMandateId, createCheckout]); + const clearStripeParams = useCallback(() => { + searchParams.delete('success'); + searchParams.delete('canceled'); + searchParams.delete('session_id'); + setSearchParams(searchParams, { replace: true }); + }, [searchParams, setSearchParams]); + return (
@@ -404,6 +413,19 @@ export const BillingAdmin: React.FC = () => {

Verwaltung von Abrechnungseinstellungen und Guthaben

+ {successParam === 'true' && ( +
+ Zahlung erfolgreich. Guthaben wird gutgeschrieben. + +
+ )} + {canceledParam === 'true' && ( +
+ Zahlung abgebrochen. + +
+ )} +
{ settings={settings} accounts={accounts} users={users} - onAddCredit={handleAddCredit} + onCreateCheckout={handleCreateCheckout} />