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

175 lines
5.9 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-07-23 17:30:46 -03: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-07-30 18:59:35 -04:00
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.
*/
export async function billTeam(team_id: string, subscription_id: string | null | undefined, credits: number) {
return withAuth(supaBillTeam)(team_id, subscription_id, credits);
}
2024-09-25 20:57:45 +02:00
export async function supaBillTeam(team_id: string, subscription_id: string, credits: number) {
2024-04-15 17:01:47 -04:00
if (team_id === "preview") {
return { success: true, message: "Preview team, no credits used" };
}
2024-07-23 17:30:46 -03:00
Logger.info(`Billing team ${team_id} for ${credits} credits`);
const { data, error } =
2024-09-25 20:57:45 +02:00
await supabase_service.rpc("bill_team", { _team_id: team_id, sub_id: subscription_id ?? null, fetch_subscription: subscription_id === undefined, credits });
if (error) {
Sentry.captureException(error);
Logger.error("Failed to bill team: " + JSON.stringify(error));
return;
2024-04-25 10:05:53 -03:00
}
(async () => {
for (const apiKey of (data ?? []).map(x => x.api_key)) {
2024-09-25 22:15:02 +02:00
await setCachedACUC(apiKey, acuc => (acuc ? {
...acuc,
credits_used: acuc.credits_used + credits,
adjusted_credits_used: acuc.adjusted_credits_used + credits,
remaining_credits: acuc.remaining_credits - credits,
} : null));
}
})();
2024-04-15 17:01:47 -04:00
}
export async function checkTeamCredits(chunk: AuthCreditUsageChunk, team_id: string, credits: number) {
return withAuth(supaCheckTeamCredits)(chunk, team_id, credits);
}
// if team has enough credits for the operation, return true, else return false
export async function supaCheckTeamCredits(chunk: AuthCreditUsageChunk, team_id: string, credits: number) {
// WARNING: chunk will be null if team_id is preview -- do not perform operations on it under ANY circumstances - mogery
2024-04-15 17:01:47 -04:00
if (team_id === "preview") {
return { success: true, message: "Preview team, no credits used", remainingCredits: Infinity };
2024-04-15 17:01:47 -04:00
}
const creditsWillBeUsed = chunk.adjusted_credits_used + credits;
2024-09-07 13:42:45 -03:00
// Removal of + credits
const creditUsagePercentage = creditsWillBeUsed / chunk.price_credits;
2024-06-05 13:20:26 -07:00
2024-04-25 10:05:53 -03:00
// Compare the adjusted total credits used with the credits allowed by the plan
if (creditsWillBeUsed > chunk.price_credits) {
sendNotification(
2024-09-07 13:42:45 -03:00
team_id,
NotificationType.LIMIT_REACHED,
chunk.sub_current_period_start,
chunk.sub_current_period_end
2024-09-07 13:42:45 -03:00
);
2024-09-26 16:07:15 -04:00
return { success: false, message: "Insufficient credits. For more credits, you can upgrade your plan at https://firecrawl.dev/pricing.", remainingCredits: chunk.remaining_credits, chunk };
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,
chunk.sub_current_period_end
2024-09-07 13:42:45 -03:00
);
2024-04-15 17:01:47 -04:00
}
return { success: true, message: "Sufficient credits available", remainingCredits: chunk.remaining_credits, chunk };
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(
team_id: string
) {
// 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,
0
);
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(
`Failed to retrieve credit usage for team_id: ${team_id}`
);
2024-04-15 17:01:47 -04:00
}
const totalCreditsUsed = creditUsages.reduce(
(acc, usage) => acc + usage.credits_used,
0
);
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,
totalCredits: FREE_CREDITS + couponCredits,
};
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(
`Failed to retrieve credit usage for subscription_id: ${subscription.id}`
);
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,
0
);
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(
`Failed to retrieve price for price_id: ${subscription.price_id}`
);
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-06-05 13:22:03 -07:00
totalCredits: price.credits,
2024-04-25 10:05:53 -03:00
};
2024-04-26 11:42:49 -03:00
}