bugfix: appointment hours now use the client timezone on UTC servers.
Logical API errors throw stable codes so users see translated messages instead of a generic bad request. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,10 +5,14 @@ import {
|
||||
IsBoolean,
|
||||
IsInt,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Matches,
|
||||
Max,
|
||||
MaxLength,
|
||||
Min,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { ErrorCode } from '../../../common/errors';
|
||||
|
||||
export class WorkingHoursBlockDto {
|
||||
@IsInt()
|
||||
@@ -41,4 +45,9 @@ export class UpsertWorkingHoursDto {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => WorkingHoursBlockDto)
|
||||
blocks: WorkingHoursBlockDto[];
|
||||
|
||||
@IsString()
|
||||
@MaxLength(64)
|
||||
@Matches(/^[A-Za-z0-9_+\-/]+$/, { message: ErrorCode.VALIDATION_TIMEZONE_INVALID })
|
||||
timeZone: string;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../../../prisma/prisma.service';
|
||||
import {
|
||||
appointmentWithinWorkingHours,
|
||||
@@ -12,10 +7,12 @@ import {
|
||||
validateWorkingHoursBlocks,
|
||||
type WorkingHoursBlockInput,
|
||||
} from '../../common/working-hours';
|
||||
import { isValidIanaTimeZone, zonedWeekdayAndMinutes } from '../../common/zoned-civil-time';
|
||||
import { UpsertWorkingHoursDto } from './dto/upsert-working-hours.dto';
|
||||
import {
|
||||
hasEffectivePermission,
|
||||
} from '../../common/membership-permissions';
|
||||
import { AppException, ErrorCode } from '../../common/errors';
|
||||
|
||||
@Injectable()
|
||||
export class StaffWorkingHoursService {
|
||||
@@ -124,14 +121,16 @@ export class StaffWorkingHoursService {
|
||||
) {
|
||||
const validationError = validateWorkingHoursBlocks(dto.blocks);
|
||||
if (validationError) {
|
||||
throw new BadRequestException(validationError);
|
||||
throw new AppException(ErrorCode.WORKING_HOURS_INVALID, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const timeZone = this.requireTimeZone(dto.timeZone);
|
||||
const normalizedBlocks = this.normalizeBlocks(dto.blocks);
|
||||
await this.assertNoConflictingAppointments(
|
||||
organizationId,
|
||||
membership.userId,
|
||||
normalizedBlocks,
|
||||
timeZone,
|
||||
);
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
@@ -207,10 +206,18 @@ export class StaffWorkingHoursService {
|
||||
}));
|
||||
}
|
||||
|
||||
private requireTimeZone(timeZone: string | undefined): string {
|
||||
if (!timeZone || !isValidIanaTimeZone(timeZone)) {
|
||||
throw new AppException(ErrorCode.VALIDATION_TIMEZONE_INVALID, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
return timeZone;
|
||||
}
|
||||
|
||||
private async assertNoConflictingAppointments(
|
||||
organizationId: string,
|
||||
providerUserId: string,
|
||||
blocks: WorkingHoursBlockInput[],
|
||||
timeZone: string,
|
||||
) {
|
||||
const now = new Date();
|
||||
const appointments = await this.prisma.appointment.findMany({
|
||||
@@ -219,47 +226,30 @@ export class StaffWorkingHoursService {
|
||||
providerUserId,
|
||||
endAt: { gt: now },
|
||||
},
|
||||
include: {
|
||||
patient: { select: { firstName: true, lastName: true } },
|
||||
},
|
||||
select: { startAt: true, endAt: true },
|
||||
orderBy: { startAt: 'asc' },
|
||||
});
|
||||
|
||||
const conflicts = appointments.filter((appointment) => {
|
||||
const startAt = new Date(appointment.startAt);
|
||||
const endAt = new Date(appointment.endAt);
|
||||
const dayOfWeek = localDayOfWeekMondayZero(startAt.getDay());
|
||||
const dayOfWeek = localDayOfWeekMondayZero(
|
||||
zonedWeekdayAndMinutes(startAt, timeZone).jsWeekday,
|
||||
);
|
||||
const dayBlocks = blocksForDay(blocks, dayOfWeek);
|
||||
if (dayBlocks.length === 0) {
|
||||
return true;
|
||||
}
|
||||
return !appointmentWithinWorkingHours(startAt, endAt, dayBlocks);
|
||||
return !appointmentWithinWorkingHours(startAt, endAt, dayBlocks, timeZone);
|
||||
});
|
||||
|
||||
if (conflicts.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const examples = conflicts.slice(0, 3).map((appointment) => {
|
||||
const startAt = new Date(appointment.startAt);
|
||||
const patientName = `${appointment.patient.firstName} ${appointment.patient.lastName}`;
|
||||
const when = startAt.toLocaleString(undefined, {
|
||||
weekday: 'short',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
});
|
||||
return `${patientName} (${when})`;
|
||||
});
|
||||
|
||||
const extra =
|
||||
conflicts.length > examples.length
|
||||
? ` and ${conflicts.length - examples.length} more`
|
||||
: '';
|
||||
|
||||
throw new BadRequestException(
|
||||
`Cannot save working hours: ${conflicts.length} upcoming appointment${conflicts.length === 1 ? '' : 's'} fall outside the new schedule (${examples.join(', ')}${extra}). Reschedule or remove those appointments first.`,
|
||||
throw new AppException(
|
||||
ErrorCode.WORKING_HOURS_CONFLICTS_WITH_APPOINTMENTS,
|
||||
HttpStatus.CONFLICT,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -269,10 +259,10 @@ export class StaffWorkingHoursService {
|
||||
select: { id: true, isOwner: true, userId: true },
|
||||
});
|
||||
if (!membership) {
|
||||
throw new NotFoundException('Member not found');
|
||||
throw new AppException(ErrorCode.STAFF_MEMBER_NOT_FOUND, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
if (membership.isOwner) {
|
||||
throw new BadRequestException('Working hours cannot be set for the organization owner');
|
||||
throw new AppException(ErrorCode.WORKING_HOURS_OWNER_NOT_ALLOWED, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
return membership;
|
||||
}
|
||||
@@ -286,7 +276,7 @@ export class StaffWorkingHoursService {
|
||||
},
|
||||
});
|
||||
if (!membership) {
|
||||
throw new ForbiddenException('Only organization owners can manage their working hours');
|
||||
throw new AppException(ErrorCode.PERMISSION_OWNER_ONLY, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
return membership;
|
||||
}
|
||||
@@ -297,23 +287,21 @@ export class StaffWorkingHoursService {
|
||||
organization: { type: { name: string }; plan?: { name: string } | null; planId?: string | null };
|
||||
}) {
|
||||
if (!hasEffectivePermission(membership, 'TAB_TREATMENT_EDIT')) {
|
||||
throw new ForbiddenException(
|
||||
'Enable treatment participation before setting working hours',
|
||||
);
|
||||
throw new AppException(ErrorCode.PERMISSION_ENABLE_PARTICIPATION_FIRST, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
private async assertCanViewStaff(userId: string, organizationId: string) {
|
||||
const actor = await this.getActorMembership(userId, organizationId);
|
||||
if (!actor || !this.canViewStaff(actor)) {
|
||||
throw new ForbiddenException('You do not have access to staff management');
|
||||
throw new AppException(ErrorCode.PERMISSION_ACCESS_STAFF, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
private async assertCanEditStaff(userId: string, organizationId: string) {
|
||||
const actor = await this.getActorMembership(userId, organizationId);
|
||||
if (!actor || !this.canEditStaff(actor)) {
|
||||
throw new ForbiddenException('You cannot manage staff working hours');
|
||||
throw new AppException(ErrorCode.PERMISSION_EDIT_STAFF, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
ForbiddenException,
|
||||
HttpStatus,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { AppException, ErrorCode } from '../../common/errors';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { createHash, randomBytes } from 'crypto';
|
||||
import { Prisma, UserNotificationType } from '@prisma/client';
|
||||
@@ -28,7 +26,7 @@ export class StaffService {
|
||||
|
||||
getOrganizationIdFromUser(user: { organizationId?: string }) {
|
||||
if (!user?.organizationId) {
|
||||
throw new BadRequestException('Organization is not selected');
|
||||
throw new AppException(ErrorCode.AUTH_ORG_NOT_SELECTED, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
return user.organizationId;
|
||||
}
|
||||
@@ -36,7 +34,7 @@ export class StaffService {
|
||||
async list(userId: string, organizationId: string) {
|
||||
const actor = await this.getActorMembership(userId, organizationId);
|
||||
if (!actor || !this.canViewStaff(actor)) {
|
||||
throw new ForbiddenException('You do not have access to staff management');
|
||||
throw new AppException(ErrorCode.PERMISSION_ACCESS_STAFF, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
const org = await this.prisma.organization.findUnique({
|
||||
@@ -44,7 +42,7 @@ export class StaffService {
|
||||
include: { plan: true },
|
||||
});
|
||||
if (!org) {
|
||||
throw new NotFoundException('Organization not found');
|
||||
throw new AppException(ErrorCode.PERMISSION_ORG_NOT_FOUND, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
const [members, seatsUsed] = await Promise.all([
|
||||
@@ -100,7 +98,7 @@ export class StaffService {
|
||||
async invite(userId: string, organizationId: string, dto: InviteStaffDto) {
|
||||
const actor = await this.getActorMembership(userId, organizationId);
|
||||
if (!actor || !this.canEditStaff(actor)) {
|
||||
throw new ForbiddenException('You cannot invite or manage staff');
|
||||
throw new AppException(ErrorCode.PERMISSION_EDIT_STAFF, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
const email = dto.email.trim().toLowerCase();
|
||||
@@ -114,7 +112,7 @@ export class StaffService {
|
||||
if (permissionRows.length !== normalizedPerms.length) {
|
||||
const ok = new Set(permissionRows.map((p) => p.name));
|
||||
const missing = normalizedPerms.filter((n) => !ok.has(n));
|
||||
throw new BadRequestException(`Unknown or invalid permissions: ${missing.join(', ')}`);
|
||||
throw new AppException(ErrorCode.STAFF_UNKNOWN_PERMISSIONS, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const plainToken = this.generateInviteToken();
|
||||
@@ -126,13 +124,11 @@ export class StaffService {
|
||||
include: { plan: true },
|
||||
});
|
||||
if (!org) {
|
||||
throw new NotFoundException('Organization not found');
|
||||
throw new AppException(ErrorCode.PERMISSION_ORG_NOT_FOUND, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!org.plan) {
|
||||
throw new BadRequestException(
|
||||
'This organization has no active subscription. Please choose a plan before inviting staff.',
|
||||
);
|
||||
throw new AppException(ErrorCode.STAFF_NO_SUBSCRIPTION, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const maxUsers = org.plan.maxUsers;
|
||||
@@ -143,9 +139,7 @@ export class StaffService {
|
||||
},
|
||||
});
|
||||
if (!isUnlimitedSeats(maxUsers) && seatsUsed >= maxUsers) {
|
||||
throw new BadRequestException(
|
||||
`Your plan allows ${maxUsers} team members. Remove a member or upgrade to add more.`,
|
||||
);
|
||||
throw new AppException(ErrorCode.STAFF_SEAT_LIMIT, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const existingUser = await tx.user.findUnique({ where: { email } });
|
||||
@@ -153,7 +147,7 @@ export class StaffService {
|
||||
|
||||
if (existingUser) {
|
||||
if (existingUser.id === org.ownerId) {
|
||||
throw new BadRequestException('Organization owner is already a member');
|
||||
throw new AppException(ErrorCode.STAFF_INVITE_OWNER_EMAIL, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
const dup = await tx.membership.findUnique({
|
||||
where: {
|
||||
@@ -164,7 +158,7 @@ export class StaffService {
|
||||
},
|
||||
});
|
||||
if (dup) {
|
||||
throw new ConflictException('This user is already a member of this organization');
|
||||
throw new AppException(ErrorCode.STAFF_ALREADY_MEMBER, HttpStatus.CONFLICT);
|
||||
}
|
||||
targetUserId = existingUser.id;
|
||||
} else {
|
||||
@@ -250,7 +244,7 @@ export class StaffService {
|
||||
async getInvitationLink(userId: string, organizationId: string, membershipId: string) {
|
||||
const actor = await this.getActorMembership(userId, organizationId);
|
||||
if (!actor || !this.canEditStaff(actor)) {
|
||||
throw new ForbiddenException('You cannot invite or manage staff');
|
||||
throw new AppException(ErrorCode.PERMISSION_EDIT_STAFF, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
const membership = await this.prisma.membership.findFirst({
|
||||
@@ -262,24 +256,24 @@ export class StaffService {
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
throw new NotFoundException('Member not found');
|
||||
throw new AppException(ErrorCode.STAFF_MEMBER_NOT_FOUND, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
if (membership.isOwner) {
|
||||
throw new BadRequestException('Owner does not use an invitation link');
|
||||
throw new AppException(ErrorCode.STAFF_OWNER_NO_INVITE_LINK, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
if (membership.isActive) {
|
||||
throw new BadRequestException('This member has already accepted their invitation');
|
||||
throw new AppException(ErrorCode.STAFF_ALREADY_ACCEPTED, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const invitation = membership.invitations[0];
|
||||
if (!invitation) {
|
||||
throw new BadRequestException('No invitation found for this member');
|
||||
throw new AppException(ErrorCode.STAFF_INVITE_MISSING, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
if (invitation.acceptedAt) {
|
||||
throw new BadRequestException('This invitation has already been accepted');
|
||||
throw new AppException(ErrorCode.STAFF_INVITE_ALREADY_ACCEPTED, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
if (invitation.revokedAt) {
|
||||
throw new BadRequestException('This invitation is no longer valid');
|
||||
throw new AppException(ErrorCode.STAFF_INVITE_INVALID, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const plainToken = this.generateInviteToken();
|
||||
@@ -324,7 +318,7 @@ export class StaffService {
|
||||
const invitation = await this.findValidInvitation(dto.token);
|
||||
|
||||
if (invitation.acceptedAt) {
|
||||
throw new BadRequestException('This invitation has already been accepted');
|
||||
throw new AppException(ErrorCode.STAFF_INVITE_ALREADY_ACCEPTED, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const passwordHash = await bcrypt.hash(dto.password, 10);
|
||||
@@ -367,7 +361,7 @@ export class StaffService {
|
||||
) {
|
||||
const actor = await this.getActorMembership(actorUserId, organizationId);
|
||||
if (!actor || !this.canEditStaff(actor)) {
|
||||
throw new ForbiddenException('You cannot edit staff');
|
||||
throw new AppException(ErrorCode.PERMISSION_EDIT_STAFF, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
const target = await this.prisma.membership.findFirst({
|
||||
@@ -379,10 +373,10 @@ export class StaffService {
|
||||
});
|
||||
|
||||
if (!target) {
|
||||
throw new NotFoundException('Member not found');
|
||||
throw new AppException(ErrorCode.STAFF_MEMBER_NOT_FOUND, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
if (target.isOwner) {
|
||||
throw new ForbiddenException('Owner membership cannot be edited here');
|
||||
throw new AppException(ErrorCode.STAFF_CANNOT_EDIT_OWNER, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
if (dto.name !== undefined) {
|
||||
@@ -402,7 +396,7 @@ export class StaffService {
|
||||
if (permissionRows.length !== normalizedPerms.length) {
|
||||
const ok = new Set(permissionRows.map((p) => p.name));
|
||||
const missing = normalizedPerms.filter((n) => !ok.has(n));
|
||||
throw new BadRequestException(`Unknown or invalid permissions: ${missing.join(', ')}`);
|
||||
throw new AppException(ErrorCode.STAFF_UNKNOWN_PERMISSIONS, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
await this.prisma.$transaction([
|
||||
@@ -426,7 +420,7 @@ export class StaffService {
|
||||
async enableMember(actorUserId: string, organizationId: string, membershipId: string) {
|
||||
const actor = await this.getActorMembership(actorUserId, organizationId);
|
||||
if (!actor || !this.canEditStaff(actor)) {
|
||||
throw new ForbiddenException('You cannot manage staff');
|
||||
throw new AppException(ErrorCode.PERMISSION_EDIT_STAFF, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
const target = await this.prisma.membership.findFirst({
|
||||
@@ -437,20 +431,18 @@ export class StaffService {
|
||||
});
|
||||
|
||||
if (!target) {
|
||||
throw new NotFoundException('Member not found');
|
||||
throw new AppException(ErrorCode.STAFF_MEMBER_NOT_FOUND, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
if (target.isOwner) {
|
||||
throw new ForbiddenException('Cannot enable the organization owner');
|
||||
throw new AppException(ErrorCode.STAFF_CANNOT_ENABLE_OWNER, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
if (target.isActive) {
|
||||
throw new BadRequestException('This member is already active');
|
||||
throw new AppException(ErrorCode.STAFF_ALREADY_ACTIVE, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const invitation = target.invitations[0];
|
||||
if (invitation && !invitation.acceptedAt) {
|
||||
throw new BadRequestException(
|
||||
'This member has not completed their invitation yet. Share the invite link instead.',
|
||||
);
|
||||
throw new AppException(ErrorCode.STAFF_INVITE_MISSING, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
@@ -470,7 +462,7 @@ export class StaffService {
|
||||
async disableMember(actorUserId: string, organizationId: string, membershipId: string) {
|
||||
const actor = await this.getActorMembership(actorUserId, organizationId);
|
||||
if (!actor || !this.canEditStaff(actor)) {
|
||||
throw new ForbiddenException('You cannot manage staff');
|
||||
throw new AppException(ErrorCode.PERMISSION_EDIT_STAFF, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
const target = await this.prisma.membership.findFirst({
|
||||
@@ -478,16 +470,16 @@ export class StaffService {
|
||||
});
|
||||
|
||||
if (!target) {
|
||||
throw new NotFoundException('Member not found');
|
||||
throw new AppException(ErrorCode.STAFF_MEMBER_NOT_FOUND, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
if (target.isOwner) {
|
||||
throw new ForbiddenException('Cannot disable the organization owner');
|
||||
throw new AppException(ErrorCode.STAFF_CANNOT_DISABLE_OWNER, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
if (actorUserId === target.userId) {
|
||||
throw new BadRequestException('You cannot disable your own access');
|
||||
throw new AppException(ErrorCode.STAFF_CANNOT_DISABLE_SELF, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
if (!target.isActive) {
|
||||
throw new BadRequestException('This member is already disabled or pending activation');
|
||||
throw new AppException(ErrorCode.STAFF_ALREADY_DISABLED, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
await this.prisma.membership.update({
|
||||
@@ -508,7 +500,7 @@ export class StaffService {
|
||||
async removeMember(actorUserId: string, organizationId: string, membershipId: string) {
|
||||
const actor = await this.getActorMembership(actorUserId, organizationId);
|
||||
if (!actor || !this.canEditStaff(actor)) {
|
||||
throw new ForbiddenException('You cannot remove staff');
|
||||
throw new AppException(ErrorCode.PERMISSION_EDIT_STAFF, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
const target = await this.prisma.membership.findFirst({
|
||||
@@ -516,10 +508,10 @@ export class StaffService {
|
||||
});
|
||||
|
||||
if (!target) {
|
||||
throw new NotFoundException('Member not found');
|
||||
throw new AppException(ErrorCode.STAFF_MEMBER_NOT_FOUND, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
if (target.isOwner) {
|
||||
throw new ForbiddenException('Cannot remove the organization owner');
|
||||
throw new AppException(ErrorCode.STAFF_CANNOT_REMOVE_OWNER, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
await this.prisma.membership.delete({ where: { id: membershipId } });
|
||||
@@ -536,12 +528,10 @@ export class StaffService {
|
||||
include: { plan: true },
|
||||
});
|
||||
if (!org) {
|
||||
throw new NotFoundException('Organization not found');
|
||||
throw new AppException(ErrorCode.PERMISSION_ORG_NOT_FOUND, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
if (!org.plan) {
|
||||
throw new BadRequestException(
|
||||
'This organization has no active subscription. Please choose a plan before adding staff.',
|
||||
);
|
||||
throw new AppException(ErrorCode.STAFF_NO_SUBSCRIPTION, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const maxUsers = org.plan.maxUsers;
|
||||
@@ -552,9 +542,7 @@ export class StaffService {
|
||||
},
|
||||
});
|
||||
if (!isUnlimitedSeats(maxUsers) && seatsUsed >= maxUsers) {
|
||||
throw new BadRequestException(
|
||||
`Your plan allows ${maxUsers} team members. Free a seat by disabling another member or upgrade your plan.`,
|
||||
);
|
||||
throw new AppException(ErrorCode.STAFF_SEAT_LIMIT, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -615,13 +603,13 @@ export class StaffService {
|
||||
});
|
||||
|
||||
if (!invitation) {
|
||||
throw new NotFoundException('Invitation not found');
|
||||
throw new AppException(ErrorCode.STAFF_INVITE_MISSING, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
if (invitation.revokedAt) {
|
||||
throw new BadRequestException('Invitation has been revoked');
|
||||
throw new AppException(ErrorCode.STAFF_INVITE_REVOKED, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
if (invitation.expiresAt.getTime() <= Date.now()) {
|
||||
throw new BadRequestException('Invitation has expired');
|
||||
throw new AppException(ErrorCode.STAFF_INVITE_EXPIRED, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
return invitation;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user