Skip to main content
Glama
lago-migration-fixup.ts•10.8 kB
/** * Lago Subscription Migration Fixup Script * * This script adds a "pay_as_you_go" subscription to follow on from their `launch_trial` subscription * * What it does: * 1. Fetches all customers from Lago billing platform * 2. For each customer with an active "launch_trial" subscription: * - Updates the existing subscription to end on 2025-08-27 * - Creates a new "launch_trial" subscription starting 2025-08-27, ending 2025-09-26 * 3. Skips customers who already have launch_trial subscriptions * * Environment Variables: * - LAGO_API_KEY: Required for actual operations (not needed for dry-run) * - LAGO_API_URL: Optional, defaults to https://api.getlago.com * * Usage: * - Dry run: `deno run --allow-net --allow-env lago-migration.ts --dry-run` * - Execute: `deno run --allow-net --allow-env lago-migration.ts` * - Batch mode: `deno run --allow-net --allow-env lago-migration.ts --batch=10` * * Safety Features: * - Dry-run mode to preview changes before execution * - Comprehensive logging of all API operations * - Error handling and validation * - Skips customers who already have launch_trial subscriptions */ // @ts-nocheck import { Client } from "npm:lago-javascript-client"; // Check for dry-run mode const isDryRun = Deno.args.includes("--dry-run"); // Parse batch size from command line args only const getBatchSize = (): number => { const batchArg = Deno.args.find((arg) => arg.startsWith("--batch=")); if (batchArg) { const size = parseInt(batchArg.split("=")[1]); return isNaN(size) || size <= 0 ? Infinity : size; } return Infinity; // Default: process all eligible migrations }; const batchSize = getBatchSize(); // Initialize Lago client const apiKey = Deno.env.get("LAGO_API_KEY"); const apiUrl = Deno.env.get("LAGO_API_URL") || "https://api.getlago.com"; if (!apiKey && !isDryRun) { console.error( "Error: LAGO_API_KEY environment variable is required (unless running in dry-run mode)", ); Deno.exit(1); } const lago = apiKey ? Client(apiKey, apiUrl) : null; async function migrateSubscriptions() { try { console.log( `Starting subscription migration${isDryRun ? " (DRY RUN MODE)" : ""}...`, ); if (isDryRun) { console.log( "šŸ” DRY RUN: No actual changes will be made to subscriptions", ); } if (batchSize !== Infinity) { console.log( `šŸ“¦ BATCH MODE: Will process maximum ${batchSize} migrations (excluding skipped customers)`, ); } // Step 1: List all customers console.log("šŸ” Fetching all customers..."); const customers = await getAllCustomers(); console.log(`Found ${customers.length} customers`); let processedCount = 0; let migratedCount = 0; let skippedCount = 0; let foundSubscriptions = []; // Step 2: Process each customer for (const customer of customers) { // Stop processing if we've reached the batch limit (excluding skips) if (migratedCount >= batchSize) { console.log( `šŸ›‘ Reached batch limit of ${batchSize} migrations. Stopping processing.`, ); const remainingCustomers = customers.length - processedCount; if (remainingCustomers > 0) { console.log( `ā­ļø ${remainingCustomers} remaining customers will be processed in next batch`, ); } break; } processedCount++; console.log( `Processing customer ${processedCount}/${customers.length}: ${customer.external_id}`, ); // Check for active launch_trial subscription const launchTrialSubId = `${customer.external_id}_launch_trial`; const activeSubscription = await findActiveSubscription( customer.external_id, launchTrialSubId, ); if (activeSubscription) { const subData = activeSubscription.data?.subscription || activeSubscription; const subscriptionId = subData.external_id || subData.subscription_id; const currentEndDate = subData.ending_at || subData.terminated_at || "No end date"; console.log( ` āœ… Found active pay_as_you_go subscription: ${subscriptionId}`, ); // Check if customer already has a pending launch_pay_as_you_go subscription const existingPayAsYouGo = await checkForPendinPayAsYouGo( customer.external_id, ); if (existingPayAsYouGo) { console.log( ` ā© SKIPPING: Customer already has ${existingPayAsYouGo.status} launch_pay_as_you_go subscription: ${existingPayAsYouGo.external_id}`, ); skippedCount++; continue; } foundSubscriptions.push({ customerId: customer.external_id, subscriptionId: subscriptionId, currentEndDate: currentEndDate, newEndDate: currentEndDate, newPayAsYouGoStart: "2025-09-26", }); if (isDryRun) { console.log( ` šŸ” DRY RUN: Would create new launch_pay_as_you_go subscription starting 2025-09-26`, ); } else { // Step 3: Create new launch_pay_as_you_go subscription await creatPayAsYouGoSubscription(customer.external_id); console.log(` āœ… Created new launch_pay_as_you_go subscription`); } migratedCount++; } else { console.log(` āŒ No active launch_trial subscription found`); } } console.log(`\nMigration completed!`); console.log(` Processed: ${processedCount} customers`); console.log( ` ${isDryRun ? "Found" : "Migrated"}: ${migratedCount} subscriptions`, ); console.log( ` Skipped: ${skippedCount} customers (already have launch_pay_as_you_go)`, ); // Print summary report for dry-run if (isDryRun && foundSubscriptions.length > 0) { console.log("\nšŸ“‹ DRY RUN SUMMARY:"); console.log("=".repeat(80)); foundSubscriptions.forEach((sub, index) => { console.log(`${index + 1}. Customer: ${sub.customerId}`); console.log(` Current subscription: ${sub.subscriptionId}`); console.log(` Current end date: ${sub.currentEndDate}`); console.log( ` → Would create launch_pay_as_you_go: ${sub.newTrialStart}`, ); console.log(""); }); console.log( `Total subscriptions to migrate: ${foundSubscriptions.length}`, ); if (skippedCount > 0) { console.log( `Total customers skipped: ${skippedCount} (already have launch_pay_as_you_go subscriptions)`, ); } console.log("\nšŸš€ To execute these changes, run without --dry-run flag"); } } catch (error) { console.error("Migration failed:", error); Deno.exit(1); } } async function getAllCustomers() { if (isDryRun && !apiKey) { // Return mock data for dry-run mode when no API key is provided console.log("šŸ” DRY RUN: Using mock customer data"); return [ { external_id: "customer-1" }, { external_id: "customer-2" }, { external_id: "customer-3" }, ]; } const customers = []; let page = 1; let hasMore = true; while (hasMore) { const response = await lago.customers.findAllCustomers({ page, per_page: 100, }); // Handle different response structures const customerData = response.customers || response.data?.customers || response; if (Array.isArray(customerData)) { customers.push(...customerData); hasMore = customerData.length === 100; } else if (customerData && Array.isArray(customerData)) { customers.push(...customerData); hasMore = customerData.length === 100; } else { console.log("Unexpected response structure, stopping pagination"); hasMore = false; } page++; } return customers; } async function findActiveSubscription( customerId: string, subscriptionId: string, ) { if (isDryRun && !apiKey) { // Return mock subscription for demo purposes (only for customer-1) if (customerId === "customer-1") { return { external_id: subscriptionId, customer: { external_id: customerId }, status: "active", ending_at: "2025-12-31", }; } return null; } try { const subscription = await lago.subscriptions.findSubscription(subscriptionId); // Check if subscription belongs to this customer and is active if ( subscription.data?.subscription?.external_customer_id === customerId && subscription.data?.subscription?.status === "active" ) { return subscription; } return null; } catch (error: any) { // Subscription not found or invalid - both 404 and 422 should be treated as "no subscription" if (error.status === 404 || error.status === 422) { console.log( ` ā„¹ļø Subscription ${subscriptionId} not found or invalid (${error.status})`, ); return null; } throw error; } } async function checkForPendinPayAsYouGo(customerId: string) { if (isDryRun && !apiKey) { // Mock: customer-2 has a pending launch_trial subscription if (customerId === "customer-2") { return { external_id: `${customerId}_launch_pay_as_you_go`, status: "pending", plan_code: "launch_pay_as_you_go", }; } return null; } try { const payAsYouGoSubId = `${customerId}_launch_pay_as_you_go`; const subscription = await lago.subscriptions.findSubscription( payAsYouGoSubId, { status: "pending" }, ); // Check if it's a pending or active launch_trial subscription const subData = subscription.data?.subscription; if ( subData?.external_customer_id === customerId && subData?.plan_code === "launch_pay_as_you_go" && (subData?.status === "pending" || subData?.status === "active") ) { return subData; } return null; } catch (error: any) { // Subscription not found or invalid - both 404 and 422 should be treated as "no subscription" if (error.status === 404 || error.status === 422) { return null; } throw error; } } async function creatPayAsYouGoSubscription(customerId: string) { const startDate = new Date("2025-09-26"); startDate.setUTCHours(0, 0, 0, 0); const formattedStartDate = `${startDate.toISOString().split(".")[0]}Z`; const external_id = `${customerId}_launch_pay_as_you_go`; return await lago.subscriptions.createSubscription({ subscription: { external_id, external_customer_id: customerId, plan_code: "launch_pay_as_you_go", subscription_at: formattedStartDate, }, }); } // Run the migration if (import.meta.main) { await migrateSubscriptions(); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/systeminit/si'

If you have feedback or need assistance with the MCP directory API, please join our Discord server