Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions apps/api/src/posthog.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { PostHog } from "posthog-node";

export const posthog = new PostHog(process.env.POSTHOG_KEY ?? "key", {
host: process.env.POSTHOG_HOST ?? "none",
disabled: !process.env.POSTHOG_KEY || !process.env.POSTHOG_HOST,
});
const posthogDisabled = !process.env.POSTHOG_KEY || !process.env.POSTHOG_HOST;

// PostHog requires a non-empty API key even when disabled, so we use a placeholder
export const posthog = new PostHog(
process.env.POSTHOG_KEY ?? "phc_placeholder",
{
host: process.env.POSTHOG_HOST ?? "https://localhost",
disabled: posthogDisabled,
},
);
51 changes: 30 additions & 21 deletions apps/api/src/routes/dev-plans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
type DevPlanTier,
} from "@llmgateway/shared";

import { stripe } from "./payments.js";
import { getStripe } from "./payments.js";

import type { ServerTypes } from "@/vars.js";

Expand Down Expand Up @@ -243,7 +243,7 @@ devPlans.openapi(subscribe, async (c) => {
try {
const stripeCustomerId = await ensureStripeCustomer(personalOrg.id);

const session = await stripe.checkout.sessions.create({
const session = await getStripe().checkout.sessions.create({
customer: stripeCustomerId,
mode: "subscription",
line_items: [
Expand Down Expand Up @@ -356,9 +356,12 @@ devPlans.openapi(cancel, async (c) => {
}

try {
await stripe.subscriptions.update(personalOrg.devPlanStripeSubscriptionId, {
cancel_at_period_end: true,
});
await getStripe().subscriptions.update(
personalOrg.devPlanStripeSubscriptionId,
{
cancel_at_period_end: true,
},
);

await logAuditEvent({
organizationId: personalOrg.id,
Expand Down Expand Up @@ -444,7 +447,7 @@ devPlans.openapi(resume, async (c) => {
}

try {
const subscription = await stripe.subscriptions.retrieve(
const subscription = await getStripe().subscriptions.retrieve(
personalOrg.devPlanStripeSubscriptionId,
);

Expand All @@ -454,9 +457,12 @@ devPlans.openapi(resume, async (c) => {
});
}

await stripe.subscriptions.update(personalOrg.devPlanStripeSubscriptionId, {
cancel_at_period_end: false,
});
await getStripe().subscriptions.update(
personalOrg.devPlanStripeSubscriptionId,
{
cancel_at_period_end: false,
},
);

await logAuditEvent({
organizationId: personalOrg.id,
Expand Down Expand Up @@ -566,24 +572,27 @@ devPlans.openapi(changeTier, async (c) => {
}

try {
const subscription = await stripe.subscriptions.retrieve(
const subscription = await getStripe().subscriptions.retrieve(
personalOrg.devPlanStripeSubscriptionId,
);

// Update subscription with new tier
await stripe.subscriptions.update(personalOrg.devPlanStripeSubscriptionId, {
items: [
{
id: subscription.items.data[0].id,
price: newPriceId,
await getStripe().subscriptions.update(
personalOrg.devPlanStripeSubscriptionId,
{
items: [
{
id: subscription.items.data[0].id,
price: newPriceId,
},
],
proration_behavior: "create_prorations",
metadata: {
...subscription.metadata,
devPlan: newTier,
},
],
proration_behavior: "create_prorations",
metadata: {
...subscription.metadata,
devPlan: newTier,
},
});
);

// Update local database immediately
const newCreditsLimit = getDevPlanCreditsLimit(newTier);
Expand Down
33 changes: 21 additions & 12 deletions apps/api/src/routes/payments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,21 @@ import { calculateFees } from "@llmgateway/shared";

import type { ServerTypes } from "@/vars.js";

export const stripe = new Stripe(
process.env.STRIPE_SECRET_KEY ?? "sk_test_123",
{
apiVersion: "2025-04-30.basil",
},
);
let _stripe: Stripe | null = null;

export function getStripe(): Stripe {
if (!_stripe) {
if (!process.env.STRIPE_SECRET_KEY) {
throw new Error(
"STRIPE_SECRET_KEY environment variable is required for Stripe operations",
);
}
_stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: "2025-04-30.basil",
});
}
return _stripe;
}

export const payments = new OpenAPIHono<ServerTypes>();

Expand Down Expand Up @@ -90,7 +99,7 @@ payments.openapi(createPaymentIntent, async (c) => {
amount,
});

const paymentIntent = await stripe.paymentIntents.create({
const paymentIntent = await getStripe().paymentIntents.create({
amount: Math.round(feeBreakdown.totalAmount * 100),
currency: "usd",
description: `Credit purchase for ${amount} USD (including fees)`,
Expand Down Expand Up @@ -161,7 +170,7 @@ payments.openapi(createSetupIntent, async (c) => {

const organizationId = userOrganization.organization.id;

const setupIntent = await stripe.setupIntents.create({
const setupIntent = await getStripe().setupIntents.create({
usage: "off_session",
metadata: {
organizationId,
Expand Down Expand Up @@ -236,7 +245,7 @@ payments.openapi(getPaymentMethods, async (c) => {

const enhancedPaymentMethods = await Promise.all(
paymentMethods.map(async (pm) => {
const stripePaymentMethod = await stripe.paymentMethods.retrieve(
const stripePaymentMethod = await getStripe().paymentMethods.retrieve(
pm.stripePaymentMethodId,
);

Expand Down Expand Up @@ -424,13 +433,13 @@ payments.openapi(deletePaymentMethod, async (c) => {
// Get card details before deleting for audit log
let cardLast4: string | undefined;
try {
const stripePaymentMethod = await stripe.paymentMethods.retrieve(
const stripePaymentMethod = await getStripe().paymentMethods.retrieve(
paymentMethod.stripePaymentMethodId,
);
cardLast4 = stripePaymentMethod.card?.last4;
} catch {}

await stripe.paymentMethods.detach(paymentMethod.stripePaymentMethodId);
await getStripe().paymentMethods.detach(paymentMethod.stripePaymentMethodId);

await db.delete(tables.paymentMethod).where(eq(tables.paymentMethod.id, id));

Expand Down Expand Up @@ -544,7 +553,7 @@ payments.openapi(topUpWithSavedMethod, async (c) => {
amount,
});

const paymentIntent = await stripe.paymentIntents.create({
const paymentIntent = await getStripe().paymentIntents.create({
amount: Math.round(feeBreakdown.totalAmount * 100),
currency: "usd",
description: `Credit purchase for ${amount} USD (including fees)`,
Expand Down
16 changes: 8 additions & 8 deletions apps/api/src/routes/subscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { logAuditEvent } from "@llmgateway/audit";
import { db } from "@llmgateway/db";
import { logger } from "@llmgateway/logger";

import { stripe } from "./payments.js";
import { getStripe } from "./payments.js";

import type { ServerTypes } from "@/vars.js";

Expand Down Expand Up @@ -117,7 +117,7 @@ subscriptions.openapi(createProSubscription, async (c) => {
}

// Create Stripe Checkout session
const session = await stripe.checkout.sessions.create({
const session = await getStripe().checkout.sessions.create({
customer: stripeCustomerId,
mode: "subscription",
line_items: [
Expand Down Expand Up @@ -234,7 +234,7 @@ subscriptions.openapi(cancelProSubscription, async (c) => {

try {
// Cancel the subscription at the end of the current period
await stripe.subscriptions.update(organization.stripeSubscriptionId, {
await getStripe().subscriptions.update(organization.stripeSubscriptionId, {
cancel_at_period_end: true,
});

Expand Down Expand Up @@ -324,7 +324,7 @@ subscriptions.openapi(resumeProSubscription, async (c) => {

try {
// Check if subscription is actually cancelled
const subscription = await stripe.subscriptions.retrieve(
const subscription = await getStripe().subscriptions.retrieve(
organization.stripeSubscriptionId,
);

Expand All @@ -335,7 +335,7 @@ subscriptions.openapi(resumeProSubscription, async (c) => {
}

// Resume the subscription by setting cancel_at_period_end to false
await stripe.subscriptions.update(organization.stripeSubscriptionId, {
await getStripe().subscriptions.update(organization.stripeSubscriptionId, {
cancel_at_period_end: false,
});

Expand Down Expand Up @@ -425,7 +425,7 @@ subscriptions.openapi(upgradeToYearlyPlan, async (c) => {

try {
// Get current subscription to check if it's already yearly
const subscription = await stripe.subscriptions.retrieve(
const subscription = await getStripe().subscriptions.retrieve(
organization.stripeSubscriptionId,
);

Expand All @@ -445,7 +445,7 @@ subscriptions.openapi(upgradeToYearlyPlan, async (c) => {
}

// Update subscription to yearly plan
await stripe.subscriptions.update(organization.stripeSubscriptionId, {
await getStripe().subscriptions.update(organization.stripeSubscriptionId, {
items: [
{
id: subscription.items.data[0].id,
Expand Down Expand Up @@ -533,7 +533,7 @@ subscriptions.openapi(getSubscriptionStatus, async (c) => {
let billingCycle: "monthly" | "yearly" | null = null;
if (organization.stripeSubscriptionId) {
try {
const subscription = await stripe.subscriptions.retrieve(
const subscription = await getStripe().subscriptions.retrieve(
organization.stripeSubscriptionId,
);
const currentPriceId = subscription.items.data[0]?.price.id;
Expand Down
17 changes: 9 additions & 8 deletions apps/api/src/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { logger } from "@llmgateway/logger";
import { getDevPlanCreditsLimit, type DevPlanTier } from "@llmgateway/shared";

import { posthog } from "./posthog.js";
import { stripe } from "./routes/payments.js";
import { getStripe } from "./routes/payments.js";
Comment thread
coderabbitai[bot] marked this conversation as resolved.
import { notifyCreditsPurchased } from "./utils/discord.js";
import {
generatePaymentFailureEmailHtml,
Expand All @@ -34,7 +34,7 @@ export async function ensureStripeCustomer(

let stripeCustomerId = organization.stripeCustomerId;
if (!stripeCustomerId) {
const customer = await stripe.customers.create({
const customer = await getStripe().customers.create({
email: organization.billingEmail,
metadata: {
organizationId,
Expand All @@ -50,7 +50,7 @@ export async function ensureStripeCustomer(
.where(eq(tables.organization.id, organizationId));
} else {
// Update existing customer email if billingEmail has changed
await stripe.customers.update(stripeCustomerId, {
await getStripe().customers.update(stripeCustomerId, {
email: organization.billingEmail,
});
}
Expand Down Expand Up @@ -95,7 +95,7 @@ async function resolveOrganizationFromStripeEvent(eventData: {
// 3. Try to get from subscription metadata if subscription ID is available
if (!organizationId && eventData.subscription) {
try {
const stripeSubscription = await stripe.subscriptions.retrieve(
const stripeSubscription = await getStripe().subscriptions.retrieve(
eventData.subscription,
);
if (stripeSubscription.metadata?.organizationId) {
Expand Down Expand Up @@ -188,7 +188,7 @@ stripeRoutes.openapi(webhookHandler, async (c) => {
const body = await c.req.raw.text();
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET ?? "";

const event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
const event = getStripe().webhooks.constructEvent(body, sig, webhookSecret);

logger.info(JSON.stringify({ kind: "stripe-event", payload: event }));

Expand Down Expand Up @@ -904,7 +904,7 @@ async function handleChargeRefunded(event: Stripe.ChargeRefundedEvent) {
}

// Fetch refunds for this charge since they're not expanded in webhook events
const refundsResponse = await stripe.refunds.list({
const refundsResponse = await getStripe().refunds.list({
charge: charge.id,
limit: 1,
});
Expand Down Expand Up @@ -1030,11 +1030,12 @@ async function handleSetupIntentSucceeded(
const paymentMethodId =
typeof payment_method === "string" ? payment_method : payment_method.id;

await stripe.paymentMethods.attach(paymentMethodId, {
await getStripe().paymentMethods.attach(paymentMethodId, {
customer: stripeCustomerId,
});

const paymentMethod = await stripe.paymentMethods.retrieve(paymentMethodId);
const paymentMethod =
await getStripe().paymentMethods.retrieve(paymentMethodId);

const existingPaymentMethods = await db.query.paymentMethod.findMany({
where: {
Expand Down
20 changes: 16 additions & 4 deletions apps/worker/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,21 @@ import { syncProvidersAndModels } from "./services/sync-models.js";
const CURRENT_MINUTE_HISTORY_INTERVAL_SECONDS =
Number(process.env.CURRENT_MINUTE_HISTORY_INTERVAL_SECONDS) || 5;

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY ?? "sk_test_123", {
apiVersion: "2025-04-30.basil",
});
let _stripe: Stripe | null = null;

function getStripe(): Stripe {
if (!_stripe) {
if (!process.env.STRIPE_SECRET_KEY) {
throw new Error(
"STRIPE_SECRET_KEY environment variable is required for Stripe operations",
);
}
_stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: "2025-04-30.basil",
});
}
return _stripe;
}

const AUTO_TOPUP_LOCK_KEY = "auto_topup_check";
const CREDIT_PROCESSING_LOCK_KEY = "credit_processing";
Expand Down Expand Up @@ -273,7 +285,7 @@ async function processAutoTopUp(): Promise<void> {
);

try {
const paymentIntent = await stripe.paymentIntents.create({
const paymentIntent = await getStripe().paymentIntents.create({
amount: Math.round(feeBreakdown.totalAmount * 100),
currency: "usd",
description: `Auto top-up for ${topUpAmount} USD (total: ${feeBreakdown.totalAmount} including fees)`,
Expand Down
Loading