- Fix Stripe SDK v14 compatibility (bracket notation for subscription items) - Fix subscription period dates from subscription items instead of subscription object - Add tier-based permissions (can_accept_payments, etc.) on tenant signup - Add stripe_customer_id field to Tenant model - Add clickable logo on login page (navigates to /) - Add database setup message during signup wizard - Add dark mode support for payment settings and Connect onboarding - Add subscription management endpoints (cancel, reactivate) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
547 lines
15 KiB
TypeScript
547 lines
15 KiB
TypeScript
/**
|
|
* 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;
|
|
tier_allows_payments: boolean;
|
|
stripe_configured: boolean;
|
|
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>('/payments/config/status/');
|
|
|
|
// ============================================================================
|
|
// API Keys (Free Tier)
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Get current API key configuration (masked keys).
|
|
*/
|
|
export const getApiKeys = () =>
|
|
apiClient.get<ApiKeysCurrentResponse>('/payments/api-keys/');
|
|
|
|
/**
|
|
* Save API keys.
|
|
* Validates and stores the provided Stripe API keys.
|
|
*/
|
|
export const saveApiKeys = (secretKey: string, publishableKey: string) =>
|
|
apiClient.post<ApiKeysInfo>('/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>('/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>('/payments/api-keys/revalidate/');
|
|
|
|
/**
|
|
* Delete stored API keys.
|
|
*/
|
|
export const deleteApiKeys = () =>
|
|
apiClient.delete<{ success: boolean; message: string }>('/payments/api-keys/delete/');
|
|
|
|
// ============================================================================
|
|
// Stripe Connect (Paid Tiers)
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Get current Connect account status.
|
|
*/
|
|
export const getConnectStatus = () =>
|
|
apiClient.get<ConnectAccountInfo>('/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>('/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 }>('/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>('/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>('/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>(
|
|
`/payments/transactions/${queryString ? `?${queryString}` : ''}`
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Get a single transaction by ID.
|
|
*/
|
|
export const getTransaction = (id: number) =>
|
|
apiClient.get<Transaction>(`/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>(
|
|
`/payments/transactions/summary/${queryString ? `?${queryString}` : ''}`
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Get charges from Stripe API.
|
|
*/
|
|
export const getStripeCharges = (limit: number = 20) =>
|
|
apiClient.get<ChargesResponse>(`/payments/transactions/charges/?limit=${limit}`);
|
|
|
|
/**
|
|
* Get payouts from Stripe API.
|
|
*/
|
|
export const getStripePayouts = (limit: number = 20) =>
|
|
apiClient.get<PayoutsResponse>(`/payments/transactions/payouts/?limit=${limit}`);
|
|
|
|
/**
|
|
* Get current balance from Stripe API.
|
|
*/
|
|
export const getStripeBalance = () =>
|
|
apiClient.get<BalanceResponse>('/payments/transactions/balance/');
|
|
|
|
/**
|
|
* Export transaction data.
|
|
* Returns the file data directly for download.
|
|
*/
|
|
export const exportTransactions = (request: ExportRequest) =>
|
|
apiClient.post('/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>(`/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>(`/payments/transactions/${transactionId}/refund/`, request || {});
|
|
|
|
// ============================================================================
|
|
// Subscription Plans & Add-ons
|
|
// ============================================================================
|
|
|
|
export interface SubscriptionPlan {
|
|
id: number;
|
|
name: string;
|
|
description: string;
|
|
plan_type: 'base' | 'addon';
|
|
business_tier: string;
|
|
price_monthly: number | null;
|
|
price_yearly: number | null;
|
|
features: string[];
|
|
permissions: Record<string, boolean>;
|
|
limits: Record<string, number>;
|
|
transaction_fee_percent: number;
|
|
transaction_fee_fixed: number;
|
|
is_most_popular: boolean;
|
|
show_price: boolean;
|
|
stripe_price_id: string;
|
|
}
|
|
|
|
export interface SubscriptionPlansResponse {
|
|
current_tier: string;
|
|
current_plan: SubscriptionPlan | null;
|
|
plans: SubscriptionPlan[];
|
|
addons: SubscriptionPlan[];
|
|
}
|
|
|
|
export interface CheckoutResponse {
|
|
checkout_url: string;
|
|
session_id: string;
|
|
}
|
|
|
|
/**
|
|
* Get available subscription plans and add-ons.
|
|
*/
|
|
export const getSubscriptionPlans = () =>
|
|
apiClient.get<SubscriptionPlansResponse>('/payments/plans/');
|
|
|
|
/**
|
|
* Create a checkout session for upgrading or purchasing add-ons.
|
|
*/
|
|
export const createCheckoutSession = (planId: number, billingPeriod: 'monthly' | 'yearly' = 'monthly') =>
|
|
apiClient.post<CheckoutResponse>('/payments/checkout/', {
|
|
plan_id: planId,
|
|
billing_period: billingPeriod,
|
|
});
|
|
|
|
// ============================================================================
|
|
// Active Subscriptions
|
|
// ============================================================================
|
|
|
|
export interface ActiveSubscription {
|
|
id: string;
|
|
plan_name: string;
|
|
plan_type: 'base' | 'addon';
|
|
status: 'active' | 'past_due' | 'canceled' | 'incomplete' | 'trialing';
|
|
current_period_start: string;
|
|
current_period_end: string;
|
|
cancel_at_period_end: boolean;
|
|
cancel_at: string | null;
|
|
canceled_at: string | null;
|
|
amount: number;
|
|
amount_display: string;
|
|
interval: 'month' | 'year';
|
|
stripe_subscription_id: string;
|
|
}
|
|
|
|
export interface SubscriptionsResponse {
|
|
subscriptions: ActiveSubscription[];
|
|
has_active_subscription: boolean;
|
|
}
|
|
|
|
export interface CancelSubscriptionResponse {
|
|
success: boolean;
|
|
message: string;
|
|
cancel_at_period_end: boolean;
|
|
current_period_end: string;
|
|
}
|
|
|
|
export interface ReactivateSubscriptionResponse {
|
|
success: boolean;
|
|
message: string;
|
|
}
|
|
|
|
/**
|
|
* Get active subscriptions for the current tenant.
|
|
*/
|
|
export const getSubscriptions = () =>
|
|
apiClient.get<SubscriptionsResponse>('/payments/subscriptions/');
|
|
|
|
/**
|
|
* Cancel a subscription.
|
|
* @param subscriptionId - Stripe subscription ID
|
|
* @param immediate - If true, cancel immediately. If false, cancel at period end.
|
|
*/
|
|
export const cancelSubscription = (subscriptionId: string, immediate: boolean = false) =>
|
|
apiClient.post<CancelSubscriptionResponse>('/payments/subscriptions/cancel/', {
|
|
subscription_id: subscriptionId,
|
|
immediate,
|
|
});
|
|
|
|
/**
|
|
* Reactivate a subscription that was set to cancel at period end.
|
|
*/
|
|
export const reactivateSubscription = (subscriptionId: string) =>
|
|
apiClient.post<ReactivateSubscriptionResponse>('/payments/subscriptions/reactivate/', {
|
|
subscription_id: subscriptionId,
|
|
});
|