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
8 changes: 4 additions & 4 deletions packages/server/src/aai-utils/billing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ The billing configuration is centralized in `config.ts` and uses environment var
| `BILLING_MARGIN_MULTIPLIER` | Margin multiplier for billing | 1.2 (20% margin) |
| `BILLING_PRO_PLAN_CREDITS` | Number of Credits included in Pro plan | 500000 |
| `BILLING_FREE_PLAN_CREDITS` | Number of Credits included in Free plan | 10000 |
| `BILLING_STRIPE_SECRET_KEY` | Stripe API secret key | - |
| `STRIPE_CREDITS_METER_ID` | Stripe meter ID for Credits | - |
| `STRIPE_FREE_PRICE_ID` | Stripe price ID for Free plan | - |
| `BILLING_STRIPE_PAID_PRICE_ID` | Stripe price ID for Paid plan | - |
| `BILLING_STRIPE_SECRET_KEY` | Stripe API secret key | - |
| `BILLING_STRIPE_CREDITS_METER_ID` | Stripe meter ID for Credits | - |
| `BILLING_STRIPE_FREE_PRICE_ID` | Stripe price ID for Free plan | - |
| `BILLING_STRIPE_PAID_PRICE_ID` | Stripe price ID for Paid plan | - |

### Resource Allocation

Expand Down
7 changes: 5 additions & 2 deletions packages/server/src/aai-utils/billing/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ export const log = console as unknown as Logger
// Default customer ID for development
export const DEFAULT_CUSTOMER_ID = process.env.BILLING_DEFAULT_STRIPE_CUSTOMER_ID

// Override flag: When true, ALWAYS use DEFAULT_CUSTOMER_ID regardless of trace metadata or user data
export const OVERRIDE_CUSTOMER_ID = process.env.BILLING_OVERRIDE_CUSTOMER_ID === 'true'

// Load environment variables with defaults
// Base rate: $20 for 500,000 credits = $0.00004 per credit
const BILLING_CREDIT_PRICE_USD = parseFloat(process.env.BILLING_CREDIT_PRICE_USD || '0.00004')
Expand All @@ -16,13 +19,13 @@ const BILLING_FREE_PLAN_CREDITS = parseInt(process.env.BILLING_FREE_PLAN_CREDITS
// Billing configuration
export const BILLING_CONFIG = {
PRICE_IDS: {
FREE_MONTHLY: process.env.STRIPE_FREE_PRICE_ID,
FREE_MONTHLY: process.env.BILLING_STRIPE_FREE_PRICE_ID,
PAID_MONTHLY: process.env.BILLING_STRIPE_PAID_PRICE_ID
},
// Base rate: $20 for 500,000 credits = $0.00004 per credit
CREDIT_TO_USD: BILLING_CREDIT_PRICE_USD,
MARGIN_MULTIPLIER: MARGIN_MULTIPLIER,
BILLING_CREDITS_METER_ID: process.env.STRIPE_CREDITS_METER_ID,
BILLING_CREDITS_METER_ID: process.env.BILLING_STRIPE_CREDITS_METER_ID,
BILLING_CREDITS_METER_NAME: 'credits',

// Plan limits
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ export class BillingService implements BillingProvider {
// Map summaries to include meter_name
const usage = allSummaries.map((summary) => ({
...summary,
meter_name: summary.meter === process.env.STRIPE_CREDITS_METER_ID ? 'credits' : 'unknown'
meter_name: summary.meter === BILLING_CONFIG.BILLING_CREDITS_METER_ID ? 'credits' : 'unknown'
}))

return {
Expand Down
22 changes: 13 additions & 9 deletions packages/server/src/aai-utils/billing/langfuse/LangfuseProvider.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CreditsData, TraceMetadata, SyncUsageResponse, UsageEvent, UsageEventsResponse, GetUsageEventsParams } from '../core/types'
import { log, DEFAULT_CUSTOMER_ID, BILLING_CONFIG } from '../config'
import { log, DEFAULT_CUSTOMER_ID, OVERRIDE_CUSTOMER_ID, BILLING_CONFIG } from '../config'
import axios from 'axios'
import { StripeProvider } from '../stripe/StripeProvider'
import { extractCredentialsAndModels } from 'flowise-components'
Expand Down Expand Up @@ -504,7 +504,8 @@ export class LangfuseProvider {
userId: metadata.userId,
organizationId: metadata.organizationId,
aiCredentialsOwnership: metadata.aiCredentialsOwnership,
stripeCustomerId: metadata.customerId || DEFAULT_CUSTOMER_ID!,
// When OVERRIDE_CUSTOMER_ID is true, ALWAYS use DEFAULT_CUSTOMER_ID (ignores trace metadata and DB values)
stripeCustomerId: OVERRIDE_CUSTOMER_ID ? DEFAULT_CUSTOMER_ID! : metadata.customerId || DEFAULT_CUSTOMER_ID!,
subscriptionTier: metadata.subscriptionTier || 'free',
timestamp: trace.timestamp.toString(),
timestampEpoch: timestampSeconds,
Expand Down Expand Up @@ -564,15 +565,18 @@ export class LangfuseProvider {
// We'll filter the results after fetching
})

// Filter traces by customerId
// Filter traces by userId and customerId
// When organizational billing override is enabled, we skip the customerId check
// because all traces have individual user customer IDs but bill to the org customer ID
const filteredTraces = (langfuseResponse?.data || []).filter((trace: Trace) => {
const metadata = trace.metadata as TraceMetadata
return (
trace.userId === userId ||
trace.userId === params.user.id ||
params.user.roles?.includes('Admin') ||
(metadata && metadata.stripeCustomerId === customerId)
)
// Always allow: own traces, admin access
const hasUserAccess = trace.userId === userId || trace.userId === params.user.id || params.user.roles?.includes('Admin')

// Only check customer ID when override is disabled
const hasCustomerAccess = !OVERRIDE_CUSTOMER_ID && metadata && metadata.stripeCustomerId === customerId

return hasUserAccess || hasCustomerAccess
})

// Transform traces to UsageEvent format
Expand Down
6 changes: 3 additions & 3 deletions packages/server/src/aai-utils/billing/stripe/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { BILLING_CONFIG } from '../config'

// Load environment variables with defaults
const BILLING_CREDIT_PRICE_USD = BILLING_CONFIG.CREDIT_TO_USD // Use the same value from billing config
const MARGIN_MULTIPLIER = parseFloat(process.env.STRIPE_MARGIN_MULTIPLIER || '1.3') // 30% margin
const MARGIN_MULTIPLIER = parseFloat(process.env.BILLING_STRIPE_MARGIN_MULTIPLIER || '1.3') // 30% margin

// Stripe-specific billing configuration
export const STRIPE_CONFIG = {
CREDIT_TO_USD: BILLING_CREDIT_PRICE_USD, // Cost per credit in USD

// Single meter for total credits
CREDITS: {
METER_ID: process.env.STRIPE_CREDITS_METER_ID || 'mtr_test_61S7tgODE3yzFip9Q41FeRAHyP6byJRI',
METER_ID: process.env.BILLING_STRIPE_CREDITS_METER_ID || 'mtr_test_61S7tgODE3yzFip9Q41FeRAHyP6byJRI',
METER_NAME: 'credits',
BASE_PRICE_USD: BILLING_CREDIT_PRICE_USD,
MARGIN_MULTIPLIER: MARGIN_MULTIPLIER // 30% margin
Expand All @@ -19,7 +19,7 @@ export const STRIPE_CONFIG = {
// Legacy meters configuration
METERS: {
AI_TOKENS: {
METER_ID: process.env.STRIPE_AI_TOKENS_METER_ID || 'mtr_test_61S7tgODE3yzFip9Q41FeRAHyP6byJRI',
METER_ID: process.env.BILLING_STRIPE_AI_TOKENS_METER_ID || 'mtr_test_61S7tgODE3yzFip9Q41FeRAHyP6byJRI',
TOKENS_PER_CREDIT: BILLING_CONFIG.AI_TOKENS.TOKENS_PER_CREDIT,
BASE_PRICE_USD: BILLING_CREDIT_PRICE_USD * 10, // 10x base price for tokens
MARGIN_MULTIPLIER: 1.2,
Expand Down
26 changes: 16 additions & 10 deletions packages/server/src/controllers/billing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import checkOwnership from '../../utils/checkOwnership'
import logger from '../../utils/logger'
// import { billingService } from '../../services/billing'
import { CustomerStatus, UsageStats } from '../../aai-utils/billing/core/types'
import { BILLING_CONFIG } from '../../aai-utils/billing/config'
import { BILLING_CONFIG, DEFAULT_CUSTOMER_ID, OVERRIDE_CUSTOMER_ID } from '../../aai-utils/billing/config'
import { UsageSummary } from '../../aai-utils/billing/core/types'
import Stripe from 'stripe'
import { ChatMessage } from '../../database/entities/ChatMessage'
Expand Down Expand Up @@ -262,7 +262,8 @@ const getSinglePublicChatbotConfig = async (req: Request, res: Response, next: N
*/
const getUsageSummary = async (req: Request, res: Response, next: NextFunction) => {
try {
const customerId = req.user?.stripeCustomerId
// Apply organizational billing override if enabled
const customerId = OVERRIDE_CUSTOMER_ID ? DEFAULT_CUSTOMER_ID : req.user?.stripeCustomerId
// if (!customerId) {
// throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, 'User has no associated Stripe customer')
// }
Expand Down Expand Up @@ -482,7 +483,8 @@ const createCheckoutSession = async (req: Request, res: Response, next: NextFunc
const billingService = new BillingService()
const session = await billingService.createCheckoutSession({
priceId: BILLING_CONFIG.PRICE_IDS.PAID_MONTHLY,
customerId: req.user?.stripeCustomerId!,
// Apply organizational billing override if enabled
customerId: OVERRIDE_CUSTOMER_ID ? DEFAULT_CUSTOMER_ID! : req.user?.stripeCustomerId!,
successUrl: `${req.headers.origin}/billing?status=success`,
cancelUrl: `${req.headers.origin}/billing?status=cancel`
})
Expand Down Expand Up @@ -522,8 +524,9 @@ const cancelSubscription = async (req: Request, res: Response, next: NextFunctio
}

// At this point TypeScript knows subscriptionId is a string
// Verify subscription belongs to the user
const subscription = await billingService.getSubscriptionWithUsage(req.user.stripeCustomerId!)
// Verify subscription belongs to the user/organization
const customerId = OVERRIDE_CUSTOMER_ID ? DEFAULT_CUSTOMER_ID! : req.user.stripeCustomerId!
const subscription = await billingService.getSubscriptionWithUsage(customerId)
if (!subscription || subscription.id !== subscriptionId) {
throw new InternalFlowiseError(StatusCodes.FORBIDDEN, 'Subscription not found or does not belong to the user')
}
Expand Down Expand Up @@ -571,9 +574,10 @@ const getSubscriptionWithUsage = async (req: Request, res: Response, next: NextF
throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, 'User not authenticated')
}

const customerId = req.user.stripeCustomerId
// Apply organizational billing override if enabled
const customerId = OVERRIDE_CUSTOMER_ID ? DEFAULT_CUSTOMER_ID : req.user.stripeCustomerId
if (!customerId) {
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, 'No Stripe customer ID associated with user')
throw new InternalFlowiseError(StatusCodes.BAD_REQUEST, 'No Stripe customer ID associated with user/organization')
}

// Get active subscription
Expand Down Expand Up @@ -632,10 +636,11 @@ const handleWebhook = async (req: Request, res: Response, next: NextFunction) =>
export const getCustomerStatus = async (req: Request, res: Response, next: NextFunction) => {
try {
const billingService = new BillingService()
const customerId = req.user?.stripeCustomerId
// Apply organizational billing override if enabled
const customerId = OVERRIDE_CUSTOMER_ID ? DEFAULT_CUSTOMER_ID : req.user?.stripeCustomerId

if (!customerId) {
return res.status(400).json({ error: 'Customer ID not found' })
return res.status(400).json({ error: 'Customer ID not found for user/organization' })
}

const [customer, subscription, usage] = await Promise.all([
Expand Down Expand Up @@ -705,7 +710,8 @@ export const getCustomerStatus = async (req: Request, res: Response, next: NextF
const getUsageEvents = async (req: Request, res: Response, next: NextFunction) => {
try {
const billingService = new BillingService()
const customerId = req.user?.stripeCustomerId
// Apply organizational billing override if enabled
const customerId = OVERRIDE_CUSTOMER_ID ? DEFAULT_CUSTOMER_ID : req.user?.stripeCustomerId
// if (!customerId) {
// throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, 'User has no associated Stripe customer')
// }
Expand Down
Loading