improvement: newly created organization's subscriptions flow improved.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user