2024-06-05 13:20:26 -07:00
|
|
|
import { NotificationType } from "../../types";
|
2024-04-21 10:36:48 -07:00
|
|
|
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-07-30 18:59:35 -04:00
|
|
|
import { getValue, setValue } from "../redis";
|
2024-08-12 13:37:47 -04:00
|
|
|
import { redlock } from "../redlock";
|
2024-09-04 15:19:45 -03:00
|
|
|
import * as Sentry from "@sentry/node";
|
2024-09-25 19:25:18 +02:00
|
|
|
import { AuthCreditUsageChunk } from "../../controllers/v1/types";
|
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-04-15 17:01:47 -04:00
|
|
|
export async function billTeam(team_id: string, credits: number) {
|
2024-04-21 10:36:48 -07:00
|
|
|
return withAuth(supaBillTeam)(team_id, credits);
|
|
|
|
|
}
|
|
|
|
|
export async function supaBillTeam(team_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`);
|
2024-04-15 17:01:47 -04:00
|
|
|
// 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.
|
|
|
|
|
|
2024-07-22 18:30:58 -04:00
|
|
|
// 1. get the subscription and check for available coupons concurrently
|
|
|
|
|
const [{ data: subscription }, { data: coupons }] = await Promise.all([
|
|
|
|
|
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"),
|
|
|
|
|
]);
|
2024-04-25 10:05:53 -03:00
|
|
|
|
2024-04-26 11:42:49 -03:00
|
|
|
let couponCredits = 0;
|
2024-09-03 21:02:41 -03:00
|
|
|
let sortedCoupons = [];
|
|
|
|
|
|
2024-04-25 10:05:53 -03:00
|
|
|
if (coupons && coupons.length > 0) {
|
2024-06-05 13:22:03 -07:00
|
|
|
couponCredits = coupons.reduce(
|
|
|
|
|
(total, coupon) => total + coupon.credits,
|
|
|
|
|
0
|
|
|
|
|
);
|
2024-09-03 21:02:41 -03:00
|
|
|
sortedCoupons = [...coupons].sort((a, b) => b.credits - a.credits);
|
2024-04-25 10:05:53 -03:00
|
|
|
}
|
2024-04-26 11:42:49 -03:00
|
|
|
// using coupon credits:
|
|
|
|
|
if (couponCredits > 0) {
|
2024-04-26 14:36:19 -07:00
|
|
|
// if there is no subscription and they have enough coupon credits
|
2024-04-26 16:27:31 -03:00
|
|
|
if (!subscription) {
|
|
|
|
|
// using only coupon credits:
|
2024-04-26 14:36:19 -07:00
|
|
|
// if there are enough coupon credits
|
2024-04-26 16:27:31 -03:00
|
|
|
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
|
2024-06-05 13:22:03 -07:00
|
|
|
.from("coupons")
|
|
|
|
|
.update({
|
|
|
|
|
credits: 0,
|
|
|
|
|
})
|
|
|
|
|
.eq("id", sortedCoupons[0].id);
|
2024-04-26 16:27:31 -03:00
|
|
|
sortedCoupons.shift();
|
|
|
|
|
} else {
|
|
|
|
|
// update coupon credits
|
|
|
|
|
await supabase_service
|
2024-06-05 13:22:03 -07:00
|
|
|
.from("coupons")
|
|
|
|
|
.update({
|
|
|
|
|
credits: sortedCoupons[0].credits - usedCredits,
|
|
|
|
|
})
|
|
|
|
|
.eq("id", sortedCoupons[0].id);
|
2024-04-26 16:27:31 -03:00
|
|
|
usedCredits = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return await createCreditUsage({ team_id, credits: 0 });
|
|
|
|
|
|
2024-06-05 13:22:03 -07:00
|
|
|
// not enough coupon credits and no subscription
|
2024-04-26 16:27:31 -03:00
|
|
|
} else {
|
|
|
|
|
// update coupon credits
|
|
|
|
|
const usedCredits = credits - couponCredits;
|
|
|
|
|
for (let i = 0; i < sortedCoupons.length; i++) {
|
|
|
|
|
await supabase_service
|
|
|
|
|
.from("coupons")
|
|
|
|
|
.update({
|
2024-06-05 13:22:03 -07:00
|
|
|
credits: 0,
|
2024-04-26 16:27:31 -03:00
|
|
|
})
|
|
|
|
|
.eq("id", sortedCoupons[i].id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return await createCreditUsage({ team_id, credits: usedCredits });
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-06-05 13:22:03 -07:00
|
|
|
|
2024-04-26 16:27:31 -03:00
|
|
|
// 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({
|
2024-06-05 13:22:03 -07:00
|
|
|
credits: 0,
|
2024-04-26 16:27:31 -03:00
|
|
|
})
|
|
|
|
|
.eq("id", sortedCoupons[i].id);
|
|
|
|
|
}
|
|
|
|
|
const usedCredits = credits - couponCredits;
|
2024-06-05 13:22:03 -07:00
|
|
|
return await createCreditUsage({
|
|
|
|
|
team_id,
|
|
|
|
|
subscription_id: subscription.id,
|
|
|
|
|
credits: usedCredits,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// using only coupon credits
|
2024-04-26 11:42:49 -03:00
|
|
|
let usedCredits = credits;
|
|
|
|
|
while (usedCredits > 0) {
|
|
|
|
|
// update coupons
|
|
|
|
|
if (sortedCoupons[0].credits < usedCredits) {
|
|
|
|
|
usedCredits = usedCredits - sortedCoupons[0].credits;
|
|
|
|
|
// update coupon credits
|
|
|
|
|
await supabase_service
|
2024-06-05 13:22:03 -07:00
|
|
|
.from("coupons")
|
|
|
|
|
.update({
|
|
|
|
|
credits: 0,
|
|
|
|
|
})
|
|
|
|
|
.eq("id", sortedCoupons[0].id);
|
2024-04-26 11:42:49 -03:00
|
|
|
sortedCoupons.shift();
|
|
|
|
|
} else {
|
|
|
|
|
// update coupon credits
|
|
|
|
|
await supabase_service
|
2024-06-05 13:22:03 -07:00
|
|
|
.from("coupons")
|
|
|
|
|
.update({
|
|
|
|
|
credits: sortedCoupons[0].credits - usedCredits,
|
|
|
|
|
})
|
|
|
|
|
.eq("id", sortedCoupons[0].id);
|
2024-04-26 11:42:49 -03:00
|
|
|
usedCredits = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-05 13:22:03 -07:00
|
|
|
return await createCreditUsage({
|
|
|
|
|
team_id,
|
|
|
|
|
subscription_id: subscription.id,
|
|
|
|
|
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
|
|
|
// not using coupon credits
|
|
|
|
|
if (!subscription) {
|
|
|
|
|
return await createCreditUsage({ team_id, credits });
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-05 13:22:03 -07:00
|
|
|
return await createCreditUsage({
|
|
|
|
|
team_id,
|
|
|
|
|
subscription_id: subscription.id,
|
|
|
|
|
credits,
|
|
|
|
|
});
|
2024-04-15 17:01:47 -04:00
|
|
|
}
|
|
|
|
|
|
2024-09-25 19:25:18 +02:00
|
|
|
export async function checkTeamCredits(chunk: AuthCreditUsageChunk, team_id: string, credits: number) {
|
|
|
|
|
return withAuth(supaCheckTeamCredits)(chunk, team_id, credits);
|
2024-04-21 10:36:48 -07:00
|
|
|
}
|
2024-08-20 14:16:54 -03:00
|
|
|
|
2024-04-21 10:36:48 -07:00
|
|
|
// if team has enough credits for the operation, return true, else return false
|
2024-09-25 19:25:18 +02:00
|
|
|
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") {
|
2024-08-20 14:16:54 -03:00
|
|
|
return { success: true, message: "Preview team, no credits used", remainingCredits: Infinity };
|
2024-04-15 17:01:47 -04:00
|
|
|
}
|
|
|
|
|
|
2024-09-25 19:25:18 +02:00
|
|
|
const creditsWillBeUsed = chunk.adjusted_credits_used + credits;
|
2024-09-07 13:42:45 -03:00
|
|
|
|
|
|
|
|
// Removal of + credits
|
2024-09-25 19:25:18 +02:00
|
|
|
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
|
2024-09-25 19:25:18 +02:00
|
|
|
if (creditsWillBeUsed >= chunk.price_credits) {
|
|
|
|
|
sendNotification(
|
2024-09-07 13:42:45 -03:00
|
|
|
team_id,
|
|
|
|
|
NotificationType.LIMIT_REACHED,
|
2024-09-25 19:25:18 +02:00
|
|
|
chunk.sub_current_period_start,
|
|
|
|
|
chunk.sub_current_period_end
|
2024-09-07 13:42:45 -03:00
|
|
|
);
|
2024-09-25 19:25:18 +02:00
|
|
|
return { success: false, message: "Insufficient credits, please upgrade!", remainingCredits: chunk.price_credits - chunk.adjusted_credits_used, 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
|
2024-09-25 19:25:18 +02:00
|
|
|
sendNotification(
|
2024-09-07 13:42:45 -03:00
|
|
|
team_id,
|
|
|
|
|
NotificationType.APPROACHING_LIMIT,
|
2024-09-25 19:25:18 +02:00
|
|
|
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
|
|
|
}
|
|
|
|
|
|
2024-09-25 19:25:18 +02:00
|
|
|
return { success: true, message: "Sufficient credits available", remainingCredits: chunk.price_credits - chunk.adjusted_credits_used, 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
|
|
|
}
|
|
|
|
|
|
2024-06-05 13:22:03 -07:00
|
|
|
async function createCreditUsage({
|
|
|
|
|
team_id,
|
|
|
|
|
subscription_id,
|
|
|
|
|
credits,
|
|
|
|
|
}: {
|
|
|
|
|
team_id: string;
|
|
|
|
|
subscription_id?: string;
|
|
|
|
|
credits: number;
|
|
|
|
|
}) {
|
2024-09-03 21:09:32 -03:00
|
|
|
await supabase_service
|
|
|
|
|
.from("credit_usage")
|
2024-04-26 11:42:49 -03:00
|
|
|
.insert([
|
|
|
|
|
{
|
|
|
|
|
team_id,
|
|
|
|
|
credits_used: credits,
|
|
|
|
|
subscription_id: subscription_id || null,
|
|
|
|
|
created_at: new Date(),
|
|
|
|
|
},
|
2024-09-03 21:09:32 -03:00
|
|
|
]);
|
2024-04-26 11:42:49 -03:00
|
|
|
|
2024-09-03 21:09:32 -03:00
|
|
|
return { success: true };
|
2024-06-05 13:22:03 -07:00
|
|
|
}
|