Files
igny8/frontend/src/services/billing.api.ts
IGNY8 VPS (Salman) da3b45d1c7 adsasdasd
2025-12-08 11:51:00 +00:00

913 lines
26 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;
subtotal: string;
tax_amount: string;
currency: string;
created_at: string;
paid_at?: string;
due_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;
}
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;
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;
}): 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);
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 response = await fetch(`/v1/billing/invoices/${invoiceId}/download_pdf/`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
},
});
if (!response.ok) {
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
// ============================================================================
// Account payment methods (CRUD)
export async function getAvailablePaymentMethods(): Promise<{
results: PaymentMethod[];
count: number;
}> {
const response = await fetchAPI('/v1/billing/payment-methods/');
// Frontend guard: only allow the simplified set we currently support
const allowed = new Set(['bank_transfer', 'manual']);
const filtered = (response.results || []).filter((m: PaymentMethod) => allowed.has(m.type));
return { results: filtered, count: filtered.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/');
}
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),
});
}
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;
currency?: string;
interval?: 'month' | 'year';
description?: string;
is_active?: boolean;
features?: string[];
limits?: Record<string, any>;
display_order?: number;
}
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',
});
}