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:
@@ -1,9 +1,8 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
HttpStatus,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { AppException, ErrorCode } from '../../common/errors';
|
||||
import { createReadStream, existsSync } from 'fs';
|
||||
import { CatalogEntityKind, LabCaseActivityType, LabTaskStatus, Prisma, UserNotificationType } from '@prisma/client';
|
||||
import { PrismaService } from '../../../prisma/prisma.service';
|
||||
@@ -103,7 +102,7 @@ export class CasesService {
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -300,7 +299,7 @@ export class CasesService {
|
||||
});
|
||||
|
||||
if (!labCase) {
|
||||
throw new NotFoundException('Case not found');
|
||||
throw new AppException(ErrorCode.CASE_NOT_FOUND, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -327,7 +326,7 @@ export class CasesService {
|
||||
});
|
||||
|
||||
if (!labCase) {
|
||||
throw new NotFoundException('Case not found');
|
||||
throw new AppException(ErrorCode.CASE_NOT_FOUND, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
return { success: true, data: await this.mapLabCaseDetail(labCase, localeInput) };
|
||||
@@ -356,11 +355,11 @@ export class CasesService {
|
||||
});
|
||||
|
||||
if (!link?.attachment) {
|
||||
throw new NotFoundException('Attachment not found');
|
||||
throw new AppException(ErrorCode.CASE_ATTACHMENT_NOT_FOUND, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!existsSync(link.attachment.storagePath)) {
|
||||
throw new NotFoundException('Attachment file is missing on disk');
|
||||
throw new AppException(ErrorCode.CASE_ATTACHMENT_MISSING_FILE, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -389,7 +388,7 @@ export class CasesService {
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
throw new NotFoundException('Case not found');
|
||||
throw new AppException(ErrorCode.CASE_NOT_FOUND, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
await this.prisma.labCase.update({
|
||||
@@ -440,7 +439,7 @@ export class CasesService {
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
throw new NotFoundException('Case not found');
|
||||
throw new AppException(ErrorCode.CASE_NOT_FOUND, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
const externalCode =
|
||||
@@ -510,7 +509,7 @@ export class CasesService {
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
throw new NotFoundException('Task not found');
|
||||
throw new AppException(ErrorCode.CASE_TASK_NOT_FOUND, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
const assigneeUserId = dto.assigneeUserId ?? null;
|
||||
@@ -530,7 +529,7 @@ export class CasesService {
|
||||
|
||||
const canReceive = memberships.some((m) => hasEffectivePermission(m, 'TAB_TASKS_EDIT'));
|
||||
if (!canReceive) {
|
||||
throw new BadRequestException('Selected user cannot be assigned tasks');
|
||||
throw new AppException(ErrorCode.TASK_ASSIGNEE_INVALID, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -577,7 +576,7 @@ export class CasesService {
|
||||
if (query.sentFrom) {
|
||||
const from = new Date(query.sentFrom);
|
||||
if (Number.isNaN(from.getTime())) {
|
||||
throw new BadRequestException('Invalid sentFrom date');
|
||||
throw new AppException(ErrorCode.INVALID_SENT_FROM, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
sentAtFilter.gte = from;
|
||||
}
|
||||
@@ -585,7 +584,7 @@ export class CasesService {
|
||||
if (query.sentTo) {
|
||||
const to = new Date(query.sentTo);
|
||||
if (Number.isNaN(to.getTime())) {
|
||||
throw new BadRequestException('Invalid sentTo date');
|
||||
throw new AppException(ErrorCode.INVALID_SENT_TO, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
to.setHours(23, 59, 59, 999);
|
||||
sentAtFilter.lte = to;
|
||||
@@ -874,27 +873,27 @@ export class CasesService {
|
||||
private async assertCanReadCases(userId: string, organizationId: string) {
|
||||
const m = await this.getMembership(userId, organizationId);
|
||||
if (!m) {
|
||||
throw new ForbiddenException('You are not a member of this organization');
|
||||
throw new AppException(ErrorCode.PERMISSION_NOT_MEMBER, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
if (m.isOwner) return;
|
||||
const names = m.permissions.map((p) => p.permission.name);
|
||||
if (names.includes('TAB_CASES_READ') || names.includes('TAB_CASES_EDIT')) {
|
||||
return;
|
||||
}
|
||||
throw new ForbiddenException('You do not have access to cases');
|
||||
throw new AppException(ErrorCode.PERMISSION_ACCESS_CASES, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
private async assertCanEditCases(userId: string, organizationId: string) {
|
||||
const m = await this.getMembership(userId, organizationId);
|
||||
if (!m) {
|
||||
throw new ForbiddenException('You are not a member of this organization');
|
||||
throw new AppException(ErrorCode.PERMISSION_NOT_MEMBER, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
if (m.isOwner) return;
|
||||
const names = m.permissions.map((p) => p.permission.name);
|
||||
if (names.includes('TAB_CASES_EDIT')) {
|
||||
return;
|
||||
}
|
||||
throw new ForbiddenException('You cannot update cases');
|
||||
throw new AppException(ErrorCode.PERMISSION_ACCESS_CASES, HttpStatus.FORBIDDEN);
|
||||
}
|
||||
|
||||
private async getMembership(userId: string, organizationId: string) {
|
||||
|
||||
Reference in New Issue
Block a user