feat: integrated stripe billing
This commit is contained in:
parent
8c28ef791c
commit
d82bb4467c
3 changed files with 100 additions and 34 deletions
|
|
@ -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<any>) => Promise<any>;
|
||||
|
||||
|
|
@ -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<CheckoutCreateResponse> {
|
||||
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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<void>;
|
||||
onCreateCheckout: (userId: string | undefined, amount: number) => Promise<{ redirectUrl: string }>;
|
||||
}
|
||||
|
||||
const CreditAdder: React.FC<CreditAdderProps> = ({ settings, accounts, users, onAddCredit }) => {
|
||||
const CreditAdder: React.FC<CreditAdderProps> = ({ settings, accounts, users, onCreateCheckout }) => {
|
||||
const [selectedUserId, setSelectedUserId] = useState<string>('');
|
||||
const [amount, setAmount] = useState<number>(10);
|
||||
const [description, setDescription] = useState<string>('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<CreditAdderProps> = ({ 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<CreditAdderProps> = ({ settings, accounts, users, on
|
|||
<div className={styles.formRow}>
|
||||
<div className={styles.formGroup}>
|
||||
<label>Betrag (CHF)</label>
|
||||
<input
|
||||
type="number"
|
||||
className={styles.input}
|
||||
<select
|
||||
className={styles.select}
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(Number(e.target.value))}
|
||||
min="0.01"
|
||||
step="0.01"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label>Beschreibung</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.input}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Grund für Gutschrift"
|
||||
/>
|
||||
>
|
||||
{STRIPE_AMOUNT_PRESETS.map((preset) => (
|
||||
<option key={preset} value={preset}>
|
||||
{preset} CHF
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -308,7 +299,7 @@ const CreditAdder: React.FC<CreditAdderProps> = ({ settings, accounts, users, on
|
|||
className={`${styles.button} ${styles.buttonPrimary}`}
|
||||
disabled={saving || (isPrepayUser && !selectedUserId)}
|
||||
>
|
||||
{saving ? 'Aufladen...' : 'Guthaben aufladen'}
|
||||
{saving ? 'Weiterleitung zu Stripe...' : 'Mit Stripe aufladen'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -379,8 +370,18 @@ const AccountsOverview: React.FC<AccountsOverviewProps> = ({ accounts, users, lo
|
|||
// ============================================================================
|
||||
|
||||
export const BillingAdmin: React.FC = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [selectedMandateId, setSelectedMandateId] = useState<string | null>(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,11 +392,19 @@ 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 (
|
||||
<div className={styles.billingDashboard}>
|
||||
|
|
@ -404,6 +413,19 @@ export const BillingAdmin: React.FC = () => {
|
|||
<p className={styles.subtitle}>Verwaltung von Abrechnungseinstellungen und Guthaben</p>
|
||||
</header>
|
||||
|
||||
{successParam === 'true' && (
|
||||
<div className={styles.successMessage} style={{ marginBottom: '1rem' }}>
|
||||
Zahlung erfolgreich. Guthaben wird gutgeschrieben.
|
||||
<button type="button" onClick={clearStripeParams} style={{ marginLeft: '1rem', background: 'none', border: 'none', cursor: 'pointer', textDecoration: 'underline' }}>Schliessen</button>
|
||||
</div>
|
||||
)}
|
||||
{canceledParam === 'true' && (
|
||||
<div className={styles.errorMessage} style={{ marginBottom: '1rem' }}>
|
||||
Zahlung abgebrochen.
|
||||
<button type="button" onClick={clearStripeParams} style={{ marginLeft: '1rem', background: 'none', border: 'none', cursor: 'pointer', textDecoration: 'underline' }}>Schliessen</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<section className={styles.section}>
|
||||
<MandateSelector
|
||||
selectedMandateId={selectedMandateId}
|
||||
|
|
@ -423,7 +445,7 @@ export const BillingAdmin: React.FC = () => {
|
|||
settings={settings}
|
||||
accounts={accounts}
|
||||
users={users}
|
||||
onAddCredit={handleAddCredit}
|
||||
onCreateCheckout={handleCreateCheckout}
|
||||
/>
|
||||
|
||||
<AccountsOverview
|
||||
|
|
|
|||
Loading…
Reference in a new issue