When VITE_API_URL=/api, axios baseURL is already set to /api. However, all endpoint calls included the /api/ prefix, creating double paths like /api/api/auth/login/. Removed /api/ prefix from 81 API endpoint calls across 22 files: - src/api/auth.ts - Fixed login, logout, me, refresh, hijack endpoints - src/api/client.ts - Fixed token refresh endpoint - src/api/profile.ts - Fixed all profile, email, password, MFA, sessions endpoints - src/hooks/*.ts - Fixed all remaining API calls (users, appointments, resources, etc) - src/pages/*.tsx - Fixed signup and email verification endpoints This ensures API requests use the correct path: /api/auth/login/ instead of /api/api/auth/login/ 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
434 lines
12 KiB
TypeScript
434 lines
12 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;
|
|
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 || {});
|