Initial commit: SmoothSchedule multi-tenant scheduling platform
This commit includes: - Django backend with multi-tenancy (django-tenants) - React + TypeScript frontend with Vite - Platform administration API with role-based access control - Authentication system with token-based auth - Quick login dev tools for testing different user roles - CORS and CSRF configuration for local development - Docker development environment setup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
433
frontend/src/api/payments.ts
Normal file
433
frontend/src/api/payments.ts
Normal file
@@ -0,0 +1,433 @@
|
||||
/**
|
||||
* Payments API
|
||||
* Functions for managing payment configuration (API keys and Connect)
|
||||
*/
|
||||
|
||||
import apiClient from './client';
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export type PaymentMode = 'direct_api' | 'connect' | 'none';
|
||||
export type KeyStatus = 'active' | 'invalid' | 'deprecated';
|
||||
export type AccountStatus = 'pending' | 'onboarding' | 'active' | 'restricted' | 'rejected';
|
||||
|
||||
export interface ApiKeysInfo {
|
||||
id: number;
|
||||
status: KeyStatus;
|
||||
secret_key_masked: string;
|
||||
publishable_key_masked: string;
|
||||
last_validated_at: string | null;
|
||||
stripe_account_id: string;
|
||||
stripe_account_name: string;
|
||||
validation_error: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ConnectAccountInfo {
|
||||
id: number;
|
||||
business: number;
|
||||
business_name: string;
|
||||
business_subdomain: string;
|
||||
stripe_account_id: string;
|
||||
account_type: 'standard' | 'express' | 'custom';
|
||||
status: AccountStatus;
|
||||
charges_enabled: boolean;
|
||||
payouts_enabled: boolean;
|
||||
details_submitted: boolean;
|
||||
onboarding_complete: boolean;
|
||||
onboarding_link: string | null;
|
||||
onboarding_link_expires_at: string | null;
|
||||
is_onboarding_link_valid: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface PaymentConfig {
|
||||
payment_mode: PaymentMode;
|
||||
tier: string;
|
||||
can_accept_payments: boolean;
|
||||
api_keys: ApiKeysInfo | null;
|
||||
connect_account: ConnectAccountInfo | null;
|
||||
}
|
||||
|
||||
export interface ApiKeysValidationResult {
|
||||
valid: boolean;
|
||||
account_id?: string;
|
||||
account_name?: string;
|
||||
environment?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ApiKeysCurrentResponse {
|
||||
configured: boolean;
|
||||
id?: number;
|
||||
status?: KeyStatus;
|
||||
secret_key_masked?: string;
|
||||
publishable_key_masked?: string;
|
||||
last_validated_at?: string | null;
|
||||
stripe_account_id?: string;
|
||||
stripe_account_name?: string;
|
||||
validation_error?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface ConnectOnboardingResponse {
|
||||
account_type: 'standard' | 'custom';
|
||||
url: string;
|
||||
stripe_account_id?: string;
|
||||
}
|
||||
|
||||
export interface AccountSessionResponse {
|
||||
client_secret: string;
|
||||
stripe_account_id: string;
|
||||
publishable_key: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Unified Configuration
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get unified payment configuration status.
|
||||
* Returns the complete payment setup for the business.
|
||||
*/
|
||||
export const getPaymentConfig = () =>
|
||||
apiClient.get<PaymentConfig>('/api/payments/config/status/');
|
||||
|
||||
// ============================================================================
|
||||
// API Keys (Free Tier)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get current API key configuration (masked keys).
|
||||
*/
|
||||
export const getApiKeys = () =>
|
||||
apiClient.get<ApiKeysCurrentResponse>('/api/payments/api-keys/');
|
||||
|
||||
/**
|
||||
* Save API keys.
|
||||
* Validates and stores the provided Stripe API keys.
|
||||
*/
|
||||
export const saveApiKeys = (secretKey: string, publishableKey: string) =>
|
||||
apiClient.post<ApiKeysInfo>('/api/payments/api-keys/', {
|
||||
secret_key: secretKey,
|
||||
publishable_key: publishableKey,
|
||||
});
|
||||
|
||||
/**
|
||||
* Validate API keys without saving.
|
||||
* Tests the keys against Stripe API.
|
||||
*/
|
||||
export const validateApiKeys = (secretKey: string, publishableKey: string) =>
|
||||
apiClient.post<ApiKeysValidationResult>('/api/payments/api-keys/validate/', {
|
||||
secret_key: secretKey,
|
||||
publishable_key: publishableKey,
|
||||
});
|
||||
|
||||
/**
|
||||
* Re-validate stored API keys.
|
||||
* Tests stored keys and updates their status.
|
||||
*/
|
||||
export const revalidateApiKeys = () =>
|
||||
apiClient.post<ApiKeysValidationResult>('/api/payments/api-keys/revalidate/');
|
||||
|
||||
/**
|
||||
* Delete stored API keys.
|
||||
*/
|
||||
export const deleteApiKeys = () =>
|
||||
apiClient.delete<{ success: boolean; message: string }>('/api/payments/api-keys/delete/');
|
||||
|
||||
// ============================================================================
|
||||
// Stripe Connect (Paid Tiers)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get current Connect account status.
|
||||
*/
|
||||
export const getConnectStatus = () =>
|
||||
apiClient.get<ConnectAccountInfo>('/api/payments/connect/status/');
|
||||
|
||||
/**
|
||||
* Initiate Connect account onboarding.
|
||||
* Returns a URL to redirect the user for Stripe onboarding.
|
||||
*/
|
||||
export const initiateConnectOnboarding = (refreshUrl: string, returnUrl: string) =>
|
||||
apiClient.post<ConnectOnboardingResponse>('/api/payments/connect/onboard/', {
|
||||
refresh_url: refreshUrl,
|
||||
return_url: returnUrl,
|
||||
});
|
||||
|
||||
/**
|
||||
* Refresh Connect onboarding link.
|
||||
* For custom Connect accounts that need a new onboarding link.
|
||||
*/
|
||||
export const refreshConnectOnboardingLink = (refreshUrl: string, returnUrl: string) =>
|
||||
apiClient.post<{ url: string }>('/api/payments/connect/refresh-link/', {
|
||||
refresh_url: refreshUrl,
|
||||
return_url: returnUrl,
|
||||
});
|
||||
|
||||
/**
|
||||
* Create an Account Session for embedded Connect onboarding.
|
||||
* Returns a client_secret for initializing Stripe's embedded Connect components.
|
||||
*/
|
||||
export const createAccountSession = () =>
|
||||
apiClient.post<AccountSessionResponse>('/api/payments/connect/account-session/');
|
||||
|
||||
/**
|
||||
* Refresh Connect account status from Stripe.
|
||||
* Syncs the local account record with the current state in Stripe.
|
||||
*/
|
||||
export const refreshConnectStatus = () =>
|
||||
apiClient.post<ConnectAccountInfo>('/api/payments/connect/refresh-status/');
|
||||
|
||||
// ============================================================================
|
||||
// Transaction Analytics
|
||||
// ============================================================================
|
||||
|
||||
export interface Transaction {
|
||||
id: number;
|
||||
business: number;
|
||||
business_name: string;
|
||||
stripe_payment_intent_id: string;
|
||||
stripe_charge_id: string;
|
||||
transaction_type: 'payment' | 'refund' | 'application_fee';
|
||||
status: 'pending' | 'succeeded' | 'failed' | 'refunded' | 'partially_refunded';
|
||||
amount: number;
|
||||
amount_display: string;
|
||||
application_fee_amount: number;
|
||||
fee_display: string;
|
||||
net_amount: number;
|
||||
currency: string;
|
||||
customer_email: string;
|
||||
customer_name: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
stripe_data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface TransactionListResponse {
|
||||
results: Transaction[];
|
||||
count: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
total_pages: number;
|
||||
}
|
||||
|
||||
export interface TransactionSummary {
|
||||
total_transactions: number;
|
||||
total_volume: number;
|
||||
total_volume_display: string;
|
||||
total_fees: number;
|
||||
total_fees_display: string;
|
||||
net_revenue: number;
|
||||
net_revenue_display: string;
|
||||
successful_transactions: number;
|
||||
failed_transactions: number;
|
||||
refunded_transactions: number;
|
||||
average_transaction: number;
|
||||
average_transaction_display: string;
|
||||
}
|
||||
|
||||
export interface TransactionFilters {
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
status?: 'all' | 'succeeded' | 'pending' | 'failed' | 'refunded';
|
||||
transaction_type?: 'all' | 'payment' | 'refund' | 'application_fee';
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}
|
||||
|
||||
export interface StripeCharge {
|
||||
id: string;
|
||||
amount: number;
|
||||
amount_display: string;
|
||||
amount_refunded: number;
|
||||
currency: string;
|
||||
status: string;
|
||||
paid: boolean;
|
||||
refunded: boolean;
|
||||
description: string | null;
|
||||
receipt_email: string | null;
|
||||
receipt_url: string | null;
|
||||
created: number;
|
||||
payment_method_details: Record<string, unknown> | null;
|
||||
billing_details: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface ChargesResponse {
|
||||
charges: StripeCharge[];
|
||||
has_more: boolean;
|
||||
}
|
||||
|
||||
export interface StripePayout {
|
||||
id: string;
|
||||
amount: number;
|
||||
amount_display: string;
|
||||
currency: string;
|
||||
status: string;
|
||||
arrival_date: number | null;
|
||||
created: number;
|
||||
description: string | null;
|
||||
destination: string | null;
|
||||
failure_message: string | null;
|
||||
method: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface PayoutsResponse {
|
||||
payouts: StripePayout[];
|
||||
has_more: boolean;
|
||||
}
|
||||
|
||||
export interface BalanceItem {
|
||||
amount: number;
|
||||
currency: string;
|
||||
amount_display: string;
|
||||
}
|
||||
|
||||
export interface BalanceResponse {
|
||||
available: BalanceItem[];
|
||||
pending: BalanceItem[];
|
||||
available_total: number;
|
||||
pending_total: number;
|
||||
}
|
||||
|
||||
export interface ExportRequest {
|
||||
format: 'csv' | 'xlsx' | 'pdf' | 'quickbooks';
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
include_details?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of transactions with optional filtering.
|
||||
*/
|
||||
export const getTransactions = (filters?: TransactionFilters) => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.start_date) params.append('start_date', filters.start_date);
|
||||
if (filters?.end_date) params.append('end_date', filters.end_date);
|
||||
if (filters?.status && filters.status !== 'all') params.append('status', filters.status);
|
||||
if (filters?.transaction_type && filters.transaction_type !== 'all') {
|
||||
params.append('transaction_type', filters.transaction_type);
|
||||
}
|
||||
if (filters?.page) params.append('page', String(filters.page));
|
||||
if (filters?.page_size) params.append('page_size', String(filters.page_size));
|
||||
|
||||
const queryString = params.toString();
|
||||
return apiClient.get<TransactionListResponse>(
|
||||
`/api/payments/transactions/${queryString ? `?${queryString}` : ''}`
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a single transaction by ID.
|
||||
*/
|
||||
export const getTransaction = (id: number) =>
|
||||
apiClient.get<Transaction>(`/api/payments/transactions/${id}/`);
|
||||
|
||||
/**
|
||||
* Get transaction summary/analytics.
|
||||
*/
|
||||
export const getTransactionSummary = (filters?: Pick<TransactionFilters, 'start_date' | 'end_date'>) => {
|
||||
const params = new URLSearchParams();
|
||||
if (filters?.start_date) params.append('start_date', filters.start_date);
|
||||
if (filters?.end_date) params.append('end_date', filters.end_date);
|
||||
|
||||
const queryString = params.toString();
|
||||
return apiClient.get<TransactionSummary>(
|
||||
`/api/payments/transactions/summary/${queryString ? `?${queryString}` : ''}`
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get charges from Stripe API.
|
||||
*/
|
||||
export const getStripeCharges = (limit: number = 20) =>
|
||||
apiClient.get<ChargesResponse>(`/api/payments/transactions/charges/?limit=${limit}`);
|
||||
|
||||
/**
|
||||
* Get payouts from Stripe API.
|
||||
*/
|
||||
export const getStripePayouts = (limit: number = 20) =>
|
||||
apiClient.get<PayoutsResponse>(`/api/payments/transactions/payouts/?limit=${limit}`);
|
||||
|
||||
/**
|
||||
* Get current balance from Stripe API.
|
||||
*/
|
||||
export const getStripeBalance = () =>
|
||||
apiClient.get<BalanceResponse>('/api/payments/transactions/balance/');
|
||||
|
||||
/**
|
||||
* Export transaction data.
|
||||
* Returns the file data directly for download.
|
||||
*/
|
||||
export const exportTransactions = (request: ExportRequest) =>
|
||||
apiClient.post('/api/payments/transactions/export/', request, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Transaction Details & Refunds
|
||||
// ============================================================================
|
||||
|
||||
export interface RefundInfo {
|
||||
id: string;
|
||||
amount: number;
|
||||
amount_display: string;
|
||||
status: string;
|
||||
reason: string | null;
|
||||
created: number;
|
||||
}
|
||||
|
||||
export interface PaymentMethodInfo {
|
||||
type: string;
|
||||
brand?: string;
|
||||
last4?: string;
|
||||
exp_month?: number;
|
||||
exp_year?: number;
|
||||
funding?: string;
|
||||
bank_name?: string;
|
||||
}
|
||||
|
||||
export interface TransactionDetail extends Transaction {
|
||||
refunds: RefundInfo[];
|
||||
refundable_amount: number;
|
||||
total_refunded: number;
|
||||
can_refund: boolean;
|
||||
payment_method_info: PaymentMethodInfo | null;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface RefundRequest {
|
||||
amount?: number;
|
||||
reason?: 'duplicate' | 'fraudulent' | 'requested_by_customer';
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface RefundResponse {
|
||||
success: boolean;
|
||||
refund_id: string;
|
||||
amount: number;
|
||||
amount_display: string;
|
||||
status: string;
|
||||
reason: string | null;
|
||||
transaction_status: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get detailed transaction information including refund data.
|
||||
*/
|
||||
export const getTransactionDetail = (id: number) =>
|
||||
apiClient.get<TransactionDetail>(`/api/payments/transactions/${id}/`);
|
||||
|
||||
/**
|
||||
* Issue a refund for a transaction.
|
||||
* @param transactionId - The ID of the transaction to refund
|
||||
* @param request - Optional refund request with amount and reason
|
||||
*/
|
||||
export const refundTransaction = (transactionId: number, request?: RefundRequest) =>
|
||||
apiClient.post<RefundResponse>(`/api/payments/transactions/${transactionId}/refund/`, request || {});
|
||||
Reference in New Issue
Block a user