Files
Benny JooGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
ab21c7f805 refactor: Cal.diy (#28903)
* feat: Cal.diy — community-driven MIT-licensed fork of Cal.com

This squashed commit contains all Cal.diy changes applied on top of calcom/cal.com main:

- Rebrand Cal.com to Cal.diy across the entire codebase
- Remove Enterprise Edition (EE) features, license checks, and AGPL restrictions
- Switch license from AGPL-3.0 to MIT
- Remove docs/ directory (migrated to Nextra at cal.diy)
- Remove dead code: org tests, EE tips, platform nav, premium username, SAML/SSO, etc.
- Clean up .env.example for self-hosted Cal.diy
- Update Docker image references to calcom/cal.diy
- Update README, CONTRIBUTING.md, and issue templates for Cal.diy community fork
- Add PR welcome bot for Cal.diy contributors
- Fix API v2 breaking changes oasdiff ignore entries
- Replace Blacksmith CI runners with default GitHub Actions

3893 files changed, 20789 insertions(+), 411020 deletions(-)

Co-Authored-By: [email protected] <[email protected]>

* refactor: remove org-specific /organizations/:orgId endpoints from API v2 atoms controllers (#1701)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: revert Cal.diy Inc to Cal.com, Inc. in license files, copyright notices, and package metadata (#1702)

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* rip out org related comments in api v2

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-04-15 09:52:36 -03:00

113 lines
3.2 KiB
TypeScript

import { WEBAPP_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import type { NextApiResponse } from "next";
import stripe from "../../server";
export interface TeamEntity {
id: number;
metadata: unknown;
isOrganization: boolean;
}
export interface BillingPortalResult {
success: boolean;
customerId?: string;
portalUrl?: string;
}
export abstract class BillingPortalService {
protected contextName = "Team";
/**
* Creates a billing portal URL for a Stripe customer
*/
protected async createBillingPortalUrl(customerId: string, returnUrl: string): Promise<string> {
const log = logger.getSubLogger({ prefix: ["createBillingPortalUrl"] });
try {
const portalSession = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: returnUrl,
});
return portalSession.url;
} catch (e) {
log.error(`Failed to create billing portal session for ${customerId}: ${e}`);
throw new Error("Failed to create billing portal session");
}
}
/**
* Builds a safe return URL for the billing portal
*/
protected buildReturnUrl(returnTo?: string): string {
const defaultUrl = `${WEBAPP_URL}/settings/billing`;
if (typeof returnTo !== "string") return defaultUrl;
const safeRedirectUrl = getSafeRedirectUrl(returnTo);
return safeRedirectUrl || defaultUrl;
}
protected getValidatedTeamSubscriptionId(metadata: Prisma.JsonValue) {
const teamMetadataParsed = teamMetadataSchema.safeParse(metadata);
if (!teamMetadataParsed.success || !teamMetadataParsed.data?.subscriptionId) {
return null;
}
return teamMetadataParsed.data.subscriptionId;
}
protected getValidatedTeamSubscriptionIdForPlatform(subscriptionId?: string | null) {
if (!subscriptionId) {
return null;
}
return subscriptionId;
}
/**
* Abstract method to check permissions - implemented by subclasses
*/
abstract checkPermissions(userId: number, teamId: number): Promise<boolean>;
/**
* Abstract method to get customer ID - implemented by subclasses
*/
abstract getCustomerId(teamId: number): Promise<string | null>;
/**
* Process billing portal request for a team
*/
async processBillingPortal(
userId: number,
teamId: number,
returnUrl: string,
res: NextApiResponse
): Promise<void> {
// Check permissions
const hasPermission = await this.checkPermissions(userId, teamId);
if (!hasPermission) {
res.status(403).json({ message: "Forbidden" });
return;
}
// Get customer ID
const customerId = await this.getCustomerId(teamId);
if (!customerId) {
res.status(400).json({
message: `${this.contextName} billing not properly configured. Please contact support.`,
});
return;
}
// Create portal URL and redirect
const billingPortalUrl = await this.createBillingPortalUrl(customerId, returnUrl);
res.redirect(302, billingPortalUrl);
}
}