Files
firecrawl/apps/api/src/services/billing/auto_charge.ts
T

199 lines
7.1 KiB
TypeScript
Raw Normal View History

2024-10-22 19:47:23 -03:00
// Import necessary dependencies and types
import { AuthCreditUsageChunk } from "../../controllers/v1/types";
2024-11-07 20:57:33 +01:00
import { getACUC } from "../../controllers/auth";
2024-10-22 19:47:23 -03:00
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";
2024-10-25 16:05:23 -03:00
import { deleteKey, getValue, setValue } from "../redis";
2024-10-22 19:47:23 -03:00
import { sendSlackWebhook } from "../alerts/slack";
2024-11-07 20:57:33 +01:00
import { logger } from "../../lib/logger";
2024-10-22 19:47:23 -03:00
// Define the number of credits to be added during auto-recharge
const AUTO_RECHARGE_CREDITS = 1000;
2024-10-25 16:05:39 -03:00
const AUTO_RECHARGE_COOLDOWN = 300; // 5 minutes in seconds
2024-10-22 19:47:23 -03:00
/**
* 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
2024-12-11 19:46:11 -03:00
): Promise<{
success: boolean;
message: string;
remainingCredits: number;
chunk: AuthCreditUsageChunk;
}> {
2024-10-22 19:47:23 -03:00
const resource = `auto-recharge:${chunk.team_id}`;
2024-10-25 16:05:23 -03:00
const cooldownKey = `auto-recharge-cooldown:${chunk.team_id}`;
2024-10-22 19:47:23 -03:00
try {
2024-10-25 16:05:23 -03:00
// Check if the team is in the cooldown period
2024-10-25 16:05:39 -03:00
// Another check to prevent race conditions, double charging - cool down of 5 minutes
2024-10-25 16:05:23 -03:00
const cooldownValue = await getValue(cooldownKey);
if (cooldownValue) {
2024-12-11 19:46:11 -03:00
logger.info(
`Auto-recharge for team ${chunk.team_id} is in cooldown period`
);
2024-10-25 16:05:23 -03:00
return {
success: false,
message: "Auto-recharge is in cooldown period",
remainingCredits: chunk.remaining_credits,
2024-12-11 19:46:11 -03:00
chunk
2024-10-25 16:05:23 -03:00
};
}
2024-10-22 19:47:23 -03:00
// Use a distributed lock to prevent concurrent auto-charge attempts
2024-12-11 19:46:11 -03:00
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();
2024-10-22 19:47:23 -03:00
2024-12-11 19:46:11 -03:00
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(
2024-10-22 19:47:23 -03:00
chunk.team_id,
2024-12-11 19:46:11 -03:00
customer.stripe_customer_id
2024-10-22 19:47:23 -03:00
);
2024-12-11 19:46:11 -03:00
// 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
);
}
2024-10-22 19:47:23 -03:00
2024-12-11 19:46:11 -03:00
// 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
);
2024-10-25 16:05:23 -03:00
2024-12-11 19:46:11 -03:00
// Set cooldown period
await setValue(cooldownKey, "true", AUTO_RECHARGE_COOLDOWN);
}
// 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) {
const webhookCooldownKey = `webhook_cooldown_${chunk.team_id}`;
const isInCooldown = await getValue(webhookCooldownKey);
2024-10-22 19:47:23 -03:00
2024-12-11 19:46:11 -03:00
if (!isInCooldown) {
sendSlackWebhook(
`Auto-recharge: Team ${chunk.team_id}. ${AUTO_RECHARGE_CREDITS} credits added. Payment status: ${paymentStatus.return_status}.`,
false,
process.env.SLACK_ADMIN_WEBHOOK_URL
).catch((error) => {
logger.debug(`Error sending slack notification: ${error}`);
});
// Set cooldown for 1 hour
await setValue(webhookCooldownKey, "true", 60 * 60);
}
2024-10-22 19:47:23 -03:00
}
2024-12-11 19:46:11 -03:00
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
};
}
2024-10-22 19:47:23 -03:00
} else {
2024-12-11 19:46:11 -03:00
logger.error("No sub_user_id found in chunk");
2024-10-22 19:47:23 -03:00
return {
success: false,
2024-12-11 19:46:11 -03:00
message: "No sub_user_id found in chunk",
2024-10-22 19:47:23 -03:00
remainingCredits: chunk.remaining_credits,
2024-12-11 19:46:11 -03:00
chunk
2024-10-22 19:47:23 -03:00
};
}
}
2024-12-11 19:46:11 -03:00
return {
success: false,
message: "No need to auto-recharge",
remainingCredits: chunk.remaining_credits,
chunk
};
2024-10-22 19:47:23 -03:00
}
2024-12-11 19:46:11 -03:00
);
2024-10-22 19:47:23 -03:00
} catch (error) {
2024-11-07 20:57:33 +01:00
logger.error(`Failed to acquire lock for auto-recharge: ${error}`);
2024-10-22 19:47:23 -03:00
return {
success: false,
message: "Failed to acquire lock for auto-recharge",
remainingCredits: chunk.remaining_credits,
2024-12-11 19:46:11 -03:00
chunk
2024-10-22 19:47:23 -03:00
};
}
}