Files
accounted/app/api/billing/status/route.ts
T
Jakob Wennberg 0e8698f538 fix(entitlements): billing reachable in settings modal + conversion redesign (#821)
- Register BillingSettingsContent in SETTINGS_SECTIONS so the settings MODAL renders Abonnemang (it was falling back to Företag — billing was only a standalone page, never a registered section). Page is now a thin wrapper over the same component.
- Add GET /api/billing/status (isPaying / configured / trialEndsAt) so the client section gets state without server-only reads.
- Redesign for conversion: trial days-left urgency banner, reactive monthly/yearly price with a 'Spara 2 mån' badge, full-width price-bearing CTA, Stripe trust line, design-system-compliant chrome (flat Card, no shadow/rounded-xl, on-scale spacing, serif headline). Trialing companies now see the upgrade path (not the manage button).

The reported 'peach band' was not reproduced in code — no peach/salmon color exists in the app CSS and the only bottom drag-handle is in a md:hidden mobile sheet; most likely a macOS screenshot/desktop artifact.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 16:51:31 +02:00

49 lines
1.7 KiB
TypeScript

import { NextResponse } from 'next/server'
import { requireAuth } from '@/lib/auth/require-auth'
import { requireCompanyId } from '@/lib/company/context'
import { isStripeConfigured } from '@/lib/stripe/client'
/**
* Billing status for the client-rendered billing section (which lives inside the
* settings modal Dialog and can't read the DB server-side). Returns whether the
* company is paying, whether Stripe checkout is configured, and the trial expiry
* (for the days-left urgency banner). Read-only.
*/
export async function GET() {
const { user, supabase, error } = await requireAuth()
if (error) return error
let companyId: string | null = null
try {
companyId = await requireCompanyId(supabase, user.id)
} catch {
companyId = null
}
let isPaying = false
let trialEndsAt: string | null = null
if (companyId) {
const { data: sub } = await supabase
.from('company_subscriptions')
.select('status')
.eq('company_id', companyId)
.maybeSingle()
const status = (sub as { status: string | null } | null)?.status ?? null
// Paying = a real subscription. Deliberately excludes 'trialing' so a
// trialing company still sees the upgrade path (not the manage button).
isPaying = status === 'active' || status === 'past_due'
const { data: trial } = await supabase
.from('capability_grants')
.select('expires_at')
.eq('company_id', companyId)
.eq('source', 'trial')
.order('expires_at', { ascending: false })
.limit(1)
.maybeSingle()
trialEndsAt = (trial as { expires_at: string | null } | null)?.expires_at ?? null
}
return NextResponse.json({ isPaying, configured: isStripeConfigured(), trialEndsAt })
}