0

Merge pull request #66 from mendableai/feat/coupons

[Feat] Coupon system
This commit is contained in:
Nicolas 2024-04-26 14:50:57 -07:00 committed by GitHub
commit fb08f28edf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 223 additions and 108 deletions

View File

@ -46,11 +46,11 @@ 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:

View File

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

View File

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

View File

@ -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 } = // Retrieve the team's active subscription
await supabase_service const { data: subscription, error: subscriptionError } = await supabase_service
.from("subscriptions") .from("subscriptions")
.select("id, price_id, current_period_start, current_period_end") .select("id, price_id, current_period_start, current_period_end")
.eq("team_id", team_id) .eq("team_id", team_id)
.eq("status", "active") .eq("status", "active")
.single(); .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 };
} }