// static/vendor/js/billing.js // Vendor billing and subscription management const billingLog = window.LogConfig?.createLogger('BILLING') || console; function billingData() { return { // State loading: true, subscription: null, tiers: [], addons: [], myAddons: [], invoices: [], // UI state showTiersModal: false, showAddonsModal: false, showCancelModal: false, showSuccessMessage: false, showCancelMessage: false, showAddonSuccessMessage: false, cancelReason: '', purchasingAddon: null, // Initialize async init() { // Check URL params for success/cancel const params = new URLSearchParams(window.location.search); if (params.get('success') === 'true') { this.showSuccessMessage = true; window.history.replaceState({}, document.title, window.location.pathname); } if (params.get('cancelled') === 'true') { this.showCancelMessage = true; window.history.replaceState({}, document.title, window.location.pathname); } if (params.get('addon_success') === 'true') { this.showAddonSuccessMessage = true; window.history.replaceState({}, document.title, window.location.pathname); } await this.loadData(); }, async loadData() { this.loading = true; try { // Load all data in parallel const [subscriptionRes, tiersRes, addonsRes, myAddonsRes, invoicesRes] = await Promise.all([ this.apiGet('/billing/subscription'), this.apiGet('/billing/tiers'), this.apiGet('/billing/addons'), this.apiGet('/billing/my-addons'), this.apiGet('/billing/invoices?limit=5'), ]); this.subscription = subscriptionRes; this.tiers = tiersRes.tiers || []; this.addons = addonsRes || []; this.myAddons = myAddonsRes || []; this.invoices = invoicesRes.invoices || []; } catch (error) { billingLog.error('Error loading billing data:', error); this.showNotification('Failed to load billing data', 'error'); } finally { this.loading = false; } }, async selectTier(tier) { if (tier.is_current) return; try { const response = await this.apiPost('/billing/checkout', { tier_code: tier.code, is_annual: false }); if (response.checkout_url) { window.location.href = response.checkout_url; } } catch (error) { billingLog.error('Error creating checkout:', error); this.showNotification('Failed to create checkout session', 'error'); } }, async openPortal() { try { const response = await this.apiPost('/billing/portal', {}); if (response.portal_url) { window.location.href = response.portal_url; } } catch (error) { billingLog.error('Error opening portal:', error); this.showNotification('Failed to open payment portal', 'error'); } }, async cancelSubscription() { try { await this.apiPost('/billing/cancel', { reason: this.cancelReason, immediately: false }); this.showCancelModal = false; this.showNotification('Subscription cancelled. You have access until the end of your billing period.', 'success'); await this.loadData(); } catch (error) { billingLog.error('Error cancelling subscription:', error); this.showNotification('Failed to cancel subscription', 'error'); } }, async reactivate() { try { await this.apiPost('/billing/reactivate', {}); this.showNotification('Subscription reactivated!', 'success'); await this.loadData(); } catch (error) { billingLog.error('Error reactivating subscription:', error); this.showNotification('Failed to reactivate subscription', 'error'); } }, async purchaseAddon(addon) { this.purchasingAddon = addon.code; try { const response = await this.apiPost('/billing/addons/purchase', { addon_code: addon.code, quantity: 1 }); if (response.checkout_url) { window.location.href = response.checkout_url; } } catch (error) { billingLog.error('Error purchasing addon:', error); this.showNotification('Failed to purchase add-on', 'error'); } finally { this.purchasingAddon = null; } }, async cancelAddon(addon) { if (!confirm(`Are you sure you want to cancel ${addon.addon_name}?`)) { return; } try { await this.apiDelete(`/billing/addons/${addon.id}`); this.showNotification('Add-on cancelled successfully', 'success'); await this.loadData(); } catch (error) { billingLog.error('Error cancelling addon:', error); this.showNotification('Failed to cancel add-on', 'error'); } }, // Check if addon is already purchased isAddonPurchased(addonCode) { return this.myAddons.some(a => a.addon_code === addonCode && a.status === 'active'); }, // API helpers async apiGet(endpoint) { const response = await fetch(`/api/v1/vendor${endpoint}`, { headers: { 'Content-Type': 'application/json', }, credentials: 'include' }); if (!response.ok) { throw new Error(`API error: ${response.status}`); } return response.json(); }, async apiPost(endpoint, data) { const response = await fetch(`/api/v1/vendor${endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', body: JSON.stringify(data) }); if (!response.ok) { const error = await response.json().catch(() => ({})); throw new Error(error.detail || `API error: ${response.status}`); } return response.json(); }, async apiDelete(endpoint) { const response = await fetch(`/api/v1/vendor${endpoint}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', }, credentials: 'include' }); if (!response.ok) { const error = await response.json().catch(() => ({})); throw new Error(error.detail || `API error: ${response.status}`); } return response.json(); }, // Formatters formatDate(dateString) { if (!dateString) return '-'; const date = new Date(dateString); return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }); }, formatCurrency(cents, currency = 'EUR') { if (cents === null || cents === undefined) return '-'; const amount = cents / 100; return new Intl.NumberFormat('en-US', { style: 'currency', currency: currency }).format(amount); }, showNotification(message, type = 'info') { // Use Alpine's $dispatch if available, or fallback to alert if (window.Alpine) { window.dispatchEvent(new CustomEvent('show-notification', { detail: { message, type } })); } else { alert(message); } } }; }