mirror of
https://git.mirrors.martin98.com/https://github.com/mendableai/firecrawl
synced 2025-08-06 06:36:28 +08:00
Nick: init
This commit is contained in:
parent
76c0073829
commit
bbfdda8867
@ -75,15 +75,19 @@ export async function setCachedACUC(
|
|||||||
|
|
||||||
export async function getACUC(
|
export async function getACUC(
|
||||||
api_key: string,
|
api_key: string,
|
||||||
cacheOnly = false
|
cacheOnly = false,
|
||||||
|
useCache = true
|
||||||
): Promise<AuthCreditUsageChunk | null> {
|
): Promise<AuthCreditUsageChunk | null> {
|
||||||
const cacheKeyACUC = `acuc_${api_key}`;
|
const cacheKeyACUC = `acuc_${api_key}`;
|
||||||
|
|
||||||
const cachedACUC = await getValue(cacheKeyACUC);
|
if (useCache) {
|
||||||
|
const cachedACUC = await getValue(cacheKeyACUC);
|
||||||
|
if (cachedACUC !== null) {
|
||||||
|
return JSON.parse(cachedACUC);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (cachedACUC !== null) {
|
if (!cacheOnly) {
|
||||||
return JSON.parse(cachedACUC);
|
|
||||||
} else if (!cacheOnly) {
|
|
||||||
let data;
|
let data;
|
||||||
let error;
|
let error;
|
||||||
let retries = 0;
|
let retries = 0;
|
||||||
@ -91,7 +95,7 @@ export async function getACUC(
|
|||||||
|
|
||||||
while (retries < maxRetries) {
|
while (retries < maxRetries) {
|
||||||
({ data, error } = await supabase_service.rpc(
|
({ data, error } = await supabase_service.rpc(
|
||||||
"auth_credit_usage_chunk_test_3",
|
"auth_credit_usage_chunk_test_17_credit_pack",
|
||||||
{ input_key: api_key }
|
{ input_key: api_key }
|
||||||
));
|
));
|
||||||
|
|
||||||
@ -118,9 +122,11 @@ export async function getACUC(
|
|||||||
data.length === 0 ? null : data[0].team_id === null ? null : data[0];
|
data.length === 0 ? null : data[0].team_id === null ? null : data[0];
|
||||||
|
|
||||||
// NOTE: Should we cache null chunks? - mogery
|
// NOTE: Should we cache null chunks? - mogery
|
||||||
if (chunk !== null) {
|
if (chunk !== null && useCache) {
|
||||||
setCachedACUC(api_key, chunk);
|
setCachedACUC(api_key, chunk);
|
||||||
}
|
}
|
||||||
|
// Log the chunk for now
|
||||||
|
console.log(chunk);
|
||||||
|
|
||||||
return chunk;
|
return chunk;
|
||||||
} else {
|
} else {
|
||||||
|
148
apps/api/src/services/billing/auto_charge.ts
Normal file
148
apps/api/src/services/billing/auto_charge.ts
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
// Import necessary dependencies and types
|
||||||
|
import { AuthCreditUsageChunk } from "../../controllers/v1/types";
|
||||||
|
import { getACUC, setCachedACUC } from "../../controllers/auth";
|
||||||
|
import { redlock } from "../redlock";
|
||||||
|
import { supabase_service } from "../supabase";
|
||||||
|
import { createPaymentIntent } from "./stripe";
|
||||||
|
import { issueCredits } from "./issue_credits";
|
||||||
|
import { sendNotification } from "../notification/email_notification";
|
||||||
|
import { NotificationType } from "../../types";
|
||||||
|
import { deleteKey } from "../redis";
|
||||||
|
import { sendSlackWebhook } from "../alerts/slack";
|
||||||
|
import { Logger } from "../../lib/logger";
|
||||||
|
|
||||||
|
// Define the number of credits to be added during auto-recharge
|
||||||
|
const AUTO_RECHARGE_CREDITS = 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to automatically charge a user's account when their credit balance falls below a threshold
|
||||||
|
* @param chunk The user's current usage data
|
||||||
|
* @param autoRechargeThreshold The credit threshold that triggers auto-recharge
|
||||||
|
*/
|
||||||
|
export async function autoCharge(
|
||||||
|
chunk: AuthCreditUsageChunk,
|
||||||
|
autoRechargeThreshold: number
|
||||||
|
): Promise<{ success: boolean; message: string; remainingCredits: number; chunk: AuthCreditUsageChunk }> {
|
||||||
|
const resource = `auto-recharge:${chunk.team_id}`;
|
||||||
|
try {
|
||||||
|
// Use a distributed lock to prevent concurrent auto-charge attempts
|
||||||
|
return await redlock.using([resource], 5000, async (signal) : Promise<{ success: boolean; message: string; remainingCredits: number; chunk: AuthCreditUsageChunk }> => {
|
||||||
|
// Recheck the condition inside the lock to prevent race conditions
|
||||||
|
const updatedChunk = await getACUC(chunk.api_key, false, false);
|
||||||
|
if (
|
||||||
|
updatedChunk &&
|
||||||
|
updatedChunk.remaining_credits < autoRechargeThreshold
|
||||||
|
) {
|
||||||
|
if (chunk.sub_user_id) {
|
||||||
|
// Fetch the customer's Stripe information
|
||||||
|
const { data: customer, error: customersError } =
|
||||||
|
await supabase_service
|
||||||
|
.from("customers")
|
||||||
|
.select("id, stripe_customer_id")
|
||||||
|
.eq("id", chunk.sub_user_id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (customersError) {
|
||||||
|
Logger.error(`Error fetching customer data: ${customersError}`);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Error fetching customer data",
|
||||||
|
remainingCredits: chunk.remaining_credits,
|
||||||
|
chunk,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customer && customer.stripe_customer_id) {
|
||||||
|
let issueCreditsSuccess = false;
|
||||||
|
// Attempt to create a payment intent
|
||||||
|
const paymentStatus = await createPaymentIntent(
|
||||||
|
chunk.team_id,
|
||||||
|
customer.stripe_customer_id
|
||||||
|
);
|
||||||
|
|
||||||
|
// If payment is successful or requires further action, issue credits
|
||||||
|
if (
|
||||||
|
paymentStatus.return_status === "succeeded" ||
|
||||||
|
paymentStatus.return_status === "requires_action"
|
||||||
|
) {
|
||||||
|
issueCreditsSuccess = await issueCredits(
|
||||||
|
chunk.team_id,
|
||||||
|
AUTO_RECHARGE_CREDITS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record the auto-recharge transaction
|
||||||
|
await supabase_service.from("auto_recharge_transactions").insert({
|
||||||
|
team_id: chunk.team_id,
|
||||||
|
initial_payment_status: paymentStatus.return_status,
|
||||||
|
credits_issued: issueCreditsSuccess ? AUTO_RECHARGE_CREDITS : 0,
|
||||||
|
stripe_charge_id: paymentStatus.charge_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send a notification if credits were successfully issued
|
||||||
|
if (issueCreditsSuccess) {
|
||||||
|
await sendNotification(
|
||||||
|
chunk.team_id,
|
||||||
|
NotificationType.AUTO_RECHARGE_SUCCESS,
|
||||||
|
chunk.sub_current_period_start,
|
||||||
|
chunk.sub_current_period_end,
|
||||||
|
chunk,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset ACUC cache to reflect the new credit balance
|
||||||
|
const cacheKeyACUC = `acuc_${chunk.api_key}`;
|
||||||
|
await deleteKey(cacheKeyACUC);
|
||||||
|
if (process.env.SLACK_ADMIN_WEBHOOK_URL ) {
|
||||||
|
sendSlackWebhook(
|
||||||
|
`Auto-recharge successful: Team ${chunk.team_id}. ${AUTO_RECHARGE_CREDITS} credits added. Payment status: ${paymentStatus.return_status}. User was notified via email.`,
|
||||||
|
false,
|
||||||
|
process.env.SLACK_ADMIN_WEBHOOK_URL
|
||||||
|
).catch((error) => {
|
||||||
|
Logger.debug(`Error sending slack notification: ${error}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Auto-recharge successful",
|
||||||
|
remainingCredits: chunk.remaining_credits + AUTO_RECHARGE_CREDITS,
|
||||||
|
chunk: {...chunk, remaining_credits: chunk.remaining_credits + AUTO_RECHARGE_CREDITS},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
Logger.error("No Stripe customer ID found for user");
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "No Stripe customer ID found for user",
|
||||||
|
remainingCredits: chunk.remaining_credits,
|
||||||
|
chunk,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger.error("No sub_user_id found in chunk");
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "No sub_user_id found in chunk",
|
||||||
|
remainingCredits: chunk.remaining_credits,
|
||||||
|
chunk,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "No need to auto-recharge",
|
||||||
|
remainingCredits: chunk.remaining_credits,
|
||||||
|
chunk,
|
||||||
|
};
|
||||||
|
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`Failed to acquire lock for auto-recharge: ${error}`);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: "Failed to acquire lock for auto-recharge",
|
||||||
|
remainingCredits: chunk.remaining_credits,
|
||||||
|
chunk,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -6,24 +6,40 @@ import { Logger } from "../../lib/logger";
|
|||||||
import * as Sentry from "@sentry/node";
|
import * as Sentry from "@sentry/node";
|
||||||
import { AuthCreditUsageChunk } from "../../controllers/v1/types";
|
import { AuthCreditUsageChunk } from "../../controllers/v1/types";
|
||||||
import { getACUC, setCachedACUC } from "../../controllers/auth";
|
import { getACUC, setCachedACUC } from "../../controllers/auth";
|
||||||
|
import { issueCredits } from "./issue_credits";
|
||||||
|
import { redlock } from "../redlock";
|
||||||
|
import { autoCharge } from "./auto_charge";
|
||||||
|
import { getValue, setValue } from "../redis";
|
||||||
|
|
||||||
const FREE_CREDITS = 500;
|
const FREE_CREDITS = 500;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If you do not know the subscription_id in the current context, pass subscription_id as undefined.
|
* If you do not know the subscription_id in the current context, pass subscription_id as undefined.
|
||||||
*/
|
*/
|
||||||
export async function billTeam(team_id: string, subscription_id: string | null | undefined, credits: number) {
|
export async function billTeam(
|
||||||
|
team_id: string,
|
||||||
|
subscription_id: string | null | undefined,
|
||||||
|
credits: number
|
||||||
|
) {
|
||||||
return withAuth(supaBillTeam)(team_id, subscription_id, credits);
|
return withAuth(supaBillTeam)(team_id, subscription_id, credits);
|
||||||
}
|
}
|
||||||
export async function supaBillTeam(team_id: string, subscription_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`);
|
||||||
|
|
||||||
const { data, error } =
|
const { data, error } = await supabase_service.rpc("bill_team", {
|
||||||
await supabase_service.rpc("bill_team", { _team_id: team_id, sub_id: subscription_id ?? null, fetch_subscription: subscription_id === undefined, credits });
|
_team_id: team_id,
|
||||||
|
sub_id: subscription_id ?? null,
|
||||||
|
fetch_subscription: subscription_id === undefined,
|
||||||
|
credits,
|
||||||
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
Sentry.captureException(error);
|
Sentry.captureException(error);
|
||||||
Logger.error("Failed to bill team: " + JSON.stringify(error));
|
Logger.error("Failed to bill team: " + JSON.stringify(error));
|
||||||
@ -31,48 +47,109 @@ export async function supaBillTeam(team_id: string, subscription_id: string, cre
|
|||||||
}
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
for (const apiKey of (data ?? []).map(x => x.api_key)) {
|
for (const apiKey of (data ?? []).map((x) => x.api_key)) {
|
||||||
await setCachedACUC(apiKey, acuc => (acuc ? {
|
await setCachedACUC(apiKey, (acuc) =>
|
||||||
...acuc,
|
acuc
|
||||||
credits_used: acuc.credits_used + credits,
|
? {
|
||||||
adjusted_credits_used: acuc.adjusted_credits_used + credits,
|
...acuc,
|
||||||
remaining_credits: acuc.remaining_credits - credits,
|
credits_used: acuc.credits_used + credits,
|
||||||
} : null));
|
adjusted_credits_used: acuc.adjusted_credits_used + credits,
|
||||||
|
remaining_credits: acuc.remaining_credits - credits,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkTeamCredits(chunk: AuthCreditUsageChunk, team_id: string, credits: number) {
|
export async function checkTeamCredits(
|
||||||
return withAuth(supaCheckTeamCredits)(chunk, team_id, credits);
|
chunk: AuthCreditUsageChunk,
|
||||||
|
team_id: string,
|
||||||
|
credits: number
|
||||||
|
): Promise<{ success: boolean; message: string; remainingCredits: number; chunk: AuthCreditUsageChunk }> {
|
||||||
|
const result = await withAuth(supaCheckTeamCredits)(chunk, team_id, credits);
|
||||||
|
return {
|
||||||
|
success: result.success,
|
||||||
|
message: result.message,
|
||||||
|
remainingCredits: result.remainingCredits,
|
||||||
|
chunk: chunk // Ensure chunk is always returned
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// if team has enough credits for the operation, return true, else return false
|
// if team has enough credits for the operation, return true, else return false
|
||||||
export async function supaCheckTeamCredits(chunk: AuthCreditUsageChunk, team_id: string, credits: number) {
|
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
|
// WARNING: chunk will be null if team_id is preview -- do not perform operations on it under ANY circumstances - mogery
|
||||||
if (team_id === "preview") {
|
if (team_id === "preview") {
|
||||||
return { success: true, message: "Preview team, no credits used", remainingCredits: Infinity };
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Preview team, no credits used",
|
||||||
|
remainingCredits: Infinity,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const creditsWillBeUsed = chunk.adjusted_credits_used + credits;
|
const creditsWillBeUsed = chunk.adjusted_credits_used + credits;
|
||||||
|
|
||||||
// In case chunk.price_credits is undefined, set it to a large number to avoid mistakes
|
// In case chunk.price_credits is undefined, set it to a large number to avoid mistakes
|
||||||
const totalPriceCredits = chunk.price_credits ?? 100000000;
|
const totalPriceCredits = chunk.total_credits_sum ?? 100000000;
|
||||||
// Removal of + credits
|
// Removal of + credits
|
||||||
const creditUsagePercentage = chunk.adjusted_credits_used / totalPriceCredits;
|
const creditUsagePercentage = chunk.adjusted_credits_used / totalPriceCredits;
|
||||||
|
|
||||||
|
let isAutoRechargeEnabled = false, autoRechargeThreshold = 1000;
|
||||||
|
const cacheKey = `team_auto_recharge_${team_id}`;
|
||||||
|
let cachedData = await getValue(cacheKey);
|
||||||
|
if (cachedData) {
|
||||||
|
const parsedData = JSON.parse(cachedData);
|
||||||
|
isAutoRechargeEnabled = parsedData.auto_recharge;
|
||||||
|
autoRechargeThreshold = parsedData.auto_recharge_threshold;
|
||||||
|
} else {
|
||||||
|
const { data, error } = await supabase_service
|
||||||
|
.from("teams")
|
||||||
|
.select("auto_recharge, auto_recharge_threshold")
|
||||||
|
.eq("id", team_id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
isAutoRechargeEnabled = data.auto_recharge;
|
||||||
|
autoRechargeThreshold = data.auto_recharge_threshold;
|
||||||
|
await setValue(cacheKey, JSON.stringify(data), 300); // Cache for 5 minutes (300 seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAutoRechargeEnabled && chunk.remaining_credits < autoRechargeThreshold) {
|
||||||
|
const autoChargeResult = await autoCharge(chunk, autoRechargeThreshold);
|
||||||
|
if (autoChargeResult.success) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: autoChargeResult.message,
|
||||||
|
remainingCredits: autoChargeResult.remainingCredits,
|
||||||
|
chunk: autoChargeResult.chunk,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Compare the adjusted total credits used with the credits allowed by the plan
|
// Compare the adjusted total credits used with the credits allowed by the plan
|
||||||
if (creditsWillBeUsed > totalPriceCredits) {
|
if (creditsWillBeUsed > totalPriceCredits) {
|
||||||
// Only notify if their actual credits (not what they will use) used is greater than the total price credits
|
// Only notify if their actual credits (not what they will use) used is greater than the total price credits
|
||||||
if(chunk.adjusted_credits_used > totalPriceCredits) {
|
if (chunk.adjusted_credits_used > totalPriceCredits) {
|
||||||
sendNotification(
|
sendNotification(
|
||||||
team_id,
|
team_id,
|
||||||
NotificationType.LIMIT_REACHED,
|
NotificationType.LIMIT_REACHED,
|
||||||
chunk.sub_current_period_start,
|
chunk.sub_current_period_start,
|
||||||
chunk.sub_current_period_end,
|
chunk.sub_current_period_end,
|
||||||
chunk
|
chunk
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return { success: false, message: "Insufficient credits to perform this request. For more credits, you can upgrade your plan at https://firecrawl.dev/pricing.", remainingCredits: chunk.remaining_credits, chunk };
|
return {
|
||||||
|
success: false,
|
||||||
|
message:
|
||||||
|
"Insufficient credits to perform this request. For more credits, you can upgrade your plan at https://firecrawl.dev/pricing.",
|
||||||
|
remainingCredits: chunk.remaining_credits,
|
||||||
|
chunk,
|
||||||
|
};
|
||||||
} else if (creditUsagePercentage >= 0.8 && creditUsagePercentage < 1) {
|
} else if (creditUsagePercentage >= 0.8 && creditUsagePercentage < 1) {
|
||||||
// Send email notification for approaching credit limit
|
// Send email notification for approaching credit limit
|
||||||
sendNotification(
|
sendNotification(
|
||||||
@ -84,7 +161,12 @@ export async function supaCheckTeamCredits(chunk: AuthCreditUsageChunk, team_id:
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true, message: "Sufficient credits available", remainingCredits: chunk.remaining_credits, chunk };
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Sufficient credits available",
|
||||||
|
remainingCredits: chunk.remaining_credits,
|
||||||
|
chunk,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count the total credits used by a team within the current billing period and return the remaining credits.
|
// Count the total credits used by a team within the current billing period and return the remaining credits.
|
||||||
|
20
apps/api/src/services/billing/issue_credits.ts
Normal file
20
apps/api/src/services/billing/issue_credits.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Logger } from "../../lib/logger";
|
||||||
|
import { supabase_service } from "../supabase";
|
||||||
|
|
||||||
|
export async function issueCredits(team_id: string, credits: number) {
|
||||||
|
// Add an entry to supabase coupons
|
||||||
|
const { data, error } = await supabase_service.from("coupons").insert({
|
||||||
|
team_id: team_id,
|
||||||
|
credits: credits,
|
||||||
|
status: "active",
|
||||||
|
// indicates that this coupon was issued from auto recharge
|
||||||
|
from_auto_recharge: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
Logger.error(`Error adding coupon: ${error}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
51
apps/api/src/services/billing/stripe.ts
Normal file
51
apps/api/src/services/billing/stripe.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { Logger } from "../../lib/logger";
|
||||||
|
import Stripe from "stripe";
|
||||||
|
|
||||||
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY ?? "");
|
||||||
|
|
||||||
|
async function getCustomerDefaultPaymentMethod(customerId: string) {
|
||||||
|
const paymentMethods = await stripe.customers.listPaymentMethods(customerId, {
|
||||||
|
limit: 3,
|
||||||
|
});
|
||||||
|
return paymentMethods.data[0]?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReturnStatus = "succeeded" | "requires_action" | "failed";
|
||||||
|
export async function createPaymentIntent(
|
||||||
|
team_id: string,
|
||||||
|
customer_id: string
|
||||||
|
): Promise<{ return_status: ReturnStatus; charge_id: string }> {
|
||||||
|
try {
|
||||||
|
const paymentIntent = await stripe.paymentIntents.create({
|
||||||
|
amount: 1100,
|
||||||
|
currency: "usd",
|
||||||
|
customer: customer_id,
|
||||||
|
description: "Firecrawl: Auto re-charge of 1000 credits",
|
||||||
|
payment_method_types: ["card"],
|
||||||
|
payment_method: await getCustomerDefaultPaymentMethod(customer_id),
|
||||||
|
off_session: true,
|
||||||
|
confirm: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (paymentIntent.status === "succeeded") {
|
||||||
|
Logger.info(`Payment succeeded for team: ${team_id}`);
|
||||||
|
return { return_status: "succeeded", charge_id: paymentIntent.id };
|
||||||
|
} else if (
|
||||||
|
paymentIntent.status === "requires_action" ||
|
||||||
|
paymentIntent.status === "processing" ||
|
||||||
|
paymentIntent.status === "requires_capture"
|
||||||
|
) {
|
||||||
|
Logger.warn(`Payment requires further action for team: ${team_id}`);
|
||||||
|
return { return_status: "requires_action", charge_id: paymentIntent.id };
|
||||||
|
} else {
|
||||||
|
Logger.error(`Payment failed for team: ${team_id}`);
|
||||||
|
return { return_status: "failed", charge_id: paymentIntent.id };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(
|
||||||
|
`Failed to create or confirm PaymentIntent for team: ${team_id}`
|
||||||
|
);
|
||||||
|
console.error(error);
|
||||||
|
return { return_status: "failed", charge_id: "" };
|
||||||
|
}
|
||||||
|
}
|
@ -24,6 +24,14 @@ const emailTemplates: Record<
|
|||||||
subject: "Rate Limit Reached - Firecrawl",
|
subject: "Rate Limit Reached - Firecrawl",
|
||||||
html: "Hey there,<br/><p>You've hit one of the Firecrawl endpoint's rate limit! Take a breather and try again in a few moments. If you need higher rate limits, consider upgrading your plan. Check out our <a href='https://firecrawl.dev/pricing'>pricing page</a> for more info.</p><p>If you have any questions, feel free to reach out to us at <a href='mailto:hello@firecrawl.com'>hello@firecrawl.com</a></p><br/>Thanks,<br/>Firecrawl Team<br/><br/>Ps. this email is only sent once every 7 days if you reach a rate limit.",
|
html: "Hey there,<br/><p>You've hit one of the Firecrawl endpoint's rate limit! Take a breather and try again in a few moments. If you need higher rate limits, consider upgrading your plan. Check out our <a href='https://firecrawl.dev/pricing'>pricing page</a> for more info.</p><p>If you have any questions, feel free to reach out to us at <a href='mailto:hello@firecrawl.com'>hello@firecrawl.com</a></p><br/>Thanks,<br/>Firecrawl Team<br/><br/>Ps. this email is only sent once every 7 days if you reach a rate limit.",
|
||||||
},
|
},
|
||||||
|
[NotificationType.AUTO_RECHARGE_SUCCESS]: {
|
||||||
|
subject: "Auto recharge successful - Firecrawl",
|
||||||
|
html: "Hey there,<br/><p>Your account was successfully recharged with 1000 credits because your remaining credits were below the threshold.</p><br/>Thanks,<br/>Firecrawl Team<br/>",
|
||||||
|
},
|
||||||
|
[NotificationType.AUTO_RECHARGE_FAILED]: {
|
||||||
|
subject: "Auto recharge failed - Firecrawl",
|
||||||
|
html: "Hey there,<br/><p>Your auto recharge failed. Please try again manually. If the issue persists, please reach out to us at <a href='mailto:hello@firecrawl.com'>hello@firecrawl.com</a></p><br/>Thanks,<br/>Firecrawl Team<br/>",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function sendNotification(
|
export async function sendNotification(
|
||||||
@ -31,18 +39,20 @@ export async function sendNotification(
|
|||||||
notificationType: NotificationType,
|
notificationType: NotificationType,
|
||||||
startDateString: string,
|
startDateString: string,
|
||||||
endDateString: string,
|
endDateString: string,
|
||||||
chunk: AuthCreditUsageChunk
|
chunk: AuthCreditUsageChunk,
|
||||||
|
bypassRecentChecks: boolean = false
|
||||||
) {
|
) {
|
||||||
return withAuth(sendNotificationInternal)(
|
return withAuth(sendNotificationInternal)(
|
||||||
team_id,
|
team_id,
|
||||||
notificationType,
|
notificationType,
|
||||||
startDateString,
|
startDateString,
|
||||||
endDateString,
|
endDateString,
|
||||||
chunk
|
chunk,
|
||||||
|
bypassRecentChecks
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendEmailNotification(
|
export async function sendEmailNotification(
|
||||||
email: string,
|
email: string,
|
||||||
notificationType: NotificationType,
|
notificationType: NotificationType,
|
||||||
) {
|
) {
|
||||||
@ -72,90 +82,92 @@ export async function sendNotificationInternal(
|
|||||||
notificationType: NotificationType,
|
notificationType: NotificationType,
|
||||||
startDateString: string,
|
startDateString: string,
|
||||||
endDateString: string,
|
endDateString: string,
|
||||||
chunk: AuthCreditUsageChunk
|
chunk: AuthCreditUsageChunk,
|
||||||
|
bypassRecentChecks: boolean = false
|
||||||
): Promise<{ success: boolean }> {
|
): Promise<{ success: boolean }> {
|
||||||
if (team_id === "preview") {
|
if (team_id === "preview") {
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
const fifteenDaysAgo = new Date();
|
if (!bypassRecentChecks) {
|
||||||
fifteenDaysAgo.setDate(fifteenDaysAgo.getDate() - 15);
|
const fifteenDaysAgo = new Date();
|
||||||
|
fifteenDaysAgo.setDate(fifteenDaysAgo.getDate() - 15);
|
||||||
|
|
||||||
const { data, error } = await supabase_service
|
const { data, error } = await supabase_service
|
||||||
.from("user_notifications")
|
|
||||||
.select("*")
|
|
||||||
.eq("team_id", team_id)
|
|
||||||
.eq("notification_type", notificationType)
|
|
||||||
.gte("sent_date", fifteenDaysAgo.toISOString());
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
Logger.debug(`Error fetching notifications: ${error}`);
|
|
||||||
return { success: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.length !== 0) {
|
|
||||||
// Logger.debug(`Notification already sent for team_id: ${team_id} and notificationType: ${notificationType} in the last 15 days`);
|
|
||||||
return { success: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data: recentData, error: recentError } = await supabase_service
|
|
||||||
.from("user_notifications")
|
|
||||||
.select("*")
|
|
||||||
.eq("team_id", team_id)
|
|
||||||
.eq("notification_type", notificationType)
|
|
||||||
.gte("sent_date", startDateString)
|
|
||||||
.lte("sent_date", endDateString);
|
|
||||||
|
|
||||||
if (recentError) {
|
|
||||||
Logger.debug(`Error fetching recent notifications: ${recentError}`);
|
|
||||||
return { success: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recentData.length !== 0) {
|
|
||||||
// Logger.debug(`Notification already sent for team_id: ${team_id} and notificationType: ${notificationType} within the specified date range`);
|
|
||||||
return { success: false };
|
|
||||||
} else {
|
|
||||||
console.log(`Sending notification for team_id: ${team_id} and notificationType: ${notificationType}`);
|
|
||||||
// get the emails from the user with the team_id
|
|
||||||
const { data: emails, error: emailsError } = await supabase_service
|
|
||||||
.from("users")
|
|
||||||
.select("email")
|
|
||||||
.eq("team_id", team_id);
|
|
||||||
|
|
||||||
if (emailsError) {
|
|
||||||
Logger.debug(`Error fetching emails: ${emailsError}`);
|
|
||||||
return { success: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const email of emails) {
|
|
||||||
await sendEmailNotification(email.email, notificationType);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { error: insertError } = await supabase_service
|
|
||||||
.from("user_notifications")
|
.from("user_notifications")
|
||||||
.insert([
|
.select("*")
|
||||||
{
|
.eq("team_id", team_id)
|
||||||
team_id: team_id,
|
.eq("notification_type", notificationType)
|
||||||
notification_type: notificationType,
|
.gte("sent_date", fifteenDaysAgo.toISOString());
|
||||||
sent_date: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (process.env.SLACK_ADMIN_WEBHOOK_URL && emails.length > 0) {
|
if (error) {
|
||||||
sendSlackWebhook(
|
Logger.debug(`Error fetching notifications: ${error}`);
|
||||||
`${getNotificationString(notificationType)}: Team ${team_id}, with email ${emails[0].email}. Number of credits used: ${chunk.adjusted_credits_used} | Number of credits in the plan: ${chunk.price_credits}`,
|
|
||||||
false,
|
|
||||||
process.env.SLACK_ADMIN_WEBHOOK_URL
|
|
||||||
).catch((error) => {
|
|
||||||
Logger.debug(`Error sending slack notification: ${error}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (insertError) {
|
|
||||||
Logger.debug(`Error inserting notification record: ${insertError}`);
|
|
||||||
return { success: false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true };
|
if (data.length !== 0) {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: recentData, error: recentError } = await supabase_service
|
||||||
|
.from("user_notifications")
|
||||||
|
.select("*")
|
||||||
|
.eq("team_id", team_id)
|
||||||
|
.eq("notification_type", notificationType)
|
||||||
|
.gte("sent_date", startDateString)
|
||||||
|
.lte("sent_date", endDateString);
|
||||||
|
|
||||||
|
if (recentError) {
|
||||||
|
Logger.debug(`Error fetching recent notifications: ${recentError}`);
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recentData.length !== 0) {
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`Sending notification for team_id: ${team_id} and notificationType: ${notificationType}`);
|
||||||
|
// get the emails from the user with the team_id
|
||||||
|
const { data: emails, error: emailsError } = await supabase_service
|
||||||
|
.from("users")
|
||||||
|
.select("email")
|
||||||
|
.eq("team_id", team_id);
|
||||||
|
|
||||||
|
if (emailsError) {
|
||||||
|
Logger.debug(`Error fetching emails: ${emailsError}`);
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const email of emails) {
|
||||||
|
await sendEmailNotification(email.email, notificationType);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error: insertError } = await supabase_service
|
||||||
|
.from("user_notifications")
|
||||||
|
.insert([
|
||||||
|
{
|
||||||
|
team_id: team_id,
|
||||||
|
notification_type: notificationType,
|
||||||
|
sent_date: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (process.env.SLACK_ADMIN_WEBHOOK_URL && emails.length > 0) {
|
||||||
|
sendSlackWebhook(
|
||||||
|
`${getNotificationString(notificationType)}: Team ${team_id}, with email ${emails[0].email}. Number of credits used: ${chunk.adjusted_credits_used} | Number of credits in the plan: ${chunk.price_credits}`,
|
||||||
|
false,
|
||||||
|
process.env.SLACK_ADMIN_WEBHOOK_URL
|
||||||
|
).catch((error) => {
|
||||||
|
Logger.debug(`Error sending slack notification: ${error}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (insertError) {
|
||||||
|
Logger.debug(`Error inserting notification record: ${insertError}`);
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,10 @@ export function getNotificationString(
|
|||||||
return "Limit reached (100%)";
|
return "Limit reached (100%)";
|
||||||
case NotificationType.RATE_LIMIT_REACHED:
|
case NotificationType.RATE_LIMIT_REACHED:
|
||||||
return "Rate limit reached";
|
return "Rate limit reached";
|
||||||
|
case NotificationType.AUTO_RECHARGE_SUCCESS:
|
||||||
|
return "Auto-recharge successful";
|
||||||
|
case NotificationType.AUTO_RECHARGE_FAILED:
|
||||||
|
return "Auto-recharge failed";
|
||||||
default:
|
default:
|
||||||
return "Unknown notification type";
|
return "Unknown notification type";
|
||||||
}
|
}
|
||||||
|
@ -130,6 +130,8 @@ export enum NotificationType {
|
|||||||
APPROACHING_LIMIT = "approachingLimit",
|
APPROACHING_LIMIT = "approachingLimit",
|
||||||
LIMIT_REACHED = "limitReached",
|
LIMIT_REACHED = "limitReached",
|
||||||
RATE_LIMIT_REACHED = "rateLimitReached",
|
RATE_LIMIT_REACHED = "rateLimitReached",
|
||||||
|
AUTO_RECHARGE_SUCCESS = "autoRechargeSuccess",
|
||||||
|
AUTO_RECHARGE_FAILED = "autoRechargeFailed",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ScrapeLog = {
|
export type ScrapeLog = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user