diff --git a/apps/api/src/billing/billing.service.ts b/apps/api/src/billing/billing.service.ts index 8402d20..a35edf8 100644 --- a/apps/api/src/billing/billing.service.ts +++ b/apps/api/src/billing/billing.service.ts @@ -21,6 +21,9 @@ import { defaultEnabledModules, isPermanentBillingPlan, isOrgModuleKey, + modulesForProductPlan, + PRODUCT_PLAN_LIMITS, + type ProductPlanCode, } from '@mento/shared'; import { PrismaService } from '../prisma/prisma.service'; import { PlatformAdminAlertService } from './platform-admin-alert.service'; @@ -414,7 +417,37 @@ export class BillingService { } defaultModulesForNewOrg(): OrgModuleKey[] { - return defaultEnabledModules(); + return modulesForProductPlan('core'); + } + + /** سطح امکانات را روی همهٔ محیط‌های مالک می‌گذارد و سقف هسته/تیم/سازمان را هم تنظیم می‌کند. */ + async applyProductPlan(organizationId: string, code: ProductPlanCode) { + const modules = modulesForProductPlan(code); + const owner = await this.prisma.membership.findFirst({ + where: { organizationId, role: 'owner', isActive: true }, + orderBy: { createdAt: 'asc' }, + select: { userId: true }, + }); + const orgIds = owner ? await this.ownedOrganizationIds(owner.userId) : [organizationId]; + for (const id of orgIds) { + await this.setOrgModules(id, modules); + } + const limits = PRODUCT_PLAN_LIMITS[code]; + if (owner && (limits.maxUsers != null || limits.maxOrganizations != null)) { + const account = await this.getByOwnerUserId(owner.userId); + if (account) { + await this.prisma.billingAccount.update({ + where: { id: account.id }, + data: { + ...(limits.maxUsers != null ? { maxUsers: limits.maxUsers } : {}), + ...(limits.maxOrganizations != null + ? { maxOrganizations: limits.maxOrganizations } + : {}), + }, + }); + } + } + return modules; } async countExpiringSoon() { diff --git a/apps/api/src/platform/dto.ts b/apps/api/src/platform/dto.ts index 74e0362..a611c35 100644 --- a/apps/api/src/platform/dto.ts +++ b/apps/api/src/platform/dto.ts @@ -13,7 +13,7 @@ import { MinLength, ValidateIf, } from 'class-validator'; -import { USER_ROLES, type UserRole } from '@mento/shared'; +import { PRODUCT_PLAN_CODES, USER_ROLES, type ProductPlanCode, type UserRole } from '@mento/shared'; export class PlatformLoginDto { @IsString() @@ -189,6 +189,11 @@ export class SetStorageQuotaDto { quotaGb!: number | null; } +export class ApplyProductPlanDto { + @IsIn(PRODUCT_PLAN_CODES) + planCode!: ProductPlanCode; +} + export class UpdateOrgModulesDto { @IsArray() @IsString({ each: true }) diff --git a/apps/api/src/platform/platform.controller.ts b/apps/api/src/platform/platform.controller.ts index f8a1d72..2a0d20d 100644 --- a/apps/api/src/platform/platform.controller.ts +++ b/apps/api/src/platform/platform.controller.ts @@ -13,6 +13,7 @@ import { import { CurrentUser } from '../auth/current-user.decorator'; import { ApplyBillingPlanDto, + ApplyProductPlanDto, SetStorageQuotaDto, SetUserCapDto, CreatePlatformAdminDto, @@ -168,6 +169,11 @@ export class PlatformController { return this.platform.setUserCap(id, dto.maxUsers); } + @Post('organizations/:id/product-plan') + applyProductPlan(@Param('id') id: string, @Body() dto: ApplyProductPlanDto) { + return this.platform.applyProductPlan(id, dto.planCode); + } + @Patch('organizations/:id/modules') updateOrganizationModules( @Param('id') id: string, diff --git a/apps/api/src/platform/platform.service.ts b/apps/api/src/platform/platform.service.ts index 5b02b0e..a618dcc 100644 --- a/apps/api/src/platform/platform.service.ts +++ b/apps/api/src/platform/platform.service.ts @@ -9,6 +9,7 @@ import type { PlatformOrganizationDto, PlatformStatsDto, PlatformUserDto, + ProductPlanCode, UserRole, } from '@mento/shared'; import { isBusinessField } from '@mento/shared'; @@ -472,6 +473,16 @@ export class PlatformService { return this.getUser(userId); } + async applyProductPlan(id: string, planCode: ProductPlanCode) { + const existing = await this.prisma.organization.findUnique({ + where: { id }, + select: { id: true }, + }); + if (!existing) throw new NotFoundException('محیط پیدا نشد'); + await this.billing.applyProductPlan(id, planCode); + return this.getOrganization(id); + } + async updateOrganizationModules(id: string, dto: UpdateOrgModulesDto) { const existing = await this.prisma.organization.findUnique({ where: { id }, diff --git a/apps/web/src/views/adm/AdmBillingPanel.vue b/apps/web/src/views/adm/AdmBillingPanel.vue index e94add1..9fdbf70 100644 --- a/apps/web/src/views/adm/AdmBillingPanel.vue +++ b/apps/web/src/views/adm/AdmBillingPanel.vue @@ -1,11 +1,13 @@