fixed to use hooks

This commit is contained in:
2026-01-28 20:06:39 +00:00
parent 260d0558ef
commit 99987e35bb
16 changed files with 270 additions and 175 deletions

View File

@@ -33,8 +33,13 @@ export async function updateSeatCount(userId: number) {
return;
}
const stripeSubscriptionItemId = subscription.stripeSubscriptionItemId;
if (!stripeSubscriptionItemId) {
return;
}
// update stripe
await stripe.subscriptionItems.update(subscription.stripeSubscriptionItemId!, {
await stripe.subscriptionItems.update(stripeSubscriptionItemId, {
quantity: newQuantity,
proration_behavior: "always_invoice",
});

View File

@@ -1,5 +1,4 @@
import type { BunRequest } from "bun";
import { withAuth, withCors, withCSRF } from "../../auth/middleware";
import { type AuthedRequest, withAuth, withCors, withCSRF } from "../../auth/middleware";
import { getOrganisationMembers, getOrganisationsByUserId } from "../../db/queries/organisations";
import { getUserById } from "../../db/queries/users";
import { STRIPE_PRICE_ANNUAL, STRIPE_PRICE_MONTHLY, stripe } from "../../stripe/client";
@@ -7,7 +6,7 @@ import { errorResponse } from "../../validation";
const BASE_URL = process.env.FRONTEND_URL || "http://localhost:1420";
async function handler(req: BunRequest) {
async function handler(req: AuthedRequest) {
if (req.method !== "POST") {
return errorResponse("method not allowed", "METHOD_NOT_ALLOWED", 405);
}
@@ -20,7 +19,7 @@ async function handler(req: BunRequest) {
return errorResponse("missing required fields", "VALIDATION_ERROR", 400);
}
const userId = (req as any).userId;
const { userId } = req;
const user = await getUserById(userId);
if (!user) {

View File

@@ -1,18 +1,17 @@
import type { BunRequest } from "bun";
import { withAuth, withCors, withCSRF } from "../../auth/middleware";
import { type AuthedRequest, withAuth, withCors, withCSRF } from "../../auth/middleware";
import { getSubscriptionByUserId } from "../../db/queries/subscriptions";
import { stripe } from "../../stripe/client";
import { errorResponse } from "../../validation";
const BASE_URL = process.env.FRONTEND_URL || "http://localhost:1420";
async function handler(req: BunRequest) {
async function handler(req: AuthedRequest) {
if (req.method !== "POST") {
return errorResponse("method not allowed", "METHOD_NOT_ALLOWED", 405);
}
try {
const userId = (req as any).userId;
const { userId } = req;
const subscription = await getSubscriptionByUserId(userId);
if (!subscription?.stripeCustomerId) {
return errorResponse("no active subscription found", "NOT_FOUND", 404);

View File

@@ -1,15 +1,14 @@
import type { BunRequest } from "bun";
import { withAuth, withCors } from "../../auth/middleware";
import { type AuthedRequest, withAuth, withCors } from "../../auth/middleware";
import { getSubscriptionByUserId } from "../../db/queries/subscriptions";
import { errorResponse } from "../../validation";
async function handler(req: BunRequest) {
async function handler(req: AuthedRequest) {
if (req.method !== "GET") {
return errorResponse("method not allowed", "METHOD_NOT_ALLOWED", 405);
}
try {
const userId = (req as any).userId;
const { userId } = req;
const subscription = await getSubscriptionByUserId(userId);
return new Response(JSON.stringify({ subscription }), {

View File

@@ -9,7 +9,15 @@ import {
import { updateUser } from "../../db/queries/users";
import { stripe } from "../../stripe/client";
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET || "";
const webhookSecret = requireEnv("STRIPE_WEBHOOK_SECRET");
function requireEnv(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`${name} is required`);
}
return value;
}
export default async function webhook(req: BunRequest) {
if (req.method !== "POST") {
@@ -61,6 +69,13 @@ export default async function webhook(req: BunRequest) {
break;
}
// stripe types use snake_case for these fields
const sub = stripeSubscription as unknown as {
current_period_start: number;
current_period_end: number;
trial_end: number | null;
};
await createSubscription({
userId,
stripeCustomerId: session.customer as string,
@@ -69,11 +84,9 @@ export default async function webhook(req: BunRequest) {
stripePriceId: session.metadata?.priceId || "",
status: stripeSubscription.status,
quantity: parseInt(session.metadata?.quantity || "1", 10),
currentPeriodStart: new Date((stripeSubscription as any).current_period_start * 1000),
currentPeriodEnd: new Date((stripeSubscription as any).current_period_end * 1000),
trialEnd: stripeSubscription.trial_end
? new Date(stripeSubscription.trial_end * 1000)
: undefined,
currentPeriodStart: new Date(sub.current_period_start * 1000),
currentPeriodEnd: new Date(sub.current_period_end * 1000),
trialEnd: sub.trial_end ? new Date(sub.trial_end * 1000) : undefined,
});
await updateUser(userId, { plan: "pro" });
@@ -99,11 +112,16 @@ export default async function webhook(req: BunRequest) {
break;
}
// safely convert timestamps to dates
const currentPeriodStart = (subscription as any).current_period_start
? new Date((subscription as any).current_period_start * 1000)
// stripe types use snake_case for these fields
const sub = subscription as unknown as {
current_period_start: number | null;
current_period_end: number | null;
};
const currentPeriodStart = sub.current_period_start
? new Date(sub.current_period_start * 1000)
: undefined;
const currentPeriodEnd = (subscription as any).current_period_end
? new Date((subscription as any).current_period_end * 1000)
const currentPeriodEnd = sub.current_period_end
? new Date(sub.current_period_end * 1000)
: undefined;
await updateSubscription(localSub.id, {
@@ -136,34 +154,45 @@ export default async function webhook(req: BunRequest) {
case "invoice.payment_succeeded": {
const invoice = event.data.object as Stripe.Invoice;
if (!(invoice as any).subscription) break;
// stripe types use snake_case for these fields
const inv = invoice as unknown as {
subscription: string | null;
payment_intent: string | null;
};
const localSub = await getSubscriptionByStripeId((invoice as any).subscription as string);
if (!inv.subscription) break;
const localSub = await getSubscriptionByStripeId(inv.subscription);
if (!localSub) break;
await createPayment({
subscriptionId: localSub.id,
stripePaymentIntentId: (invoice as any).payment_intent as string,
stripePaymentIntentId: inv.payment_intent || "",
amount: invoice.amount_paid,
currency: invoice.currency,
status: "succeeded",
});
console.log(`payment recorded for subscription ${(invoice as any).subscription}`);
console.log(`payment recorded for subscription ${inv.subscription}`);
break;
}
case "invoice.payment_failed": {
const invoice = event.data.object as Stripe.Invoice;
if (!(invoice as any).subscription) break;
// stripe types use snake_case for these fields
const inv = invoice as unknown as {
subscription: string | null;
};
const localSub = await getSubscriptionByStripeId((invoice as any).subscription as string);
if (!inv.subscription) break;
const localSub = await getSubscriptionByStripeId(inv.subscription);
if (!localSub) break;
await updateSubscription(localSub.id, { status: "past_due" });
console.log(`payment failed for subscription ${(invoice as any).subscription}`);
console.log(`payment failed for subscription ${inv.subscription}`);
break;
}
}

View File

@@ -1,14 +1,18 @@
import Stripe from "stripe";
const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
if (!stripeSecretKey) {
throw new Error("STRIPE_SECRET_KEY is required");
}
const stripeSecretKey = requireEnv("STRIPE_SECRET_KEY");
export const stripe = new Stripe(stripeSecretKey, {
apiVersion: "2024-12-18.acacia",
apiVersion: "2025-12-15.clover",
});
export const STRIPE_PRICE_MONTHLY = process.env.STRIPE_PRICE_MONTHLY!;
export const STRIPE_PRICE_ANNUAL = process.env.STRIPE_PRICE_ANNUAL!;
export const STRIPE_PRICE_MONTHLY = requireEnv("STRIPE_PRICE_MONTHLY");
export const STRIPE_PRICE_ANNUAL = requireEnv("STRIPE_PRICE_ANNUAL");
function requireEnv(name: string): string {
const value = process.env[name];
if (!value) {
throw new Error(`${name} is required`);
}
return value;
}