SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
7.3 KB · 162 lines tsx
Raw Blame History
1import Link from "next/link";2import { CreditCard, Receipt } from "lucide-react";3import { requireAdmin } from "@/lib/session";4import { getBillingOverview } from "@/lib/queries/admin";5import { formatNumber, formatUsd } from "@/lib/format";6import { PageHeader } from "@/components/ui/page-header";7import { Alert } from "@/components/ui/alert";8import { Badge, StatusBadge } from "@/components/ui/badge";9import { EmptyState } from "@/components/ui/empty-state";10import { Stat, StatGrid } from "@/components/ui/stat";11import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";12import { DateCell, MarginText, Mono, Panel, PlanBadge, numCell } from "@/components/admin/primitives";1314export const dynamic = "force-dynamic";1516export default async function AdminBillingPage() {17  await requireAdmin();18  const b = await getBillingOverview();19  const paying = b.distribution.filter((d) => d.price > 0).reduce((s, d) => s + d.orgs, 0);2021  return (22    <>23      <PageHeader eyebrow="Revenue" title="Billing" description="Private platform: a single Unlimited plan with no list price, so MRR is always zero. Usage revenue reflects metered price_usd only. Stripe checkout and invoicing are not connected." />2425      {!b.stripeConfigured ? (26        <Alert variant="info" title="Stripe is not connected" className="mb-4">27          `STRIPE_SECRET_KEY` is not set and there is nothing to bill: every organization is on the Unlimited plan (private platform, no billing). Subscriptions and billing events below stay empty unless a Stripe integration is added later.28        </Alert>29      ) : null}3031      <StatGrid cols={4} className="mb-6">32        <Stat label="MRR estimate" value={formatUsd(b.mrr)} hint="orgs × plan list price" />33        <Stat label="Paying organizations" value={formatNumber(paying)} hint={`${b.stripeCustomers} with a Stripe customer id`} />34        <Stat label="Usage revenue 30d" value={formatUsd(b.usageRevenue30d)} hint="metered price_usd" />35        <Stat label="Usage margin 30d" value={<MarginText value={b.usageRevenue30d - b.usageCost30d} pct={b.usageRevenue30d > 0 ? ((b.usageRevenue30d - b.usageCost30d) / b.usageRevenue30d) * 100 : null} />} hint="vs upstream cost" />36      </StatGrid>3738      <Panel title="Plan distribution" flush>39        <Table>40          <TableHeader>41            <TableRow>42              <TableHead>Plan</TableHead>43              <TableHead className="text-right">Organizations</TableHead>44              <TableHead className="text-right">List price / mo</TableHead>45              <TableHead className="text-right">MRR</TableHead>46              <TableHead></TableHead>47            </TableRow>48          </TableHeader>49          <TableBody>50            {b.distribution.map((d) => (51              <TableRow key={d.plan}>52                <TableCell>53                  <PlanBadge plan={d.plan} />54                </TableCell>55                <TableCell className={numCell}>{formatNumber(d.orgs)}</TableCell>56                <TableCell className={numCell}>{formatUsd(d.price)}</TableCell>57                <TableCell className={numCell}>{formatUsd(d.mrr)}</TableCell>58                <TableCell className="text-right">59                  <Link href={`/admin/organizations?plan=${d.plan}`} className="text-[12.5px] text-accent hover:underline">60                    view →61                  </Link>62                </TableCell>63              </TableRow>64            ))}65            {b.unknown.map((u) => (66              <TableRow key={u.plan}>67                <TableCell>68                  <Badge variant="danger">{u.plan}</Badge> <span className="text-[12px] text-fg-muted">legacy plan value — treated as Unlimited; run `pnpm db:seed` to normalize</span>69                </TableCell>70                <TableCell className={numCell}>{u.n}</TableCell>71                <TableCell className={numCell}>—</TableCell>72                <TableCell className={numCell}>—</TableCell>73                <TableCell />74              </TableRow>75            ))}76          </TableBody>77        </Table>78      </Panel>7980      <div className="mt-6 grid gap-6 xl:grid-cols-2">81        <Panel title="Subscriptions" flush>82          {b.subs.length === 0 ? (83            <div className="p-4">84              <EmptyState icon={CreditCard} title="No subscriptions" description="Stripe is not connected yet. Rows appear here when checkout goes live and webhooks create subscriptions." compact />85            </div>86          ) : (87            <Table>88              <TableHeader>89                <TableRow>90                  <TableHead>Organization</TableHead>91                  <TableHead>Plan</TableHead>92                  <TableHead>Status</TableHead>93                  <TableHead>Period</TableHead>94                  <TableHead>Stripe</TableHead>95                </TableRow>96              </TableHeader>97              <TableBody>98                {b.subs.map((s) => (99                  <TableRow key={s.id}>100                    <TableCell>101                      <Link href={`/admin/organizations/${s.organizationId}`} className="hover:underline">102                        {s.orgName ?? s.organizationId}103                      </Link>104                    </TableCell>105                    <TableCell>106                      <PlanBadge plan={s.plan} />107                    </TableCell>108                    <TableCell>109                      <StatusBadge status={s.status} />110                      {s.cancelAtPeriodEnd ? <Badge variant="warning" className="ml-1">cancels</Badge> : null}111                    </TableCell>112                    <TableCell className="text-[12.5px] text-fg-muted">113                      <DateCell value={s.currentPeriodStart} relative={false} /> → <DateCell value={s.currentPeriodEnd} relative={false} />114                    </TableCell>115                    <TableCell>116                      <Mono>{s.stripeSubscriptionId ?? "—"}</Mono>117                    </TableCell>118                  </TableRow>119                ))}120              </TableBody>121            </Table>122          )}123        </Panel>124        <Panel title="Billing events" flush>125          {b.events.length === 0 ? (126            <div className="p-4">127              <EmptyState icon={Receipt} title="No billing events" description="Stripe webhook events (invoices, payment failures, plan changes) will be listed here once the integration is live." compact />128            </div>129          ) : (130            <Table>131              <TableHeader>132                <TableRow>133                  <TableHead>When</TableHead>134                  <TableHead>Type</TableHead>135                  <TableHead>Organization</TableHead>136                  <TableHead>Stripe event</TableHead>137                </TableRow>138              </TableHeader>139              <TableBody>140                {b.events.map((e) => (141                  <TableRow key={e.id}>142                    <TableCell>143                      <DateCell value={e.createdAt} relative={false} />144                    </TableCell>145                    <TableCell>146                      <Mono>{e.type}</Mono>147                    </TableCell>148                    <TableCell>{e.orgName ?? e.organizationId ?? "—"}</TableCell>149                    <TableCell>150                      <Mono>{e.stripeEventId ?? "—"}</Mono>151                    </TableCell>152                  </TableRow>153                ))}154              </TableBody>155            </Table>156          )}157        </Panel>158      </div>159    </>160  );161}162