import React from 'react' import { Head, Link } from '@inertiajs/react' import AdminLayout from '../../../Layouts/AdminLayout' function StatCard({ label, value, hint = null }) { return (

{label}

{value.toLocaleString()}

{hint ?

{hint}

: null}
) } function formatTimestamp(value) { if (!value) return 'No webhook processed yet' try { return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'short', }).format(new Date(value)) } catch { return value } } function formatEventSummary(summary) { const payload = summary && typeof summary === 'object' ? summary : {} const preferredKeys = [ 'action', 'outcome', 'local_subscription_status', 'status', 'tracked', 'user_resolved', ] const prioritized = preferredKeys .filter((key) => Object.prototype.hasOwnProperty.call(payload, key)) .map((key) => [key, payload[key]]) const priceIds = Array.isArray(payload.price_ids) && payload.price_ids.length ? [['price_ids', payload.price_ids.join(', ')]] : [] const cacheCleared = typeof payload.cache_cleared === 'boolean' ? [['cache_cleared', payload.cache_cleared ? 'yes' : 'no']] : [] const lines = [...prioritized, ...priceIds, ...cacheCleared] .filter(([, value]) => value !== null && value !== undefined && value !== '') .slice(0, 4) return lines.length ? lines.map(([key, value]) => `${key}: ${String(value)}`).join(' · ') : 'No summary fields captured' } export default function AcademyBilling({ summary, planBreakdown, recentEvents, links }) { const missingPlans = Array.isArray(summary.missing_plan_keys) ? summary.missing_plan_keys : [] const noData = summary.enabled && (summary.active_subscribers || 0) === 0 && (summary.ended_subscriptions || 0) === 0 && (summary.recent_webhook_count || 0) === 0 return ( {noData ? (

No subscriber data in the database yet.

Subscription records are created when Stripe sends webhook events to this server after a completed checkout. In local development, use{' '} stripe listen --forward-to {window.location.origin}/stripe/webhook{' '} to forward events. On production, confirm the Stripe webhook is configured and active.

) : null}

Plan Health

Configured Academy plans

Dashboard Public pricing My billing account
{missingPlans.length ? (
Missing Stripe price IDs for: {missingPlans.join(', ')}
) : (
All configured Academy plans have Stripe price IDs.
)}
{planBreakdown.map((plan) => (

{plan.label}

{plan.tier} · {plan.interval}

{plan.configured ? 'configured' : 'missing'}

{(plan.subscribers || 0).toLocaleString()}

active subscriptions on this plan

))}

Webhook Sync

Recent Stripe activity

Billing enabled

{summary.enabled ? 'Yes' : 'No'}

Webhook audits stored

{(summary.recent_webhook_count || 0).toLocaleString()}

Last processed webhook

{formatTimestamp(summary.last_webhook_at)}

Ended subscriptions

{(summary.ended_subscriptions || 0).toLocaleString()}

Audit Trail

Latest academy billing events

Only the safe local summary is stored, not the raw Stripe payload.

{recentEvents.length ? recentEvents.map((event) => ( )) : ( )}
Event Plan Tier User Processed Summary
{event.event_type} {event.academy_plan || 'n/a'} {event.academy_tier || 'n/a'} {event.user_id || 'guest/unresolved'} {formatTimestamp(event.processed_at || event.created_at)} {formatEventSummary(event.payload_summary)}
No Academy billing webhook audits have been stored yet.
) }