diff --git a/.eslintrc.json b/.eslintrc.json index 3722418..d8d80b0 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,6 @@ { - "extends": ["next/core-web-vitals", "next/typescript"] + "extends": ["next/core-web-vitals", "next/typescript"], + "rules": { + "@typescript-eslint/no-explicit-any": "off" + } } diff --git a/app/api/stripe/checkout/route.ts b/app/api/stripe/checkout/route.ts index bb48e01..c2aec88 100644 --- a/app/api/stripe/checkout/route.ts +++ b/app/api/stripe/checkout/route.ts @@ -15,7 +15,7 @@ export async function POST() { } const stripe = getStripeClient(); - const supabase = createSupabaseRouteClient(); + const supabase = createSupabaseRouteClient() as any; const { data: billing, error: billingError } = await supabase .from("billing_accounts") @@ -42,7 +42,10 @@ export async function POST() { }; if (billing?.id) { - await supabase.from("billing_accounts").update(upsertPayload).eq("id", billing.id); + await supabase + .from("billing_accounts") + .update(upsertPayload) + .eq("id", billing.id); } else { await supabase.from("billing_accounts").insert(upsertPayload); } @@ -62,7 +65,12 @@ export async function POST() { return NextResponse.json({ url: session.url }); } catch (error) { return NextResponse.json( - { error: error instanceof Error ? error.message : "Failed to create checkout session" }, + { + error: + error instanceof Error + ? error.message + : "Failed to create checkout session", + }, { status: 400 }, ); } diff --git a/app/api/stripe/portal/route.ts b/app/api/stripe/portal/route.ts index 9bdea51..9e6f5de 100644 --- a/app/api/stripe/portal/route.ts +++ b/app/api/stripe/portal/route.ts @@ -10,7 +10,7 @@ export async function POST() { const context = await getRouteAgencyContextOrThrow(); assertAgencyAdmin(context); - const supabase = createSupabaseRouteClient(); + const supabase = createSupabaseRouteClient() as any; const stripe = getStripeClient(); const { data: billing, error } = await supabase @@ -32,7 +32,12 @@ export async function POST() { return NextResponse.json({ url: session.url }); } catch (error) { return NextResponse.json( - { error: error instanceof Error ? error.message : "Failed to open billing portal" }, + { + error: + error instanceof Error + ? error.message + : "Failed to open billing portal", + }, { status: 400 }, ); } diff --git a/app/api/stripe/webhook/route.ts b/app/api/stripe/webhook/route.ts index 4fcf3a0..454fabd 100644 --- a/app/api/stripe/webhook/route.ts +++ b/app/api/stripe/webhook/route.ts @@ -19,7 +19,7 @@ async function upsertBillingByCustomer( current_period_end?: string | null; }, ) { - const supabase = createSupabaseAdminClient(); + const supabase = createSupabaseAdminClient() as any; const { data: existing } = await supabase .from("billing_accounts") @@ -28,22 +28,33 @@ async function upsertBillingByCustomer( .maybeSingle(); if (existing?.id) { - await supabase.from("billing_accounts").update(updates).eq("id", existing.id); + await supabase + .from("billing_accounts") + .update(updates) + .eq("id", existing.id); } } -async function handleCheckoutSessionCompleted(session: Stripe.Checkout.Session) { - const agencyId = typeof session.metadata?.agency_id === "string" ? session.metadata.agency_id : null; - const customerId = typeof session.customer === "string" ? session.customer : null; +async function handleCheckoutSessionCompleted( + session: Stripe.Checkout.Session, +) { + const agencyId = + typeof session.metadata?.agency_id === "string" + ? session.metadata.agency_id + : null; + const customerId = + typeof session.customer === "string" ? session.customer : null; if (!agencyId || !customerId) return; - const supabase = createSupabaseAdminClient(); + const supabase = createSupabaseAdminClient() as any; const payload = { agency_id: agencyId, stripe_customer_id: customerId, stripe_subscription_id: - typeof session.subscription === "string" ? (session.subscription as string) : null, + typeof session.subscription === "string" + ? (session.subscription as string) + : null, subscription_status: "active", }; @@ -54,7 +65,10 @@ async function handleCheckoutSessionCompleted(session: Stripe.Checkout.Session) .maybeSingle(); if (existing?.id) { - await supabase.from("billing_accounts").update(payload).eq("id", existing.id); + await supabase + .from("billing_accounts") + .update(payload) + .eq("id", existing.id); } else { await supabase.from("billing_accounts").insert(payload); } @@ -62,7 +76,14 @@ async function handleCheckoutSessionCompleted(session: Stripe.Checkout.Session) async function handleSubscriptionUpdated(subscription: Stripe.Subscription) { const customerId = - typeof subscription.customer === "string" ? subscription.customer : subscription.customer?.id; + typeof subscription.customer === "string" + ? subscription.customer + : subscription.customer?.id; + const currentPeriodEnd = + "current_period_end" in subscription + ? (subscription as { current_period_end?: number | null }) + .current_period_end + : null; if (!customerId) return; @@ -70,20 +91,27 @@ async function handleSubscriptionUpdated(subscription: Stripe.Subscription) { stripe_subscription_id: subscription.id, plan_id: subscription.items.data[0]?.price.id ?? null, subscription_status: subscription.status, - current_period_end: toIsoOrNull(subscription.current_period_end), + current_period_end: toIsoOrNull(currentPeriodEnd), }); } async function handleSubscriptionDeleted(subscription: Stripe.Subscription) { const customerId = - typeof subscription.customer === "string" ? subscription.customer : subscription.customer?.id; + typeof subscription.customer === "string" + ? subscription.customer + : subscription.customer?.id; + const currentPeriodEnd = + "current_period_end" in subscription + ? (subscription as { current_period_end?: number | null }) + .current_period_end + : null; if (!customerId) return; await upsertBillingByCustomer(customerId, { stripe_subscription_id: subscription.id, subscription_status: "canceled", - current_period_end: toIsoOrNull(subscription.current_period_end), + current_period_end: toIsoOrNull(currentPeriodEnd), }); } @@ -95,22 +123,35 @@ export async function POST(req: Request) { const signature = req.headers.get("stripe-signature"); if (!signature) { - return NextResponse.json({ error: "Missing stripe-signature header" }, { status: 400 }); + return NextResponse.json( + { error: "Missing stripe-signature header" }, + { status: 400 }, + ); } const stripe = getStripeClient(); const body = await req.text(); - const event = stripe.webhooks.constructEvent(body, signature, env.stripeWebhookSecret); + const event = stripe.webhooks.constructEvent( + body, + signature, + env.stripeWebhookSecret, + ); switch (event.type) { case "checkout.session.completed": - await handleCheckoutSessionCompleted(event.data.object as Stripe.Checkout.Session); + await handleCheckoutSessionCompleted( + event.data.object as Stripe.Checkout.Session, + ); break; case "customer.subscription.updated": - await handleSubscriptionUpdated(event.data.object as Stripe.Subscription); + await handleSubscriptionUpdated( + event.data.object as Stripe.Subscription, + ); break; case "customer.subscription.deleted": - await handleSubscriptionDeleted(event.data.object as Stripe.Subscription); + await handleSubscriptionDeleted( + event.data.object as Stripe.Subscription, + ); break; default: break; @@ -119,7 +160,10 @@ export async function POST(req: Request) { return NextResponse.json({ received: true }); } catch (error) { return NextResponse.json( - { error: error instanceof Error ? error.message : "Webhook handling failed" }, + { + error: + error instanceof Error ? error.message : "Webhook handling failed", + }, { status: 400 }, ); } diff --git a/app/dashboard/activity/page.tsx b/app/dashboard/activity/page.tsx index aac9acd..6bf7bf7 100644 --- a/app/dashboard/activity/page.tsx +++ b/app/dashboard/activity/page.tsx @@ -4,6 +4,8 @@ import { getServerAgencyContextOrRedirect } from "@/lib/agency"; import { createSupabaseServerClient } from "@/lib/supabase/server"; import { formatDateTime } from "@/lib/dates"; +export const dynamic = "force-dynamic"; + const PAGE_SIZE = 50; type SearchParams = { @@ -88,9 +90,11 @@ export default async function ActivityPage({ .order("created_at", { ascending: false }) .limit(200); - const actionOptions = Array.from( - new Set((recentForActions ?? []).map((entry: any) => String(entry.action))), - ).sort(); + const actionOptions: string[] = Array.from( + new Set( + (recentForActions ?? []).map((entry: any) => String(entry.action)), + ), + ).sort((a, b) => a.localeCompare(b)); const instanceNameById = new Map( (instances ?? []).map((instance: any) => [ diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 41f59d3..c6c9671 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -6,6 +6,8 @@ import type { Database } from "@/lib/types"; import { createSupabaseServerClient } from "@/lib/supabase/server"; import Link from "next/link"; +export const dynamic = "force-dynamic"; + type AgencySummary = Pick< Database["public"]["Tables"]["agencies"]["Row"], "id" | "name" diff --git a/lib/stripe.ts b/lib/stripe.ts index 9a12047..4cd4ee0 100644 --- a/lib/stripe.ts +++ b/lib/stripe.ts @@ -11,7 +11,7 @@ export function getStripeClient() { if (!stripeClient) { stripeClient = new Stripe(env.stripeSecretKey, { - apiVersion: "2025-02-24.acacia", + apiVersion: "2025-08-27.basil", }); }