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.
Event
Plan
Tier
User
Processed
Summary
{recentEvents.length ? recentEvents.map((event) => (
{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.
)}
)
}