feat(billing): Add customer billing page with payment method management
- Add CustomerBilling page for customers to view payment history and manage cards - Create AddPaymentMethodModal with Stripe Elements for secure card saving - Support both Stripe Connect and direct API payment modes - Auto-set first payment method as default when no default exists - Add dark mode support for Stripe card input styling - Add customer billing API endpoints for payment history and saved cards - Add stripe_customer_id field to User model for Stripe customer tracking 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
155
frontend/src/hooks/useCustomerBilling.ts
Normal file
155
frontend/src/hooks/useCustomerBilling.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Customer Billing Hooks
|
||||
*
|
||||
* React Query hooks for fetching customer billing data including
|
||||
* payment history, outstanding payments, and saved payment methods.
|
||||
*/
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import apiClient from '../api/client';
|
||||
|
||||
// Types
|
||||
export interface OutstandingPayment {
|
||||
id: number;
|
||||
title: string;
|
||||
service_name: string;
|
||||
amount: number;
|
||||
amount_display: string;
|
||||
status: string;
|
||||
start_time: string | null;
|
||||
end_time: string | null;
|
||||
payment_status: 'pending' | 'unpaid';
|
||||
payment_intent_id: string | null;
|
||||
}
|
||||
|
||||
export interface PaymentHistoryItem {
|
||||
id: number;
|
||||
event_id: number;
|
||||
event_title: string;
|
||||
service_name: string;
|
||||
amount: number;
|
||||
amount_display: string;
|
||||
currency: string;
|
||||
status: string;
|
||||
payment_intent_id: string;
|
||||
created_at: string;
|
||||
completed_at: string | null;
|
||||
event_date: string | null;
|
||||
}
|
||||
|
||||
export interface BillingSummary {
|
||||
total_spent: number;
|
||||
total_spent_display: string;
|
||||
total_outstanding: number;
|
||||
total_outstanding_display: string;
|
||||
payment_count: number;
|
||||
}
|
||||
|
||||
export interface CustomerBillingData {
|
||||
outstanding: OutstandingPayment[];
|
||||
payment_history: PaymentHistoryItem[];
|
||||
summary: BillingSummary;
|
||||
}
|
||||
|
||||
export interface SavedPaymentMethod {
|
||||
id: string;
|
||||
type: string;
|
||||
brand: string | null;
|
||||
last4: string | null;
|
||||
exp_month: number | null;
|
||||
exp_year: number | null;
|
||||
is_default: boolean;
|
||||
}
|
||||
|
||||
export interface CustomerPaymentMethodsData {
|
||||
payment_methods: SavedPaymentMethod[];
|
||||
has_stripe_customer: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch customer billing data (payment history + outstanding payments)
|
||||
*/
|
||||
export const useCustomerBilling = () => {
|
||||
return useQuery<CustomerBillingData>({
|
||||
queryKey: ['customerBilling'],
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.get('/payments/customer/billing/');
|
||||
return data;
|
||||
},
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
retry: false,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to fetch customer's saved payment methods
|
||||
*/
|
||||
export const useCustomerPaymentMethods = () => {
|
||||
return useQuery<CustomerPaymentMethodsData>({
|
||||
queryKey: ['customerPaymentMethods'],
|
||||
queryFn: async () => {
|
||||
const { data } = await apiClient.get('/payments/customer/payment-methods/');
|
||||
return data;
|
||||
},
|
||||
staleTime: 60 * 1000, // 1 minute
|
||||
retry: false,
|
||||
});
|
||||
};
|
||||
|
||||
// SetupIntent response type
|
||||
export interface SetupIntentResponse {
|
||||
client_secret: string;
|
||||
setup_intent_id: string;
|
||||
customer_id: string;
|
||||
stripe_account: string; // Connected account ID for Stripe Connect (empty for direct_api)
|
||||
publishable_key?: string; // Tenant's publishable key for direct_api mode
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to create a SetupIntent for adding a new payment method
|
||||
*/
|
||||
export const useCreateSetupIntent = () => {
|
||||
return useMutation<SetupIntentResponse>({
|
||||
mutationFn: async () => {
|
||||
const { data } = await apiClient.post('/payments/customer/setup-intent/');
|
||||
return data;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to delete a payment method
|
||||
*/
|
||||
export const useDeletePaymentMethod = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{ success: boolean; message: string }, Error, string>({
|
||||
mutationFn: async (paymentMethodId: string) => {
|
||||
const { data } = await apiClient.delete(`/payments/customer/payment-methods/${paymentMethodId}/`);
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate payment methods query to refresh the list
|
||||
queryClient.invalidateQueries({ queryKey: ['customerPaymentMethods'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to set a payment method as default
|
||||
*/
|
||||
export const useSetDefaultPaymentMethod = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{ success: boolean; message: string }, Error, string>({
|
||||
mutationFn: async (paymentMethodId: string) => {
|
||||
const { data } = await apiClient.post(`/payments/customer/payment-methods/${paymentMethodId}/default/`);
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate payment methods query to refresh the list
|
||||
queryClient.invalidateQueries({ queryKey: ['customerPaymentMethods'] });
|
||||
},
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user