Files
firecrawl/apps/api/src/services/billing/credit_billing.ts
T

279 lines
8.6 KiB
TypeScript
Raw Normal View History

2024-06-05 13:20:26 -07:00
import { NotificationType } from "../../types";
import { withAuth } from "../../lib/withAuth";
2024-06-05 13:20:26 -07:00
import { sendNotification } from "../notification/email_notification";
2024-04-15 17:01:47 -04:00
import { supabase_service } from "../supabase";
2024-11-07 20:57:33 +01:00
import { logger } from "../../lib/logger";
2024-09-04 15:19:45 -03:00
import * as Sentry from "@sentry/node";
import { AuthCreditUsageChunk } from "../../controllers/v1/types";
import { getACUC, setCachedACUC } from "../../controllers/auth";
2024-10-22 19:47:23 -03:00
import { issueCredits } from "./issue_credits";
import { redlock } from "../redlock";
import { autoCharge } from "./auto_charge";
import { getValue, setValue } from "../redis";
2025-02-27 16:18:03 -03:00
import { queueBillingOperation } from "./batch_billing";
2024-12-17 22:01:41 +01:00
import type { Logger } from "winston";
2024-07-30 18:59:35 -04:00
2025-01-18 21:10:11 -03:00
// Deprecated, done via rpc
2024-08-12 13:37:47 -04:00
const FREE_CREDITS = 500;
2024-07-30 18:59:35 -04:00
2024-09-25 20:57:45 +02:00
/**
* If you do not know the subscription_id in the current context, pass subscription_id as undefined.
*/
2024-12-11 19:46:11 -03:00
export async function billTeam(
team_id: string,
subscription_id: string | null | undefined,
2024-12-11 19:51:08 -03:00
credits: number,
2024-12-17 22:01:41 +01:00
logger?: Logger,
is_extract: boolean = false,
2024-12-11 19:46:11 -03:00
) {
2025-02-27 16:18:03 -03:00
// Maintain the withAuth wrapper for authentication
return withAuth(
async (team_id, subscription_id, credits, logger, is_extract) => {
// Within the authenticated context, queue the billing operation
return queueBillingOperation(team_id, subscription_id, credits, is_extract);
},
{ success: true, message: "No DB, bypassed." }
)(team_id, subscription_id, credits, logger, is_extract);
}
2025-02-27 16:18:03 -03:00
2024-12-11 19:46:11 -03:00
export async function supaBillTeam(
team_id: string,
subscription_id: string | null | undefined,
2024-12-11 19:51:08 -03:00
credits: number,
2024-12-17 22:01:41 +01:00
__logger?: Logger,
is_extract: boolean = false,
2024-12-11 19:46:11 -03:00
) {
2025-02-27 16:18:03 -03:00
// This function should no longer be called directly
// It has been moved to batch_billing.ts
2024-12-17 22:01:41 +01:00
const _logger = (__logger ?? logger).child({
module: "credit_billing",
method: "supaBillTeam",
2025-01-20 10:16:47 +01:00
teamId: team_id,
subscriptionId: subscription_id,
credits,
2024-12-17 22:01:41 +01:00
});
2025-02-27 16:18:03 -03:00
_logger.warn("supaBillTeam was called directly. This function is deprecated and should only be called from batch_billing.ts");
queueBillingOperation(team_id, subscription_id, credits, is_extract).catch((err) => {
_logger.error("Error queuing billing operation", { err });
Sentry.captureException(err);
2024-10-22 19:47:23 -03:00
});
2025-02-27 16:18:03 -03:00
// Forward to the batch billing system
return {
success: true,
message: "Billing operation queued",
};
2024-04-15 17:01:47 -04:00
}
2024-11-07 20:57:33 +01:00
export type CheckTeamCreditsResponse = {
2024-12-11 19:46:11 -03:00
success: boolean;
message: string;
remainingCredits: number;
chunk?: AuthCreditUsageChunk;
};
2024-11-07 20:57:33 +01:00
2024-12-11 19:46:11 -03:00
export async function checkTeamCredits(
chunk: AuthCreditUsageChunk | null,
team_id: string,
2024-12-11 19:51:08 -03:00
credits: number,
2024-12-11 19:46:11 -03:00
): Promise<CheckTeamCreditsResponse> {
return withAuth(supaCheckTeamCredits, {
success: true,
message: "No DB, bypassed",
2024-12-11 19:51:08 -03:00
remainingCredits: Infinity,
2024-12-11 19:46:11 -03:00
})(chunk, team_id, credits);
}
// if team has enough credits for the operation, return true, else return false
2024-12-11 19:46:11 -03:00
export async function supaCheckTeamCredits(
chunk: AuthCreditUsageChunk | null,
team_id: string,
2024-12-11 19:51:08 -03:00
credits: number,
2024-12-11 19:46:11 -03:00
): Promise<CheckTeamCreditsResponse> {
// WARNING: chunk will be null if team_id is preview -- do not perform operations on it under ANY circumstances - mogery
2025-01-25 19:02:32 +01:00
if (team_id === "preview" || team_id.startsWith("preview_")) {
2024-12-11 19:46:11 -03:00
return {
success: true,
message: "Preview team, no credits used",
2024-12-11 19:51:08 -03:00
remainingCredits: Infinity,
2024-12-11 19:46:11 -03:00
};
2024-11-07 20:57:33 +01:00
} else if (chunk === null) {
throw new Error("NULL ACUC passed to supaCheckTeamCredits");
2024-04-15 17:01:47 -04:00
}
const creditsWillBeUsed = chunk.adjusted_credits_used + credits;
2024-09-07 13:42:45 -03:00
// In case chunk.price_credits is undefined, set it to a large number to avoid mistakes
2024-10-22 19:47:23 -03:00
const totalPriceCredits = chunk.total_credits_sum ?? 100000000;
2024-09-07 13:42:45 -03:00
// Removal of + credits
2024-10-16 01:06:10 -03:00
const creditUsagePercentage = chunk.adjusted_credits_used / totalPriceCredits;
2024-06-05 13:20:26 -07:00
2024-12-11 19:46:11 -03:00
let isAutoRechargeEnabled = false,
autoRechargeThreshold = 1000;
2024-10-22 19:47:23 -03:00
const cacheKey = `team_auto_recharge_${team_id}`;
let cachedData = await getValue(cacheKey);
if (cachedData) {
const parsedData = JSON.parse(cachedData);
isAutoRechargeEnabled = parsedData.auto_recharge;
autoRechargeThreshold = parsedData.auto_recharge_threshold;
} else {
const { data, error } = await supabase_service
.from("teams")
.select("auto_recharge, auto_recharge_threshold")
.eq("id", team_id)
.single();
if (data) {
isAutoRechargeEnabled = data.auto_recharge;
autoRechargeThreshold = data.auto_recharge_threshold;
await setValue(cacheKey, JSON.stringify(data), 300); // Cache for 5 minutes (300 seconds)
}
}
2024-12-11 19:46:11 -03:00
if (
isAutoRechargeEnabled &&
chunk.remaining_credits < autoRechargeThreshold &&
!chunk.is_extract
2024-12-11 19:46:11 -03:00
) {
2024-10-22 19:47:23 -03:00
const autoChargeResult = await autoCharge(chunk, autoRechargeThreshold);
if (autoChargeResult.success) {
return {
success: true,
2024-12-11 19:46:11 -03:00
message: autoChargeResult.message,
remainingCredits: autoChargeResult.remainingCredits,
2024-12-11 19:51:08 -03:00
chunk: autoChargeResult.chunk,
2024-12-11 19:46:11 -03:00
};
}
2024-10-22 19:47:23 -03:00
}
2024-04-25 10:05:53 -03:00
// Compare the adjusted total credits used with the credits allowed by the plan
if (creditsWillBeUsed > totalPriceCredits) {
2024-10-16 01:06:10 -03:00
// Only notify if their actual credits (not what they will use) used is greater than the total price credits
2024-10-22 19:47:23 -03:00
if (chunk.adjusted_credits_used > totalPriceCredits) {
2024-10-16 01:06:10 -03:00
sendNotification(
team_id,
2024-10-22 19:47:23 -03:00
NotificationType.LIMIT_REACHED,
chunk.sub_current_period_start,
chunk.sub_current_period_end,
2024-12-11 19:51:08 -03:00
chunk,
2024-10-22 19:47:23 -03:00
);
}
return {
success: false,
message:
"Insufficient credits to perform this request. For more credits, you can upgrade your plan at https://firecrawl.dev/pricing.",
remainingCredits: chunk.remaining_credits,
2024-12-11 19:51:08 -03:00
chunk,
2024-10-22 19:47:23 -03:00
};
2024-09-07 13:42:45 -03:00
} else if (creditUsagePercentage >= 0.8 && creditUsagePercentage < 1) {
2024-06-05 13:20:26 -07:00
// Send email notification for approaching credit limit
sendNotification(
2024-09-07 13:42:45 -03:00
team_id,
NotificationType.APPROACHING_LIMIT,
chunk.sub_current_period_start,
2024-10-15 17:28:28 -03:00
chunk.sub_current_period_end,
2024-12-11 19:51:08 -03:00
chunk,
2024-09-07 13:42:45 -03:00
);
2024-04-15 17:01:47 -04:00
}
2024-10-22 19:47:23 -03:00
return {
success: true,
message: "Sufficient credits available",
remainingCredits: chunk.remaining_credits,
2024-12-11 19:51:08 -03:00
chunk,
2024-10-22 19:47:23 -03:00
};
2024-04-15 17:01:47 -04:00
}
// Count the total credits used by a team within the current billing period and return the remaining credits.
export async function countCreditsAndRemainingForCurrentBillingPeriod(
2024-12-11 19:51:08 -03:00
team_id: string,
2024-04-15 17:01:47 -04:00
) {
// 1. Retrieve the team's active subscription based on the team_id.
const { data: subscription, error: subscriptionError } =
await supabase_service
.from("subscriptions")
.select("id, price_id, current_period_start, current_period_end")
.eq("team_id", team_id)
.single();
2024-04-26 11:42:49 -03:00
const { data: coupons } = await supabase_service
.from("coupons")
.select("credits")
.eq("team_id", team_id)
.eq("status", "active");
2024-04-25 10:05:53 -03:00
2024-04-26 11:42:49 -03:00
let couponCredits = 0;
if (coupons && coupons.length > 0) {
2024-06-05 13:22:03 -07:00
couponCredits = coupons.reduce(
(total, coupon) => total + coupon.credits,
2024-12-11 19:51:08 -03:00
0,
2024-06-05 13:22:03 -07:00
);
2024-04-26 11:42:49 -03:00
}
2024-04-15 17:01:47 -04:00
2024-04-26 11:42:49 -03:00
if (subscriptionError || !subscription) {
2024-04-15 17:01:47 -04:00
// Free
const { data: creditUsages, error: creditUsageError } =
await supabase_service
.from("credit_usage")
.select("credits_used")
.is("subscription_id", null)
.eq("team_id", team_id);
if (creditUsageError || !creditUsages) {
2024-06-05 13:22:03 -07:00
throw new Error(
2024-12-11 19:51:08 -03:00
`Failed to retrieve credit usage for team_id: ${team_id}`,
2024-06-05 13:22:03 -07:00
);
2024-04-15 17:01:47 -04:00
}
const totalCreditsUsed = creditUsages.reduce(
(acc, usage) => acc + usage.credits_used,
2024-12-11 19:51:08 -03:00
0,
2024-04-15 17:01:47 -04:00
);
2024-04-26 11:42:49 -03:00
const remainingCredits = FREE_CREDITS + couponCredits - totalCreditsUsed;
2024-06-05 13:22:03 -07:00
return {
totalCreditsUsed: totalCreditsUsed,
remainingCredits,
2024-12-11 19:51:08 -03:00
totalCredits: FREE_CREDITS + couponCredits,
2024-06-05 13:22:03 -07:00
};
2024-04-15 17:01:47 -04:00
}
const { data: creditUsages, error: creditUsageError } = await supabase_service
2024-04-26 11:42:49 -03:00
.from("credit_usage")
.select("credits_used")
.eq("subscription_id", subscription.id)
.gte("created_at", subscription.current_period_start)
.lte("created_at", subscription.current_period_end);
2024-04-15 17:01:47 -04:00
if (creditUsageError || !creditUsages) {
2024-06-05 13:22:03 -07:00
throw new Error(
2024-12-11 19:51:08 -03:00
`Failed to retrieve credit usage for subscription_id: ${subscription.id}`,
2024-06-05 13:22:03 -07:00
);
2024-04-15 17:01:47 -04:00
}
2024-06-05 13:22:03 -07:00
const totalCreditsUsed = creditUsages.reduce(
(acc, usage) => acc + usage.credits_used,
2024-12-11 19:51:08 -03:00
0,
2024-06-05 13:22:03 -07:00
);
2024-04-15 17:01:47 -04:00
2024-04-25 10:05:53 -03:00
const { data: price, error: priceError } = await supabase_service
2024-04-26 11:42:49 -03:00
.from("prices")
.select("credits")
.eq("id", subscription.price_id)
.single();
2024-04-25 10:05:53 -03:00
if (priceError || !price) {
2024-06-05 13:22:03 -07:00
throw new Error(
2024-12-11 19:51:08 -03:00
`Failed to retrieve price for price_id: ${subscription.price_id}`,
2024-06-05 13:22:03 -07:00
);
2024-04-25 10:05:53 -03:00
}
2024-04-26 11:42:49 -03:00
const remainingCredits = price.credits + couponCredits - totalCreditsUsed;
2024-04-25 10:05:53 -03:00
return {
2024-04-26 11:42:49 -03:00
totalCreditsUsed,
remainingCredits,
2024-12-11 19:51:08 -03:00
totalCredits: price.credits,
2024-04-25 10:05:53 -03:00
};
2024-04-26 11:42:49 -03:00
}