feat(billing): cap active users on an owner subscription

Super admin can set a seat limit of 3, 10, 20, or a negotiated number so an owner cannot add users past that cap.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-02 18:33:51 +03:30
co-authored by Cursor
parent 06977d27ed
commit acb5d51f24
9 changed files with 220 additions and 2 deletions
+62
View File
@@ -11,6 +11,7 @@ import type {
BillingAccountStatus,
BillingPlanCode,
OrgModuleKey,
OwnerSeatUsageDto,
SubscriptionSummaryDto,
} from '@mento/shared';
import {
@@ -64,6 +65,7 @@ export class BillingService {
startsAt: Date;
expiresAt: Date;
maxOrganizations: number;
maxUsers?: number | null;
externalPaymentRef: string | null;
notes: string | null;
}): BillingAccountDto {
@@ -72,11 +74,71 @@ export class BillingService {
id: row.id,
ownerUserId: row.ownerUserId,
...summary,
maxUsers: row.maxUsers ?? null,
externalPaymentRef: row.externalPaymentRef,
notes: row.notes,
};
}
async seatUsage(ownerUserId: string): Promise<OwnerSeatUsageDto> {
const account = await this.getByOwnerUserId(ownerUserId);
const orgIds = await this.ownedOrganizationIds(ownerUserId);
const usedUsers = await this.countActiveUsers(orgIds);
return { usedUsers, maxUsers: account?.maxUsers ?? null };
}
async setMaxUsers(ownerUserId: string, maxUsers: number): Promise<OwnerSeatUsageDto> {
await this.prisma.billingAccount.update({
where: { ownerUserId },
data: { maxUsers },
});
return this.seatUsage(ownerUserId);
}
/** کاربر جدید فقط وقتی سقف اشتراک جا دارد. کسی که از قبل در محیط‌های همین مالک فعال است صندلی تازه نمی‌گیرد. */
async assertSeatAvailable(organizationId: string, userId: string | null): Promise<void> {
const owner = await this.prisma.membership.findFirst({
where: { organizationId, role: 'owner', isActive: true },
orderBy: { createdAt: 'asc' },
select: { userId: true },
});
if (!owner) return;
const account = await this.getByOwnerUserId(owner.userId);
if (!account || account.maxUsers == null) return;
const orgIds = await this.ownedOrganizationIds(owner.userId);
if (userId) {
const already = await this.prisma.membership.findFirst({
where: { userId, organizationId: { in: orgIds }, isActive: true },
select: { id: true },
});
if (already) return;
}
const used = await this.countActiveUsers(orgIds);
if (used >= account.maxUsers) {
throw new ForbiddenException(
`سقف کاربر این اشتراک ${account.maxUsers} نفر است. برای افزودن کاربر باید سقف اشتراک بیشتر شود.`,
);
}
}
private async ownedOrganizationIds(ownerUserId: string): Promise<string[]> {
const rows = await this.prisma.membership.findMany({
where: { userId: ownerUserId, role: 'owner', isActive: true },
select: { organizationId: true },
});
return rows.map((row) => row.organizationId);
}
private async countActiveUsers(organizationIds: string[]): Promise<number> {
if (!organizationIds.length) return 0;
const rows = await this.prisma.membership.findMany({
where: { organizationId: { in: organizationIds }, isActive: true },
select: { userId: true },
distinct: ['userId'],
});
return rows.length;
}
async getByOwnerUserId(ownerUserId: string) {
return this.prisma.billingAccount.findUnique({ where: { ownerUserId } });
}