improvement: newly created organization's subscriptions flow improved.

This commit is contained in:
2026-04-30 18:15:40 +03:30
parent 9fa406e35e
commit 64215f07e6
15 changed files with 264 additions and 92 deletions

View File

@@ -32,6 +32,16 @@ const ALL_PERMISSIONS = [
'TAB_REPORTS_EDIT',
];
const READ_ONLY_PERMISSIONS = [
'TAB_TODAY_READ',
'TAB_PATIENTS_READ',
'TAB_APPOINTMENTS_READ',
'TAB_STAFF_READ',
'TAB_LAB_READ',
'TAB_BILLING_READ',
'TAB_REPORTS_READ',
];
@Injectable()
export class AuthService {
constructor(
@@ -141,9 +151,7 @@ export class AuthService {
name: membership.organization.name,
type: membership.organization.type.name, // 'CLINIC' or 'LAB'
isOwner: membership.isOwner,
permissions: membership.isOwner
? ALL_PERMISSIONS
: membership.permissions?.map(p => p.permission.name) || [],
permissions: this.getMembershipPermissions(membership),
plan: membership.organization.plan
? {
name: membership.organization.plan.name,
@@ -253,16 +261,13 @@ export class AuthService {
async createOrganization(userId: string, dto: CreateOrganizationDto) {
const owner = await this.prisma.user.findUnique({
where: { id: userId },
select: { id: true, trialUsedAt: true },
select: { id: true },
});
if (!owner) {
throw new UnauthorizedException('User not found');
}
const planName = dto.planName?.trim() || 'Small';
const effectivePlanName = owner.trialUsedAt ? planName : 'trial';
const organization = await this.prisma.$transaction(async (tx) => {
const createdOrganization = await tx.organization.create({
data: {
@@ -271,9 +276,6 @@ export class AuthService {
owner: {
connect: { id: userId },
},
plan: {
connect: { name: effectivePlanName },
},
type: {
connect: { name: dto.organizationType },
},
@@ -287,14 +289,6 @@ export class AuthService {
isOwner: true,
},
});
if (!owner.trialUsedAt) {
await tx.user.update({
where: { id: userId },
data: { trialUsedAt: new Date() },
});
}
return createdOrganization;
});
@@ -350,9 +344,7 @@ export class AuthService {
name: membership.organization.name,
type: membership.organization.type.name,
isOwner: membership.isOwner,
permissions: membership.isOwner
? ALL_PERMISSIONS
: membership.permissions?.map(p => p.permission.name) || [],
permissions: this.getMembershipPermissions(membership),
plan: membership.organization.plan
? {
name: membership.organization.plan.name,
@@ -471,9 +463,7 @@ export class AuthService {
name: membership.organization.name,
type: membership.organization.type.name,
isOwner: membership.isOwner,
permissions: membership.isOwner
? ALL_PERMISSIONS
: membership.permissions?.map(p => p.permission.name) || [],
permissions: this.getMembershipPermissions(membership),
plan: membership.organization.plan
? {
name: membership.organization.plan.name,
@@ -678,9 +668,7 @@ export class AuthService {
name: membership.organization.name,
type: membership.organization.type.name,
isOwner: membership.isOwner,
permissions: membership.isOwner
? ALL_PERMISSIONS
: membership.permissions?.map(p => p.permission.name) || [],
permissions: this.getMembershipPermissions(membership),
plan: membership.organization.plan
? {
name: membership.organization.plan.name,
@@ -747,9 +735,7 @@ export class AuthService {
});
// 4. Format permissions
const permissions = membership.isOwner
? ALL_PERMISSIONS
: membership.permissions.map(p => p.permission.name);
const permissions = this.getMembershipPermissions(membership);
return {
success: true,
@@ -789,6 +775,19 @@ export class AuthService {
return memberships.filter((m) => m.isOwner || m.isActive);
}
private getMembershipPermissions(membership: {
isOwner: boolean;
organization: {
plan?: { name: string; maxUsers: number; price: number } | null;
};
permissions?: Array<{ permission: { name: string } }>;
}): string[] {
if (membership.isOwner) {
return membership.organization.plan ? ALL_PERMISSIONS : READ_ONLY_PERMISSIONS;
}
return membership.permissions?.map((p) => p.permission.name) || [];
}
/**
* Owner-only subscription / seat alerts for the current org (from JWT).
* Used for a subtle warning indicator in the app shell (not staff-facing banners).
@@ -799,6 +798,7 @@ export class AuthService {
success: true,
data: {
showWarning: false,
noActiveSubscription: false,
seatsLow: false,
trialEndingSoon: false,
trialExpired: false,
@@ -822,6 +822,7 @@ export class AuthService {
success: true,
data: {
showWarning: false,
noActiveSubscription: false,
seatsLow: false,
trialEndingSoon: false,
trialExpired: false,
@@ -833,6 +834,24 @@ export class AuthService {
const org = membership.organization;
const plan = org.plan;
if (!plan) {
return {
success: true,
data: {
showWarning: true,
noActiveSubscription: true,
seatsLow: false,
trialEndingSoon: false,
trialExpired: false,
seatsUsed: 0,
seatsLimit: null,
daysUntilTrialEnd: null,
trialEndsAt: null,
daysUntilPlanEnd: null,
planEndsAt: null,
},
};
}
const maxUsers = plan.maxUsers;
const seatsUsed = await this.prisma.membership.count({
where: {
@@ -863,6 +882,7 @@ export class AuthService {
success: true,
data: {
showWarning,
noActiveSubscription: false,
seatsLow,
trialEndingSoon,
trialExpired,

View File

@@ -59,7 +59,7 @@ export class StaffService {
}),
]);
const maxUsers = org.plan.maxUsers;
const maxUsers = org.plan?.maxUsers ?? 0;
const unlimited = isUnlimitedSeats(maxUsers);
return {
@@ -119,6 +119,12 @@ export class StaffService {
throw new NotFoundException('Organization not found');
}
if (!org.plan) {
throw new BadRequestException(
'This organization has no active subscription. Please choose a plan before inviting staff.',
);
}
const maxUsers = org.plan.maxUsers;
const seatsUsed = await tx.membership.count({
where: {
@@ -362,7 +368,10 @@ export class StaffService {
private async getActorMembership(userId: string, organizationId: string) {
return this.prisma.membership.findFirst({
where: { userId, organizationId },
include: { permissions: { include: { permission: true } } },
include: {
permissions: { include: { permission: true } },
organization: { select: { planId: true } },
},
});
}
@@ -424,6 +433,7 @@ export class StaffService {
private canViewStaff(m: {
isOwner: boolean;
organization?: { planId: string | null };
permissions: { permission: { name: string } }[];
}): boolean {
if (m.isOwner) return true;
@@ -435,9 +445,10 @@ export class StaffService {
private canEditStaff(m: {
isOwner: boolean;
organization?: { planId: string | null };
permissions: { permission: { name: string } }[];
}): boolean {
if (m.isOwner) return true;
if (m.isOwner) return Boolean(m.organization?.planId);
return m.permissions.some((p) => p.permission.name === 'TAB_STAFF_EDIT');
}
}