Files
calendar/packages/lib/server/repository/team.ts
T
+13 a4a3eb5ea9 refactor: Billing service (#15366)
* WIP: Billing service

* WIP

* Update __handler.ts

* Update index.ts

* WIP

* WIP

Signed-off-by: zomars <zomars@me.com>

* WIP

Signed-off-by: zomars <zomars@me.com>

* Update delete.handler.ts

* Update delete.handler.ts

* Update internal-team-billing.ts

* WIP

* Type fixes

Signed-off-by: zomars <zomars@me.com>

* WIP

Signed-off-by: zomars <zomars@me.com>

* Discard changes to apps/api/v2/src/ee/event-types/event-types_2024_04_15/controllers/event-types.controller.ts

* Discard changes to packages/app-store/skype/config.json

* style: update text area (#15934)

* chore: update text-editor input

* chore: update text-editor input

* final update

* Update Editor.tsx

---------

Co-authored-by: unknown <adhabal2002@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>

* fix: #15697 Assignment warning modal poping up ,even when hosts are added . (#15699)

* removed reduntand check on assignedusers

* update for managed eventtype

* formatted

* comments

* Updated comments

* extracted checkForEmptyAssignment and added tests

* updated comments

* updated to show dialog also when not yet saved

* update translations

* updated comments

* updated translation

* updated types

* chore: update naming and comments

* Delete apps/web/lib/checkForEmptyAssignment.ts

deleted since file was renamed

* Delete apps/web/test/lib/checkForEmptyAssignment.test.ts

deleted as the file is renamed to be more descriptive

* update to do teamcheck before

* chore: revert name to CheckForEmptyAssignment

* updated check conditions

* updated useEffect dependencies

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>

* fix: getting an error when a team member tries to leave (#15925)

a team

Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>

* feat: add transitions to more components (#15916)

* add transitions to more components

* chore: Remove duplicate class

* fix: Change minimum number of teams required for org self-serve (#15955)

* fix: Change minimum number of teams required for org self-serve

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* refactor: Use a new constant for displaying org helper text dialogue

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>

* Update packages/lib/constants.ts

* Update turbo.json

---------

Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>

* feat: pwa ready and add shortcut for quick access (#15957)

* chore: Update site.webmanifest with additional display properties

* chore: Update site.webmanifest with additional display properties

* Update apps/web/public/site.webmanifest

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>

* i18n: update translations for "by proceeding, ..." (#15921)

* feat: Add title attribute to iframe in embed.ts (#15944)

* feat: Add title attribute to iframe in embed.ts

* Update packages/embeds/embed-core/src/embed.ts

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>

* fix: Assignment reminder modal appears when deleting a team event type (#15544)

* fix: Assignment reminder modal appears when deleting a team event type

* update

* Update EventTypeSingleLayout.tsx

* Update event-types-single-view.tsx

* update

* Early return https://handbook.cal.com/engineering/best-practices/prefer-early-returns

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>

* fix: Add CORP and COEP headers to embedded pages (#15275)

* Add CORP headers

* Improve test

* Add unit tests as well

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>

* feat: No-show bookings - do not count towards a member's round robin assignment (#15323)

* feat: No-show bookings - do not count towards a member's round robin assignment

* filter no show organizer

* remove logs

* fix: filter bookings where host attendee is no show

* fix: use not condition for noShow

* chore: test

* add default false for noShowHost

* Revert "chore: test"

This reverts commit c914a8fc4a14ca9c7d8c4a08ff8b0f463789171c.

* chore: integration test

* fix: noShowHost not working for null

* Update packages/lib/server/getLuckyUser.ts

* chore: update test description

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: gh0st91848 <gh0st91848@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>

* refactor: v1 orgs unlimited teams (#15908)

* refactor: v1 orgs unlimited teams

* fix: response message ownerId

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>

* fix: refresh connect atoms on token change (#15933)

* added dropdown

* change comment

* fix: added types

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>

* fix: Next.js bundle analysis (#15903)

* fix: Next.js bundle analysis

* Added the main checkout back

* fix: type in webhook payload (#15912)

Co-authored-by: CarinaWolli <wollencarina@gmail.com>

* fix: auto accept (#15975)

* fix: auto accept

* Update packages/lib/server/repository/organization.ts

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>

* fix: Broken GitHub workflows (#15979)

* test: Delete credential endpoint (#15322)

* Init

* Add mock trpc context

* Intro mockDatabaseClient

* Introduce mockDatabaseClient class

* Add delete video app test

* Add calendar test

* Remove unused func from bookingScenario

* Remove console.log

* Add app repository

* Add createMany method to event type repository

* Remove instance of MockDatabaseClient from video test

* Add destination calendar repository

* Remove instances of MockDatabaseClient

* abstract logic to own handler

* Remove dev dependency

* Clean up

* Type fix

* Pull yarn.lock from main

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>

* fix: #15967 Show better error when wrong variable is used in custom event name (#15974)

* update to show exact err msg with custom event name

* updated tests for validateCustomEventName

---------

Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>

* WIP

Signed-off-by: zomars <zomars@me.com>

* Update yarn.lock

* Update publish.handler.ts

* Update internal-team-billing.test.ts

* rename create to init

* remove sync services

* wip tests for delete team

* wip tets

* wip tests

* tests/internal-test

* remove return that is inaccessable

* remove team calls to use repository

* remove redudant file

* update team repo

* add stripe mocks and test

* fix constants

* tests to main stripe handler

* remove logs

* fix constants

* Update packages/features/ee/billing/api/webhook/_customer.subscription.deleted.team-plan.ts

Co-authored-by: Omar López <zomars@me.com>

* move workflow logic to a service

* use static method > creating new class

* fix: remove http code deep in repo + tests

* Feedback

* Update index.ts

* Type fix

* Lazy load fixes

---------

Signed-off-by: zomars <zomars@me.com>
Signed-off-by: Souptik Datta <souptikdatta2001@gmail.com>
Co-authored-by: Rohan Advani <122167726+RNAdvani@users.noreply.github.com>
Co-authored-by: unknown <adhabal2002@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Vijay <vijayraghav22@gmail.com>
Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: Hichem Fantar <hichemfantar2049@gmail.com>
Co-authored-by: Souptik Datta <souptikdatta2001@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Johannes Maendle <141119154+jomaendle2@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: gh0st91848 <gh0st91848@gmail.com>
Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: sean-brydon <sean@cal.com>
Co-authored-by: sean <sean@brydon.io>
2024-10-04 13:44:50 -07:00

304 lines
9.0 KiB
TypeScript

import { Prisma } from "@prisma/client";
import type { z } from "zod";
import { whereClauseForOrgWithSlugOrRequestedSlug } from "@calcom/ee/organizations/lib/orgDomains";
import { TeamBilling } from "@calcom/features/ee/billing/teams";
import removeMember from "@calcom/features/ee/teams/lib/removeMember";
import { deleteDomain } from "@calcom/lib/domainManager/organization";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
import { TRPCError } from "@trpc/server";
import { WorkflowService } from "../service/workflows";
import { getParsedTeam } from "./teamUtils";
type TeamGetPayloadWithParsedMetadata<TeamSelect extends Prisma.TeamSelect> =
| (Omit<Prisma.TeamGetPayload<{ select: TeamSelect }>, "metadata" | "isOrganization"> & {
metadata: z.infer<typeof teamMetadataSchema>;
isOrganization: boolean;
})
| null;
type GetTeamOrOrgArg<TeamSelect extends Prisma.TeamSelect> = {
lookupBy: (
| {
id: number;
}
| {
slug: string;
}
) & {
havingMemberWithId?: number;
};
/**
* If we are fetching a team, this is the slug of the organization that the team belongs to.
*/
forOrgWithSlug: string | null;
/**
* If true, means that we need to fetch an organization with the given slug. Otherwise, we need to fetch a team with the given slug.
*/
isOrg: boolean;
teamSelect: TeamSelect;
};
const log = logger.getSubLogger({ prefix: ["repository", "team"] });
/**
* Get's the team or organization with the given slug or id reliably along with parsed metadata.
*/
async function getTeamOrOrg<TeamSelect extends Prisma.TeamSelect>({
lookupBy,
forOrgWithSlug: forOrgWithSlug,
isOrg,
teamSelect,
}: GetTeamOrOrgArg<TeamSelect>): Promise<TeamGetPayloadWithParsedMetadata<TeamSelect>> {
const where: Prisma.TeamFindFirstArgs["where"] = {};
teamSelect = {
...teamSelect,
metadata: true,
isOrganization: true,
} satisfies TeamSelect;
if (lookupBy.havingMemberWithId) where.members = { some: { userId: lookupBy.havingMemberWithId } };
if ("id" in lookupBy) {
where.id = lookupBy.id;
} else {
where.slug = lookupBy.slug;
}
if (isOrg) {
// We must fetch only the organization here.
// Note that an organization and a team that doesn't belong to an organization, both have parentId null
// If the organization has null slug(but requestedSlug is 'test') and the team also has slug 'test', we can't distinguish them without explicitly checking the metadata.isOrganization
// Note that, this isn't possible now to have same requestedSlug as the slug of a team not part of an organization. This is legacy teams handling mostly. But it is still safer to be sure that you are fetching an Organization only in case of isOrgView
where.isOrganization = true;
// We must fetch only the team here.
} else {
if (forOrgWithSlug) {
where.parent = whereClauseForOrgWithSlugOrRequestedSlug(forOrgWithSlug);
}
}
log.debug({
orgSlug: forOrgWithSlug,
teamLookupBy: lookupBy,
isOrgView: isOrg,
where,
});
const teams = await prisma.team.findMany({
where,
select: teamSelect,
});
const teamsWithParsedMetadata = teams
.map((team) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ts types are way too complciated for this now
const parsedMetadata = teamMetadataSchema.parse(team.metadata ?? {});
return {
...team,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore It does exist
isOrganization: team.isOrganization as boolean,
metadata: parsedMetadata,
};
})
// In cases where there are many teams with the same slug, we need to find out the one and only one that matches our criteria
.filter((team) => {
// We need an org if isOrgView otherwise we need a team
return isOrg ? team.isOrganization : !team.isOrganization;
});
if (teamsWithParsedMetadata.length > 1) {
log.error("Found more than one team/Org. We should be doing something wrong.", {
isOrgView: isOrg,
where,
teams: teamsWithParsedMetadata.map((team) => {
const t = team as unknown as { id: number; slug: string };
return {
id: t.id,
slug: t.slug,
};
}),
});
}
const team = teamsWithParsedMetadata[0];
if (!team) return null;
// HACK: I am not sure how to make Prisma in peace with TypeScript with this repository pattern
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return team as any;
}
export async function getTeam<TeamSelect extends Prisma.TeamSelect>({
lookupBy,
forOrgWithSlug: forOrgWithSlug,
teamSelect,
}: Omit<GetTeamOrOrgArg<TeamSelect>, "isOrg">): Promise<TeamGetPayloadWithParsedMetadata<TeamSelect>> {
return getTeamOrOrg({
lookupBy,
forOrgWithSlug: forOrgWithSlug,
isOrg: false,
teamSelect,
});
}
export async function getOrg<TeamSelect extends Prisma.TeamSelect>({
lookupBy,
forOrgWithSlug: forOrgWithSlug,
teamSelect,
}: Omit<GetTeamOrOrgArg<TeamSelect>, "isOrg">): Promise<TeamGetPayloadWithParsedMetadata<TeamSelect>> {
return getTeamOrOrg({
lookupBy,
forOrgWithSlug: forOrgWithSlug,
isOrg: true,
teamSelect,
});
}
const teamSelect = Prisma.validator<Prisma.TeamSelect>()({
id: true,
name: true,
slug: true,
logoUrl: true,
parentId: true,
metadata: true,
isOrganization: true,
organizationSettings: true,
isPlatform: true,
});
export class TeamRepository {
static async findById({ id }: { id: number }) {
const team = await prisma.team.findUnique({
where: {
id,
},
select: teamSelect,
});
if (!team) {
return null;
}
return getParsedTeam(team);
}
static async deleteById({ id }: { id: number }) {
try {
await WorkflowService.deleteWorkflowRemindersOfRemovedTeam(id);
} catch (e) {
console.error(e);
}
const deletedTeam = await prisma.$transaction(async (tx) => {
await tx.eventType.deleteMany({
where: {
teamId: id,
schedulingType: "MANAGED",
},
});
// delete all memberships
await tx.membership.deleteMany({
where: {
teamId: id,
},
});
const deletedTeam = await tx.team.delete({
where: {
id: id,
},
});
const teamBilling = await TeamBilling.findAndInit(id);
await teamBilling.cancel();
return deletedTeam;
});
if (deletedTeam?.isOrganization && deletedTeam.slug) deleteDomain(deletedTeam.slug);
return deletedTeam;
}
// TODO: Move errors away from TRPC error to make it more generic
static async inviteMemberByToken(token: string, userId: number) {
const verificationToken = await prisma.verificationToken.findFirst({
where: {
token,
OR: [{ expiresInDays: null }, { expires: { gte: new Date() } }],
},
include: {
team: {
select: {
name: true,
},
},
},
});
if (!verificationToken) throw new TRPCError({ code: "NOT_FOUND", message: "Invite not found" });
if (!verificationToken.teamId || !verificationToken.team)
throw new TRPCError({
code: "NOT_FOUND",
message: "Invite token is not associated with any team",
});
try {
await prisma.membership.create({
data: {
teamId: verificationToken.teamId,
userId: userId,
role: MembershipRole.MEMBER,
accepted: false,
},
});
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {
if (e.code === "P2002") {
throw new TRPCError({
code: "FORBIDDEN",
message: "This user is a member of this team / has a pending invitation.",
});
}
} else throw e;
}
const teamBilling = await TeamBilling.findAndInit(verificationToken.teamId);
await teamBilling.updateQuantity();
return verificationToken.team.name;
}
static async publish(teamId: number) {
const teamBilling = await TeamBilling.findAndInit(teamId);
return teamBilling.publish();
}
static async removeMembers(teamIds: number[], memberIds: number[], isOrg = false) {
const deleteMembershipPromises = [];
for (const memberId of memberIds) {
for (const teamId of teamIds) {
deleteMembershipPromises.push(
// This removeMember function is from @calcom/features/ee/teams/lib/removeMember.ts we should probably move it to this repository.
removeMember({
teamId,
memberId,
isOrg,
})
);
}
}
await Promise.all(deleteMembershipPromises);
const teamsBilling = await TeamBilling.findAndInitMany(teamIds);
const teamBillingPromises = teamsBilling.map((teamBilling) => teamBilling.updateQuantity());
await Promise.allSettled(teamBillingPromises);
}
}