mirror of
https://git.mirrors.martin98.com/https://github.com/mendableai/firecrawl
synced 2025-08-06 08:26:05 +08:00
feat(db): implement bill_team RPC
This commit is contained in:
parent
415fd9f333
commit
5a8eb17a82
@ -244,7 +244,7 @@ export async function scrapeController(req: Request, res: Response) {
|
|||||||
}
|
}
|
||||||
if (creditsToBeBilled > 0) {
|
if (creditsToBeBilled > 0) {
|
||||||
// billing for doc done on queue end, bill only for llm extraction
|
// billing for doc done on queue end, bill only for llm extraction
|
||||||
billTeam(team_id, creditsToBeBilled).catch(error => {
|
billTeam(team_id, chunk.sub_id, creditsToBeBilled).catch(error => {
|
||||||
Logger.error(`Failed to bill team ${team_id} for ${creditsToBeBilled} credits: ${error}`);
|
Logger.error(`Failed to bill team ${team_id} for ${creditsToBeBilled} credits: ${error}`);
|
||||||
// Optionally, you could notify an admin or add to a retry queue here
|
// Optionally, you could notify an admin or add to a retry queue here
|
||||||
});
|
});
|
||||||
|
@ -18,6 +18,7 @@ export async function searchHelper(
|
|||||||
jobId: string,
|
jobId: string,
|
||||||
req: Request,
|
req: Request,
|
||||||
team_id: string,
|
team_id: string,
|
||||||
|
subscription_id: string,
|
||||||
crawlerOptions: any,
|
crawlerOptions: any,
|
||||||
pageOptions: PageOptions,
|
pageOptions: PageOptions,
|
||||||
searchOptions: SearchOptions,
|
searchOptions: SearchOptions,
|
||||||
@ -54,7 +55,7 @@ export async function searchHelper(
|
|||||||
|
|
||||||
|
|
||||||
if (justSearch) {
|
if (justSearch) {
|
||||||
billTeam(team_id, res.length).catch(error => {
|
billTeam(team_id, subscription_id, res.length).catch(error => {
|
||||||
Logger.error(`Failed to bill team ${team_id} for ${res.length} credits: ${error}`);
|
Logger.error(`Failed to bill team ${team_id} for ${res.length} credits: ${error}`);
|
||||||
// Optionally, you could notify an admin or add to a retry queue here
|
// Optionally, you could notify an admin or add to a retry queue here
|
||||||
});
|
});
|
||||||
@ -169,6 +170,7 @@ export async function searchController(req: Request, res: Response) {
|
|||||||
jobId,
|
jobId,
|
||||||
req,
|
req,
|
||||||
team_id,
|
team_id,
|
||||||
|
chunk.sub_id,
|
||||||
crawlerOptions,
|
crawlerOptions,
|
||||||
pageOptions,
|
pageOptions,
|
||||||
searchOptions,
|
searchOptions,
|
||||||
|
@ -152,7 +152,7 @@ export async function mapController(
|
|||||||
// remove duplicates that could be due to http/https or www
|
// remove duplicates that could be due to http/https or www
|
||||||
links = removeDuplicateUrls(links);
|
links = removeDuplicateUrls(links);
|
||||||
|
|
||||||
billTeam(req.auth.team_id, 1).catch((error) => {
|
billTeam(req.auth.team_id, req.acuc.sub_id, 1).catch((error) => {
|
||||||
Logger.error(
|
Logger.error(
|
||||||
`Failed to bill team ${req.auth.team_id} for 1 credit: ${error}`
|
`Failed to bill team ${req.auth.team_id} for 1 credit: ${error}`
|
||||||
);
|
);
|
||||||
|
@ -108,7 +108,7 @@ export async function scrapeController(
|
|||||||
creditsToBeBilled = 5;
|
creditsToBeBilled = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
billTeam(req.auth.team_id, creditsToBeBilled).catch(error => {
|
billTeam(req.auth.team_id, req.acuc.sub_id, creditsToBeBilled).catch(error => {
|
||||||
Logger.error(`Failed to bill team ${req.auth.team_id} for ${creditsToBeBilled} credits: ${error}`);
|
Logger.error(`Failed to bill team ${req.auth.team_id} for ${creditsToBeBilled} credits: ${error}`);
|
||||||
// Optionally, you could notify an admin or add to a retry queue here
|
// Optionally, you could notify an admin or add to a retry queue here
|
||||||
});
|
});
|
||||||
|
@ -318,10 +318,10 @@ type Account = {
|
|||||||
export type AuthCreditUsageChunk = {
|
export type AuthCreditUsageChunk = {
|
||||||
api_key: string;
|
api_key: string;
|
||||||
team_id: string;
|
team_id: string;
|
||||||
sub_id: string;
|
sub_id: string | null;
|
||||||
sub_current_period_start: string;
|
sub_current_period_start: string | null;
|
||||||
sub_current_period_end: string;
|
sub_current_period_end: string | null;
|
||||||
price_id: string;
|
price_id: string | null;
|
||||||
price_credits: number; // credit limit with assoicated price, or free_credits (500) if free plan
|
price_credits: number; // credit limit with assoicated price, or free_credits (500) if free plan
|
||||||
credits_used: number;
|
credits_used: number;
|
||||||
coupon_credits: number;
|
coupon_credits: number;
|
||||||
|
@ -120,7 +120,7 @@ export async function runWebScraper({
|
|||||||
: docs;
|
: docs;
|
||||||
|
|
||||||
if(is_scrape === false) {
|
if(is_scrape === false) {
|
||||||
billTeam(team_id, filteredDocs.length).catch(error => {
|
billTeam(team_id, undefined, filteredDocs.length).catch(error => {
|
||||||
Logger.error(`Failed to bill team ${team_id} for ${filteredDocs.length} credits: ${error}`);
|
Logger.error(`Failed to bill team ${team_id} for ${filteredDocs.length} credits: ${error}`);
|
||||||
// Optionally, you could notify an admin or add to a retry queue here
|
// Optionally, you could notify an admin or add to a retry queue here
|
||||||
});
|
});
|
||||||
|
@ -3,168 +3,30 @@ import { withAuth } from "../../lib/withAuth";
|
|||||||
import { sendNotification } from "../notification/email_notification";
|
import { sendNotification } from "../notification/email_notification";
|
||||||
import { supabase_service } from "../supabase";
|
import { supabase_service } from "../supabase";
|
||||||
import { Logger } from "../../lib/logger";
|
import { Logger } from "../../lib/logger";
|
||||||
import { getValue, setValue } from "../redis";
|
|
||||||
import { redlock } from "../redlock";
|
|
||||||
import * as Sentry from "@sentry/node";
|
import * as Sentry from "@sentry/node";
|
||||||
import { AuthCreditUsageChunk } from "../../controllers/v1/types";
|
import { AuthCreditUsageChunk } from "../../controllers/v1/types";
|
||||||
|
|
||||||
const FREE_CREDITS = 500;
|
const FREE_CREDITS = 500;
|
||||||
|
|
||||||
|
/**
|
||||||
export async function billTeam(team_id: string, credits: number) {
|
* If you do not know the subscription_id in the current context, pass subscription_id as undefined.
|
||||||
return withAuth(supaBillTeam)(team_id, credits);
|
*/
|
||||||
|
export async function billTeam(team_id: string, subscription_id: string | null | undefined, credits: number) {
|
||||||
|
return withAuth(supaBillTeam)(team_id, subscription_id, credits);
|
||||||
}
|
}
|
||||||
export async function supaBillTeam(team_id: string, credits: number) {
|
export async function supaBillTeam(team_id: string, subscription_id: string, credits: number) {
|
||||||
if (team_id === "preview") {
|
if (team_id === "preview") {
|
||||||
return { success: true, message: "Preview team, no credits used" };
|
return { success: true, message: "Preview team, no credits used" };
|
||||||
}
|
}
|
||||||
Logger.info(`Billing team ${team_id} for ${credits} credits`);
|
Logger.info(`Billing team ${team_id} for ${credits} credits`);
|
||||||
// When the API is used, you can log the credit usage in the credit_usage table:
|
|
||||||
// team_id: The ID of the team using the API.
|
|
||||||
// subscription_id: The ID of the team's active subscription.
|
|
||||||
// credits_used: The number of credits consumed by the API call.
|
|
||||||
// created_at: The timestamp of the API usage.
|
|
||||||
|
|
||||||
// 1. get the subscription and check for available coupons concurrently
|
const { error } =
|
||||||
const [{ data: subscription }, { data: coupons }] = await Promise.all([
|
await supabase_service.rpc("bill_team", { _team_id: team_id, sub_id: subscription_id ?? null, fetch_subscription: subscription_id === undefined, credits });
|
||||||
supabase_service
|
|
||||||
.from("subscriptions")
|
|
||||||
.select("*")
|
|
||||||
.eq("team_id", team_id)
|
|
||||||
.eq("status", "active")
|
|
||||||
.single(),
|
|
||||||
supabase_service
|
|
||||||
.from("coupons")
|
|
||||||
.select("id, credits")
|
|
||||||
.eq("team_id", team_id)
|
|
||||||
.eq("status", "active"),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let couponCredits = 0;
|
if (error) {
|
||||||
let sortedCoupons = [];
|
Sentry.captureException(error);
|
||||||
|
Logger.error("Failed to bill team: " + JSON.stringify(error));
|
||||||
if (coupons && coupons.length > 0) {
|
|
||||||
couponCredits = coupons.reduce(
|
|
||||||
(total, coupon) => total + coupon.credits,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
sortedCoupons = [...coupons].sort((a, b) => b.credits - a.credits);
|
|
||||||
}
|
}
|
||||||
// using coupon credits:
|
|
||||||
if (couponCredits > 0) {
|
|
||||||
// if there is no subscription and they have enough coupon credits
|
|
||||||
if (!subscription) {
|
|
||||||
// using only coupon credits:
|
|
||||||
// if there are enough coupon credits
|
|
||||||
if (couponCredits >= credits) {
|
|
||||||
// remove credits from coupon credits
|
|
||||||
let usedCredits = credits;
|
|
||||||
while (usedCredits > 0) {
|
|
||||||
// update coupons
|
|
||||||
if (sortedCoupons[0].credits < usedCredits) {
|
|
||||||
usedCredits = usedCredits - sortedCoupons[0].credits;
|
|
||||||
// update coupon credits
|
|
||||||
await supabase_service
|
|
||||||
.from("coupons")
|
|
||||||
.update({
|
|
||||||
credits: 0,
|
|
||||||
})
|
|
||||||
.eq("id", sortedCoupons[0].id);
|
|
||||||
sortedCoupons.shift();
|
|
||||||
} else {
|
|
||||||
// update coupon credits
|
|
||||||
await supabase_service
|
|
||||||
.from("coupons")
|
|
||||||
.update({
|
|
||||||
credits: sortedCoupons[0].credits - usedCredits,
|
|
||||||
})
|
|
||||||
.eq("id", sortedCoupons[0].id);
|
|
||||||
usedCredits = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await createCreditUsage({ team_id, credits: 0 });
|
|
||||||
|
|
||||||
// not enough coupon credits and no subscription
|
|
||||||
} else {
|
|
||||||
// update coupon credits
|
|
||||||
const usedCredits = credits - couponCredits;
|
|
||||||
for (let i = 0; i < sortedCoupons.length; i++) {
|
|
||||||
await supabase_service
|
|
||||||
.from("coupons")
|
|
||||||
.update({
|
|
||||||
credits: 0,
|
|
||||||
})
|
|
||||||
.eq("id", sortedCoupons[i].id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await createCreditUsage({ team_id, credits: usedCredits });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// with subscription
|
|
||||||
// using coupon + subscription credits:
|
|
||||||
if (credits > couponCredits) {
|
|
||||||
// update coupon credits
|
|
||||||
for (let i = 0; i < sortedCoupons.length; i++) {
|
|
||||||
await supabase_service
|
|
||||||
.from("coupons")
|
|
||||||
.update({
|
|
||||||
credits: 0,
|
|
||||||
})
|
|
||||||
.eq("id", sortedCoupons[i].id);
|
|
||||||
}
|
|
||||||
const usedCredits = credits - couponCredits;
|
|
||||||
return await createCreditUsage({
|
|
||||||
team_id,
|
|
||||||
subscription_id: subscription.id,
|
|
||||||
credits: usedCredits,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// using only coupon credits
|
|
||||||
let usedCredits = credits;
|
|
||||||
while (usedCredits > 0) {
|
|
||||||
// update coupons
|
|
||||||
if (sortedCoupons[0].credits < usedCredits) {
|
|
||||||
usedCredits = usedCredits - sortedCoupons[0].credits;
|
|
||||||
// update coupon credits
|
|
||||||
await supabase_service
|
|
||||||
.from("coupons")
|
|
||||||
.update({
|
|
||||||
credits: 0,
|
|
||||||
})
|
|
||||||
.eq("id", sortedCoupons[0].id);
|
|
||||||
sortedCoupons.shift();
|
|
||||||
} else {
|
|
||||||
// update coupon credits
|
|
||||||
await supabase_service
|
|
||||||
.from("coupons")
|
|
||||||
.update({
|
|
||||||
credits: sortedCoupons[0].credits - usedCredits,
|
|
||||||
})
|
|
||||||
.eq("id", sortedCoupons[0].id);
|
|
||||||
usedCredits = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await createCreditUsage({
|
|
||||||
team_id,
|
|
||||||
subscription_id: subscription.id,
|
|
||||||
credits: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// not using coupon credits
|
|
||||||
if (!subscription) {
|
|
||||||
return await createCreditUsage({ team_id, credits });
|
|
||||||
}
|
|
||||||
|
|
||||||
return await createCreditUsage({
|
|
||||||
team_id,
|
|
||||||
subscription_id: subscription.id,
|
|
||||||
credits,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkTeamCredits(chunk: AuthCreditUsageChunk, team_id: string, credits: number) {
|
export async function checkTeamCredits(chunk: AuthCreditUsageChunk, team_id: string, credits: number) {
|
||||||
@ -297,26 +159,3 @@ export async function countCreditsAndRemainingForCurrentBillingPeriod(
|
|||||||
totalCredits: price.credits,
|
totalCredits: price.credits,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createCreditUsage({
|
|
||||||
team_id,
|
|
||||||
subscription_id,
|
|
||||||
credits,
|
|
||||||
}: {
|
|
||||||
team_id: string;
|
|
||||||
subscription_id?: string;
|
|
||||||
credits: number;
|
|
||||||
}) {
|
|
||||||
await supabase_service
|
|
||||||
.from("credit_usage")
|
|
||||||
.insert([
|
|
||||||
{
|
|
||||||
team_id,
|
|
||||||
credits_used: credits,
|
|
||||||
subscription_id: subscription_id || null,
|
|
||||||
created_at: new Date(),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user