Merge pull request #66 from mendableai/feat/coupons
[Feat] Coupon system
This commit is contained in:
commit
fb08f28edf
@ -46,18 +46,18 @@ export async function scrapeHelper(
|
|||||||
return { success: true, error: "No page found", returnCode: 200 };
|
return { success: true, error: "No page found", returnCode: 200 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const { success, credit_usage } = await billTeam(
|
const billingResult = await billTeam(
|
||||||
team_id,
|
team_id,
|
||||||
filteredDocs.length
|
filteredDocs.length
|
||||||
);
|
);
|
||||||
if (!success) {
|
if (!billingResult.success) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error:
|
error:
|
||||||
"Failed to bill team. Insufficient credits or subscription not found.",
|
"Failed to bill team. Insufficient credits or subscription not found.",
|
||||||
returnCode: 402,
|
returnCode: 402,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
@ -83,11 +83,11 @@ export async function searchHelper(
|
|||||||
return { success: true, error: "No page found", returnCode: 200 };
|
return { success: true, error: "No page found", returnCode: 200 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const { success, credit_usage } = await billTeam(
|
const billingResult = await billTeam(
|
||||||
team_id,
|
team_id,
|
||||||
filteredDocs.length
|
filteredDocs.length
|
||||||
);
|
);
|
||||||
if (!success) {
|
if (!billingResult.success) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error:
|
error:
|
||||||
|
@ -89,12 +89,12 @@ export async function runWebScraper({
|
|||||||
: docs.filter((doc) => doc.content.trim().length > 0);
|
: docs.filter((doc) => doc.content.trim().length > 0);
|
||||||
|
|
||||||
|
|
||||||
const { success, credit_usage } = await billTeam(
|
const billingResult = await billTeam(
|
||||||
team_id,
|
team_id,
|
||||||
filteredDocs.length
|
filteredDocs.length
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!success) {
|
if (!billingResult.success) {
|
||||||
// throw new Error("Failed to bill team, no subscription was found");
|
// throw new Error("Failed to bill team, no subscription was found");
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
@ -18,7 +18,6 @@ export async function supaBillTeam(team_id: string, credits: number) {
|
|||||||
// created_at: The timestamp of the API usage.
|
// created_at: The timestamp of the API usage.
|
||||||
|
|
||||||
// 1. get the subscription
|
// 1. get the subscription
|
||||||
|
|
||||||
const { data: subscription } = await supabase_service
|
const { data: subscription } = await supabase_service
|
||||||
.from("subscriptions")
|
.from("subscriptions")
|
||||||
.select("*")
|
.select("*")
|
||||||
@ -26,35 +25,124 @@ export async function supaBillTeam(team_id: string, credits: number) {
|
|||||||
.eq("status", "active")
|
.eq("status", "active")
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (!subscription) {
|
// 2. Check for available coupons
|
||||||
const { data: credit_usage } = await supabase_service
|
const { data: coupons } = await supabase_service
|
||||||
.from("credit_usage")
|
.from("coupons")
|
||||||
.insert([
|
.select("id, credits")
|
||||||
{
|
.eq("team_id", team_id)
|
||||||
team_id,
|
.eq("status", "active");
|
||||||
credits_used: credits,
|
|
||||||
created_at: new Date(),
|
|
||||||
},
|
|
||||||
])
|
|
||||||
.select();
|
|
||||||
|
|
||||||
return { success: true, credit_usage };
|
let couponCredits = 0;
|
||||||
|
if (coupons && coupons.length > 0) {
|
||||||
|
couponCredits = coupons.reduce((total, coupon) => total + coupon.credits, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. add the credits to the credits_usage
|
let sortedCoupons = coupons.sort((a, b) => b.credits - a.credits);
|
||||||
const { data: credit_usage } = await supabase_service
|
// using coupon credits:
|
||||||
.from("credit_usage")
|
if (couponCredits > 0) {
|
||||||
.insert([
|
// if there is no subscription and they have enough coupon credits
|
||||||
{
|
if (!subscription) {
|
||||||
team_id,
|
// using only coupon credits:
|
||||||
subscription_id: subscription.id,
|
// if there are enough coupon credits
|
||||||
credits_used: credits,
|
if (couponCredits >= credits) {
|
||||||
created_at: new Date(),
|
// remove credits from coupon credits
|
||||||
},
|
let usedCredits = credits;
|
||||||
])
|
while (usedCredits > 0) {
|
||||||
.select();
|
// 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();
|
||||||
|
|
||||||
return { success: true, credit_usage };
|
} 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(team_id: string, credits: number) {
|
export async function checkTeamCredits(team_id: string, credits: number) {
|
||||||
@ -65,16 +153,34 @@ export async function supaCheckTeamCredits(team_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" };
|
||||||
}
|
}
|
||||||
// 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)
|
|
||||||
.eq("status", "active")
|
|
||||||
.single();
|
|
||||||
|
|
||||||
|
// Retrieve the team's active subscription
|
||||||
|
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)
|
||||||
|
.eq("status", "active")
|
||||||
|
.single();
|
||||||
|
|
||||||
|
// Check for available coupons
|
||||||
|
const { data: coupons } = await supabase_service
|
||||||
|
.from("coupons")
|
||||||
|
.select("credits")
|
||||||
|
.eq("team_id", team_id)
|
||||||
|
.eq("status", "active");
|
||||||
|
|
||||||
|
let couponCredits = 0;
|
||||||
|
if (coupons && coupons.length > 0) {
|
||||||
|
couponCredits = coupons.reduce((total, coupon) => total + coupon.credits, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free credits, no coupons
|
||||||
if (subscriptionError || !subscription) {
|
if (subscriptionError || !subscription) {
|
||||||
|
// If there is no active subscription but there are available coupons
|
||||||
|
if (couponCredits >= credits) {
|
||||||
|
return { success: true, message: "Sufficient credits available" };
|
||||||
|
}
|
||||||
|
|
||||||
const { data: creditUsages, error: creditUsageError } =
|
const { data: creditUsages, error: creditUsageError } =
|
||||||
await supabase_service
|
await supabase_service
|
||||||
.from("credit_usage")
|
.from("credit_usage")
|
||||||
@ -106,20 +212,7 @@ export async function supaCheckTeamCredits(team_id: string, credits: number) {
|
|||||||
return { success: true, message: "Sufficient credits available" };
|
return { success: true, message: "Sufficient credits available" };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Get the price_id from the subscription.
|
// Calculate the total credits used by the team within the current billing period
|
||||||
const { data: price, error: priceError } = await supabase_service
|
|
||||||
.from("prices")
|
|
||||||
.select("credits")
|
|
||||||
.eq("id", subscription.price_id)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (priceError) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to retrieve price for price_id: ${subscription.price_id}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Calculate the total credits used by the team within the current billing period.
|
|
||||||
const { data: creditUsages, error: creditUsageError } = await supabase_service
|
const { data: creditUsages, error: creditUsageError } = await supabase_service
|
||||||
.from("credit_usage")
|
.from("credit_usage")
|
||||||
.select("credits_used")
|
.select("credits_used")
|
||||||
@ -128,18 +221,27 @@ export async function supaCheckTeamCredits(team_id: string, credits: number) {
|
|||||||
.lte("created_at", subscription.current_period_end);
|
.lte("created_at", subscription.current_period_end);
|
||||||
|
|
||||||
if (creditUsageError) {
|
if (creditUsageError) {
|
||||||
throw new Error(
|
throw new Error(`Failed to retrieve credit usage for subscription_id: ${subscription.id}`);
|
||||||
`Failed to retrieve credit usage for subscription_id: ${subscription.id}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalCreditsUsed = creditUsages.reduce(
|
const totalCreditsUsed = creditUsages.reduce((acc, usage) => acc + usage.credits_used, 0);
|
||||||
(acc, usage) => acc + usage.credits_used,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
// 5. Compare the total credits used with the credits allowed by the plan.
|
// Adjust total credits used by subtracting coupon value
|
||||||
if (totalCreditsUsed + credits > price.credits) {
|
const adjustedCreditsUsed = Math.max(0, totalCreditsUsed - couponCredits);
|
||||||
|
|
||||||
|
// Get the price details
|
||||||
|
const { data: price, error: priceError } = await supabase_service
|
||||||
|
.from("prices")
|
||||||
|
.select("credits")
|
||||||
|
.eq("id", subscription.price_id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (priceError) {
|
||||||
|
throw new Error(`Failed to retrieve price for price_id: ${subscription.price_id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare the adjusted total credits used with the credits allowed by the plan
|
||||||
|
if (adjustedCreditsUsed + credits > price.credits) {
|
||||||
return { success: false, message: "Insufficient credits, please upgrade!" };
|
return { success: false, message: "Insufficient credits, please upgrade!" };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,9 +260,18 @@ export async function countCreditsAndRemainingForCurrentBillingPeriod(
|
|||||||
.eq("team_id", team_id)
|
.eq("team_id", team_id)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (subscriptionError || !subscription) {
|
const { data: coupons } = await supabase_service
|
||||||
// throw new Error(`Failed to retrieve subscription for team_id: ${team_id}`);
|
.from("coupons")
|
||||||
|
.select("credits")
|
||||||
|
.eq("team_id", team_id)
|
||||||
|
.eq("status", "active");
|
||||||
|
|
||||||
|
let couponCredits = 0;
|
||||||
|
if (coupons && coupons.length > 0) {
|
||||||
|
couponCredits = coupons.reduce((total, coupon) => total + coupon.credits, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscriptionError || !subscription) {
|
||||||
// Free
|
// Free
|
||||||
const { data: creditUsages, error: creditUsageError } =
|
const { data: creditUsages, error: creditUsageError } =
|
||||||
await supabase_service
|
await supabase_service
|
||||||
@ -168,13 +279,9 @@ export async function countCreditsAndRemainingForCurrentBillingPeriod(
|
|||||||
.select("credits_used")
|
.select("credits_used")
|
||||||
.is("subscription_id", null)
|
.is("subscription_id", null)
|
||||||
.eq("team_id", team_id);
|
.eq("team_id", team_id);
|
||||||
// .gte("created_at", subscription.current_period_start)
|
|
||||||
// .lte("created_at", subscription.current_period_end);
|
|
||||||
|
|
||||||
if (creditUsageError || !creditUsages) {
|
if (creditUsageError || !creditUsages) {
|
||||||
throw new Error(
|
throw new Error(`Failed to retrieve credit usage for team_id: ${team_id}`);
|
||||||
`Failed to retrieve credit usage for subscription_id: ${subscription.id}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalCreditsUsed = creditUsages.reduce(
|
const totalCreditsUsed = creditUsages.reduce(
|
||||||
@ -182,26 +289,10 @@ export async function countCreditsAndRemainingForCurrentBillingPeriod(
|
|||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4. Calculate remaining credits.
|
const remainingCredits = FREE_CREDITS + couponCredits - totalCreditsUsed;
|
||||||
const remainingCredits = FREE_CREDITS - totalCreditsUsed;
|
return { totalCreditsUsed: totalCreditsUsed, remainingCredits, totalCredits: FREE_CREDITS + couponCredits };
|
||||||
|
|
||||||
return { totalCreditsUsed, remainingCredits, totalCredits: FREE_CREDITS };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Get the price_id from the subscription to retrieve the total credits available.
|
|
||||||
const { data: price, error: priceError } = await supabase_service
|
|
||||||
.from("prices")
|
|
||||||
.select("credits")
|
|
||||||
.eq("id", subscription.price_id)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (priceError || !price) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to retrieve price for price_id: ${subscription.price_id}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Calculate the total credits used by the team within the current billing period.
|
|
||||||
const { data: creditUsages, error: creditUsageError } = await supabase_service
|
const { data: creditUsages, error: creditUsageError } = await supabase_service
|
||||||
.from("credit_usage")
|
.from("credit_usage")
|
||||||
.select("credits_used")
|
.select("credits_used")
|
||||||
@ -210,18 +301,42 @@ export async function countCreditsAndRemainingForCurrentBillingPeriod(
|
|||||||
.lte("created_at", subscription.current_period_end);
|
.lte("created_at", subscription.current_period_end);
|
||||||
|
|
||||||
if (creditUsageError || !creditUsages) {
|
if (creditUsageError || !creditUsages) {
|
||||||
throw new Error(
|
throw new Error(`Failed to retrieve credit usage for subscription_id: ${subscription.id}`);
|
||||||
`Failed to retrieve credit usage for subscription_id: ${subscription.id}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalCreditsUsed = creditUsages.reduce(
|
const totalCreditsUsed = creditUsages.reduce((acc, usage) => acc + usage.credits_used, 0);
|
||||||
(acc, usage) => acc + usage.credits_used,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
|
|
||||||
// 4. Calculate remaining credits.
|
const { data: price, error: priceError } = await supabase_service
|
||||||
const remainingCredits = price.credits - totalCreditsUsed;
|
.from("prices")
|
||||||
|
.select("credits")
|
||||||
|
.eq("id", subscription.price_id)
|
||||||
|
.single();
|
||||||
|
|
||||||
return { totalCreditsUsed, remainingCredits, totalCredits: price.credits };
|
if (priceError || !price) {
|
||||||
|
throw new Error(`Failed to retrieve price for price_id: ${subscription.price_id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainingCredits = price.credits + couponCredits - totalCreditsUsed;
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalCreditsUsed,
|
||||||
|
remainingCredits,
|
||||||
|
totalCredits: price.credits
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createCreditUsage({ team_id, subscription_id, credits }: { team_id: string, subscription_id?: string, credits: number }) {
|
||||||
|
const { data: credit_usage } = await supabase_service
|
||||||
|
.from("credit_usage")
|
||||||
|
.insert([
|
||||||
|
{
|
||||||
|
team_id,
|
||||||
|
credits_used: credits,
|
||||||
|
subscription_id: subscription_id || null,
|
||||||
|
created_at: new Date(),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
.select();
|
||||||
|
|
||||||
|
return { success: true, credit_usage };
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user