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
+28
View File
@@ -425,6 +425,30 @@ export class PlatformService {
return this.storage.usageForOwner(userId, refresh);
}
async seatUsage(userId: string) {
await this.requireBillingOwner(userId);
return this.billing.seatUsage(userId);
}
async setUserCap(userId: string, maxUsers: number) {
await this.requireBillingOwner(userId);
return this.billing.setMaxUsers(userId, maxUsers);
}
private async requireBillingOwner(userId: string) {
const owner = await this.prisma.membership.findFirst({
where: { userId, role: 'owner', isActive: true },
select: { id: true },
});
if (!owner) {
throw new BadRequestException('سقف کاربر فقط برای مالک محیط معنی دارد');
}
const account = await this.billing.getByOwnerUserId(userId);
if (!account) {
throw new BadRequestException('اول برای این مالک یک پلن اعمال کنید');
}
}
async setStorageQuota(userId: string, quotaGb: number | null) {
await this.storageUsage(userId, false);
return this.storage.setQuotaGb(userId, quotaGb);
@@ -495,9 +519,13 @@ export class PlatformService {
userId: true,
organizationId: true,
role: true,
isActive: true,
},
});
if (!row) throw new NotFoundException('عضویت پیدا نشد');
if (isActive && !row.isActive) {
await this.billing.assertSeatAvailable(row.organizationId, row.userId);
}
if (!isActive && row.role === 'owner') {
const activeOwners = await this.memberships.countActiveOwners(
row.organizationId,