mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
1423 lines
44 KiB
TypeScript
Executable File
1423 lines
44 KiB
TypeScript
Executable File
import ResellerPlan from "Common/Models/DatabaseModels/ResellerPlan";
|
|
import {
|
|
IsBillingEnabled,
|
|
NotificationSlackWebhookOnCreateProject,
|
|
NotificationSlackWebhookOnDeleteProject,
|
|
NotificationSlackWebhookOnSubscriptionUpdate,
|
|
getAllEnvVars,
|
|
} from "../EnvironmentConfig";
|
|
import AllMeteredPlans from "../Types/Billing/MeteredPlan/AllMeteredPlans";
|
|
import CreateBy from "../Types/Database/CreateBy";
|
|
import DeleteBy from "../Types/Database/DeleteBy";
|
|
import FindBy from "../Types/Database/FindBy";
|
|
import { OnCreate, OnDelete, OnFind, OnUpdate } from "../Types/Database/Hooks";
|
|
import QueryHelper from "../Types/Database/QueryHelper";
|
|
import UpdateBy from "../Types/Database/UpdateBy";
|
|
import logger from "../Utils/Logger";
|
|
import AccessTokenService from "./AccessTokenService";
|
|
import BillingService from "./BillingService";
|
|
import DatabaseService from "./DatabaseService";
|
|
import IncidentSeverityService from "./IncidentSeverityService";
|
|
import IncidentStateService from "./IncidentStateService";
|
|
import MailService from "./MailService";
|
|
import MonitorStatusService from "./MonitorStatusService";
|
|
import NotificationService from "./NotificationService";
|
|
import PromoCodeService from "./PromoCodeService";
|
|
import ScheduledMaintenanceStateService from "./ScheduledMaintenanceStateService";
|
|
import TeamMemberService from "./TeamMemberService";
|
|
import TeamPermissionService from "./TeamPermissionService";
|
|
import TeamService from "./TeamService";
|
|
import UserNotificationRuleService from "./UserNotificationRuleService";
|
|
import UserNotificationSettingService from "./UserNotificationSettingService";
|
|
import UserService from "./UserService";
|
|
import SubscriptionPlan, {
|
|
PlanType,
|
|
} from "../../Types/Billing/SubscriptionPlan";
|
|
import SubscriptionStatus from "../../Types/Billing/SubscriptionStatus";
|
|
import {
|
|
Black,
|
|
Green,
|
|
Moroon500,
|
|
Red,
|
|
Yellow,
|
|
Yellow500,
|
|
} from "../../Types/BrandColors";
|
|
import Color from "../../Types/Color";
|
|
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
|
import OneUptimeDate from "../../Types/Date";
|
|
import Email from "../../Types/Email";
|
|
import EmailTemplateType from "../../Types/Email/EmailTemplateType";
|
|
import BadDataException from "../../Types/Exception/BadDataException";
|
|
import NotAuthorizedException from "../../Types/Exception/NotAuthorizedException";
|
|
import ObjectID from "../../Types/ObjectID";
|
|
import Permission from "../../Types/Permission";
|
|
import IncidentSeverity from "Common/Models/DatabaseModels/IncidentSeverity";
|
|
import IncidentState from "Common/Models/DatabaseModels/IncidentState";
|
|
import MonitorStatus from "Common/Models/DatabaseModels/MonitorStatus";
|
|
import Model from "Common/Models/DatabaseModels/Project";
|
|
import PromoCode from "Common/Models/DatabaseModels/PromoCode";
|
|
import ScheduledMaintenanceState from "Common/Models/DatabaseModels/ScheduledMaintenanceState";
|
|
import Team from "Common/Models/DatabaseModels/Team";
|
|
import TeamMember from "Common/Models/DatabaseModels/TeamMember";
|
|
import TeamPermission from "Common/Models/DatabaseModels/TeamPermission";
|
|
import User from "Common/Models/DatabaseModels/User";
|
|
import Select from "../Types/Database/Select";
|
|
import Query from "../Types/Database/Query";
|
|
import AlertSeverity from "../../Models/DatabaseModels/AlertSeverity";
|
|
import AlertSeverityService from "./AlertSeverityService";
|
|
import AlertState from "../../Models/DatabaseModels/AlertState";
|
|
import AlertStateService from "./AlertStateService";
|
|
import SlackUtil from "../Utils/Slack";
|
|
import URL from "../../Types/API/URL";
|
|
import Exception from "../../Types/Exception/Exception";
|
|
|
|
export interface CurrentPlan {
|
|
plan: PlanType | null;
|
|
isSubscriptionUnpaid: boolean;
|
|
}
|
|
|
|
export class ProjectService extends DatabaseService<Model> {
|
|
public constructor() {
|
|
super(Model);
|
|
}
|
|
|
|
public getPlanType(planId: string): PlanType {
|
|
if (!SubscriptionPlan.isValidPlanId(planId, getAllEnvVars())) {
|
|
throw new BadDataException("Plan is invalid.");
|
|
}
|
|
|
|
return SubscriptionPlan.getPlanType(planId);
|
|
}
|
|
|
|
protected override async onBeforeCreate(
|
|
data: CreateBy<Model>,
|
|
): Promise<OnCreate<Model>> {
|
|
if (!data.data.name) {
|
|
throw new BadDataException("Project name is required");
|
|
}
|
|
|
|
if (data.props.userId) {
|
|
data.data.createdByUserId = data.props.userId;
|
|
} else {
|
|
throw new NotAuthorizedException(
|
|
"User should be logged in to create the project.",
|
|
);
|
|
}
|
|
|
|
const user: User | null = await UserService.findOneById({
|
|
id: data.props.userId,
|
|
select: {
|
|
name: true,
|
|
email: true,
|
|
companyPhoneNumber: true,
|
|
companyName: true,
|
|
utmCampaign: true,
|
|
utmSource: true,
|
|
utmMedium: true,
|
|
utmTerm: true,
|
|
utmContent: true,
|
|
utmUrl: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!user) {
|
|
throw new BadDataException("User not found.");
|
|
}
|
|
|
|
if (IsBillingEnabled) {
|
|
if (!data.data.paymentProviderPlanId) {
|
|
throw new BadDataException("Plan required to create the project.");
|
|
}
|
|
|
|
data.data.planName = this.getPlanType(data.data.paymentProviderPlanId);
|
|
|
|
if (data.data.paymentProviderPromoCode) {
|
|
// check if it exists in promcode table. Not all promocodes are in the table, only reseller ones are.
|
|
// If they are not in the table, allow projetc creation to proceed.
|
|
// If they are in the project table, then see if anyn restrictions on reseller plan apply and if it does,
|
|
// apply those restictions to the project.
|
|
|
|
const promoCode: PromoCode | null = await PromoCodeService.findOneBy({
|
|
query: {
|
|
promoCodeId: data.data.paymentProviderPromoCode,
|
|
},
|
|
select: {
|
|
isPromoCodeUsed: true,
|
|
userEmail: true,
|
|
resellerPlan: {
|
|
_id: true,
|
|
planType: true,
|
|
monitorLimit: true,
|
|
teamMemberLimit: true,
|
|
} as Select<ResellerPlan>,
|
|
resellerId: true,
|
|
resellerLicenseId: true,
|
|
planType: true,
|
|
resellerPlanId: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (promoCode) {
|
|
// check if the same user is creating the project.
|
|
if (promoCode.userEmail?.toString() !== user.email?.toString()) {
|
|
throw new BadDataException(
|
|
"This promocode is assigned to a different user and cannot be used.",
|
|
);
|
|
}
|
|
|
|
if (promoCode.isPromoCodeUsed) {
|
|
throw new BadDataException("This promocode has already been used.");
|
|
}
|
|
|
|
if (promoCode.resellerPlan?.monitorLimit) {
|
|
data.data.activeMonitorsLimit =
|
|
promoCode.resellerPlan?.monitorLimit;
|
|
}
|
|
|
|
if (promoCode.resellerPlan?.teamMemberLimit) {
|
|
data.data.seatLimit = promoCode.resellerPlan?.teamMemberLimit;
|
|
}
|
|
|
|
if (promoCode.planType !== data.data.planName) {
|
|
throw new BadDataException(
|
|
"Promocode is not valid for this plan. Please select the " +
|
|
promoCode.planType +
|
|
" plan.",
|
|
);
|
|
}
|
|
|
|
if (promoCode.resellerLicenseId) {
|
|
data.data.resellerLicenseId = promoCode.resellerLicenseId;
|
|
}
|
|
|
|
if (promoCode.resellerId) {
|
|
data.data.resellerId = promoCode.resellerId;
|
|
}
|
|
|
|
if (promoCode.resellerPlanId) {
|
|
data.data.resellerPlanId = promoCode.resellerPlanId;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (
|
|
data.data.paymentProviderPromoCode &&
|
|
!(await BillingService.isPromoCodeValid(
|
|
data.data.paymentProviderPromoCode,
|
|
))
|
|
) {
|
|
throw new BadDataException("Promo code is invalid.");
|
|
}
|
|
|
|
// check if promocode is valid.
|
|
}
|
|
|
|
// check if the user has the project with the same name. If yes, reject.
|
|
|
|
let existingProjectWithSameNameCount: number = 0;
|
|
if (
|
|
data.props.userGlobalAccessPermission &&
|
|
data.props.userGlobalAccessPermission?.projectIds.length > 0
|
|
) {
|
|
existingProjectWithSameNameCount = (
|
|
await this.countBy({
|
|
query: {
|
|
_id: QueryHelper.any(
|
|
data.props.userGlobalAccessPermission?.projectIds.map(
|
|
(item: ObjectID) => {
|
|
return item.toString();
|
|
},
|
|
) || [],
|
|
),
|
|
name: QueryHelper.findWithSameText(data.data.name!),
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
})
|
|
).toNumber();
|
|
}
|
|
|
|
if (existingProjectWithSameNameCount > 0) {
|
|
throw new BadDataException("Project with the same name already exists");
|
|
}
|
|
|
|
data.data.createdOwnerName = user.name!;
|
|
data.data.createdOwnerEmail = user.email!;
|
|
data.data.createdOwnerPhone = user.companyPhoneNumber!;
|
|
data.data.createdOwnerCompanyName = user.companyName!;
|
|
|
|
// UTM info.
|
|
data.data.utmCampaign = user.utmCampaign!;
|
|
data.data.utmSource = user.utmSource!;
|
|
data.data.utmMedium = user.utmMedium!;
|
|
data.data.utmTerm = user.utmTerm!;
|
|
data.data.utmContent = user.utmContent!;
|
|
data.data.utmUrl = user.utmUrl!;
|
|
|
|
return Promise.resolve({ createBy: data, carryForward: null });
|
|
}
|
|
|
|
protected override async onBeforeUpdate(
|
|
updateBy: UpdateBy<Model>,
|
|
): Promise<OnUpdate<Model>> {
|
|
if (IsBillingEnabled) {
|
|
if (updateBy.data.enableAutoRechargeSmsOrCallBalance) {
|
|
await NotificationService.rechargeIfBalanceIsLow(
|
|
new ObjectID(updateBy.query._id! as string),
|
|
{
|
|
autoRechargeSmsOrCallByBalanceInUSD: updateBy.data
|
|
.autoRechargeSmsOrCallByBalanceInUSD as number,
|
|
autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD: updateBy.data
|
|
.autoRechargeSmsOrCallWhenCurrentBalanceFallsInUSD as number,
|
|
enableAutoRechargeSmsOrCallBalance: updateBy.data
|
|
.enableAutoRechargeSmsOrCallBalance as boolean,
|
|
},
|
|
);
|
|
}
|
|
|
|
if (updateBy.data.paymentProviderPlanId) {
|
|
// payment provider id changed.
|
|
const project: Model | null = await this.findOneById({
|
|
id: new ObjectID(updateBy.query._id! as string),
|
|
select: {
|
|
paymentProviderSubscriptionId: true,
|
|
paymentProviderMeteredSubscriptionId: true,
|
|
paymentProviderSubscriptionSeats: true,
|
|
paymentProviderPlanId: true,
|
|
trialEndsAt: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!project) {
|
|
throw new BadDataException("Project not found");
|
|
}
|
|
|
|
if (
|
|
project.paymentProviderPlanId !== updateBy.data.paymentProviderPlanId
|
|
) {
|
|
logger.debug("Changing plan for project " + project.id);
|
|
|
|
const plan: SubscriptionPlan | undefined =
|
|
SubscriptionPlan.getSubscriptionPlanById(
|
|
updateBy.data.paymentProviderPlanId! as string,
|
|
getAllEnvVars(),
|
|
);
|
|
|
|
if (!plan) {
|
|
throw new BadDataException("Invalid plan");
|
|
}
|
|
|
|
logger.debug(
|
|
"Changing plan for project " +
|
|
project.id?.toString() +
|
|
" to " +
|
|
plan.getName(),
|
|
);
|
|
|
|
if (!project.paymentProviderSubscriptionSeats) {
|
|
project.paymentProviderSubscriptionSeats =
|
|
await TeamMemberService.getUniqueTeamMemberCountInProject(
|
|
project.id!,
|
|
);
|
|
}
|
|
|
|
logger.debug(
|
|
"Changing plan for project " +
|
|
project.id?.toString() +
|
|
" to " +
|
|
plan.getName() +
|
|
" with seats " +
|
|
project.paymentProviderSubscriptionSeats,
|
|
);
|
|
|
|
const subscription: {
|
|
subscriptionId: string;
|
|
meteredSubscriptionId: string;
|
|
trialEndsAt?: Date | undefined;
|
|
} = await BillingService.changePlan({
|
|
projectId: project.id!,
|
|
subscriptionId: project.paymentProviderSubscriptionId as string,
|
|
meteredSubscriptionId:
|
|
project.paymentProviderMeteredSubscriptionId as string,
|
|
serverMeteredPlans: AllMeteredPlans,
|
|
newPlan: plan,
|
|
quantity: project.paymentProviderSubscriptionSeats as number,
|
|
isYearly:
|
|
plan.getYearlyPlanId() === updateBy.data.paymentProviderPlanId,
|
|
endTrialAt: project.trialEndsAt,
|
|
});
|
|
|
|
logger.debug(
|
|
"Changing plan for project " +
|
|
project.id?.toString() +
|
|
" to " +
|
|
plan.getName() +
|
|
" with seats " +
|
|
project.paymentProviderSubscriptionSeats +
|
|
" completed.",
|
|
);
|
|
|
|
// refresh subscription status.
|
|
const subscriptionState: SubscriptionStatus =
|
|
await BillingService.getSubscriptionStatus(
|
|
subscription.subscriptionId as string,
|
|
);
|
|
|
|
const meteredSubscriptionState: SubscriptionStatus =
|
|
await BillingService.getSubscriptionStatus(
|
|
subscription.meteredSubscriptionId as string,
|
|
);
|
|
|
|
await this.updateOneById({
|
|
id: new ObjectID(updateBy.query._id! as string),
|
|
data: {
|
|
paymentProviderSubscriptionId: subscription.subscriptionId,
|
|
paymentProviderMeteredSubscriptionId:
|
|
subscription.meteredSubscriptionId,
|
|
trialEndsAt: subscription.trialEndsAt || new Date(),
|
|
planName: SubscriptionPlan.getPlanType(
|
|
updateBy.data.paymentProviderPlanId! as string,
|
|
),
|
|
paymentProviderMeteredSubscriptionStatus:
|
|
meteredSubscriptionState,
|
|
paymentProviderSubscriptionStatus: subscriptionState,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
ignoreHooks: true,
|
|
},
|
|
});
|
|
|
|
logger.debug(
|
|
"Changing plan for project " +
|
|
project.id?.toString() +
|
|
" to " +
|
|
plan.getName() +
|
|
" with seats " +
|
|
project.paymentProviderSubscriptionSeats +
|
|
" completed and project updated.",
|
|
);
|
|
|
|
if (project.id) {
|
|
// send slack message on plan change.
|
|
await this.sendSubscriptionChangeWebhookSlackNotification(
|
|
project.id,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return { updateBy, carryForward: [] };
|
|
}
|
|
|
|
private async sendSubscriptionChangeWebhookSlackNotification(
|
|
projectId: ObjectID,
|
|
): Promise<void> {
|
|
if (NotificationSlackWebhookOnSubscriptionUpdate) {
|
|
// fetch project again.
|
|
const project: Model | null = await this.findOneById({
|
|
id: new ObjectID(projectId.toString()),
|
|
select: {
|
|
name: true,
|
|
_id: true,
|
|
createdOwnerName: true,
|
|
createdOwnerEmail: true,
|
|
planName: true,
|
|
createdByUserId: true,
|
|
paymentProviderSubscriptionStatus: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!project) {
|
|
throw new BadDataException("Project not found");
|
|
}
|
|
|
|
let slackMessage: string = `*Project Plan Changed:*
|
|
*Project Name:* ${project.name?.toString() || "N/A"}
|
|
*Project ID:* ${project.id?.toString() || "N/A"}
|
|
`;
|
|
|
|
if (project.createdOwnerName && project.createdOwnerEmail) {
|
|
slackMessage += `*Project Created By:* ${project?.createdOwnerName?.toString() + " (" + project.createdOwnerEmail.toString() + ")" || "N/A"}
|
|
`;
|
|
}
|
|
|
|
if (IsBillingEnabled) {
|
|
// which plan?
|
|
slackMessage += `*Plan:* ${project.planName?.toString() || "N/A"}
|
|
*Subscription Status:* ${project.paymentProviderSubscriptionStatus?.toString() || "N/A"}
|
|
`;
|
|
}
|
|
|
|
SlackUtil.sendMessageToChannel({
|
|
url: URL.fromString(NotificationSlackWebhookOnSubscriptionUpdate),
|
|
text: slackMessage,
|
|
}).catch((error: Exception) => {
|
|
logger.error("Error sending slack message: " + error);
|
|
});
|
|
}
|
|
}
|
|
|
|
private async addDefaultScheduledMaintenanceState(
|
|
createdItem: Model,
|
|
): Promise<Model> {
|
|
let createdScheduledMaintenanceState: ScheduledMaintenanceState =
|
|
new ScheduledMaintenanceState();
|
|
createdScheduledMaintenanceState.name = "Scheduled";
|
|
createdScheduledMaintenanceState.description =
|
|
"When an event is scheduled, it belongs to this state";
|
|
createdScheduledMaintenanceState.color = Black;
|
|
createdScheduledMaintenanceState.isScheduledState = true;
|
|
createdScheduledMaintenanceState.projectId = createdItem.id!;
|
|
createdScheduledMaintenanceState.order = 1;
|
|
|
|
createdScheduledMaintenanceState =
|
|
await ScheduledMaintenanceStateService.create({
|
|
data: createdScheduledMaintenanceState,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
let ongoingScheduledMaintenanceState: ScheduledMaintenanceState =
|
|
new ScheduledMaintenanceState();
|
|
ongoingScheduledMaintenanceState.name = "Ongoing";
|
|
ongoingScheduledMaintenanceState.description =
|
|
"When an event is ongoing, it belongs to this state.";
|
|
ongoingScheduledMaintenanceState.color = Yellow;
|
|
ongoingScheduledMaintenanceState.isOngoingState = true;
|
|
ongoingScheduledMaintenanceState.projectId = createdItem.id!;
|
|
ongoingScheduledMaintenanceState.order = 2;
|
|
|
|
ongoingScheduledMaintenanceState =
|
|
await ScheduledMaintenanceStateService.create({
|
|
data: ongoingScheduledMaintenanceState,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
let endedScheduledMaintenanceState: ScheduledMaintenanceState =
|
|
new ScheduledMaintenanceState();
|
|
endedScheduledMaintenanceState.name = "Ended";
|
|
endedScheduledMaintenanceState.description =
|
|
"Scheduled maintenance events switch to this state when they end.";
|
|
endedScheduledMaintenanceState.color = new Color("#4A4A4A");
|
|
endedScheduledMaintenanceState.isEndedState = true;
|
|
endedScheduledMaintenanceState.projectId = createdItem.id!;
|
|
endedScheduledMaintenanceState.order = 3;
|
|
|
|
endedScheduledMaintenanceState =
|
|
await ScheduledMaintenanceStateService.create({
|
|
data: endedScheduledMaintenanceState,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
let completedScheduledMaintenanceState: ScheduledMaintenanceState =
|
|
new ScheduledMaintenanceState();
|
|
completedScheduledMaintenanceState.name = "Completed";
|
|
completedScheduledMaintenanceState.description =
|
|
"When an event is completed, it belongs to this state.";
|
|
completedScheduledMaintenanceState.color = Green;
|
|
completedScheduledMaintenanceState.isResolvedState = true;
|
|
completedScheduledMaintenanceState.projectId = createdItem.id!;
|
|
completedScheduledMaintenanceState.order = 4;
|
|
|
|
completedScheduledMaintenanceState =
|
|
await ScheduledMaintenanceStateService.create({
|
|
data: completedScheduledMaintenanceState,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
return createdItem;
|
|
}
|
|
|
|
protected override async onCreateSuccess(
|
|
_onCreate: OnCreate<Model>,
|
|
createdItem: Model,
|
|
): Promise<Model> {
|
|
// Create billing.
|
|
|
|
if (IsBillingEnabled) {
|
|
const customerId: string = await BillingService.createCustomer({
|
|
name: createdItem.name!,
|
|
email: createdItem.createdOwnerEmail!,
|
|
id: createdItem.id!,
|
|
});
|
|
|
|
const plan: SubscriptionPlan | undefined =
|
|
SubscriptionPlan.getSubscriptionPlanById(
|
|
createdItem.paymentProviderPlanId!,
|
|
getAllEnvVars(),
|
|
);
|
|
|
|
if (!plan) {
|
|
throw new BadDataException("Invalid plan.");
|
|
}
|
|
// add subscription to this customer.
|
|
|
|
const { subscriptionId, meteredSubscriptionId, trialEndsAt } =
|
|
await BillingService.subscribeToPlan({
|
|
projectId: createdItem.id!,
|
|
customerId,
|
|
serverMeteredPlans: AllMeteredPlans,
|
|
plan,
|
|
quantity: 1,
|
|
isYearly:
|
|
plan.getYearlyPlanId() === createdItem.paymentProviderPlanId!,
|
|
trial: true,
|
|
promoCode: createdItem.paymentProviderPromoCode,
|
|
});
|
|
|
|
await this.updateOneById({
|
|
id: createdItem.id!,
|
|
data: {
|
|
paymentProviderCustomerId: customerId,
|
|
paymentProviderSubscriptionId: subscriptionId,
|
|
paymentProviderMeteredSubscriptionId: meteredSubscriptionId,
|
|
paymentProviderSubscriptionSeats: 1,
|
|
trialEndsAt: (trialEndsAt || null) as any,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
// mark the promo code as used it it exists.
|
|
|
|
if (createdItem.paymentProviderPromoCode) {
|
|
await PromoCodeService.updateOneBy({
|
|
query: {
|
|
promoCodeId: createdItem.paymentProviderPromoCode,
|
|
},
|
|
data: {
|
|
isPromoCodeUsed: true,
|
|
promoCodeUsedAt: OneUptimeDate.getCurrentDate(),
|
|
projectId: createdItem.id!,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
createdItem = await this.addDefaultIncidentSeverity(createdItem);
|
|
createdItem = await this.addDefaultAlertSeverity(createdItem);
|
|
createdItem = await this.addDefaultProjectTeams(createdItem);
|
|
createdItem = await this.addDefaultMonitorStatus(createdItem);
|
|
createdItem = await this.addDefaultIncidentState(createdItem);
|
|
createdItem = await this.addDefaultScheduledMaintenanceState(createdItem);
|
|
createdItem = await this.addDefaultAlertState(createdItem);
|
|
|
|
if (NotificationSlackWebhookOnCreateProject) {
|
|
// fetch project again.
|
|
const project: Model | null = await this.findOneById({
|
|
id: createdItem.id!,
|
|
select: {
|
|
name: true,
|
|
_id: true,
|
|
createdOwnerName: true,
|
|
createdOwnerEmail: true,
|
|
planName: true,
|
|
createdByUserId: true,
|
|
paymentProviderSubscriptionStatus: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!project) {
|
|
throw new BadDataException("Project not found");
|
|
}
|
|
|
|
let slackMessage: string = `*Project Created:*
|
|
*Project Name:* ${project.name?.toString() || "N/A"}
|
|
*Project ID:* ${project.id?.toString() || "N/A"}
|
|
`;
|
|
|
|
if (project.createdOwnerName && project.createdOwnerEmail) {
|
|
slackMessage += `*Created By:* ${project?.createdOwnerName?.toString() + " (" + project.createdOwnerEmail.toString() + ")" || "N/A"}
|
|
`;
|
|
|
|
if (IsBillingEnabled) {
|
|
// which plan?
|
|
slackMessage += `*Plan:* ${project.planName?.toString() || "N/A"}
|
|
*Subscription Status:* ${project.paymentProviderSubscriptionStatus?.toString() || "N/A"}
|
|
`;
|
|
}
|
|
|
|
SlackUtil.sendMessageToChannel({
|
|
url: URL.fromString(NotificationSlackWebhookOnCreateProject),
|
|
text: slackMessage,
|
|
}).catch((error: Exception) => {
|
|
logger.error("Error sending slack message: " + error);
|
|
});
|
|
}
|
|
}
|
|
|
|
return createdItem;
|
|
}
|
|
|
|
private async addDefaultIncidentState(createdItem: Model): Promise<Model> {
|
|
let createdIncidentState: IncidentState = new IncidentState();
|
|
createdIncidentState.name = "Identified";
|
|
createdIncidentState.description =
|
|
"When an incident is created, it belongs to this state";
|
|
createdIncidentState.color = Red;
|
|
createdIncidentState.isCreatedState = true;
|
|
createdIncidentState.projectId = createdItem.id!;
|
|
createdIncidentState.order = 1;
|
|
|
|
createdIncidentState = await IncidentStateService.create({
|
|
data: createdIncidentState,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
let acknowledgedIncidentState: IncidentState = new IncidentState();
|
|
acknowledgedIncidentState.name = "Acknowledged";
|
|
acknowledgedIncidentState.description =
|
|
"When an incident is acknowledged, it belongs to this state.";
|
|
acknowledgedIncidentState.color = Yellow;
|
|
acknowledgedIncidentState.isAcknowledgedState = true;
|
|
acknowledgedIncidentState.projectId = createdItem.id!;
|
|
acknowledgedIncidentState.order = 2;
|
|
|
|
acknowledgedIncidentState = await IncidentStateService.create({
|
|
data: acknowledgedIncidentState,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
let resolvedIncidentState: IncidentState = new IncidentState();
|
|
resolvedIncidentState.name = "Resolved";
|
|
resolvedIncidentState.description =
|
|
"When an incident is resolved, it belongs to this state.";
|
|
resolvedIncidentState.color = Green;
|
|
resolvedIncidentState.isResolvedState = true;
|
|
resolvedIncidentState.projectId = createdItem.id!;
|
|
resolvedIncidentState.order = 3;
|
|
|
|
resolvedIncidentState = await IncidentStateService.create({
|
|
data: resolvedIncidentState,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
return createdItem;
|
|
}
|
|
|
|
public async addDefaultAlertState(createdItem: Model): Promise<Model> {
|
|
let createdAlertState: AlertState = new AlertState();
|
|
createdAlertState.name = "Identified";
|
|
createdAlertState.description =
|
|
"When an alert is created, it belongs to this state";
|
|
createdAlertState.color = Red;
|
|
createdAlertState.isCreatedState = true;
|
|
createdAlertState.projectId = createdItem.id!;
|
|
createdAlertState.order = 1;
|
|
|
|
createdAlertState = await AlertStateService.create({
|
|
data: createdAlertState,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
let acknowledgedAlertState: AlertState = new AlertState();
|
|
acknowledgedAlertState.name = "Acknowledged";
|
|
acknowledgedAlertState.description =
|
|
"When an alert is acknowledged, it belongs to this state.";
|
|
acknowledgedAlertState.color = Yellow;
|
|
acknowledgedAlertState.isAcknowledgedState = true;
|
|
acknowledgedAlertState.projectId = createdItem.id!;
|
|
acknowledgedAlertState.order = 2;
|
|
|
|
acknowledgedAlertState = await AlertStateService.create({
|
|
data: acknowledgedAlertState,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
let resolvedAlertState: AlertState = new AlertState();
|
|
resolvedAlertState.name = "Resolved";
|
|
resolvedAlertState.description =
|
|
"When an incident is resolved, it belongs to this state.";
|
|
resolvedAlertState.color = Green;
|
|
resolvedAlertState.isResolvedState = true;
|
|
resolvedAlertState.projectId = createdItem.id!;
|
|
resolvedAlertState.order = 3;
|
|
|
|
resolvedAlertState = await AlertStateService.create({
|
|
data: resolvedAlertState,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
return createdItem;
|
|
}
|
|
|
|
public async addDefaultAlertSeverity(createdItem: Model): Promise<Model> {
|
|
let highSeverity: AlertSeverity = new AlertSeverity();
|
|
highSeverity.name = "High";
|
|
highSeverity.description =
|
|
"Issues causing very high impact to customers. Immediate attention is required.";
|
|
highSeverity.color = Moroon500;
|
|
highSeverity.projectId = createdItem.id!;
|
|
highSeverity.order = 1;
|
|
|
|
highSeverity = await AlertSeverityService.create({
|
|
data: highSeverity,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
let lowSeverity: AlertSeverity = new AlertSeverity();
|
|
lowSeverity.name = "Low";
|
|
lowSeverity.description = "Issues causing low impact to customers.";
|
|
lowSeverity.color = Yellow500;
|
|
lowSeverity.projectId = createdItem.id!;
|
|
lowSeverity.order = 2;
|
|
|
|
lowSeverity = await AlertSeverityService.create({
|
|
data: lowSeverity,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
return createdItem;
|
|
}
|
|
|
|
private async addDefaultIncidentSeverity(createdItem: Model): Promise<Model> {
|
|
let criticalIncident: IncidentSeverity = new IncidentSeverity();
|
|
criticalIncident.name = "Critical Incident";
|
|
criticalIncident.description =
|
|
"Issues causing very high impact to customers. Immediate response is required. Examples include a full outage, or a data breach.";
|
|
criticalIncident.color = Moroon500;
|
|
criticalIncident.projectId = createdItem.id!;
|
|
criticalIncident.order = 1;
|
|
|
|
criticalIncident = await IncidentSeverityService.create({
|
|
data: criticalIncident,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
let majorIncident: IncidentSeverity = new IncidentSeverity();
|
|
majorIncident.name = "Major Incident";
|
|
majorIncident.description =
|
|
"Issues causing significant impact. Immediate response is usually required. We might have some workarounds that mitigate the impact on customers. Examples include an important sub-system failing.";
|
|
majorIncident.color = Red;
|
|
majorIncident.projectId = createdItem.id!;
|
|
majorIncident.order = 2;
|
|
|
|
majorIncident = await IncidentSeverityService.create({
|
|
data: majorIncident,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
let minorIncident: IncidentSeverity = new IncidentSeverity();
|
|
minorIncident.name = "Minor Incident";
|
|
minorIncident.description =
|
|
"Issues with low impact, which can usually be handled within working hours. Most customers are unlikely to notice any problems. Examples include a slight drop in application performance.";
|
|
minorIncident.color = Yellow;
|
|
minorIncident.projectId = createdItem.id!;
|
|
minorIncident.order = 3;
|
|
|
|
minorIncident = await IncidentSeverityService.create({
|
|
data: minorIncident,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
return createdItem;
|
|
}
|
|
|
|
private async addDefaultMonitorStatus(createdItem: Model): Promise<Model> {
|
|
let operationalStatus: MonitorStatus = new MonitorStatus();
|
|
operationalStatus.name = "Operational";
|
|
operationalStatus.description = "Monitor operating normally";
|
|
operationalStatus.projectId = createdItem.id!;
|
|
operationalStatus.priority = 1;
|
|
operationalStatus.isOperationalState = true;
|
|
operationalStatus.color = Green;
|
|
|
|
operationalStatus = await MonitorStatusService.create({
|
|
data: operationalStatus,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
let degradedStatus: MonitorStatus = new MonitorStatus();
|
|
degradedStatus.name = "Degraded";
|
|
degradedStatus.description = "Monitor is operating at reduced performance.";
|
|
degradedStatus.priority = 2;
|
|
degradedStatus.projectId = createdItem.id!;
|
|
degradedStatus.color = Yellow;
|
|
|
|
degradedStatus = await MonitorStatusService.create({
|
|
data: degradedStatus,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
let downStatus: MonitorStatus = new MonitorStatus();
|
|
downStatus.name = "Offline";
|
|
downStatus.description = "Monitor is offline.";
|
|
downStatus.isOfflineState = true;
|
|
downStatus.projectId = createdItem.id!;
|
|
downStatus.priority = 3;
|
|
downStatus.color = Red;
|
|
|
|
downStatus = await MonitorStatusService.create({
|
|
data: downStatus,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
return createdItem;
|
|
}
|
|
|
|
private async addDefaultProjectTeams(createdItem: Model): Promise<Model> {
|
|
// add a team member.
|
|
|
|
// Owner Team.
|
|
let ownerTeam: Team = new Team();
|
|
ownerTeam.projectId = createdItem.id!;
|
|
ownerTeam.name = "Owners";
|
|
ownerTeam.shouldHaveAtLeastOneMember = true;
|
|
ownerTeam.isPermissionsEditable = false;
|
|
ownerTeam.isTeamEditable = false;
|
|
ownerTeam.isTeamDeleteable = false;
|
|
ownerTeam.description =
|
|
"This team is for project owners. Adding team members to this team will give them root level permissions.";
|
|
|
|
ownerTeam = await TeamService.create({
|
|
data: ownerTeam,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
// Add current user to owners team.
|
|
|
|
let ownerTeamMember: TeamMember = new TeamMember();
|
|
ownerTeamMember.projectId = createdItem.id!;
|
|
ownerTeamMember.userId = createdItem.createdByUserId!;
|
|
ownerTeamMember.hasAcceptedInvitation = true;
|
|
ownerTeamMember.invitationAcceptedAt = OneUptimeDate.getCurrentDate();
|
|
ownerTeamMember.teamId = ownerTeam.id!;
|
|
|
|
ownerTeamMember = await TeamMemberService.create({
|
|
data: ownerTeamMember,
|
|
props: {
|
|
isRoot: true,
|
|
ignoreHooks: true,
|
|
},
|
|
});
|
|
|
|
// Add permissions for this team.
|
|
|
|
const ownerPermissions: TeamPermission = new TeamPermission();
|
|
ownerPermissions.permission = Permission.ProjectOwner;
|
|
ownerPermissions.teamId = ownerTeam.id!;
|
|
ownerPermissions.projectId = createdItem.id!;
|
|
|
|
await TeamPermissionService.create({
|
|
data: ownerPermissions,
|
|
props: {
|
|
isRoot: true,
|
|
ignoreHooks: true,
|
|
},
|
|
});
|
|
|
|
// Admin Team.
|
|
const adminTeam: Team = new Team();
|
|
adminTeam.projectId = createdItem.id!;
|
|
adminTeam.name = "Admin";
|
|
adminTeam.isPermissionsEditable = false;
|
|
adminTeam.isTeamDeleteable = false;
|
|
adminTeam.isTeamEditable = false;
|
|
adminTeam.description =
|
|
"This team is for project admins. Admins can invite members to any team and create project resources.";
|
|
|
|
await TeamService.create({
|
|
data: adminTeam,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
const adminPermissions: TeamPermission = new TeamPermission();
|
|
adminPermissions.permission = Permission.ProjectAdmin;
|
|
adminPermissions.teamId = adminTeam.id!;
|
|
adminPermissions.projectId = createdItem.id!;
|
|
|
|
await TeamPermissionService.create({
|
|
data: adminPermissions,
|
|
props: {
|
|
isRoot: true,
|
|
ignoreHooks: true,
|
|
},
|
|
});
|
|
|
|
// Members Team.
|
|
const memberTeam: Team = new Team();
|
|
memberTeam.projectId = createdItem.id!;
|
|
memberTeam.isPermissionsEditable = true;
|
|
memberTeam.name = "Members";
|
|
memberTeam.isTeamDeleteable = true;
|
|
memberTeam.description =
|
|
"This team is for project members. Members can interact with any project resources like monitors, incidents, etc.";
|
|
|
|
await TeamService.create({
|
|
data: memberTeam,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
const memberPermissions: TeamPermission = new TeamPermission();
|
|
memberPermissions.permission = Permission.ProjectMember;
|
|
memberPermissions.teamId = memberTeam.id!;
|
|
memberPermissions.projectId = createdItem.id!;
|
|
|
|
await TeamPermissionService.create({
|
|
data: memberPermissions,
|
|
props: {
|
|
isRoot: true,
|
|
ignoreHooks: true,
|
|
},
|
|
});
|
|
|
|
await AccessTokenService.refreshUserAllPermissions(
|
|
createdItem.createdByUserId!,
|
|
);
|
|
|
|
const user: User | null = await UserService.findOneById({
|
|
id: createdItem.createdByUserId!,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
select: {
|
|
isEmailVerified: true,
|
|
email: true,
|
|
},
|
|
});
|
|
|
|
if (user && user.isEmailVerified) {
|
|
await UserNotificationRuleService.addDefaultNotificationRuleForUser(
|
|
createdItem.id!,
|
|
user.id!,
|
|
user.email!,
|
|
);
|
|
|
|
await UserNotificationSettingService.addDefaultNotificationSettingsForUser(
|
|
user.id!,
|
|
createdItem.id!,
|
|
);
|
|
}
|
|
|
|
return createdItem;
|
|
}
|
|
|
|
public async updateLastActive(projectId: ObjectID): Promise<void> {
|
|
await this.updateOneById({
|
|
id: projectId,
|
|
data: {
|
|
lastActive: OneUptimeDate.getCurrentDate(),
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
public async getOwners(projectId: ObjectID): Promise<Array<User>> {
|
|
if (!projectId) {
|
|
throw new BadDataException("Project ID is required");
|
|
}
|
|
|
|
// get teams with project owner permissions.
|
|
const teamPermissions: Array<TeamPermission> =
|
|
await TeamPermissionService.findBy({
|
|
query: {
|
|
projectId: projectId,
|
|
permission: Permission.ProjectOwner,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
limit: LIMIT_MAX,
|
|
skip: 0,
|
|
select: {
|
|
teamId: true,
|
|
},
|
|
});
|
|
|
|
if (teamPermissions.length === 0) {
|
|
return [];
|
|
}
|
|
|
|
const teamIds: Array<ObjectID> = teamPermissions.map(
|
|
(item: TeamPermission) => {
|
|
return item.teamId!;
|
|
},
|
|
);
|
|
|
|
return TeamMemberService.getUsersInTeams(teamIds);
|
|
}
|
|
|
|
protected override async onBeforeFind(
|
|
findBy: FindBy<Model>,
|
|
): Promise<OnFind<Model>> {
|
|
// if user has no project id, then he should not be able to access any project.
|
|
if (
|
|
(!findBy.props.isRoot &&
|
|
!findBy.props.userGlobalAccessPermission?.projectIds) ||
|
|
findBy.props.userGlobalAccessPermission?.projectIds.length === 0
|
|
) {
|
|
findBy.props.isRoot = true;
|
|
findBy.query._id = ObjectID.getZeroObjectID().toString(); // should not get any projects.
|
|
}
|
|
|
|
return { findBy, carryForward: null };
|
|
}
|
|
|
|
protected override async onBeforeDelete(
|
|
deleteBy: DeleteBy<Model>,
|
|
): Promise<OnDelete<Model>> {
|
|
const projects: Array<Model> = await this.findBy({
|
|
query: deleteBy.query,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
limit: LIMIT_MAX,
|
|
skip: 0,
|
|
select: {
|
|
_id: true,
|
|
paymentProviderSubscriptionId: true,
|
|
paymentProviderMeteredSubscriptionId: true,
|
|
name: true,
|
|
createdByUser: {
|
|
name: true,
|
|
email: true,
|
|
},
|
|
},
|
|
});
|
|
|
|
return { deleteBy, carryForward: projects };
|
|
}
|
|
|
|
protected override async onDeleteSuccess(
|
|
onDelete: OnDelete<Model>,
|
|
_itemIdsBeforeDelete: ObjectID[],
|
|
): Promise<OnDelete<Model>> {
|
|
if (NotificationSlackWebhookOnDeleteProject) {
|
|
for (const project of onDelete.carryForward) {
|
|
let subscriptionStatus: SubscriptionStatus | null = null;
|
|
|
|
if (IsBillingEnabled) {
|
|
subscriptionStatus = await BillingService.getSubscriptionStatus(
|
|
project.paymentProviderSubscriptionId!,
|
|
);
|
|
}
|
|
|
|
let slackMessage: string = `*Project Deleted:*
|
|
*Project Name:* ${project.name?.toString() || "N/A"}
|
|
*Project ID:* ${project._id?.toString() || "N/A"}
|
|
`;
|
|
|
|
if (subscriptionStatus) {
|
|
slackMessage += `*Project Subscription Status:* ${subscriptionStatus?.toString() || "N/A"}
|
|
`;
|
|
}
|
|
|
|
if (
|
|
project.createdByUser &&
|
|
project.createdByUser.name &&
|
|
project.createdByUser.email
|
|
) {
|
|
slackMessage += `*Created By:* ${project?.createdByUser.name?.toString() + " (" + project.createdByUser.email.toString() + ")" || "N/A"}
|
|
`;
|
|
}
|
|
|
|
SlackUtil.sendMessageToChannel({
|
|
url: URL.fromString(NotificationSlackWebhookOnDeleteProject),
|
|
text: slackMessage,
|
|
}).catch((err: Error) => {
|
|
// log this error but do not throw it. Not important enough to stop the process.
|
|
logger.error(err);
|
|
});
|
|
}
|
|
}
|
|
|
|
// get project id
|
|
if (IsBillingEnabled) {
|
|
for (const project of onDelete.carryForward) {
|
|
if (project.paymentProviderSubscriptionId) {
|
|
await BillingService.cancelSubscription(
|
|
project.paymentProviderSubscriptionId,
|
|
);
|
|
}
|
|
|
|
if (project.paymentProviderMeteredSubscriptionId) {
|
|
await BillingService.cancelSubscription(
|
|
project.paymentProviderMeteredSubscriptionId,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return onDelete;
|
|
}
|
|
|
|
public async getCurrentPlan(projectId: ObjectID): Promise<CurrentPlan> {
|
|
if (!IsBillingEnabled) {
|
|
return { plan: null, isSubscriptionUnpaid: false };
|
|
}
|
|
|
|
const project: Model | null = await this.findOneById({
|
|
id: projectId,
|
|
select: {
|
|
paymentProviderPlanId: true,
|
|
paymentProviderSubscriptionStatus: true,
|
|
paymentProviderMeteredSubscriptionStatus: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
ignoreHooks: true,
|
|
},
|
|
});
|
|
|
|
if (!project) {
|
|
throw new BadDataException("Project ID is invalid");
|
|
}
|
|
|
|
if (!project.paymentProviderPlanId) {
|
|
throw new BadDataException("Project does not have any plans");
|
|
}
|
|
|
|
const plan: PlanType = SubscriptionPlan.getPlanType(
|
|
project.paymentProviderPlanId,
|
|
getAllEnvVars(),
|
|
);
|
|
|
|
return {
|
|
plan: plan,
|
|
isSubscriptionUnpaid:
|
|
!BillingService.isSubscriptionActive(
|
|
project.paymentProviderSubscriptionStatus!,
|
|
) ||
|
|
!BillingService.isSubscriptionActive(
|
|
project.paymentProviderMeteredSubscriptionStatus!,
|
|
),
|
|
};
|
|
}
|
|
|
|
public async sendEmailToProjectOwners(
|
|
projectId: ObjectID,
|
|
subject: string,
|
|
message: string,
|
|
): Promise<void> {
|
|
const owners: Array<User> = await this.getOwners(projectId);
|
|
|
|
if (owners.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const emails: Array<Email> = owners.map((owner: User) => {
|
|
return owner.email!;
|
|
});
|
|
|
|
for (const email of emails) {
|
|
MailService.sendMail(
|
|
{
|
|
toEmail: email,
|
|
templateType: EmailTemplateType.SimpleMessage,
|
|
vars: {
|
|
subject: subject,
|
|
message: message,
|
|
},
|
|
subject: subject,
|
|
},
|
|
{
|
|
projectId,
|
|
},
|
|
).catch((err: Error) => {
|
|
logger.error(err);
|
|
});
|
|
}
|
|
}
|
|
|
|
public async reactiveSubscription(projectId: ObjectID): Promise<void> {
|
|
logger.debug("Reactivating subscription for project " + projectId);
|
|
|
|
const project: Model | null = await this.findOneById({
|
|
id: projectId!,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
select: {
|
|
_id: true,
|
|
paymentProviderCustomerId: true,
|
|
paymentProviderSubscriptionId: true,
|
|
paymentProviderMeteredSubscriptionId: true,
|
|
paymentProviderSubscriptionSeats: true,
|
|
paymentProviderPlanId: true,
|
|
},
|
|
});
|
|
|
|
if (!project) {
|
|
throw new BadDataException("Project not found");
|
|
}
|
|
|
|
if (!project.paymentProviderCustomerId) {
|
|
throw new BadDataException("Payment Provider customer not found");
|
|
}
|
|
|
|
if (!project.paymentProviderSubscriptionId) {
|
|
throw new BadDataException("Payment Provider subscription not found");
|
|
}
|
|
|
|
if (!project.paymentProviderMeteredSubscriptionId) {
|
|
throw new BadDataException(
|
|
"Payment Provider metered subscription not found",
|
|
);
|
|
}
|
|
|
|
if (!project.paymentProviderSubscriptionSeats) {
|
|
throw new BadDataException(
|
|
"Payment Provider subscription seats not found",
|
|
);
|
|
}
|
|
|
|
if (!project.paymentProviderPlanId) {
|
|
throw new BadDataException("Payment Provider plan id not found");
|
|
}
|
|
|
|
const subscriptionPlan: SubscriptionPlan | undefined =
|
|
SubscriptionPlan.getSubscriptionPlanById(
|
|
project.paymentProviderPlanId,
|
|
getAllEnvVars(),
|
|
);
|
|
|
|
if (!subscriptionPlan) {
|
|
throw new BadDataException("Subscription plan not found");
|
|
}
|
|
|
|
const result: {
|
|
subscriptionId: string;
|
|
meteredSubscriptionId: string;
|
|
trialEndsAt?: Date | undefined;
|
|
} = await BillingService.changePlan({
|
|
projectId: project.id as ObjectID,
|
|
subscriptionId: project.paymentProviderSubscriptionId,
|
|
meteredSubscriptionId: project.paymentProviderMeteredSubscriptionId,
|
|
serverMeteredPlans: AllMeteredPlans,
|
|
newPlan: subscriptionPlan,
|
|
quantity: project.paymentProviderSubscriptionSeats,
|
|
isYearly: SubscriptionPlan.isYearlyPlan(project.paymentProviderPlanId),
|
|
endTrialAt: undefined,
|
|
});
|
|
|
|
// refresh subscription status.
|
|
const subscriptionState: SubscriptionStatus =
|
|
await BillingService.getSubscriptionStatus(
|
|
result.subscriptionId as string,
|
|
);
|
|
|
|
const meteredSubscriptionState: SubscriptionStatus =
|
|
await BillingService.getSubscriptionStatus(
|
|
project.paymentProviderMeteredSubscriptionId as string,
|
|
);
|
|
|
|
// now update project with new subscription id.
|
|
|
|
await this.updateOneById({
|
|
id: project.id!,
|
|
data: {
|
|
paymentProviderSubscriptionId: result.subscriptionId,
|
|
paymentProviderMeteredSubscriptionId: result.meteredSubscriptionId,
|
|
paymentProviderSubscriptionStatus: subscriptionState,
|
|
paymentProviderMeteredSubscriptionStatus: meteredSubscriptionState,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
// send slack message on plan change.
|
|
await this.sendSubscriptionChangeWebhookSlackNotification(projectId);
|
|
}
|
|
|
|
public getActiveProjectStatusQuery(): Query<Model> {
|
|
return {
|
|
// get only active projects
|
|
paymentProviderSubscriptionStatus: QueryHelper.equalToOrNull([
|
|
SubscriptionStatus.Active,
|
|
SubscriptionStatus.Trialing,
|
|
]),
|
|
paymentProviderMeteredSubscriptionStatus: QueryHelper.equalToOrNull([
|
|
SubscriptionStatus.Active,
|
|
SubscriptionStatus.Trialing,
|
|
]),
|
|
};
|
|
}
|
|
|
|
public async isSMSNotificationsEnabled(
|
|
projectId: ObjectID,
|
|
): Promise<boolean> {
|
|
const project: Model | null = await this.findOneById({
|
|
id: projectId,
|
|
select: {
|
|
enableSmsNotifications: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!project) {
|
|
throw new BadDataException("Project not found");
|
|
}
|
|
|
|
return Boolean(project.enableSmsNotifications);
|
|
}
|
|
}
|
|
export default new ProjectService();
|