1
0
forked from Yara724/api

Fix searching on GET APIs

This commit is contained in:
SepehrYahyaee
2026-06-23 10:22:46 +03:30
parent 16d3c54613
commit c6b417ced7
9 changed files with 223 additions and 45 deletions

View File

@@ -9,9 +9,10 @@ import {
MaxLength,
Min,
} from "class-validator";
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
import { UNIFIED_FILE_STATUS_KEYS } from "src/helpers/unified-file-status";
/** Allowed sort keys for V2 list endpoints (claims / blame). */
/** Allowed sort keys for V2 list endpoints (claims / blame / insurer files). */
export const LIST_SORT_BY_V2 = [
"publicId",
"createdAt",
@@ -21,9 +22,16 @@ export const LIST_SORT_BY_V2 = [
export type ListSortByV2 = (typeof LIST_SORT_BY_V2)[number];
export const LIST_FILE_TYPE_V2 = [
BlameRequestType.THIRD_PARTY,
BlameRequestType.CAR_BODY,
] as const;
export type ListFileTypeV2 = (typeof LIST_FILE_TYPE_V2)[number];
/**
* Optional query params for V2 lists. If neither `page` nor `limit` is sent,
* the full filtered+sorted list is returned (backward compatible).
* Optional query params for V2 expert/insurer file lists.
* If neither `page` nor `limit` is sent, the full filtered+sorted list is returned.
*/
export class ListQueryV2Dto {
@ApiPropertyOptional({
@@ -68,7 +76,7 @@ export class ListQueryV2Dto {
@ApiPropertyOptional({
description:
"Case-insensitive substring match on `publicId` and `requestNo` (and other endpoint-specific fields).",
"Case-insensitive substring match on `publicId`, `requestNo`, status, and other endpoint-specific fields.",
example: "A142",
maxLength: 64,
})
@@ -85,4 +93,13 @@ export class ListQueryV2Dto {
@IsOptional()
@IsIn([...UNIFIED_FILE_STATUS_KEYS])
unifiedStatus?: (typeof UNIFIED_FILE_STATUS_KEYS)[number];
@ApiPropertyOptional({
enum: LIST_FILE_TYPE_V2,
description:
"Filter by blame file type: third-party liability (`THIRD_PARTY`) or car body (`CAR_BODY`).",
})
@IsOptional()
@IsIn([...LIST_FILE_TYPE_V2])
fileType?: ListFileTypeV2;
}

View File

@@ -1,10 +1,42 @@
import { ApiProperty } from "@nestjs/swagger";
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsIn, IsISO8601, IsOptional } from "class-validator";
import {
LIST_FILE_TYPE_V2,
ListFileTypeV2,
} from "src/common/dto/list-query-v2.dto";
import {
UNIFIED_FILE_STATUS_KEYS,
UnifiedFileStatusDefinition,
UnifiedFileStatusKey,
} from "src/helpers/unified-file-status";
export class UnifiedFileStatusReportQueryDto {
@ApiPropertyOptional({
description: "Optional start datetime (ISO 8601) for portfolio date filter.",
example: "2026-01-01T00:00:00.000Z",
})
@IsOptional()
@IsISO8601({ strict: false })
from?: string;
@ApiPropertyOptional({
description: "Optional end datetime (ISO 8601) for portfolio date filter.",
example: "2026-12-31T23:59:59.999Z",
})
@IsOptional()
@IsISO8601({ strict: false })
to?: string;
@ApiPropertyOptional({
enum: LIST_FILE_TYPE_V2,
description:
"Count only files of this blame type (`THIRD_PARTY` or `CAR_BODY`).",
})
@IsOptional()
@IsIn([...LIST_FILE_TYPE_V2])
fileType?: ListFileTypeV2;
}
export class UnifiedFileStatusDefinitionDto implements UnifiedFileStatusDefinition {
@ApiProperty({ enum: UNIFIED_FILE_STATUS_KEYS })
key: UnifiedFileStatusKey;

View File

@@ -28,9 +28,13 @@ import {
getUnifiedFileStatusCatalog,
resolveUnifiedFileStatus,
} from "src/helpers/unified-file-status";
import { applyListQueryV2 } from "src/helpers/list-query-v2";
import { applyListQueryV2, isInListDateRange, parseListDateRange } from "src/helpers/list-query-v2";
import { ClaimCaseDbService } from "src/claim-request-management/entites/db-service/claim-case.db.service";
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
import {
UnifiedFileStatusReportDto,
UnifiedFileStatusReportQueryDto,
} from "src/common/dto/unified-file-status-report.dto";
import {
ExpertFileAssignResultDto,
ExpertFileAssignStatus,
@@ -156,8 +160,25 @@ export class ExpertBlameService {
});
}
async getUnifiedFileStatusReportV2(actor: any) {
const rows = await this.collectBlamePortfolioDocs(actor);
async getUnifiedFileStatusReportV2(
actor: any,
query: UnifiedFileStatusReportQueryDto = {},
): Promise<UnifiedFileStatusReportDto> {
let rows = await this.collectBlamePortfolioDocs(actor);
const { fromDate, toDate } = parseListDateRange(query.from, query.to);
rows = rows.filter((doc) =>
isInListDateRange(
(doc as { createdAt?: Date }).createdAt,
fromDate,
toDate,
),
);
if (query.fileType) {
rows = rows.filter(
(doc) =>
String((doc as { type?: string }).type ?? "") === query.fileType,
);
}
const claimByPublicId = await this.loadClaimsByPublicIds(
rows.map((d) => String((d as { publicId?: string }).publicId ?? "")),
);
@@ -590,6 +611,8 @@ export class ExpertBlameService {
claimStatus: claim?.status,
});
},
fileType: (doc) =>
String((doc as { type?: string }).type ?? "") || undefined,
searchExtras: (doc) => {
const d = doc as {
_id?: unknown;

View File

@@ -26,6 +26,10 @@ import { CurrentUser } from "src/decorators/user.decorator";
import { RoleEnum } from "src/Types&Enums/role.enum";
import { ExpertBlameService } from "./expert-blame.service";
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
import {
UnifiedFileStatusReportDto,
UnifiedFileStatusReportQueryDto,
} from "src/common/dto/unified-file-status-report.dto";
import { AllRequestDtoRsV2 } from "./dto/all-request.dto";
import { SubmitReplyDto } from "./dto/reply.dto";
import { ResendRequestDto } from "./dto/resend.dto";
@@ -44,8 +48,9 @@ export class ExpertBlameV2Controller {
description:
"Damage experts (`expert`): tenant-scoped **DISAGREEMENT** queue (available, locked, or decided by you). " +
"Field experts (`field_expert`): all expert-initiated blame files they created (LINK + IN_PERSON). Use expert-claim for claims after blame completion. " +
"Optional query: `search`, `sortBy` (publicId | createdAt | requestNo | status), `sortOrder`, `page`, `limit`, `unifiedStatus` (calculated blame+claim status).",
"Optional query: `search`, `sortBy` (publicId | createdAt | requestNo | status), `sortOrder`, `page`, `limit`, `unifiedStatus`, `fileType` (THIRD_PARTY | CAR_BODY).",
})
@ApiResponse({ status: 200, type: AllRequestDtoRsV2 })
async findAll(
@CurrentUser() actor: any,
@Query() query: ListQueryV2Dto,
@@ -64,11 +69,18 @@ export class ExpertBlameV2Controller {
@ApiOperation({
summary: "Unified file status catalog + counts (blame + linked claim)",
description:
"Calculatable statuses for this expert's blame portfolio. Filter lists with `unifiedStatus` query param.",
"Calculatable statuses for this expert's blame portfolio. Filter lists with `unifiedStatus` and `fileType` query params. Optional `from` / `to` ISO dates narrow the counted portfolio.",
})
async getUnifiedFileStatusReportV2(@CurrentUser() actor: any) {
@ApiResponse({ status: 200, type: UnifiedFileStatusReportDto })
async getUnifiedFileStatusReportV2(
@CurrentUser() actor: any,
@Query() query: UnifiedFileStatusReportQueryDto,
): Promise<UnifiedFileStatusReportDto> {
try {
return await this.expertBlameService.getUnifiedFileStatusReportV2(actor);
return await this.expertBlameService.getUnifiedFileStatusReportV2(
actor,
query,
);
} catch (error) {
if (error instanceof HttpException) throw error;
throw new InternalServerErrorException(

View File

@@ -141,7 +141,15 @@ import {
/** Maximum sum of line `totalPayment` across the claim (Toman; priced parts + factor lines after validation). */
import { getClaimV2TotalPaymentCapToman } from "src/constants/repair-amount-limits";
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
import { applyListQueryV2 } from "src/helpers/list-query-v2";
import {
UnifiedFileStatusReportDto,
UnifiedFileStatusReportQueryDto,
} from "src/common/dto/unified-file-status-report.dto";
import {
applyListQueryV2,
isInListDateRange,
parseListDateRange,
} from "src/helpers/list-query-v2";
import {
countUnifiedFileStatuses,
getUnifiedFileStatusCatalog,
@@ -3416,13 +3424,37 @@ export class ExpertClaimService {
return buckets;
}
async getUnifiedFileStatusReportV2(actor: any) {
async getUnifiedFileStatusReportV2(
actor: any,
query: UnifiedFileStatusReportQueryDto = {},
): Promise<UnifiedFileStatusReportDto> {
const { claims, blameById } =
await this.collectClaimPortfolioWithBlame(actor);
const { fromDate, toDate } = parseListDateRange(query.from, query.to);
const filteredClaims = claims.filter((c) => {
if (
!isInListDateRange(
c.createdAt as Date | string | undefined,
fromDate,
toDate,
)
) {
return false;
}
if (!query.fileType) return true;
const blame = c.blameRequestId
? blameById.get(String(c.blameRequestId))
: undefined;
const type =
blame?.type ??
(c as { snapshot?: { accident?: { type?: string } } })?.snapshot
?.accident?.type;
return type === query.fileType;
});
return {
statuses: getUnifiedFileStatusCatalog(),
counts: countUnifiedFileStatuses(
claims.map((c) => {
filteredClaims.map((c) => {
const blame = c.blameRequestId
? blameById.get(String(c.blameRequestId))
: undefined;
@@ -3532,6 +3564,7 @@ export class ExpertClaimService {
createdAt: (r) => r.createdAt,
requestNo: (r) => r.publicId,
status: (r) => r.unifiedFileStatus ?? r.status,
fileType: (r) => r.blameRequestType,
searchExtras: (r) =>
[
r.claimRequestId,

View File

@@ -31,6 +31,10 @@ import { RoleEnum } from "src/Types&Enums/role.enum";
import { ExpertFileAssignResultDto } from "src/common/dto/expert-file-assign-result.dto";
import { ExpertClaimService } from "./expert-claim.service";
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
import {
UnifiedFileStatusReportDto,
UnifiedFileStatusReportQueryDto,
} from "src/common/dto/unified-file-status-report.dto";
import { GetClaimListV2ResponseDto } from "./dto/claim-list-v2.dto";
import {
ClaimSubmitResendV2Dto,
@@ -69,10 +73,17 @@ export class ExpertClaimV2Controller {
@ApiOperation({
summary: "Unified file status catalog + counts (claim + linked blame)",
description:
"Calculatable statuses for this expert's claim portfolio. Filter lists with `unifiedStatus` query param.",
"Calculatable statuses for this expert's claim portfolio. Filter lists with `unifiedStatus` and `fileType` query params. Optional `from` / `to` ISO dates narrow the counted portfolio.",
})
async getUnifiedFileStatusReportV2(@CurrentUser() actor: any) {
return await this.expertClaimService.getUnifiedFileStatusReportV2(actor);
@ApiResponse({ status: 200, type: UnifiedFileStatusReportDto })
async getUnifiedFileStatusReportV2(
@CurrentUser() actor: any,
@Query() query: UnifiedFileStatusReportQueryDto,
): Promise<UnifiedFileStatusReportDto> {
return await this.expertClaimService.getUnifiedFileStatusReportV2(
actor,
query,
);
}
@Get("report/status-counts")
@@ -127,7 +138,8 @@ export class ExpertClaimV2Controller {
summary: "List available claim requests for damage expert",
description:
"Damage experts: tenant queue (`WAITING_FOR_DAMAGE_EXPERT`), locked/in-progress, factor validation. " +
"Field experts: all claims linked to blame files they initiated (`initiatedByFieldExpertId` or matching `blameRequestId`). Optional query: `search`, `sortBy`, `sortOrder`, `page`, `limit`, `unifiedStatus`.",
"Field experts: all claims linked to blame files they initiated (`initiatedByFieldExpertId` or matching `blameRequestId`). " +
"Optional query: `search`, `sortBy`, `sortOrder`, `page`, `limit`, `unifiedStatus`, `fileType` (THIRD_PARTY | CAR_BODY).",
})
@ApiResponse({ status: 200, type: GetClaimListV2ResponseDto })
async getClaimListV2(

View File

@@ -16,6 +16,7 @@ import {
ApiOperation,
ApiParam,
ApiQuery,
ApiResponse,
ApiTags,
} from "@nestjs/swagger";
import { Types } from "mongoose";
@@ -27,6 +28,10 @@ import { FileRating } from "src/request-management/entities/schema/request-manag
import { RoleEnum } from "src/Types&Enums/role.enum";
import { ExpertInsurerService } from "./expert-insurer.service";
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
import {
UnifiedFileStatusReportDto,
UnifiedFileStatusReportQueryDto,
} from "src/common/dto/unified-file-status-report.dto";
import { CreateBranchDto } from "src/client/dto/create-branch.dto";
import {
CreateBlameExpertByInsurerDto,
@@ -166,7 +171,7 @@ export class ExpertInsurerController {
@ApiOperation({
summary: "List insurer files (blame + claim merged by publicId)",
description:
"Optional query: `search`, `sortBy` (publicId | createdAt | requestNo | status), `sortOrder`, `page`, `limit`, `unifiedStatus`.",
"Optional query: `search`, `sortBy` (publicId | createdAt | requestNo | status), `sortOrder`, `page`, `limit`, `unifiedStatus`, `fileType` (THIRD_PARTY | CAR_BODY).",
})
async getAllFiles(
@CurrentUser() insurer,
@@ -182,27 +187,16 @@ export class ExpertInsurerController {
@ApiOperation({
summary: "Unified file status catalog + counts (blame + claim)",
description:
"Returns calculatable unified statuses and per-status counts for this insurer's files. Use `unifiedStatus` on GET files to filter.",
})
@ApiQuery({
name: "from",
required: false,
description: "Optional start datetime (ISO string)",
})
@ApiQuery({
name: "to",
required: false,
description: "Optional end datetime (ISO string)",
"Calculatable unified statuses and per-status counts for this insurer's files. Filter lists with `unifiedStatus` and `fileType`. Optional `from` / `to` ISO dates narrow the counted portfolio.",
})
@ApiResponse({ status: 200, type: UnifiedFileStatusReportDto })
async getUnifiedFileStatusReport(
@CurrentUser() actor,
@Query("from") from?: string,
@Query("to") to?: string,
) {
@Query() query: UnifiedFileStatusReportQueryDto,
): Promise<UnifiedFileStatusReportDto> {
return await this.expertInsurerService.getInsurerUnifiedFileStatusReport(
actor,
from,
to,
query,
);
}

View File

@@ -40,7 +40,15 @@ import { enrichBlamePartiesForAgreementView } from "src/helpers/blame-party-agre
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
import { applyListQueryV2 } from "src/helpers/list-query-v2";
import {
UnifiedFileStatusReportDto,
UnifiedFileStatusReportQueryDto,
} from "src/common/dto/unified-file-status-report.dto";
import {
applyListQueryV2,
isInListDateRange,
parseListDateRange,
} from "src/helpers/list-query-v2";
import {
countUnifiedFileStatuses,
getUnifiedFileStatusCatalog,
@@ -1328,6 +1336,7 @@ export class ExpertInsurerService {
createdAt: (f) => f.updatedAt ?? f.createdAt,
requestNo: (f) => f.publicId,
status: (f) => f.unifiedFileStatus,
fileType: (f) => f.fileType,
searchExtras: (f) =>
[
f.fileType,
@@ -1352,16 +1361,18 @@ export class ExpertInsurerService {
async getInsurerUnifiedFileStatusReport(
actor: any,
from?: string,
to?: string,
) {
query: UnifiedFileStatusReportQueryDto = {},
): Promise<UnifiedFileStatusReportDto> {
const clientObjectId = this.getClientId(actor);
const { fromDate, toDate } = this.parseDateRange(from, to);
const { fromDate, toDate } = parseListDateRange(query.from, query.to);
const files = await this.buildMergedInsurerFiles(clientObjectId);
const inRange = files.filter((f) =>
this.isInDateRange(f.createdAt, fromDate, toDate),
let inRange = files.filter((f) =>
isInListDateRange(f.createdAt, fromDate, toDate),
);
if (query.fileType) {
inRange = inRange.filter((f) => f.fileType === query.fileType);
}
return {
statuses: getUnifiedFileStatusCatalog(),
@@ -1946,7 +1957,10 @@ export class ExpertInsurerService {
under_review: number;
waiting_for_documents_resend: number;
}> {
const report = await this.getInsurerUnifiedFileStatusReport(actor, from, to);
const report = await this.getInsurerUnifiedFileStatusReport(actor, {
from,
to,
});
const counts = report.counts;
return {
all: counts.all ?? 0,

View File

@@ -1,4 +1,5 @@
import type { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
import { BadRequestException } from "@nestjs/common";
const MAX_LIMIT = 100;
const DEFAULT_LIMIT_WHEN_PAGE_SET = 20;
@@ -8,10 +9,45 @@ export type ListItemGetters<T> = {
createdAt: (row: T) => Date | string | number | undefined;
requestNo?: (row: T) => string | undefined;
status: (row: T) => string | undefined;
/** Blame file type (`THIRD_PARTY` | `CAR_BODY`) when available. */
fileType?: (row: T) => string | undefined;
/** Extra strings to match against `search` (e.g. claimRequestId, vehicle). */
searchExtras?: (row: T) => string[];
};
export function parseListDateRange(
from?: string,
to?: string,
): { fromDate?: Date; toDate?: Date } {
const fromDate = from ? new Date(from) : undefined;
const toDate = to ? new Date(to) : undefined;
if (fromDate && Number.isNaN(fromDate.getTime())) {
throw new BadRequestException("Invalid 'from' date");
}
if (toDate && Number.isNaN(toDate.getTime())) {
throw new BadRequestException("Invalid 'to' date");
}
if (fromDate && toDate && fromDate > toDate) {
throw new BadRequestException("'from' must be before or equal to 'to'");
}
return { fromDate, toDate };
}
export function isInListDateRange(
value: Date | string | number | undefined,
fromDate?: Date,
toDate?: Date,
): boolean {
if (!fromDate && !toDate) return true;
if (value == null) return false;
const d = value instanceof Date ? value : new Date(value);
if (Number.isNaN(d.getTime())) return false;
if (fromDate && d < fromDate) return false;
if (toDate && d > toDate) return false;
return true;
}
function normalizeSearch(raw?: string): string | undefined {
const t = raw?.trim().toLowerCase();
return t || undefined;
@@ -40,8 +76,13 @@ export function applyListQueryV2<T>(
} {
const search = normalizeSearch(query.search);
let filtered = rows;
if (query.fileType && getters.fileType) {
filtered = filtered.filter(
(row) => getters.fileType!(row) === query.fileType,
);
}
if (search) {
filtered = rows.filter((row) => {
filtered = filtered.filter((row) => {
const chunks: string[] = [];
const pid = getters.publicId(row);
if (pid) chunks.push(pid);