Files
smoothschedule/frontend/src/api/payments.ts
poduck f1b1f18bc5 Add Stripe notifications, messaging improvements, and code cleanup
Stripe Notifications:
- Add periodic task to check Stripe Connect accounts for requirements
- Create in-app notifications for business owners when action needed
- Add management command to setup Stripe periodic tasks
- Display Stripe notifications with credit card icon in notification bell
- Navigate to payments page when Stripe notification clicked

Messaging Improvements:
- Add "Everyone" option to broadcast message recipients
- Allow sending messages to yourself (remove self-exclusion)
- Fix broadcast message ID not returned after creation
- Add real-time websocket support for broadcast notifications
- Show toast when broadcast message received via websocket

UI Fixes:
- Remove "View all" button from notifications (no page exists)
- Add StripeNotificationBanner component for Connect alerts
- Connect useUserNotifications hook in TopBar for app-wide websocket

Code Cleanup:
- Remove legacy automations app and plugin system
- Remove safe_scripting module (moved to Activepieces)
- Add migration to remove plugin-related models
- Various test improvements and coverage additions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-22 15:35:53 -05:00

652 lines
18 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';
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,
});
// ============================================================================
// Stripe Settings (Connect Accounts)
// ============================================================================
export type PayoutInterval = 'daily' | 'weekly' | 'monthly' | 'manual';
export type WeeklyAnchor = 'monday' | 'tuesday' | 'wednesday' | 'thursday' | 'friday' | 'saturday' | 'sunday';
export interface PayoutSchedule {
interval: PayoutInterval;
delay_days: number;
weekly_anchor: WeeklyAnchor | null;
monthly_anchor: number | null;
}
export interface PayoutSettings {
schedule: PayoutSchedule;
statement_descriptor: string;
}
export interface BusinessProfile {
name: string;
support_email: string;
support_phone: string;
support_url: string;
}
export interface BrandingSettings {
primary_color: string;
secondary_color: string;
icon: string;
logo: string;
}
export interface BankAccount {
id: string;
bank_name: string;
last4: string;
currency: string;
default_for_currency: boolean;
status: string;
}
export interface StripeSettings {
payouts: PayoutSettings;
business_profile: BusinessProfile;
branding: BrandingSettings;
bank_accounts: BankAccount[];
}
export interface StripeSettingsUpdatePayouts {
schedule?: Partial<PayoutSchedule>;
statement_descriptor?: string;
}
export interface StripeSettingsUpdate {
payouts?: StripeSettingsUpdatePayouts;
business_profile?: Partial<BusinessProfile>;
branding?: Pick<BrandingSettings, 'primary_color' | 'secondary_color'>;
}
export interface StripeSettingsUpdateResponse {
success: boolean;
message: string;
}
export interface StripeSettingsErrorResponse {
errors: Record<string, string>;
}
/**
* Get Stripe account settings for Connect accounts.
* Includes payout schedule, business profile, branding, and bank accounts.
*/
export const getStripeSettings = () =>
apiClient.get<StripeSettings>('/payments/settings/');
/**
* Update Stripe account settings.
* Can update payout settings, business profile, or branding.
*/
export const updateStripeSettings = (updates: StripeSettingsUpdate) =>
apiClient.patch<StripeSettingsUpdateResponse>('/payments/settings/', updates);
// ============================================================================
// Connect Login Link
// ============================================================================
export interface LoginLinkRequest {
return_url?: string;
refresh_url?: string;
}
export interface LoginLinkResponse {
url: string;
type: 'login_link' | 'account_link';
expires_at?: number;
}
/**
* Create a dashboard link for the Connect account.
* For Express accounts: Returns a one-time login link.
* For Custom accounts: Returns an account link (requires return/refresh URLs).
*/
export const createConnectLoginLink = (request?: LoginLinkRequest) =>
apiClient.post<LoginLinkResponse>('/payments/connect/login-link/', request || {});