Skip to content

Commit d335bb9

Browse files
authored
Merge pull request #99 from ModusCreateOrg/ADE-66
[ADE-66] fix: Enhance report retrieval and deletion logic; update processing status checks and improve error handling
2 parents 5f5b72b + 848cb64 commit d335bb9

File tree

4 files changed

+95
-25
lines changed

4 files changed

+95
-25
lines changed

backend/src/reports/reports.controller.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
Req,
1010
UnauthorizedException,
1111
Post,
12+
NotFoundException,
1213
} from '@nestjs/common';
1314
import {
1415
ApiTags,
@@ -20,7 +21,7 @@ import {
2021
ApiBody,
2122
} from '@nestjs/swagger';
2223
import { ReportsService } from './reports.service';
23-
import { Report } from './models/report.model';
24+
import { ProcessingStatus, Report } from './models/report.model';
2425
import { GetReportsQueryDto } from './dto/get-reports.dto';
2526
import { UpdateReportStatusDto } from './dto/update-report-status.dto';
2627
import { RequestWithUser } from '../auth/auth.middleware';
@@ -80,7 +81,17 @@ export class ReportsController {
8081
@Get(':id')
8182
async getReport(@Param('id') id: string, @Req() request: RequestWithUser): Promise<Report> {
8283
const userId = this.extractUserId(request);
83-
return this.reportsService.findOne(id, userId);
84+
const report = await this.reportsService.findOne(id, userId);
85+
86+
if (!report) {
87+
throw new NotFoundException('Report not found');
88+
}
89+
90+
if (report.processingStatus === ProcessingStatus.FAILED) {
91+
throw new NotFoundException('Processing failed');
92+
}
93+
94+
return report;
8495
}
8596

8697
@ApiOperation({ summary: 'Update report status' })

backend/src/reports/reports.service.ts

Lines changed: 80 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
DynamoDBServiceException,
1414
PutItemCommand,
1515
QueryCommand,
16+
DeleteItemCommand,
1617
} from '@aws-sdk/client-dynamodb';
1718
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
1819
import { Report, ReportStatus, ProcessingStatus } from './models/report.model';
@@ -54,19 +55,24 @@ export class ReportsService {
5455
this.tableName = this.configService.get<string>('dynamodbReportsTable')!;
5556
}
5657

57-
async findAll(userId: string): Promise<Report[]> {
58+
async findAll(userId: string, withFailed = false): Promise<Report[]> {
5859
if (!userId) {
5960
throw new ForbiddenException('User ID is required');
6061
}
6162

6263
try {
63-
// Use QueryCommand instead of ScanCommand since userId is the partition key
64+
const expressionAttributeValues: any = { ':userId': userId };
65+
const processingStatusFilter = 'processingStatus <> :failedStatus';
66+
67+
if (!withFailed) {
68+
expressionAttributeValues[':failedStatus'] = ProcessingStatus.FAILED;
69+
}
70+
6471
const command = new QueryCommand({
6572
TableName: this.tableName,
6673
KeyConditionExpression: 'userId = :userId',
67-
ExpressionAttributeValues: marshall({
68-
':userId': userId,
69-
}),
74+
FilterExpression: !withFailed ? processingStatusFilter : undefined,
75+
ExpressionAttributeValues: marshall(expressionAttributeValues),
7076
});
7177

7278
const response = await this.dynamoClient.send(command);
@@ -91,7 +97,11 @@ export class ReportsService {
9197
}
9298
}
9399

94-
async findLatest(queryDto: GetReportsQueryDto, userId: string): Promise<Report[]> {
100+
async findLatest(
101+
queryDto: GetReportsQueryDto,
102+
userId: string,
103+
withFailed = false,
104+
): Promise<Report[]> {
95105
this.logger.log(
96106
`Running findLatest with params: ${JSON.stringify(queryDto)} for user ${userId}`,
97107
);
@@ -100,22 +110,26 @@ export class ReportsService {
100110
throw new ForbiddenException('User ID is required');
101111
}
102112

103-
// Convert limit to a number to avoid serialization errors
104113
const limit =
105114
typeof queryDto.limit === 'string' ? parseInt(queryDto.limit, 10) : queryDto.limit || 10;
106115

116+
const expressionAttributeValues: any = { ':userId': userId };
117+
107118
try {
108-
// Use the GSI userIdCreatedAtIndex with QueryCommand for efficient retrieval
109-
// This is much more efficient than a ScanCommand
119+
const processingStatusFilter = 'processingStatus <> :failedStatus';
120+
121+
if (!withFailed) {
122+
expressionAttributeValues[':failedStatus'] = ProcessingStatus.FAILED;
123+
}
124+
110125
const command = new QueryCommand({
111126
TableName: this.tableName,
112-
IndexName: 'userIdCreatedAtIndex', // Use the GSI for efficient queries
127+
IndexName: 'userIdCreatedAtIndex',
113128
KeyConditionExpression: 'userId = :userId',
114-
ExpressionAttributeValues: marshall({
115-
':userId': userId,
116-
}),
117-
ScanIndexForward: false, // Get items in descending order (newest first)
118-
Limit: limit, // Only fetch the number of items we need
129+
FilterExpression: !withFailed ? processingStatusFilter : undefined,
130+
ExpressionAttributeValues: marshall(expressionAttributeValues),
131+
ScanIndexForward: false,
132+
Limit: limit,
119133
});
120134

121135
const response = await this.dynamoClient.send(command);
@@ -130,22 +144,17 @@ export class ReportsService {
130144
`Table "${this.tableName}" not found. Please check your database configuration.`,
131145
);
132146
} else if (error.name === 'ValidationException') {
133-
// This could happen if the GSI doesn't exist
134147
this.logger.warn('GSI validation error, falling back to standard query');
135148

136-
// Fallback to standard query and sort in memory if GSI has issues
137149
const fallbackCommand = new QueryCommand({
138150
TableName: this.tableName,
139151
KeyConditionExpression: 'userId = :userId',
140-
ExpressionAttributeValues: marshall({
141-
':userId': userId,
142-
}),
152+
ExpressionAttributeValues: marshall(expressionAttributeValues),
143153
});
144154

145155
const fallbackResponse = await this.dynamoClient.send(fallbackCommand);
146156
const reports = (fallbackResponse.Items || []).map(item => unmarshall(item) as Report);
147157

148-
// Sort by createdAt in descending order
149158
return reports
150159
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
151160
.slice(0, limit);
@@ -469,4 +478,54 @@ export class ReportsService {
469478
throw new InternalServerErrorException(`Failed to toggle bookmark for report ID ${id}`);
470479
}
471480
}
481+
482+
/**
483+
* Delete a report by ID
484+
* @param reportId Report ID
485+
* @param userId User ID
486+
* @returns A confirmation message
487+
*/
488+
async deleteReport(reportId: string, userId: string): Promise<string> {
489+
if (!reportId) {
490+
throw new NotFoundException('Report ID is required');
491+
}
492+
493+
if (!userId) {
494+
throw new ForbiddenException('User ID is required');
495+
}
496+
497+
try {
498+
const command = new DeleteItemCommand({
499+
TableName: this.tableName,
500+
Key: marshall({
501+
userId,
502+
id: reportId,
503+
}),
504+
ConditionExpression: 'userId = :userId',
505+
ExpressionAttributeValues: marshall({
506+
':userId': userId,
507+
}),
508+
});
509+
510+
await this.dynamoClient.send(command);
511+
this.logger.log(`Successfully deleted report with ID ${reportId} for user ${userId}`);
512+
513+
return `Report with ID ${reportId} successfully deleted`;
514+
} catch (error: unknown) {
515+
this.logger.error(`Error deleting report with ID ${reportId}:`);
516+
this.logger.error(error);
517+
518+
if (error instanceof DynamoDBServiceException) {
519+
if (error.name === 'ConditionalCheckFailedException') {
520+
throw new ForbiddenException('You do not have permission to delete this report');
521+
} else if (error.name === 'ResourceNotFoundException') {
522+
throw new InternalServerErrorException(
523+
`Table "${this.tableName}" not found. Please check your database configuration.`,
524+
);
525+
}
526+
}
527+
528+
throw new InternalServerErrorException(`Failed to delete report with ID ${reportId}`);
529+
}
530+
}
472531
}

backend/src/services/perplexity.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ export class PerplexityService {
158158
'Your goal is to help patients understand their medical reports by translating medical jargon into plain language.\n' +
159159
'You must be accurate, concise, comprehensive, and easy to understand. Use everyday analogies when helpful.\n';
160160

161-
const userPrompt = `Please explain the following medical text in simple terms, in a single paragraph that's between 100 to 500 characters:\n\n${medicalText}`;
161+
const userPrompt = `Please explain the following medical text in simple terms, in a single paragraph that's between 10 to 200 words, all in normal text NOT .md style, the more concise the better:\n\n${medicalText}`;
162162

163163
const messages: PerplexityMessage[] = [
164164
{ role: 'system', content: systemPrompt },

frontend/src/pages/Reports/components/ReportHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const ReportHeader: React.FC<ReportHeaderProps> = ({ reportData, onClose }) => {
2525
{/* Category & Title */}
2626
<div className="report-detail-page__category-wrapper">
2727
<span className="report-detail-page__category">
28-
{reportData.category && t(`list.${reportData.category}Category`, { ns: 'report' })}
28+
{reportData.category && t(`list.${reportData.category}Category`, { ns: 'reportDetail' })}
2929
</span>
3030
<button className="report-detail-page__bookmark-button">
3131
{reportData.bookmarked ? (

0 commit comments

Comments
 (0)