1471 lines
41 KiB
TypeScript
1471 lines
41 KiB
TypeScript
/**
|
|
* Billing API Service
|
|
* Uses STANDARD billing endpoints from /v1/billing and /v1/admin/billing
|
|
*/
|
|
|
|
import { fetchAPI } from './api';
|
|
|
|
// Coalesce concurrent credit balance requests to avoid hitting throttle for normal users
|
|
let creditBalanceInFlight: Promise<CreditBalance> | null = null;
|
|
|
|
// ============================================================================
|
|
// TYPES
|
|
// ============================================================================
|
|
|
|
export interface CreditBalance {
|
|
credits: number; // Current balance
|
|
plan_credits_per_month: number; // Monthly allocation from plan
|
|
credits_used_this_month: number; // Used this billing period
|
|
credits_remaining: number; // Remaining credits
|
|
}
|
|
|
|
export interface CreditTransaction {
|
|
id: number;
|
|
amount: number;
|
|
transaction_type: 'purchase' | 'subscription' | 'refund' | 'deduction' | 'adjustment' | 'grant';
|
|
description: string;
|
|
created_at: string;
|
|
reference_id?: string;
|
|
metadata?: Record<string, any>;
|
|
balance_after?: number;
|
|
}
|
|
|
|
export interface CreditUsageLog {
|
|
id: number;
|
|
operation_type: string;
|
|
credits_used: number;
|
|
cost_usd: string;
|
|
model_used?: string;
|
|
tokens_input?: number;
|
|
tokens_output?: number;
|
|
created_at: string;
|
|
metadata?: Record<string, any>;
|
|
}
|
|
|
|
export interface AdminBillingStats {
|
|
total_accounts: number;
|
|
active_subscriptions: number;
|
|
total_revenue: string;
|
|
revenue_this_month?: string;
|
|
new_accounts_this_month?: number;
|
|
active_accounts?: number;
|
|
credits_issued_30d?: number;
|
|
credits_used_30d?: number;
|
|
pending_approvals?: number;
|
|
invoices_pending?: number;
|
|
invoices_overdue?: number;
|
|
system_health?: {
|
|
status: string;
|
|
last_check: string;
|
|
};
|
|
recent_activity?: Array<{
|
|
id: number;
|
|
type: string;
|
|
account_name: string;
|
|
amount: string;
|
|
currency: string;
|
|
timestamp: string;
|
|
description: string;
|
|
}>;
|
|
}
|
|
|
|
export interface CreditCostConfig {
|
|
id: number;
|
|
operation_type: string;
|
|
credits_cost: number;
|
|
unit: string;
|
|
display_name: string;
|
|
description: string;
|
|
is_active: boolean;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface AdminUser {
|
|
id: number;
|
|
email: string;
|
|
account_name: string;
|
|
account_id: number;
|
|
role: string;
|
|
is_active: boolean;
|
|
credit_balance: number;
|
|
plan_name?: string;
|
|
created_at: string;
|
|
last_login?: string;
|
|
}
|
|
|
|
export interface Invoice {
|
|
id: number;
|
|
invoice_number: string;
|
|
status: 'draft' | 'pending' | 'paid' | 'void' | 'uncollectible';
|
|
total_amount: string;
|
|
total?: string; // Alias
|
|
subtotal: string;
|
|
tax_amount: string;
|
|
currency: string;
|
|
created_at: string;
|
|
paid_at?: string;
|
|
due_date?: string;
|
|
invoice_date?: string;
|
|
line_items: Array<{
|
|
description: string;
|
|
amount: string;
|
|
quantity?: number;
|
|
}>;
|
|
billing_email?: string;
|
|
notes?: string;
|
|
stripe_invoice_id?: string;
|
|
billing_period_start?: string;
|
|
billing_period_end?: string;
|
|
account_name?: string;
|
|
// New fields for payment flow
|
|
payment_method?: string;
|
|
subscription?: {
|
|
id: number;
|
|
plan?: {
|
|
id: number;
|
|
name: string;
|
|
slug?: string;
|
|
};
|
|
} | null;
|
|
}
|
|
|
|
export interface Payment {
|
|
id: number;
|
|
invoice_id: number;
|
|
invoice_number?: string;
|
|
amount: string;
|
|
currency: string;
|
|
status: 'pending' | 'processing' | 'succeeded' | 'failed' | 'refunded' | 'cancelled' | 'pending_approval' | 'completed';
|
|
payment_method: 'stripe' | 'paypal' | 'bank_transfer' | 'local_wallet' | 'manual';
|
|
created_at: string;
|
|
processed_at?: string;
|
|
failed_at?: string;
|
|
refunded_at?: string;
|
|
failure_reason?: string;
|
|
manual_reference?: string;
|
|
manual_notes?: string;
|
|
transaction_reference?: string;
|
|
}
|
|
|
|
export interface CreditPackage {
|
|
id: number;
|
|
name: string;
|
|
slug: string;
|
|
credits: number;
|
|
price: string;
|
|
discount_percentage: number;
|
|
is_featured: boolean;
|
|
description: string;
|
|
display_order: number;
|
|
is_active?: boolean;
|
|
sort_order?: number;
|
|
stripe_price_id?: string | null;
|
|
}
|
|
|
|
export interface TeamMember {
|
|
id: number;
|
|
email: string;
|
|
first_name: string;
|
|
last_name: string;
|
|
role: string;
|
|
is_active: boolean;
|
|
is_staff?: boolean;
|
|
created_at: string;
|
|
date_joined?: string;
|
|
last_login?: string;
|
|
}
|
|
|
|
export interface PaymentMethod {
|
|
id: string;
|
|
type: 'stripe' | 'paypal' | 'bank_transfer' | 'local_wallet' | 'manual';
|
|
display_name: string;
|
|
name?: string;
|
|
is_enabled: boolean;
|
|
is_default?: boolean;
|
|
is_verified?: boolean;
|
|
instructions?: string;
|
|
country_code?: string;
|
|
bank_details?: {
|
|
bank_name?: string;
|
|
account_number?: string;
|
|
routing_number?: string;
|
|
swift_code?: string;
|
|
};
|
|
wallet_details?: {
|
|
wallet_type?: string;
|
|
wallet_id?: string;
|
|
};
|
|
}
|
|
|
|
export interface PendingPayment extends Payment {
|
|
account_name: string;
|
|
user_email: string;
|
|
invoice_number?: string;
|
|
admin_notes?: string;
|
|
}
|
|
|
|
// ============================================================================
|
|
// CREDIT BALANCE & TRANSACTIONS
|
|
// ============================================================================
|
|
|
|
export async function getCreditBalance(): Promise<CreditBalance> {
|
|
// Use canonical balance endpoint (credits/balance) per unified API structure
|
|
if (creditBalanceInFlight) {
|
|
return creditBalanceInFlight;
|
|
}
|
|
|
|
creditBalanceInFlight = fetchAPI('/v1/billing/credits/balance/');
|
|
try {
|
|
return await creditBalanceInFlight;
|
|
} finally {
|
|
creditBalanceInFlight = null;
|
|
}
|
|
}
|
|
|
|
export async function getCreditTransactions(): Promise<{
|
|
results: CreditTransaction[];
|
|
count: number;
|
|
current_balance?: number;
|
|
}> {
|
|
return fetchAPI('/v1/billing/credits/transactions/');
|
|
}
|
|
|
|
// ============================================================================
|
|
// CREDIT USAGE LOGS
|
|
// ============================================================================
|
|
|
|
export async function getCreditUsage(params?: {
|
|
operation_type?: string;
|
|
start_date?: string;
|
|
end_date?: string;
|
|
page?: number;
|
|
page_size?: number;
|
|
}): Promise<{
|
|
results: CreditUsageLog[];
|
|
count: number;
|
|
}> {
|
|
const queryParams = new URLSearchParams();
|
|
if (params?.operation_type) queryParams.append('operation_type', params.operation_type);
|
|
if (params?.start_date) queryParams.append('start_date', params.start_date);
|
|
if (params?.end_date) queryParams.append('end_date', params.end_date);
|
|
if (params?.page) queryParams.append('page', params.page.toString());
|
|
if (params?.page_size) queryParams.append('page_size', params.page_size.toString());
|
|
|
|
const url = `/v1/billing/credits/usage/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
|
return fetchAPI(url);
|
|
}
|
|
|
|
export async function getCreditUsageSummary(params?: {
|
|
start_date?: string;
|
|
end_date?: string;
|
|
}): Promise<{
|
|
total_credits_used: number;
|
|
total_cost_usd: string;
|
|
by_operation: Record<string, {
|
|
credits: number;
|
|
cost: number;
|
|
count: number;
|
|
}>;
|
|
by_model: Record<string, {
|
|
credits: number;
|
|
cost: number;
|
|
count: number;
|
|
}>;
|
|
}> {
|
|
const queryParams = new URLSearchParams();
|
|
if (params?.start_date) queryParams.append('start_date', params.start_date);
|
|
if (params?.end_date) queryParams.append('end_date', params.end_date);
|
|
|
|
const url = `/v1/billing/credits/usage/summary/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
|
return fetchAPI(url);
|
|
}
|
|
|
|
export async function getCreditUsageLimits(): Promise<{
|
|
plan_name: string;
|
|
plan_credits_per_month: number;
|
|
credits_used_this_month: number;
|
|
credits_remaining: number;
|
|
percentage_used: number;
|
|
approaching_limit: boolean;
|
|
}> {
|
|
return fetchAPI('/v1/billing/credits/usage/limits/');
|
|
}
|
|
|
|
// ============================================================================
|
|
// ADMIN - BILLING STATS & MANAGEMENT
|
|
// ============================================================================
|
|
|
|
export async function getAdminBillingStats(): Promise<AdminBillingStats> {
|
|
// Admin billing dashboard metrics
|
|
// Use business billing stats endpoint to include revenue, accounts, credits, and recent payments
|
|
return fetchAPI('/v1/admin/billing/stats/');
|
|
}
|
|
|
|
export async function getAdminInvoices(params?: { status?: string; account_id?: number; search?: string }): Promise<{
|
|
results: Invoice[];
|
|
count: number;
|
|
}> {
|
|
const queryParams = new URLSearchParams();
|
|
if (params?.status) queryParams.append('status', params.status);
|
|
if (params?.account_id) queryParams.append('account_id', String(params.account_id));
|
|
if (params?.search) queryParams.append('search', params.search);
|
|
|
|
const url = `/v1/admin/billing/invoices/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
|
return fetchAPI(url);
|
|
}
|
|
|
|
export async function getAdminPayments(params?: { status?: string; account_id?: number; payment_method?: string }): Promise<{
|
|
results: Payment[];
|
|
count: number;
|
|
}> {
|
|
const queryParams = new URLSearchParams();
|
|
if (params?.status) queryParams.append('status', params.status);
|
|
if (params?.account_id) queryParams.append('account_id', String(params.account_id));
|
|
if (params?.payment_method) queryParams.append('payment_method', params.payment_method);
|
|
|
|
const url = `/v1/admin/billing/payments/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
|
return fetchAPI(url);
|
|
}
|
|
|
|
// Admin payment method configs (country-level)
|
|
export interface PaymentMethodConfig {
|
|
id: number;
|
|
country_code: string;
|
|
payment_method: PaymentMethod['type'];
|
|
display_name: string;
|
|
is_enabled: boolean;
|
|
instructions?: string;
|
|
sort_order?: number;
|
|
}
|
|
|
|
export async function getAdminPaymentMethodConfigs(params?: { country_code?: string; payment_method?: string }): Promise<{
|
|
results: PaymentMethodConfig[];
|
|
count: number;
|
|
}> {
|
|
const queryParams = new URLSearchParams();
|
|
if (params?.country_code) queryParams.append('country_code', params.country_code);
|
|
if (params?.payment_method) queryParams.append('payment_method', params.payment_method);
|
|
const url = `/v1/admin/billing/payment-method-configs/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
|
return fetchAPI(url);
|
|
}
|
|
|
|
export async function createAdminPaymentMethodConfig(data: Partial<PaymentMethodConfig>): Promise<PaymentMethodConfig> {
|
|
return fetchAPI('/v1/admin/billing/payment-method-configs/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
export async function updateAdminPaymentMethodConfig(id: number, data: Partial<PaymentMethodConfig>): Promise<PaymentMethodConfig> {
|
|
return fetchAPI(`/v1/admin/billing/payment-method-configs/${id}/`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
export async function deleteAdminPaymentMethodConfig(id: number): Promise<void> {
|
|
await fetchAPI(`/v1/admin/billing/payment-method-configs/${id}/`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
// Admin account payment methods (cross-account)
|
|
export interface AdminAccountPaymentMethod extends PaymentMethod {
|
|
account: number;
|
|
metadata?: Record<string, any>;
|
|
}
|
|
|
|
export async function getAdminAccountPaymentMethods(params?: { account_id?: number }): Promise<{
|
|
results: AdminAccountPaymentMethod[];
|
|
count: number;
|
|
}> {
|
|
const queryParams = new URLSearchParams();
|
|
if (params?.account_id) queryParams.append('account_id', String(params.account_id));
|
|
const url = `/v1/admin/billing/account-payment-methods/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
|
return fetchAPI(url);
|
|
}
|
|
|
|
export async function createAdminAccountPaymentMethod(data: Partial<AdminAccountPaymentMethod>): Promise<AdminAccountPaymentMethod> {
|
|
return fetchAPI('/v1/admin/billing/account-payment-methods/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
export async function updateAdminAccountPaymentMethod(id: number | string, data: Partial<AdminAccountPaymentMethod>): Promise<AdminAccountPaymentMethod> {
|
|
return fetchAPI(`/v1/admin/billing/account-payment-methods/${id}/`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
export async function deleteAdminAccountPaymentMethod(id: number | string): Promise<void> {
|
|
await fetchAPI(`/v1/admin/billing/account-payment-methods/${id}/`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
export async function setAdminDefaultAccountPaymentMethod(id: number | string): Promise<{ message: string; id: number | string }> {
|
|
return fetchAPI(`/v1/admin/billing/account-payment-methods/${id}/set_default/`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
export async function getAdminUsers(params?: {
|
|
search?: string;
|
|
role?: string;
|
|
is_active?: boolean;
|
|
}): Promise<{
|
|
results: AdminUser[];
|
|
count: number;
|
|
}> {
|
|
const queryParams = new URLSearchParams();
|
|
if (params?.search) queryParams.append('search', params.search);
|
|
if (params?.role) queryParams.append('role', params.role);
|
|
if (params?.is_active !== undefined) queryParams.append('is_active', String(params.is_active));
|
|
|
|
const url = `/v1/admin/users/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
|
return fetchAPI(url);
|
|
}
|
|
|
|
export async function adjustUserCredits(
|
|
userId: number,
|
|
data: {
|
|
amount: number;
|
|
reason: string;
|
|
}
|
|
): Promise<{
|
|
message: string;
|
|
new_balance: number;
|
|
}> {
|
|
return fetchAPI(`/v1/admin/users/${userId}/adjust-credits/`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// ADMIN - CREDIT COSTS CONFIGURATION
|
|
// ============================================================================
|
|
|
|
export async function getCreditCosts(): Promise<{
|
|
results: CreditCostConfig[];
|
|
count: number;
|
|
}> {
|
|
// credit costs are served from the admin namespace (modules billing)
|
|
return fetchAPI('/v1/admin/credit-costs/');
|
|
}
|
|
|
|
export async function updateCreditCosts(
|
|
costs: Array<{
|
|
operation_type: string;
|
|
credits_cost: number;
|
|
}>
|
|
): Promise<{
|
|
message: string;
|
|
updated_count: number;
|
|
}> {
|
|
return fetchAPI('/v1/admin/credit-costs/', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ costs }),
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// INVOICES
|
|
// ============================================================================
|
|
|
|
export async function getInvoices(params?: {
|
|
status?: string;
|
|
}): Promise<{
|
|
results: Invoice[];
|
|
count: number;
|
|
}> {
|
|
const queryParams = new URLSearchParams();
|
|
if (params?.status) queryParams.append('status', params.status);
|
|
|
|
const url = `/v1/billing/invoices/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
|
return fetchAPI(url);
|
|
}
|
|
|
|
export async function getInvoiceDetail(invoiceId: number): Promise<Invoice> {
|
|
return fetchAPI(`/v1/billing/invoices/${invoiceId}/`);
|
|
}
|
|
|
|
export async function downloadInvoicePDF(invoiceId: number): Promise<Blob> {
|
|
const token = localStorage.getItem('access_token') ||
|
|
(() => {
|
|
try {
|
|
const authStorage = localStorage.getItem('auth-storage');
|
|
if (authStorage) {
|
|
const parsed = JSON.parse(authStorage);
|
|
return parsed?.state?.token || null;
|
|
}
|
|
} catch (e) {}
|
|
return null;
|
|
})();
|
|
|
|
// Use the full API URL from the api.ts module
|
|
const apiUrl = (await import('./api')).API_BASE_URL;
|
|
const response = await fetch(`${apiUrl}/v1/billing/invoices/${invoiceId}/download_pdf/`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error('PDF download failed:', response.status, errorText);
|
|
throw new Error('Failed to download invoice PDF');
|
|
}
|
|
|
|
return response.blob();
|
|
}
|
|
|
|
// ============================================================================
|
|
// PAYMENTS
|
|
// ============================================================================
|
|
|
|
export async function getPayments(params?: {
|
|
status?: string;
|
|
invoice_id?: number;
|
|
}): Promise<{
|
|
results: Payment[];
|
|
count: number;
|
|
}> {
|
|
const queryParams = new URLSearchParams();
|
|
if (params?.status) queryParams.append('status', params.status);
|
|
if (params?.invoice_id) queryParams.append('invoice_id', String(params.invoice_id));
|
|
|
|
const url = `/v1/billing/payments/${queryParams.toString() ? '?' + queryParams.toString() : ''}`;
|
|
return fetchAPI(url);
|
|
}
|
|
|
|
export async function submitManualPayment(data: {
|
|
invoice_id?: number;
|
|
payment_method: 'bank_transfer' | 'local_wallet' | 'manual';
|
|
amount: string;
|
|
currency?: string;
|
|
reference?: string;
|
|
notes?: string;
|
|
proof_file?: File;
|
|
}): Promise<{
|
|
id: number;
|
|
status: string;
|
|
message: string;
|
|
}> {
|
|
return fetchAPI('/v1/billing/payments/manual/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// CREDIT PACKAGES
|
|
// ============================================================================
|
|
|
|
export async function getCreditPackages(): Promise<{
|
|
results: CreditPackage[];
|
|
count: number;
|
|
}> {
|
|
return fetchAPI('/v1/billing/credit-packages/');
|
|
}
|
|
|
|
// Admin credit packages (CRUD capable backend; currently read-only here)
|
|
export async function getAdminCreditPackages(): Promise<{
|
|
results: CreditPackage[];
|
|
count: number;
|
|
}> {
|
|
// Backend does not expose a dedicated admin credit-packages list; fall back to the shared packages list
|
|
return fetchAPI('/v1/billing/credit-packages/');
|
|
}
|
|
|
|
export async function createAdminCreditPackage(data: {
|
|
name: string;
|
|
slug?: string;
|
|
credits: number;
|
|
price: string | number;
|
|
discount_percentage?: number;
|
|
description?: string;
|
|
is_active?: boolean;
|
|
is_featured?: boolean;
|
|
sort_order?: number;
|
|
}): Promise<{ package: CreditPackage; message?: string }> {
|
|
return fetchAPI('/v1/billing/credit-packages/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
export async function updateAdminCreditPackage(
|
|
id: number,
|
|
data: Partial<{
|
|
name: string;
|
|
slug: string;
|
|
credits: number;
|
|
price: string | number;
|
|
discount_percentage: number;
|
|
description: string;
|
|
is_active: boolean;
|
|
is_featured: boolean;
|
|
sort_order: number;
|
|
}>
|
|
): Promise<{ package: CreditPackage; message?: string }> {
|
|
return fetchAPI(`/v1/billing/credit-packages/${id}/`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
export async function deleteAdminCreditPackage(id: number): Promise<{ message?: string }> {
|
|
return fetchAPI(`/v1/billing/credit-packages/${id}/`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
export async function purchaseCreditPackage(data: {
|
|
package_id: number;
|
|
payment_method: 'stripe' | 'paypal' | 'bank_transfer' | 'local_wallet';
|
|
}): Promise<{
|
|
invoice_id?: number;
|
|
invoice_number?: string;
|
|
total_amount?: string;
|
|
status?: string;
|
|
message?: string;
|
|
next_action?: string;
|
|
stripe_client_secret?: string;
|
|
paypal_order_id?: string;
|
|
}> {
|
|
return fetchAPI(`/v1/billing/credit-packages/${data.package_id}/purchase/`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ payment_method: data.payment_method }),
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// TEAM MANAGEMENT
|
|
// ============================================================================
|
|
|
|
export async function getTeamMembers(): Promise<{
|
|
results: TeamMember[];
|
|
count: number;
|
|
}> {
|
|
return fetchAPI('/v1/account/team/');
|
|
}
|
|
|
|
export async function inviteTeamMember(data: {
|
|
email: string;
|
|
first_name?: string;
|
|
last_name?: string;
|
|
role?: string;
|
|
}): Promise<{
|
|
message: string;
|
|
member?: TeamMember;
|
|
}> {
|
|
return fetchAPI('/v1/account/team/invite/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
export async function removeTeamMember(memberId: number): Promise<{
|
|
message: string;
|
|
}> {
|
|
return fetchAPI(`/v1/account/team/${memberId}/`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// PAYMENT METHODS
|
|
// ============================================================================
|
|
|
|
// Get GLOBAL payment method configs (system-wide available payment options like stripe, paypal, bank_transfer)
|
|
// This is used on Plans page to show what payment methods are available to choose
|
|
export async function getAvailablePaymentMethods(): Promise<{
|
|
results: PaymentMethod[];
|
|
count: number;
|
|
}> {
|
|
// Call the payment-configs endpoint which returns global PaymentMethodConfig records
|
|
const response = await fetchAPI('/v1/billing/payment-configs/payment-methods/');
|
|
// Return all payment methods - stripe, paypal, bank_transfer, manual, local_wallet
|
|
const results = Array.isArray(response.results) ? response.results : [];
|
|
// Map payment_method to type for consistent API
|
|
const mapped = results.map((m: any) => ({
|
|
...m,
|
|
type: m.payment_method || m.type, // PaymentMethodConfig uses payment_method field
|
|
}));
|
|
return { results: mapped, count: mapped.length };
|
|
}
|
|
|
|
// Get account-specific payment methods (user's saved/verified payment methods)
|
|
export async function getAccountPaymentMethods(): Promise<{
|
|
results: PaymentMethod[];
|
|
count: number;
|
|
}> {
|
|
const response = await fetchAPI('/v1/billing/payment-methods/');
|
|
const results = Array.isArray(response.results) ? response.results : [];
|
|
return { results, count: results.length };
|
|
}
|
|
|
|
export async function createPaymentMethod(data: {
|
|
type: 'stripe' | 'paypal' | 'bank_transfer' | 'local_wallet' | 'manual';
|
|
display_name: string;
|
|
instructions?: string;
|
|
metadata?: Record<string, any>;
|
|
country_code?: string;
|
|
is_default?: boolean;
|
|
}): Promise<{ payment_method: PaymentMethod; message?: string }> {
|
|
return fetchAPI('/v1/billing/payment-methods/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
export async function deletePaymentMethod(id: string): Promise<{ message?: string }> {
|
|
return fetchAPI(`/v1/billing/payment-methods/${id}/`, {
|
|
method: 'DELETE',
|
|
});
|
|
}
|
|
|
|
export async function updatePaymentMethod(id: string, data: Partial<PaymentMethod>): Promise<{ payment_method?: PaymentMethod; message?: string }> {
|
|
return fetchAPI(`/v1/billing/payment-methods/${id}/`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
export async function setDefaultPaymentMethod(id: string): Promise<{ payment_method?: PaymentMethod; message?: string }> {
|
|
return fetchAPI(`/v1/billing/payment-methods/${id}/set_default/`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
// Country/config-driven available methods (legacy)
|
|
export async function getPaymentMethodConfigs(): Promise<{
|
|
results: PaymentMethod[];
|
|
count: number;
|
|
}> {
|
|
return fetchAPI('/v1/billing/payment-methods/available/');
|
|
}
|
|
|
|
// Get payment methods for a specific country
|
|
export async function getPaymentMethodsByCountry(countryCode?: string): Promise<{
|
|
success: boolean;
|
|
results: PaymentMethodConfig[];
|
|
count: number;
|
|
}> {
|
|
const params = countryCode ? `?country=${countryCode}` : '';
|
|
return fetchAPI(`/v1/billing/admin/payment-methods/${params}`);
|
|
}
|
|
|
|
// Confirm manual payment (submit proof of payment)
|
|
export async function confirmPayment(data: {
|
|
invoice_id: number;
|
|
payment_method: string;
|
|
amount: string;
|
|
manual_reference: string;
|
|
manual_notes?: string;
|
|
proof_url?: string;
|
|
}): Promise<{
|
|
success: boolean;
|
|
message: string;
|
|
payment: Payment;
|
|
}> {
|
|
return fetchAPI('/v1/billing/admin/payments/confirm/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
export async function createManualPayment(data: {
|
|
invoice_id?: number;
|
|
amount: string;
|
|
payment_method: string;
|
|
reference: string;
|
|
notes?: string;
|
|
}): Promise<{
|
|
id: number;
|
|
status: string;
|
|
message: string;
|
|
}> {
|
|
return fetchAPI('/v1/billing/payments/manual/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// ADMIN - PAYMENT APPROVALS
|
|
// ============================================================================
|
|
|
|
export async function getPendingPayments(): Promise<{
|
|
results: PendingPayment[];
|
|
count: number;
|
|
}> {
|
|
return fetchAPI('/v1/admin/billing/pending_payments/');
|
|
}
|
|
|
|
export async function approvePayment(paymentId: number, data?: {
|
|
notes?: string;
|
|
}): Promise<{
|
|
message: string;
|
|
payment: Payment;
|
|
}> {
|
|
return fetchAPI(`/v1/admin/billing/${paymentId}/approve_payment/`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data || {}),
|
|
});
|
|
}
|
|
|
|
export async function rejectPayment(paymentId: number, data: {
|
|
reason: string;
|
|
notes?: string;
|
|
}): Promise<{
|
|
message: string;
|
|
payment: Payment;
|
|
}> {
|
|
return fetchAPI(`/v1/admin/billing/${paymentId}/reject_payment/`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// ACCOUNT MANAGEMENT
|
|
// ============================================================================
|
|
|
|
export interface AccountSettings {
|
|
id: number;
|
|
name: string;
|
|
slug: string;
|
|
billing_address_line1?: string;
|
|
billing_address_line2?: string;
|
|
billing_city?: string;
|
|
billing_state?: string;
|
|
billing_postal_code?: string;
|
|
billing_country?: string;
|
|
tax_id?: string;
|
|
billing_email?: string;
|
|
credit_balance: number;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export async function getAccountSettings(): Promise<AccountSettings> {
|
|
return fetchAPI('/v1/account/settings/');
|
|
}
|
|
|
|
export async function updateAccountSettings(data: Partial<AccountSettings>): Promise<{
|
|
message: string;
|
|
account: AccountSettings;
|
|
}> {
|
|
return fetchAPI('/v1/account/settings/', {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// PROFILE & SECURITY
|
|
// ============================================================================
|
|
|
|
export interface UserProfile {
|
|
id: number;
|
|
email: string;
|
|
username?: string;
|
|
first_name: string;
|
|
last_name: string;
|
|
phone?: string;
|
|
timezone?: string;
|
|
language?: string;
|
|
email_notifications?: boolean;
|
|
marketing_emails?: boolean;
|
|
}
|
|
|
|
export async function getUserProfile(): Promise<{ user: UserProfile }> {
|
|
return fetchAPI('/v1/auth/me/');
|
|
}
|
|
|
|
export async function updateUserProfile(data: Partial<UserProfile>): Promise<{ user: UserProfile }> {
|
|
// User profile updates go through users endpoint
|
|
return fetchAPI('/v1/auth/users/me/', {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
export async function changePassword(oldPassword: string, newPassword: string): Promise<{ message: string }> {
|
|
return fetchAPI('/v1/auth/change-password/', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
old_password: oldPassword,
|
|
new_password: newPassword,
|
|
}),
|
|
});
|
|
}
|
|
|
|
export interface UsageAnalytics {
|
|
period_days: number;
|
|
start_date: string;
|
|
end_date: string;
|
|
current_balance: number;
|
|
usage_by_type: Array<{
|
|
transaction_type: string;
|
|
total: number;
|
|
count: number;
|
|
}>;
|
|
purchases_by_type: Array<{
|
|
transaction_type: string;
|
|
total: number;
|
|
count: number;
|
|
}>;
|
|
daily_usage: Array<{
|
|
date: string;
|
|
usage: number;
|
|
purchases: number;
|
|
net: number;
|
|
}>;
|
|
total_usage: number;
|
|
total_purchases: number;
|
|
}
|
|
|
|
export async function getUsageAnalytics(days: number = 30): Promise<UsageAnalytics> {
|
|
return fetchAPI(`/v1/account/usage/analytics/?days=${days}`);
|
|
}
|
|
|
|
// ============================================================================
|
|
// PLANS & SUBSCRIPTIONS (TENANT)
|
|
// ============================================================================
|
|
|
|
export interface Plan {
|
|
id: number;
|
|
name: string;
|
|
slug?: string;
|
|
price?: number | string;
|
|
original_price?: number;
|
|
currency?: string;
|
|
interval?: 'month' | 'year';
|
|
description?: string;
|
|
is_active?: boolean;
|
|
is_featured?: boolean;
|
|
annual_discount_percent?: number;
|
|
features?: string[];
|
|
limits?: Record<string, any>;
|
|
display_order?: number;
|
|
// Hard Limits (only 3 persistent limits)
|
|
max_sites?: number;
|
|
max_users?: number;
|
|
max_keywords?: number;
|
|
// Monthly Limits (only ahrefs queries)
|
|
max_ahrefs_queries?: number;
|
|
// Credits
|
|
included_credits?: number;
|
|
}
|
|
|
|
export interface LimitUsage {
|
|
display_name: string;
|
|
current: number;
|
|
limit: number;
|
|
remaining: number;
|
|
percentage_used: number;
|
|
}
|
|
|
|
export interface UsageSummary {
|
|
account_id: number;
|
|
account_name: string;
|
|
plan_name: string;
|
|
period_start: string;
|
|
period_end: string;
|
|
days_until_reset: number;
|
|
// Simplified to only 3 hard limits
|
|
hard_limits: {
|
|
sites?: LimitUsage;
|
|
users?: LimitUsage;
|
|
keywords?: LimitUsage;
|
|
};
|
|
// Simplified to only 1 monthly limit (Ahrefs queries)
|
|
monthly_limits: {
|
|
ahrefs_queries?: LimitUsage;
|
|
};
|
|
}
|
|
|
|
export interface Subscription {
|
|
id: number;
|
|
plan: Plan | number;
|
|
status: string;
|
|
current_period_start?: string;
|
|
current_period_end?: string;
|
|
cancel_at_period_end?: boolean;
|
|
created_at?: string;
|
|
}
|
|
|
|
export async function getPlans(): Promise<{ results: Plan[] }> {
|
|
// Coalesce concurrent plan fetches to avoid 429s on first load
|
|
if (!(getPlans as any)._inFlight) {
|
|
(getPlans as any)._inFlight = fetchAPI('/v1/auth/plans/').finally(() => {
|
|
(getPlans as any)._inFlight = null;
|
|
});
|
|
}
|
|
return (getPlans as any)._inFlight;
|
|
}
|
|
|
|
export async function getSubscriptions(): Promise<{ results: Subscription[] }> {
|
|
const now = Date.now();
|
|
const self: any = getSubscriptions as any;
|
|
|
|
// Return cached result if fetched within 5s to avoid hitting throttle
|
|
if (self._lastResult && self._lastFetched && now - self._lastFetched < 5000) {
|
|
return self._lastResult;
|
|
}
|
|
|
|
// Respect cooldown if previous call was throttled
|
|
if (self._cooldownUntil && now < self._cooldownUntil) {
|
|
if (self._lastResult) return self._lastResult;
|
|
return { results: [] };
|
|
}
|
|
|
|
// Coalesce concurrent subscription fetches
|
|
if (!self._inFlight) {
|
|
self._inFlight = fetchAPI('/v1/auth/subscriptions/')
|
|
.then((res) => {
|
|
self._lastResult = res;
|
|
self._lastFetched = Date.now();
|
|
return res;
|
|
})
|
|
.catch((err) => {
|
|
if (err?.status === 429) {
|
|
// Set a short cooldown to prevent immediate re-hits
|
|
self._cooldownUntil = Date.now() + 5000;
|
|
// Return cached or empty to avoid surfacing the 429 upstream
|
|
return self._lastResult || { results: [] };
|
|
}
|
|
throw err;
|
|
})
|
|
.finally(() => {
|
|
self._inFlight = null;
|
|
});
|
|
}
|
|
|
|
return self._inFlight;
|
|
}
|
|
|
|
export async function createSubscription(data: {
|
|
plan_id: number;
|
|
payment_method?: string;
|
|
}): Promise<{ message?: string; subscription?: Subscription }> {
|
|
return fetchAPI('/v1/auth/subscriptions/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(data),
|
|
});
|
|
}
|
|
|
|
export async function cancelSubscription(subscriptionId: number): Promise<{ message?: string }> {
|
|
return fetchAPI(`/v1/auth/subscriptions/${subscriptionId}/cancel/`, {
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// PUBLIC PLANS (for marketing/pricing page)
|
|
// ============================================================================
|
|
|
|
export async function getPublicPlans(): Promise<Plan[]> {
|
|
// Use the existing auth/plans endpoint which already filters for public plans
|
|
const response = await fetchAPI('/v1/auth/plans/');
|
|
return response.results || response;
|
|
}
|
|
|
|
// ============================================================================
|
|
// USAGE SUMMARY (PLAN LIMITS)
|
|
// ============================================================================
|
|
|
|
export async function getUsageSummary(): Promise<UsageSummary> {
|
|
return fetchAPI('/v1/billing/usage-summary/');
|
|
}
|
|
|
|
// ============================================================================
|
|
// DASHBOARD STATS (Real data for home page)
|
|
// ============================================================================
|
|
|
|
export interface DashboardAIOperation {
|
|
type: string;
|
|
count: number;
|
|
credits: number;
|
|
}
|
|
|
|
export interface DashboardAIOperations {
|
|
period: string;
|
|
operations: DashboardAIOperation[];
|
|
totals: {
|
|
count: number;
|
|
credits: number;
|
|
successRate: number;
|
|
avgCreditsPerOp: number;
|
|
};
|
|
}
|
|
|
|
export interface DashboardActivityItem {
|
|
id: string;
|
|
type: string;
|
|
title: string;
|
|
description: string;
|
|
timestamp: string;
|
|
href: string;
|
|
}
|
|
|
|
export interface DashboardContentVelocity {
|
|
thisWeek: {
|
|
articles: number;
|
|
words: number;
|
|
images: number;
|
|
};
|
|
thisMonth: {
|
|
articles: number;
|
|
words: number;
|
|
images: number;
|
|
};
|
|
total: {
|
|
articles: number;
|
|
words: number;
|
|
images: number;
|
|
};
|
|
trend: number;
|
|
}
|
|
|
|
export interface DashboardPipeline {
|
|
sites: number;
|
|
keywords: number;
|
|
clusters: number;
|
|
ideas: number;
|
|
tasks: number;
|
|
drafts: number;
|
|
published: number;
|
|
}
|
|
|
|
export interface DashboardStats {
|
|
ai_operations: DashboardAIOperations;
|
|
recent_activity: DashboardActivityItem[];
|
|
content_velocity: DashboardContentVelocity;
|
|
pipeline: DashboardPipeline;
|
|
counts: {
|
|
content: {
|
|
total: number;
|
|
draft: number;
|
|
review: number;
|
|
published: number;
|
|
};
|
|
images: {
|
|
total: number;
|
|
generated: number;
|
|
pending: number;
|
|
};
|
|
};
|
|
}
|
|
|
|
export async function getDashboardStats(params?: { site_id?: number; days?: number }): Promise<DashboardStats> {
|
|
const searchParams = new URLSearchParams();
|
|
if (params?.site_id) searchParams.append('site_id', params.site_id.toString());
|
|
if (params?.days) searchParams.append('days', params.days.toString());
|
|
const query = searchParams.toString();
|
|
return fetchAPI(`/v1/account/dashboard/stats/${query ? `?${query}` : ''}`);
|
|
}
|
|
|
|
// ============================================================================
|
|
// STRIPE INTEGRATION
|
|
// ============================================================================
|
|
|
|
export interface StripeConfig {
|
|
publishable_key: string;
|
|
is_sandbox: boolean;
|
|
}
|
|
|
|
export interface StripeCheckoutSession {
|
|
checkout_url: string;
|
|
session_id: string;
|
|
}
|
|
|
|
export interface StripeBillingPortalSession {
|
|
portal_url: string;
|
|
}
|
|
|
|
/**
|
|
* Get Stripe publishable key for frontend initialization
|
|
*/
|
|
export async function getStripeConfig(): Promise<StripeConfig> {
|
|
return fetchAPI('/v1/billing/stripe/config/');
|
|
}
|
|
|
|
/**
|
|
* Create Stripe Checkout session for plan subscription
|
|
* Redirects user to Stripe's hosted checkout page
|
|
*/
|
|
export async function createStripeCheckout(planId: string, options?: {
|
|
success_url?: string;
|
|
cancel_url?: string;
|
|
}): Promise<StripeCheckoutSession> {
|
|
return fetchAPI('/v1/billing/stripe/checkout/', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
plan_id: planId,
|
|
...options,
|
|
}),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create Stripe Checkout session for credit package purchase
|
|
* Redirects user to Stripe's hosted checkout page
|
|
*/
|
|
export async function createStripeCreditCheckout(packageId: string, options?: {
|
|
success_url?: string;
|
|
cancel_url?: string;
|
|
}): Promise<StripeCheckoutSession> {
|
|
return fetchAPI('/v1/billing/stripe/credit-checkout/', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
package_id: packageId,
|
|
...options,
|
|
}),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create Stripe Billing Portal session for subscription management
|
|
* Allows customers to manage payment methods, view invoices, cancel subscription
|
|
*/
|
|
export async function openStripeBillingPortal(options?: {
|
|
return_url?: string;
|
|
}): Promise<StripeBillingPortalSession> {
|
|
return fetchAPI('/v1/billing/stripe/billing-portal/', {
|
|
method: 'POST',
|
|
body: JSON.stringify(options || {}),
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// PAYPAL INTEGRATION
|
|
// ============================================================================
|
|
|
|
export interface PayPalConfig {
|
|
client_id: string;
|
|
is_sandbox: boolean;
|
|
currency: string;
|
|
}
|
|
|
|
export interface PayPalOrder {
|
|
order_id: string;
|
|
status: string;
|
|
approval_url: string;
|
|
links?: Array<{ rel: string; href: string }>;
|
|
credit_package_id?: string;
|
|
credit_amount?: number;
|
|
plan_id?: string;
|
|
plan_name?: string;
|
|
}
|
|
|
|
export interface PayPalCaptureResult {
|
|
order_id: string;
|
|
status: string;
|
|
capture_id: string;
|
|
amount: string;
|
|
currency: string;
|
|
credits_added?: number;
|
|
new_balance?: number;
|
|
subscription_id?: string;
|
|
plan_name?: string;
|
|
payment_id?: number;
|
|
}
|
|
|
|
export interface PayPalSubscription {
|
|
subscription_id: string;
|
|
status: string;
|
|
approval_url: string;
|
|
links?: Array<{ rel: string; href: string }>;
|
|
}
|
|
|
|
/**
|
|
* Get PayPal client ID for frontend initialization
|
|
*/
|
|
export async function getPayPalConfig(): Promise<PayPalConfig> {
|
|
return fetchAPI('/v1/billing/paypal/config/');
|
|
}
|
|
|
|
/**
|
|
* Create PayPal order for credit package purchase
|
|
* Returns approval URL for PayPal redirect
|
|
*/
|
|
export async function createPayPalCreditOrder(packageId: string, options?: {
|
|
return_url?: string;
|
|
cancel_url?: string;
|
|
}): Promise<PayPalOrder> {
|
|
return fetchAPI('/v1/billing/paypal/create-order/', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
package_id: packageId,
|
|
...options,
|
|
}),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create PayPal order for plan subscription (one-time payment model)
|
|
* Returns approval URL for PayPal redirect
|
|
*/
|
|
export async function createPayPalSubscriptionOrder(planId: string, options?: {
|
|
return_url?: string;
|
|
cancel_url?: string;
|
|
}): Promise<PayPalOrder> {
|
|
return fetchAPI('/v1/billing/paypal/create-subscription-order/', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
plan_id: planId,
|
|
...options,
|
|
}),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Capture PayPal order after user approval
|
|
* Call this when user returns from PayPal with approved order
|
|
*/
|
|
export async function capturePayPalOrder(orderId: string, options?: {
|
|
package_id?: string;
|
|
plan_id?: string;
|
|
}): Promise<PayPalCaptureResult> {
|
|
return fetchAPI('/v1/billing/paypal/capture-order/', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
order_id: orderId,
|
|
...options,
|
|
}),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create PayPal recurring subscription
|
|
* Requires plan to have paypal_plan_id configured
|
|
*/
|
|
export async function createPayPalSubscription(planId: string, options?: {
|
|
return_url?: string;
|
|
cancel_url?: string;
|
|
}): Promise<PayPalSubscription> {
|
|
return fetchAPI('/v1/billing/paypal/create-subscription/', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
plan_id: planId,
|
|
...options,
|
|
}),
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// PAYMENT GATEWAY HELPERS
|
|
// ============================================================================
|
|
|
|
export type PaymentGateway = 'stripe' | 'paypal' | 'manual';
|
|
|
|
/**
|
|
* Helper to check if Stripe is configured
|
|
*/
|
|
export async function isStripeConfigured(): Promise<boolean> {
|
|
try {
|
|
const config = await getStripeConfig();
|
|
return !!config.publishable_key;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper to check if PayPal is configured
|
|
*/
|
|
export async function isPayPalConfigured(): Promise<boolean> {
|
|
try {
|
|
const config = await getPayPalConfig();
|
|
return !!config.client_id;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get available payment gateways based on user's country
|
|
* - Pakistan (PK): Stripe + Bank Transfer (manual), NO PayPal
|
|
* - Global: Stripe + PayPal, NO Bank Transfer
|
|
*/
|
|
export async function getAvailablePaymentGateways(userCountry?: string): Promise<{
|
|
stripe: boolean;
|
|
paypal: boolean;
|
|
manual: boolean;
|
|
}> {
|
|
const [stripeAvailable, paypalAvailable] = await Promise.all([
|
|
isStripeConfigured(),
|
|
isPayPalConfigured(),
|
|
]);
|
|
|
|
const isPakistan = userCountry?.toUpperCase() === 'PK';
|
|
|
|
return {
|
|
stripe: stripeAvailable,
|
|
// PayPal: available globally EXCEPT Pakistan
|
|
paypal: !isPakistan && paypalAvailable,
|
|
// Manual (Bank Transfer): available for Pakistan only
|
|
manual: isPakistan,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Subscribe to plan using preferred payment gateway
|
|
*/
|
|
export async function subscribeToPlan(
|
|
planId: string,
|
|
gateway: PaymentGateway,
|
|
options?: { return_url?: string; cancel_url?: string }
|
|
): Promise<{ redirect_url: string }> {
|
|
switch (gateway) {
|
|
case 'stripe': {
|
|
const session = await createStripeCheckout(planId, options);
|
|
// FIX: Return URL should include session_id for verification
|
|
const redirectUrl = new URL(session.checkout_url);
|
|
return { redirect_url: redirectUrl.toString() };
|
|
}
|
|
case 'paypal': {
|
|
const order = await createPayPalSubscriptionOrder(planId, options);
|
|
// FIX: Store order_id in localStorage before redirect
|
|
localStorage.setItem('paypal_order_id', order.order_id);
|
|
return { redirect_url: order.approval_url };
|
|
}
|
|
case 'manual':
|
|
throw new Error('Manual payment requires different flow - use submitManualPayment()');
|
|
default:
|
|
throw new Error(`Unsupported payment gateway: ${gateway}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Purchase credit package using preferred payment gateway
|
|
*/
|
|
export async function purchaseCredits(
|
|
packageId: string,
|
|
gateway: PaymentGateway,
|
|
options?: { return_url?: string; cancel_url?: string }
|
|
): Promise<{ redirect_url: string }> {
|
|
switch (gateway) {
|
|
case 'stripe': {
|
|
const session = await createStripeCreditCheckout(packageId, options);
|
|
return { redirect_url: session.checkout_url };
|
|
}
|
|
case 'paypal': {
|
|
const order = await createPayPalCreditOrder(packageId, options);
|
|
// FIX: Store order_id in localStorage before redirect
|
|
localStorage.setItem('paypal_order_id', order.order_id);
|
|
return { redirect_url: order.approval_url };
|
|
}
|
|
case 'manual':
|
|
throw new Error('Manual payment requires different flow - use submitManualPayment()');
|
|
default:
|
|
throw new Error(`Unsupported payment gateway: ${gateway}`);
|
|
}
|
|
}
|