Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
130 commits
Select commit Hold shift + click to select a range
612e16d
feat: Add email template for failed team subscription payments
devin-ai-integration[bot] Oct 16, 2025
3fd49d4
refactor: Make subscription payment failed email generic for teams an…
devin-ai-integration[bot] Oct 16, 2025
85a9346
feat: Add sendPaymentFailedEmail method to InternalTeamBilling
devin-ai-integration[bot] Oct 16, 2025
9a606f0
docs: Add usage example for sendPaymentFailedEmail method
devin-ai-integration[bot] Oct 16, 2025
a9c8cc6
feat: Add email preview endpoint for subscription payment failed email
devin-ai-integration[bot] Oct 16, 2025
bcc4b47
feat: Add SubscriptionPaymentFailedEmail to email preview endpoint
devin-ai-integration[bot] Oct 16, 2025
ad50ee9
Revert "feat: Add SubscriptionPaymentFailedEmail to email preview end…
joeauyeung Oct 16, 2025
cb63e6a
Revert "feat: Add email preview endpoint for subscription payment fai…
joeauyeung Oct 16, 2025
b869354
Ass a getUsersWithPermissionInTeam function to get all users
sean-brydon Oct 17, 2025
d4ee916
Merge branch 'main' into feat/pbac-getUsersWithPermissionInTeam
sean-brydon Oct 17, 2025
26a4d3b
remove step and status
sean-brydon Oct 17, 2025
5d3c541
fix user table
sean-brydon Oct 17, 2025
d2950b3
Merge branch 'main' into feat/pbac-getUsersWithPermissionInTeam
joeauyeung Oct 17, 2025
67d269b
Undo yarn.lock commit
joeauyeung Oct 17, 2025
6ec4945
Merge branch 'feat/pbac-getUsersWithPermissionInTeam' into devin/team…
joeauyeung Oct 17, 2025
db39851
Change strings
joeauyeung Oct 18, 2025
b4259a0
Update payment failed email
joeauyeung Oct 18, 2025
f22a4f8
Add additional fields to return for
joeauyeung Oct 18, 2025
7a0cba1
Add `permissionService.getUsersWithPermissionForTeam`
joeauyeung Oct 18, 2025
b1d3f00
Add `billingPortalService.processBillingPortalWithoutPermissionChecks`
joeauyeung Oct 18, 2025
be21877
Add `InternalTeamBilling.sendPaymentFailedEmails`
joeauyeung Oct 18, 2025
82dc722
Move TeamBillingRepositories
joeauyeung Oct 30, 2025
9378e39
WIP refactor team internal billing service
joeauyeung Oct 30, 2025
2a9b721
Remove duplicate billing repository files
joeauyeung Oct 30, 2025
c3a6283
Remove logic check in repository for billing is enabled
joeauyeung Oct 30, 2025
46e1ca2
Rename repository to `TeamBillingData`
joeauyeung Oct 30, 2025
803a015
Use repository factory in main service
joeauyeung Oct 30, 2025
fc81ce1
Fix new import paths
joeauyeung Oct 30, 2025
de6e2fc
Merge branch 'main' into implement-team-billing-repository-factor
joeauyeung Oct 30, 2025
29b4239
Rename to
joeauyeung Oct 30, 2025
059d819
Ensure `IS_TEAM_BILLING_ENABLED` is of type boolean
joeauyeung Oct 30, 2025
015e8e0
Rename classes to TeamBillingService and TeamBillingServiceFactory
joeauyeung Oct 30, 2025
ad59eb7
Implement DI in `BookingServiceFactory`
joeauyeung Oct 30, 2025
65b42e0
`TeamBillingService` use repository in `getOrgIfNeeded`
joeauyeung Oct 30, 2025
b6b9722
DI `isTeamBillingEnabled` to `TeamBillingServiceFactory`
joeauyeung Oct 31, 2025
76cb6f9
Rename files for consistency
joeauyeung Oct 31, 2025
5412960
Return stub BillingRepository if billing is not enabled
joeauyeung Oct 31, 2025
1cbfe9f
Move Stripe billing service to service folder
joeauyeung Oct 31, 2025
fe50537
Rename file
joeauyeung Oct 31, 2025
6c7b0f2
`StripeBillingService.getSubscriptionStatus` return `SubscriptionStatus`
joeauyeung Oct 31, 2025
239dbeb
Type fices in StripeBillingService
joeauyeung Oct 31, 2025
c5417e1
Type fix in `stubTeamBillingService`
joeauyeung Oct 31, 2025
0c0e573
DI the `BillingProviderService` into the `TeamBillingService`
joeauyeung Oct 31, 2025
cfea43f
Implement DI in `skipTeamTrials.handler`
joeauyeung Oct 31, 2025
9d3d457
Implement DI for team billing in `inviteMember.handler`
joeauyeung Oct 31, 2025
c434f9a
`skipTeamTrials.handler` use `team.isOrganization`
joeauyeung Oct 31, 2025
d844c3f
Implement DI for billing in `hasActiveTeamPlan.handler`
joeauyeung Oct 31, 2025
34f3d8a
Type fixes
joeauyeung Oct 31, 2025
e11f458
Implement DI in `bulkDeleteUsers.handler`
joeauyeung Oct 31, 2025
b91a461
Implement `BillingProviderServiceFactory` in `updateProfile.handler`
joeauyeung Oct 31, 2025
76ba33b
Implment `BillingProviderServiceFactory` in `buyCredits.handler`
joeauyeung Oct 31, 2025
635a88f
Fix import in `stripeCustomer.handler`
joeauyeung Oct 31, 2025
3a49799
Add a constructor to `teamBillingServiceFactory`
joeauyeung Nov 4, 2025
37e24c5
Add DI to `PrismaTeamBillingRepository`
joeauyeung Nov 4, 2025
4251ebf
Add DI to `StripeBillingService`
joeauyeung Nov 4, 2025
81f1e01
Implement singleton in `BillingProviderServiceFactory`
joeauyeung Nov 4, 2025
a8927f8
Add DI folder and contents to billing folder
joeauyeung Nov 4, 2025
fff0958
Use `getTeamBillingServiceFactory` in `inviteMember.handler`
joeauyeung Nov 4, 2025
6ecf42f
Add `saveTeamBilling` method to `ITeamBillingService`
joeauyeung Nov 4, 2025
4751813
Implement DI in new team route
joeauyeung Nov 4, 2025
ae363cd
Implement DI in `teamService`
joeauyeung Nov 4, 2025
1bb98a5
Implement DI in `OrganizationPaymentService`
joeauyeung Nov 4, 2025
b22fc84
Implement DI in `credit-service`
joeauyeung Nov 5, 2025
9b3ffaf
In `StripeBillingService` remove `static` from status methods
joeauyeung Nov 5, 2025
de674d9
Implemnt DI in `_invoice.paid.org`
joeauyeung Nov 5, 2025
154eeeb
Refactor `hasActiveTeamPlan` to use `getTeamBillingFactory`
joeauyeung Nov 5, 2025
e762e85
Refactor `skipTeamTrials` to use `getTeamBillingFactory`
joeauyeung Nov 5, 2025
14f97c1
Refactor `skipTeamTrials` to use `getTeamBillingServiceFactory`
joeauyeung Nov 5, 2025
811bd8b
`stripeCustomer.handler` to use `getBillingProviderService`
joeauyeung Nov 5, 2025
60796ea
Remove old factories
joeauyeung Nov 5, 2025
b344299
Type fix
joeauyeung Nov 5, 2025
6ecc387
Remove unused factory
joeauyeung Nov 5, 2025
5488c05
Refactor `updateProfile.handler` to use `getBillingProviderService`
joeauyeung Nov 5, 2025
d5b46f0
Change name to `TeamBillingDataRepositoryFactory`
joeauyeung Nov 6, 2025
53f0489
Type Prisma return in `prisma.module`
joeauyeung Nov 6, 2025
70d980e
Type fix
joeauyeung Nov 6, 2025
8e436cc
Refactor `buyCredits.handler` to use `getBillingProviderService`
joeauyeung Nov 6, 2025
4e707e6
Refactor `credit-service` to use billing DI containers
joeauyeung Nov 6, 2025
618b7bb
Type fix
joeauyeung Nov 6, 2025
25cfbb1
Add `getTeamBillingDataRepository`
joeauyeung Nov 6, 2025
817be7c
Refactor `_invoice.paid.org` to use DI container
joeauyeung Nov 6, 2025
a346d66
Refactor `_customer.subscription.deleted.team-plan` to use DI container
joeauyeung Nov 6, 2025
14f6670
Refactor `calcomHandler` to use DI container
joeauyeung Nov 6, 2025
f599ecb
Refactor `getCustomerAndCheckoutSession` to use DI container
joeauyeung Nov 6, 2025
7ebe67b
Refactor `verify-email` to use DI containers
joeauyeung Nov 6, 2025
4e214bf
Refactor `api/create/route` to use DI container
joeauyeung Nov 6, 2025
48214a7
Refactor downgradeUsers to use DI container
joeauyeung Nov 6, 2025
a60194d
Merge
joeauyeung Nov 6, 2025
cb8f610
Type fix
joeauyeung Nov 6, 2025
fbf586f
Clean up console.logs
joeauyeung Nov 6, 2025
45c5527
Add await to `this.billingRepository.create` in `saveTeamBilling`
joeauyeung Nov 6, 2025
b4910e7
Fix type errors
joeauyeung Nov 7, 2025
40b74fb
Address comments
joeauyeung Nov 7, 2025
8dc9464
fix: update tests to work with new DI pattern
devin-ai-integration[bot] Nov 7, 2025
3dac4dc
fix: add compatibility layer and env setup for unit tests
devin-ai-integration[bot] Nov 7, 2025
80f7839
fix: update unit tests to mock DI container properly
devin-ai-integration[bot] Nov 7, 2025
ed0f815
Merge branch 'main' into implement-team-billing-repository-factor
joeauyeung Nov 7, 2025
e9cc790
fix: update remaining unit tests to use DI pattern
devin-ai-integration[bot] Nov 7, 2025
7ea9bbe
Undo changes made to Prisma module
joeauyeung Nov 7, 2025
5d2f514
fix: address test-related PR comments
devin-ai-integration[bot] Nov 7, 2025
158a226
Address feedback
joeauyeung Nov 10, 2025
6f50d92
fix: update teamService integration test to mock new DI factory pattern
devin-ai-integration[bot] Nov 10, 2025
f6e847a
Merge branch 'main' into implement-team-billing-repository-factor
joeauyeung Nov 10, 2025
5cac485
refactor: remove duplicate imports in credit-service.test.ts
devin-ai-integration[bot] Nov 12, 2025
93726c4
Remove unused index file
joeauyeung Nov 12, 2025
dcc151b
`getBySubscriptionId` to return team or null
joeauyeung Nov 12, 2025
159ea5f
Address feedback
joeauyeung Nov 12, 2025
bcbce07
Merge branch
joeauyeung Nov 12, 2025
9586afb
Merge fix
joeauyeung Nov 12, 2025
55ea14e
Merge branch 'main' into implement-team-billing-repository-factor
alishaz-polymath Nov 13, 2025
26ecf86
Refactor file names
joeauyeung Nov 17, 2025
f9badeb
Merge with
joeauyeung Nov 17, 2025
aef65be
fix: correct mockStripe variable name to stripeMock in StripeBillingS…
devin-ai-integration[bot] Nov 17, 2025
927c35e
refactor: update internal-team-billing.test.ts to use new DI structur…
devin-ai-integration[bot] Nov 17, 2025
0c75b5b
fix: update createWithPaymentIntent.handler.test.ts to mock DI contai…
devin-ai-integration[bot] Nov 18, 2025
801ba07
Merge branch 'main' into implement-team-billing-repository-factor
joeauyeung Nov 18, 2025
f892874
Merge branch
joeauyeung Nov 18, 2025
5083faa
Type fixes
joeauyeung Nov 18, 2025
0299078
Remove READMEs
joeauyeung Nov 18, 2025
fd6ac3f
Merge with
joeauyeung Dec 1, 2025
c3250de
Merge origin/main into devin/team-subscription-payment-failed-email-1…
devin-ai-integration[bot] Jan 11, 2026
ea6a3ab
Merge branch 'main' into devin/team-subscription-payment-failed-email…
devin-ai-integration[bot] Jan 23, 2026
6569183
fix: update TeamRepository.test.ts to match findTeamWithMembers select
devin-ai-integration[bot] Jan 23, 2026
1c94218
Merge branch 'main' into devin/team-subscription-payment-failed-email…
joeauyeung Jan 27, 2026
6a701c5
fix: remove duplicate exports in email templates index
devin-ai-integration[bot] Jan 27, 2026
5f98bea
fix: quote users table name and add enabled check in getUsersWithPerm…
devin-ai-integration[bot] Jan 27, 2026
e67507e
Merge branch 'main' into devin/team-subscription-payment-failed-email…
joeauyeung Jan 27, 2026
3768a4a
Merge branch 'devin/team-subscription-payment-failed-email-1760634153…
devin-ai-integration[bot] Jan 27, 2026
d12293f
revert: undo PermissionRepository changes - tests were failing before…
devin-ai-integration[bot] Jan 27, 2026
17d7c5b
fix: quote users table name and add enabled check in getUsersWithPerm…
devin-ai-integration[bot] Jan 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/app/api/cron/downgradeUsers/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ async function postHandler(request: NextRequest) {
metadata: true,
isOrganization: true,
parentId: true,
name: true,
},
skip: pageNumber * pageSize,
take: pageSize,
Expand Down
8 changes: 7 additions & 1 deletion apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,14 @@
"refund_failed": "The refund for the event {{eventType}} with {{userName}} on {{date}} failed.",
"check_with_provider_and_user": "Please check with your payment provider and {{user}} how to handle this.",
"a_refund_failed": "A refund failed",
"subscription_payment_failed_subject": "Payment Failed: {{entityName}} Subscription",
"subscription_payment_failed_title": "Payment Failed for {{entityName}}: Please Update Your Details",
"subscription_payment_failed_description": "We were unable to process the payment for your {{entityName}} subscription.",
"subscription_payment_failed_next_steps": "Looks like your card didn't go through. Let's get this fixed so your bookings stay live. \n Click the button below to update your payment details. It only takes a moment.",
"subscription_payment_failed_contact_support": "If you need assistance, please contact our support team at support@cal.com.",
"update_payment_method": "Update Payment Method",
"awaiting_payment_subject": "Awaiting payment: {{title}} on {{date}}",
"meeting_awaiting_payment": "Your meeting is awaiting payment",
"meeting_awaiting_payment":"Your meeting is awaiting payment",
"dark_theme_contrast_error": "Dark theme color doesn't pass contrast check. We recommend you change this colour so your buttons will be more visible.",
"light_theme_contrast_error": "Light theme color doesn't pass contrast check. We recommend you change this colour so your buttons will be more visible.",
"event_type_color_light_theme_contrast_error": "Light theme color doesn't pass contrast check. We recommend you change this color so your event types color will be more visible.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,16 @@ export abstract class BillingPortalService {
const billingPortalUrl = await this.createBillingPortalUrl(customerId, returnUrl);
res.redirect(302, billingPortalUrl);
}

/** Generates a team billing portal URL without permission checks */
async processBillingPortalWithoutPermissionChecks({ teamId }: { teamId: number }) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need this to add a link directly to the billing portal in the email

const customerId = await this.getCustomerId(teamId);
if (!customerId) {
throw new Error(`Customer ID not found for team ${teamId}`);
}

const returnUrl = `${WEBAPP_URL}/settings/teams/${teamId}/billing`;
const billingPortalUrl = await this.createBillingPortalUrl(customerId, returnUrl);
return billingPortalUrl;
}
}
11 changes: 11 additions & 0 deletions packages/emails/email-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ import OrganizerRequestReminderEmail from "./templates/organizer-request-reminde
import OrganizerRequestedToRescheduleEmail from "./templates/organizer-requested-to-reschedule-email";
import OrganizerRescheduledEmail from "./templates/organizer-rescheduled-email";
import OrganizerScheduledEmail from "./templates/organizer-scheduled-email";
import SlugReplacementEmail from "./templates/slug-replacement-email";
import type { SubscriptionPaymentFailedEmailData } from "./templates/subscription-payment-failed-email";
import SubscriptionPaymentFailedEmail from "./templates/subscription-payment-failed-email";
import type { TeamInvite } from "./templates/team-invite-email";
import TeamInviteEmail from "./templates/team-invite-email";
import type { WorkflowEmailData } from "./templates/workflow-email";
import WorkflowEmail from "./templates/workflow-email";

type EventTypeMetadata = z.infer<typeof EventTypeMetaDataSchema>;

Expand Down Expand Up @@ -770,3 +777,7 @@ export const sendAddGuestsEmailsAndSMS = async (args: {

await Promise.all(emailsAndSMSToSend);
};

export const sendSubscriptionPaymentFailedEmail = async (emailData: SubscriptionPaymentFailedEmailData) => {
await sendEmail(() => new SubscriptionPaymentFailedEmail(emailData));
};
69 changes: 69 additions & 0 deletions packages/emails/src/templates/SubscriptionPaymentFailedEmail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { BaseEmailHtml, CallToAction } from "../components";

type SubscriptionPaymentFailedEmailProps = {
entityName: string;
billingPortalUrl: string;
supportEmail: string;
language: {
translate: (key: string, variables?: Record<string, string | number>) => string;
};
};

export const SubscriptionPaymentFailedEmail = (props: SubscriptionPaymentFailedEmailProps) => {
const t = props.language.translate;

return (
<BaseEmailHtml
headerType="xCircle"
subject="subscription_payment_failed_subject"
title={t("subscription_payment_failed_title", { entityName: props.entityName })}
callToAction={
props.billingPortalUrl ? (
<CallToAction label={t("update_payment_method")} href={props.billingPortalUrl} />
) : null
}>
<PaymentFailedInformation {...props} />
</BaseEmailHtml>
);
};

function PaymentFailedInformation(props: SubscriptionPaymentFailedEmailProps) {
const t = props.language.translate;

return (
<>
<tr>
<td align="center" style={{ fontSize: "0px", padding: "10px 25px", wordBreak: "break-word" }}>
<div
style={{
fontFamily: "Roboto, Helvetica, sans-serif",
fontSize: "16px",
fontWeight: 400,
lineHeight: "24px",
textAlign: "center",
color: "#494949",
}}>
{t("subscription_payment_failed_next_steps")}
</div>
</td>
</tr>
<tr>
<td align="center" style={{ fontSize: "0px", padding: "10px 25px", wordBreak: "break-word" }}>
<div
style={{
fontFamily: "Roboto, Helvetica, sans-serif",
fontSize: "14px",
fontWeight: 400,
lineHeight: "20px",
textAlign: "center",
color: "#666666",
}}>
{t("subscription_payment_failed_contact_support", {
supportEmail: props.supportEmail,
})}
</div>
</td>
</tr>
</>
);
}
1 change: 1 addition & 0 deletions packages/emails/src/templates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,4 @@ export { TeamInviteEmail } from "./TeamInviteEmail";
export { VerifyAccountEmail } from "./VerifyAccountEmail";
export { VerifyEmailByCode } from "./VerifyEmailByCode";
export { VerifyEmailChangeEmail } from "./VerifyEmailChangeEmail";
export { SubscriptionPaymentFailedEmail } from "./SubscriptionPaymentFailedEmail";
67 changes: 67 additions & 0 deletions packages/emails/templates/subscription-payment-failed-email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";

import renderEmail from "../src/renderEmail";
import BaseEmail from "./_base-email";

export interface SubscriptionPaymentFailedEmailData {
entityName: string;
billingPortalUrl: string;
to: string;
language: {
translate: (key: string, variables?: Record<string, string | number>) => string;
};
}

export default class SubscriptionPaymentFailedEmail extends BaseEmail {
emailData: SubscriptionPaymentFailedEmailData;

constructor(emailData: SubscriptionPaymentFailedEmailData) {
super();
this.name = "SEND_SUBSCRIPTION_PAYMENT_FAILED_EMAIL";
this.emailData = emailData;
}

protected override getLocale(): string {
return "";
}

protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
const t = this.emailData.language.translate;

return {
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
to: this.emailData.to,
subject: t("subscription_payment_failed_subject", {
entityName: this.emailData.entityName,
}),
html: await renderEmail("SubscriptionPaymentFailedEmail", {
entityName: this.emailData.entityName,
billingPortalUrl: this.emailData.billingPortalUrl,
supportEmail: process.env.NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS || "support@cal.com",
language: this.emailData.language,
}),
text: this.getTextBody(),
};
}

protected getTextBody(): string {
const t = this.emailData.language.translate;
const supportEmail = process.env.NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS || "support@cal.com";

return `
${t("subscription_payment_failed_title")}

${t("subscription_payment_failed_description", {
entityName: this.emailData.entityName,
})}

${t("subscription_payment_failed_next_steps")}

${t("update_payment_method")}: ${this.emailData.billingPortalUrl}

${t("subscription_payment_failed_contact_support", {
supportEmail,
})}
`.trim();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
IBillingRepositoryCreateArgs,
} from "../../repository/billing/IBillingRepository";

export type TeamBillingInput = Pick<Team, "id" | "parentId" | "metadata" | "isOrganization">;
export type TeamBillingInput = Pick<Team, "id" | "parentId" | "metadata" | "isOrganization" | "name">;
export const TeamBillingPublishResponseStatus = {
REQUIRES_PAYMENT: "REQUIRES_PAYMENT",
REQUIRES_UPGRADE: "REQUIRES_UPGRADE",
Expand Down
57 changes: 56 additions & 1 deletion packages/features/ee/billing/service/teams/TeamBillingService.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import type { z } from "zod";

import { BillingPortalServiceFactory } from "@calcom/app-store/stripepayment/lib/services/factory/BillingPortalServiceFactory";
import { getRequestedSlugError } from "@calcom/app-store/stripepayment/lib/team-billing";
import { sendSubscriptionPaymentFailedEmail } from "@calcom/emails/email-manager";
import { purchaseTeamOrOrgSubscription } from "@calcom/features/ee/teams/lib/payments";
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { getMetadataHelpers } from "@calcom/lib/getMetadataHelpers";
import logger from "@calcom/lib/logger";
import { Redirect } from "@calcom/lib/redirect";
import { safeStringify } from "@calcom/lib/safeStringify";
import { getTranslation } from "@calcom/lib/server/i18n";
import { prisma } from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataStrictSchema } from "@calcom/prisma/zod-utils";
import type { z } from "zod";
import { updateSubscriptionQuantity } from "../../lib/subscription-updates";
// import billing from "../..";
import type {
Expand Down Expand Up @@ -232,4 +238,53 @@ export class TeamBillingService implements ITeamBillingService {
async saveTeamBilling(args: IBillingRepositoryCreateArgs) {
await this.billingRepository.create(args);
}

/**
* Sends a payment failed email to team/organization admins with billing portal link
*/
async sendPaymentFailedEmails(): Promise<void> {
try {
// Get members of the team that have access to billing
const billingPortalService = await BillingPortalServiceFactory.createService(this.team.id);
const billingPortalUrl = await billingPortalService.processBillingPortalWithoutPermissionChecks({
teamId: this.team.id,
});

const permissionService = new PermissionCheckService();
// Use the correct permission based on whether this is an organization or team
const billingPermission = this.team.isOrganization
? "organization.manageBilling"
: "team.manageBilling";
const membersToSendBillingEmail = await permissionService.getUsersWithPermissionForTeam({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New permissionService method to get all members of a team/org that have a specific permission.

teamId: this.team.id,
permission: billingPermission,
fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER],
});

const emailsSent = await Promise.allSettled(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Send payment failed email to all members that have billing permissions

membersToSendBillingEmail.map(async (member) => {
const translate = await getTranslation(member.locale || "en", "common");

await sendSubscriptionPaymentFailedEmail({
entityName: `${this.team.name} ${this.team.isOrganization ? "organization" : "team"}`,
billingPortalUrl: billingPortalUrl,
to: member.email,
language: { translate },
});
})
);

const failedEmails = emailsSent.filter((result) => result.status === "rejected");
const failedEmailCount = failedEmails.length;

log.info(
`Sent payment failed email for team ${this.team.id} to ${
membersToSendBillingEmail.length - failedEmailCount
} members. Failed to send email to ${failedEmailCount} members.`
);
} catch (error) {
this.logErrorFromUnknown(error);
throw error;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ describe("TeamRepository", () => {
metadata: true,
parentId: true,
isOrganization: true,
name: true,
},
});
expect(result).toEqual(mockTeam);
Expand Down
1 change: 1 addition & 0 deletions packages/features/ee/teams/repositories/TeamRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ export class TeamRepository {
metadata: true,
parentId: true,
isOrganization: true,
name: true,
},
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,14 @@ export interface IPermissionRepository {
fallbackRoles: MembershipRole[];
orgId?: number;
}): Promise<number[]>;

/**
* Gets all users in a team who have a specific permission
*/
getUsersWithPermissionInTeam(params: {
teamId: number;
permission: PermissionString;
fallbackRoles: MembershipRole[];
take?: number;
}): Promise<{ id: number; name: string | null; email: string; locale: string | null }[]>;
}
Loading
Loading