|
| 1 | +#!/usr/bin/env npx tsx |
| 2 | +/** |
| 3 | + * Cleanup script for proration test data |
| 4 | + * |
| 5 | + * Usage: |
| 6 | + * npx tsx packages/features/ee/billing/service/dueInvoice/cleanup-proration-test.ts |
| 7 | + * |
| 8 | + * Options: |
| 9 | + * --skip-stripe Skip Stripe API cleanup |
| 10 | + */ |
| 11 | + |
| 12 | +import { config } from "dotenv"; |
| 13 | +import { resolve } from "node:path"; |
| 14 | + |
| 15 | +// Load environment variables from .env file |
| 16 | +config({ path: resolve(process.cwd(), ".env") }); |
| 17 | + |
| 18 | +import Stripe from "stripe"; |
| 19 | + |
| 20 | +import { prisma } from "@calcom/prisma"; |
| 21 | + |
| 22 | +const SKIP_STRIPE = process.argv.includes("--skip-stripe"); |
| 23 | + |
| 24 | +function getStripeClient(): Stripe | null { |
| 25 | + if (SKIP_STRIPE) { |
| 26 | + console.log("Skipping Stripe cleanup (--skip-stripe flag)"); |
| 27 | + return null; |
| 28 | + } |
| 29 | + |
| 30 | + if (!process.env.STRIPE_PRIVATE_KEY) { |
| 31 | + console.log("STRIPE_PRIVATE_KEY not set, skipping Stripe cleanup"); |
| 32 | + return null; |
| 33 | + } |
| 34 | + |
| 35 | + return new Stripe(process.env.STRIPE_PRIVATE_KEY, { |
| 36 | + apiVersion: "2020-08-27", |
| 37 | + }); |
| 38 | +} |
| 39 | + |
| 40 | +async function cleanupStripeResources(stripe: Stripe) { |
| 41 | + console.log("\nCleaning up Stripe resources..."); |
| 42 | + |
| 43 | + // Find and delete test customers by email |
| 44 | + const customers = await stripe.customers.list({ |
| 45 | + limit: 100, |
| 46 | + email: "proration-admin@example.com", |
| 47 | + }); |
| 48 | + |
| 49 | + for (const customer of customers.data) { |
| 50 | + try { |
| 51 | + // Cancel all subscriptions first |
| 52 | + const subscriptions = await stripe.subscriptions.list({ |
| 53 | + customer: customer.id, |
| 54 | + status: "all", |
| 55 | + }); |
| 56 | + |
| 57 | + for (const sub of subscriptions.data) { |
| 58 | + if (sub.status !== "canceled") { |
| 59 | + await stripe.subscriptions.cancel(sub.id); |
| 60 | + console.log(` Cancelled subscription: ${sub.id}`); |
| 61 | + } |
| 62 | + } |
| 63 | + |
| 64 | + // Void any open invoices |
| 65 | + const invoices = await stripe.invoices.list({ |
| 66 | + customer: customer.id, |
| 67 | + status: "open", |
| 68 | + }); |
| 69 | + |
| 70 | + for (const invoice of invoices.data) { |
| 71 | + try { |
| 72 | + await stripe.invoices.voidInvoice(invoice.id); |
| 73 | + console.log(` Voided invoice: ${invoice.id}`); |
| 74 | + } catch { |
| 75 | + console.log(` Could not void invoice ${invoice.id}`); |
| 76 | + } |
| 77 | + } |
| 78 | + |
| 79 | + // Delete the customer |
| 80 | + await stripe.customers.del(customer.id); |
| 81 | + console.log(` Deleted customer: ${customer.id}`); |
| 82 | + } catch (error) { |
| 83 | + console.log(` Error cleaning up customer ${customer.id}:`, error); |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + // Archive test products |
| 88 | + const products = await stripe.products.list({ limit: 100 }); |
| 89 | + |
| 90 | + for (const product of products.data) { |
| 91 | + if (product.name.startsWith("Proration Test") && product.active) { |
| 92 | + try { |
| 93 | + await stripe.products.update(product.id, { active: false }); |
| 94 | + console.log(` Archived product: ${product.id}`); |
| 95 | + } catch { |
| 96 | + console.log(` Could not archive product ${product.id}`); |
| 97 | + } |
| 98 | + } |
| 99 | + } |
| 100 | + |
| 101 | + console.log("Stripe cleanup complete"); |
| 102 | +} |
| 103 | + |
| 104 | +async function cleanupDatabaseResources() { |
| 105 | + console.log("\nCleaning up database resources..."); |
| 106 | + |
| 107 | + // Find test organization |
| 108 | + const org = await prisma.team.findFirst({ |
| 109 | + where: { slug: "proration-test-org" }, |
| 110 | + }); |
| 111 | + |
| 112 | + if (!org) { |
| 113 | + console.log(" No test organization found"); |
| 114 | + return; |
| 115 | + } |
| 116 | + |
| 117 | + console.log(` Found test organization: ${org.name} (ID: ${org.id})`); |
| 118 | + |
| 119 | + // Delete proration records |
| 120 | + const deletedProrations = await prisma.monthlyProration.deleteMany({ |
| 121 | + where: { teamId: org.id }, |
| 122 | + }); |
| 123 | + console.log(` Deleted ${deletedProrations.count} proration records`); |
| 124 | + |
| 125 | + // Delete seat change logs |
| 126 | + const deletedLogs = await prisma.seatChangeLog.deleteMany({ |
| 127 | + where: { teamId: org.id }, |
| 128 | + }); |
| 129 | + console.log(` Deleted ${deletedLogs.count} seat change logs`); |
| 130 | + |
| 131 | + // Delete organization billing |
| 132 | + await prisma.organizationBilling |
| 133 | + .delete({ where: { teamId: org.id } }) |
| 134 | + .catch(() => console.log(" No organization billing to delete")); |
| 135 | + |
| 136 | + // Delete organization settings |
| 137 | + await prisma.organizationSettings |
| 138 | + .delete({ where: { organizationId: org.id } }) |
| 139 | + .catch(() => console.log(" No organization settings to delete")); |
| 140 | + |
| 141 | + // Delete profiles for org members |
| 142 | + await prisma.profile.deleteMany({ where: { organizationId: org.id } }); |
| 143 | + console.log(" Deleted profiles"); |
| 144 | + |
| 145 | + // Find and delete child teams |
| 146 | + const childTeams = await prisma.team.findMany({ |
| 147 | + where: { parentId: org.id }, |
| 148 | + }); |
| 149 | + |
| 150 | + for (const team of childTeams) { |
| 151 | + await prisma.membership.deleteMany({ where: { teamId: team.id } }); |
| 152 | + await prisma.team.delete({ where: { id: team.id } }); |
| 153 | + console.log(` Deleted team: ${team.name}`); |
| 154 | + } |
| 155 | + |
| 156 | + // Delete org memberships |
| 157 | + const deletedMemberships = await prisma.membership.deleteMany({ |
| 158 | + where: { teamId: org.id }, |
| 159 | + }); |
| 160 | + console.log(` Deleted ${deletedMemberships.count} memberships`); |
| 161 | + |
| 162 | + // Delete organization |
| 163 | + await prisma.team.delete({ where: { id: org.id } }); |
| 164 | + console.log(` Deleted organization: ${org.name}`); |
| 165 | + |
| 166 | + // Delete test users (admin, member, and additional users 1-6) |
| 167 | + const testEmails = [ |
| 168 | + "proration-admin@example.com", |
| 169 | + "proration-member@example.com", |
| 170 | + ...Array.from({ length: 6 }, (_, i) => `proration-user-${i + 1}@example.com`), |
| 171 | + ]; |
| 172 | + for (const email of testEmails) { |
| 173 | + try { |
| 174 | + const user = await prisma.user.findUnique({ where: { email } }); |
| 175 | + if (user) { |
| 176 | + await prisma.password.deleteMany({ where: { userId: user.id } }); |
| 177 | + await prisma.membership.deleteMany({ where: { userId: user.id } }); |
| 178 | + await prisma.user.delete({ where: { id: user.id } }); |
| 179 | + console.log(` Deleted user: ${email}`); |
| 180 | + } |
| 181 | + } catch { |
| 182 | + // Ignore errors |
| 183 | + } |
| 184 | + } |
| 185 | + |
| 186 | + console.log("Database cleanup complete"); |
| 187 | +} |
| 188 | + |
| 189 | +async function main() { |
| 190 | + console.log("=== Proration Test Cleanup Script ==="); |
| 191 | + console.log("\nOptions:"); |
| 192 | + console.log(` --skip-stripe: ${SKIP_STRIPE}`); |
| 193 | + |
| 194 | + const stripe = getStripeClient(); |
| 195 | + |
| 196 | + try { |
| 197 | + await cleanupDatabaseResources(); |
| 198 | + |
| 199 | + if (stripe) { |
| 200 | + await cleanupStripeResources(stripe); |
| 201 | + } |
| 202 | + |
| 203 | + console.log("\n=== Cleanup Complete ==="); |
| 204 | + } catch (error) { |
| 205 | + console.error("\nCleanup failed:", error); |
| 206 | + process.exit(1); |
| 207 | + } finally { |
| 208 | + await prisma.$disconnect(); |
| 209 | + } |
| 210 | +} |
| 211 | + |
| 212 | +main(); |
0 commit comments