Skip to content

Commit ea76df4

Browse files
sean-brydonclaude
andauthored
feat: add invoice URL to proration and test seed scripts (calcom#27297)
* feat: add invoice URL to proration and test seed scripts - Add invoiceUrl field to MonthlyProration schema - Update billing service to return hosted_invoice_url after finalizing - Save invoice URL when creating proration invoices - Add seed script for testing proration with real Stripe data - Add cleanup script for test data removal Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * add invoice url * add invoice URL to tests --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 8319666 commit ea76df4

10 files changed

Lines changed: 971 additions & 5 deletions

File tree

packages/features/ee/billing/repository/proration/MonthlyProrationRepository.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export class MonthlyProrationRepository {
4747
additionalData?: {
4848
invoiceItemId?: string;
4949
invoiceId?: string;
50+
invoiceUrl?: string;
5051
chargedAt?: Date;
5152
failedAt?: Date;
5253
failureReason?: string;

packages/features/ee/billing/service/billingProvider/IBillingProviderService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export interface IBillingProviderService {
9090
metadata?: Record<string, string>;
9191
}): Promise<{ invoiceId: string }>;
9292

93-
finalizeInvoice(invoiceId: string): Promise<void>;
93+
finalizeInvoice(invoiceId: string): Promise<{ invoiceUrl: string | null }>;
9494

9595
voidInvoice(invoiceId: string): Promise<void>;
9696

packages/features/ee/billing/service/billingProvider/StripeBillingService.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,8 @@ export class StripeBillingService implements IBillingProviderService {
292292
}
293293

294294
async finalizeInvoice(invoiceId: string) {
295-
await this.stripe.invoices.finalizeInvoice(invoiceId);
295+
const invoice = await this.stripe.invoices.finalizeInvoice(invoiceId);
296+
return { invoiceUrl: invoice.hosted_invoice_url ?? null };
296297
}
297298

298299
async voidInvoice(invoiceId: string) {
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
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

Comments
 (0)