Files
calendar/packages/trpc/server/routers/viewer/me/getUserTopBanners.handler.ts
3c91703415 feat: add invoice banner + block additions to teams/orgs (#27327)
* 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 <[email protected]>

* feat: add due invoice banner and invitation blocking for orgs

- Add DueInvoiceBanner component showing overdue prorations
- Add DueInvoiceService for checking blocking status and banner data
- Block invitations when proration invoices are 7+ days overdue
- Allow sub-team invites for existing org members (exception)
- Show banner to users with billing management permissions
- Link directly to Stripe invoice URL when available

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* fix: set subscriptionId in team metadata in seed script

The seed script was not setting subscriptionId on the team metadata,
causing checkIfOrgNeedsUpgrade to treat the org as needing upgrade
and showing the "trialing" banner.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* push trigger test to script

* fix mocks

---------

Co-authored-by: Claude Opus 4.5 <[email protected]>
2026-01-29 11:23:27 +00:00

77 lines
3.0 KiB
TypeScript

import {
getCalendarCredentials,
getConnectedCalendars,
} from "@calcom/features/calendars/lib/CalendarManager";
import { buildNonDelegationCredentials } from "@calcom/lib/delegationCredential";
import { prisma } from "@calcom/prisma";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
import { checkIfOrgNeedsUpgradeHandler } from "../organizations/checkIfOrgNeedsUpgrade.handler";
import { getUpgradeableHandler } from "../teams/getUpgradeable.handler";
import { checkInvalidAppCredentials } from "./checkForInvalidAppCredentials";
import { getDueInvoiceBannerDataHandler } from "./getDueInvoiceBannerData.handler";
import { shouldVerifyEmailHandler } from "./shouldVerifyEmail.handler";
type Props = {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
};
const checkInvalidGoogleCalendarCredentials = async ({ ctx }: Props) => {
const userCredentials = await prisma.credential.findMany({
where: {
userId: ctx.user.id,
type: "google_calendar",
},
select: credentialForCalendarServiceSelect,
});
// TODO: Call top buildNonDelegationCredentials here can be avoided by moving credential prisma query to repository.
const calendarCredentials = getCalendarCredentials(buildNonDelegationCredentials(userCredentials));
const { connectedCalendars } = await getConnectedCalendars(
calendarCredentials,
ctx.user.userLevelSelectedCalendars,
ctx.user.destinationCalendar?.externalId
);
return connectedCalendars.some((calendar) => !!calendar.error);
};
export const getUserTopBannersHandler = async ({ ctx }: Props) => {
const upgradeableTeamMememberships = getUpgradeableHandler({ userId: ctx.user.id });
const upgradeableOrgMememberships = checkIfOrgNeedsUpgradeHandler({ ctx });
const shouldEmailVerify = shouldVerifyEmailHandler({ ctx });
// const isInvalidCalendarCredential = checkInvalidGoogleCalendarCredentials({ ctx });
const appsWithInavlidCredentials = checkInvalidAppCredentials({ ctx });
const dueInvoiceBannerData = getDueInvoiceBannerDataHandler({ ctx });
const [
teamUpgradeBanner,
orgUpgradeBanner,
verifyEmailBanner,
// calendarCredentialBanner,
invalidAppCredentialBanners,
dueInvoiceBanner,
] = await Promise.allSettled([
upgradeableTeamMememberships,
upgradeableOrgMememberships,
shouldEmailVerify,
// isInvalidCalendarCredential,
appsWithInavlidCredentials,
dueInvoiceBannerData,
]);
return {
teamUpgradeBanner: teamUpgradeBanner.status === "fulfilled" ? teamUpgradeBanner.value : [],
orgUpgradeBanner: orgUpgradeBanner.status === "fulfilled" ? orgUpgradeBanner.value : [],
verifyEmailBanner: verifyEmailBanner.status === "fulfilled" ? !verifyEmailBanner.value.isVerified : false,
calendarCredentialBanner: false,
invalidAppCredentialBanners:
invalidAppCredentialBanners.status === "fulfilled" ? invalidAppCredentialBanners.value : [],
dueInvoiceBanner: dueInvoiceBanner.status === "fulfilled" ? dueInvoiceBanner.value : [],
};
};