feat(db): implement bill_team RPC

This commit is contained in:
Gergő Móricz 2024-09-25 20:57:45 +02:00
parent 415fd9f333
commit 5a8eb17a82
7 changed files with 23 additions and 182 deletions

View File

@ -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
}); });

View File

@ -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,

View File

@ -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}`
); );

View File

@ -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
}); });

View File

@ -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;

View File

@ -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
}); });

View File

@ -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 };
}