forked from Yara724/api
Compare commits
4 Commits
9296795166
...
8ae6f2c91b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ae6f2c91b | ||
|
|
f999313476 | ||
|
|
98f1d2caf5 | ||
|
|
bbd83da2d5 |
@@ -7,25 +7,27 @@ import {
|
||||
} from "@nestjs/common";
|
||||
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
|
||||
import { GlobalGuard } from "src/auth/guards/global.guard";
|
||||
import { RolesGuard } from "src/auth/guards/role.guard";
|
||||
import { Roles } from "src/decorators/roles.decorator";
|
||||
import { CurrentUser } from "src/decorators/user.decorator";
|
||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { ClientService } from "./client.service";
|
||||
import { ClientDto } from "./dto/create-client.dto";
|
||||
|
||||
@Controller("client")
|
||||
@ApiTags("client-management")
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(GlobalGuard, RolesGuard)
|
||||
@Roles(RoleEnum.ADMIN)
|
||||
export class ClientController {
|
||||
constructor(private readonly clientService: ClientService) {}
|
||||
|
||||
@Post()
|
||||
@UseGuards(GlobalGuard)
|
||||
@ApiBearerAuth()
|
||||
async addClient(@Body() client: ClientDto) {
|
||||
return await this.clientService.addClient(client);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(GlobalGuard)
|
||||
async getClient(@CurrentUser() user) {
|
||||
return await this.clientService.getClients();
|
||||
}
|
||||
|
||||
@@ -1,31 +1,44 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsIn, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { Types } from "mongoose";
|
||||
|
||||
class ClientName {
|
||||
@ApiProperty({})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
persian: string;
|
||||
|
||||
@ApiProperty({})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
english: string;
|
||||
}
|
||||
class Property {
|
||||
@ApiProperty({})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
smsApiKey: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ClientDto {
|
||||
@ApiProperty({ required: true })
|
||||
@ValidateNested()
|
||||
@Type(() => ClientName)
|
||||
clientName: ClientName;
|
||||
|
||||
@ApiProperty({ required: true })
|
||||
@IsNumber()
|
||||
clientCode: number;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
property: Property;
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => Property)
|
||||
property?: Property;
|
||||
|
||||
@ApiProperty({ examples: ["legal", "genuine"] })
|
||||
@IsIn(["legal", "genuine"])
|
||||
useExpertMode: "legal" | "genuine";
|
||||
}
|
||||
export class ClientDtoRs {
|
||||
@@ -35,6 +48,9 @@ export class ClientDtoRs {
|
||||
useExpertsMode: string;
|
||||
constructor(readonly client) {
|
||||
this.persian = client.clientName.persian;
|
||||
this.english = client.clientName.english;
|
||||
this.clientId = client._id;
|
||||
this.useExpertsMode = client.useExpertMode;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export class BranchDbService {
|
||||
async findAll(insuranceId: string): Promise<BranchModel[]> {
|
||||
return await this.branchModel.find({
|
||||
clientKey: new Types.ObjectId(insuranceId),
|
||||
});
|
||||
}, { _id: 1, name: 1, code: 1, city: 1, state: 1, address: 1, phoneNumber: 1, createdAtFa: 1, updatedAtFa: 1 });
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<BranchModel | null> {
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { ExecutionContext, createParamDecorator } from "@nestjs/common";
|
||||
|
||||
export const CustomHeader = createParamDecorator(
|
||||
(data, ctx: ExecutionContext) => {
|
||||
},
|
||||
);
|
||||
83
src/expert-insurer/dto/create-insurer-expert.dto.ts
Normal file
83
src/expert-insurer/dto/create-insurer-expert.dto.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsEmail, IsEnum, IsMongoId, IsNotEmpty, IsOptional, IsString } from "class-validator";
|
||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { UserType } from "src/Types&Enums/userType.enum";
|
||||
|
||||
export class CreateInsurerExpertDto {
|
||||
@ApiProperty({ example: "Ali" })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
firstName: string;
|
||||
|
||||
@ApiProperty({ example: "Ahmadi" })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
lastName: string;
|
||||
|
||||
@ApiProperty({ example: "1234567890" })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
nationalCode: string;
|
||||
|
||||
@ApiProperty({ example: "expert@example.com" })
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@ApiProperty({ example: "StrongPass!123" })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
password: string;
|
||||
|
||||
@ApiProperty({ example: "09121234567" })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
mobile: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: "665f0e5ab74d670939b08920",
|
||||
description: "Branch id this expert belongs to",
|
||||
})
|
||||
@IsMongoId()
|
||||
branchId: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: UserType, example: UserType.LEGAL })
|
||||
@IsOptional()
|
||||
@IsEnum(UserType)
|
||||
userType?: UserType;
|
||||
|
||||
@ApiPropertyOptional({ example: "02112345678" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: "Tehran" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
state?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: "Tehran" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
city?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: "No. 12, Sample St." })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
address?: string;
|
||||
}
|
||||
|
||||
export class CreateBlameExpertByInsurerDto extends CreateInsurerExpertDto {
|
||||
@ApiPropertyOptional({
|
||||
enum: [RoleEnum.EXPERT],
|
||||
default: RoleEnum.EXPERT,
|
||||
})
|
||||
role?: RoleEnum.EXPERT;
|
||||
}
|
||||
|
||||
export class CreateClaimExpertByInsurerDto extends CreateInsurerExpertDto {
|
||||
@ApiPropertyOptional({
|
||||
enum: [RoleEnum.DAMAGE_EXPERT],
|
||||
default: RoleEnum.DAMAGE_EXPERT,
|
||||
})
|
||||
role?: RoleEnum.DAMAGE_EXPERT;
|
||||
}
|
||||
@@ -26,6 +26,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 { CreateBranchDto } from "src/client/dto/create-branch.dto";
|
||||
import {
|
||||
CreateBlameExpertByInsurerDto,
|
||||
CreateClaimExpertByInsurerDto,
|
||||
} from "./dto/create-insurer-expert.dto";
|
||||
|
||||
@Controller("expert-insurer")
|
||||
@ApiTags("expert-insurer-panel")
|
||||
@@ -35,16 +39,59 @@ import { CreateBranchDto } from "src/client/dto/create-branch.dto";
|
||||
export class ExpertInsurerController {
|
||||
constructor(private readonly expertInsurerService: ExpertInsurerService) {}
|
||||
|
||||
@Get("files")
|
||||
async getAllFiles(@CurrentUser() insurer) {
|
||||
return await this.expertInsurerService.retrieveAllFilesOfClient(
|
||||
@Get("branches")
|
||||
async getInsuranceBranches(@CurrentUser() insurer) {
|
||||
if (!insurer) {
|
||||
throw new UnauthorizedException("Could not identify the current user.");
|
||||
}
|
||||
return await this.expertInsurerService.retrieveInsuranceBranches(
|
||||
insurer.clientKey,
|
||||
);
|
||||
}
|
||||
|
||||
@Post("branches")
|
||||
@ApiBody({ type: CreateBranchDto })
|
||||
async addBranch(
|
||||
@CurrentUser() insurer,
|
||||
@Body() createBranchDto: CreateBranchDto,
|
||||
) {
|
||||
if (!insurer) {
|
||||
throw new UnauthorizedException("Could not identify the current user.");
|
||||
}
|
||||
|
||||
return await this.expertInsurerService.addBranch(
|
||||
insurer.clientKey,
|
||||
createBranchDto,
|
||||
);
|
||||
}
|
||||
|
||||
@Post("experts/blame")
|
||||
@ApiBody({ type: CreateBlameExpertByInsurerDto })
|
||||
async addBlameExpert(
|
||||
@CurrentUser() insurer,
|
||||
@Body() body: CreateBlameExpertByInsurerDto,
|
||||
) {
|
||||
if (!insurer) {
|
||||
throw new UnauthorizedException("Could not identify the current user.");
|
||||
}
|
||||
return this.expertInsurerService.addBlameExpert(insurer.clientKey, body);
|
||||
}
|
||||
|
||||
@Post("experts/claim")
|
||||
@ApiBody({ type: CreateClaimExpertByInsurerDto })
|
||||
async addClaimExpert(
|
||||
@CurrentUser() insurer,
|
||||
@Body() body: CreateClaimExpertByInsurerDto,
|
||||
) {
|
||||
if (!insurer) {
|
||||
throw new UnauthorizedException("Could not identify the current user.");
|
||||
}
|
||||
return this.expertInsurerService.addClaimExpert(insurer.clientKey, body);
|
||||
}
|
||||
|
||||
@ApiQuery({ name: "page", type: Number })
|
||||
@ApiQuery({ name: "response_count", type: Number })
|
||||
@Get("list")
|
||||
@Get("experts/list")
|
||||
async getAllExperts(
|
||||
@Query("page") page: number,
|
||||
@Query("response_count") count: number,
|
||||
@@ -57,11 +104,30 @@ export class ExpertInsurerController {
|
||||
);
|
||||
}
|
||||
|
||||
@Get("top-experts")
|
||||
@Get("experts/top")
|
||||
async getTopExperts(@CurrentUser() actor) {
|
||||
return await this.expertInsurerService.getTopExpertsForClient(actor);
|
||||
}
|
||||
|
||||
@Get("files")
|
||||
async getAllFiles(@CurrentUser() insurer) {
|
||||
return await this.expertInsurerService.retrieveAllFilesOfClient(
|
||||
insurer.clientKey,
|
||||
);
|
||||
}
|
||||
|
||||
@Get("files/:publicId")
|
||||
@ApiParam({ name: "publicId" })
|
||||
async getFileDetailsByPublicId(
|
||||
@CurrentUser() insurer,
|
||||
@Param("publicId") publicId: string,
|
||||
) {
|
||||
return await this.expertInsurerService.retrieveFileDetailsByPublicId(
|
||||
insurer.clientKey,
|
||||
publicId,
|
||||
);
|
||||
}
|
||||
|
||||
@Get("top-files")
|
||||
async getTopFiles(@CurrentUser() insurer) {
|
||||
return await this.expertInsurerService.getTopFilesForClient(
|
||||
@@ -74,6 +140,29 @@ export class ExpertInsurerController {
|
||||
return await this.expertInsurerService.getExpertStatisticsReport(actor);
|
||||
}
|
||||
|
||||
@Get("report/status-counts")
|
||||
@ApiQuery({
|
||||
name: "from",
|
||||
required: false,
|
||||
description: "Optional start datetime (ISO string)",
|
||||
})
|
||||
@ApiQuery({
|
||||
name: "to",
|
||||
required: false,
|
||||
description: "Optional end datetime (ISO string)",
|
||||
})
|
||||
async getInsurerStatusReport(
|
||||
@CurrentUser() actor,
|
||||
@Query("from") from?: string,
|
||||
@Query("to") to?: string,
|
||||
) {
|
||||
return await this.expertInsurerService.getInsurerFileStatusCounts(
|
||||
actor,
|
||||
from,
|
||||
to,
|
||||
);
|
||||
}
|
||||
|
||||
@ApiParam({ name: "expertId" })
|
||||
@ApiQuery({ name: "role", enum: ["claim", "blame"] })
|
||||
@Get("/:expertId")
|
||||
@@ -174,28 +263,4 @@ export class ExpertInsurerController {
|
||||
insurer.clientKey,
|
||||
);
|
||||
}
|
||||
|
||||
@Get("branches/:insuranceId")
|
||||
@ApiParam({ name: "insuranceId" })
|
||||
async getInsuranceBranches(@Param("insuranceId") insuranceId: string) {
|
||||
return await this.expertInsurerService.retrieveInsuranceBranches(
|
||||
insuranceId,
|
||||
);
|
||||
}
|
||||
|
||||
@Post("branches")
|
||||
@ApiBody({ type: CreateBranchDto })
|
||||
async addBranch(
|
||||
@CurrentUser() insurer,
|
||||
@Body() createBranchDto: CreateBranchDto,
|
||||
) {
|
||||
if (!insurer) {
|
||||
throw new UnauthorizedException("Could not identify the current user.");
|
||||
}
|
||||
|
||||
return await this.expertInsurerService.addBranch(
|
||||
insurer.clientKey,
|
||||
createBranchDto,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,12 +19,14 @@ import {
|
||||
BranchModel,
|
||||
BranchSchema,
|
||||
} from "src/client/entities/schema/branch.schema";
|
||||
import { HashModule } from "src/utils/hash/hash.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ClaimRequestManagementModule,
|
||||
RequestManagementModule,
|
||||
AuthModule,
|
||||
HashModule,
|
||||
UsersModule,
|
||||
ClientModule,
|
||||
MongooseModule.forFeature([
|
||||
|
||||
@@ -9,6 +9,11 @@ import { Types } from "mongoose";
|
||||
import { ClaimCaseDbService } from "src/claim-request-management/entites/db-service/claim-case.db.service";
|
||||
import { CreateBranchDto } from "src/client/dto/create-branch.dto";
|
||||
import { BranchDbService } from "src/client/entities/db-service/branch.db.service";
|
||||
import {
|
||||
CreateBlameExpertByInsurerDto,
|
||||
CreateClaimExpertByInsurerDto,
|
||||
CreateInsurerExpertDto,
|
||||
} from "./dto/create-insurer-expert.dto";
|
||||
import {
|
||||
blameCaseTouchesClient,
|
||||
claimCaseTouchesClient,
|
||||
@@ -16,10 +21,14 @@ import {
|
||||
import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service";
|
||||
import { FileRating } from "src/request-management/entities/schema/request-management.schema";
|
||||
import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
|
||||
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
|
||||
import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service";
|
||||
import { ExpertDbService } from "src/users/entities/db-service/expert.db.service";
|
||||
import { ExpertFileActivityDbService } from "src/users/entities/db-service/expert-file-activity.db.service";
|
||||
import { ExpertFileActivityType } from "src/users/entities/schema/expert-file-activity.schema";
|
||||
import { HashService } from "src/utils/hash/hash.service";
|
||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { UserType } from "src/Types&Enums/userType.enum";
|
||||
|
||||
@Injectable()
|
||||
export class ExpertInsurerService {
|
||||
@@ -30,6 +39,7 @@ export class ExpertInsurerService {
|
||||
private readonly blameRequestDbService: BlameRequestDbService,
|
||||
private readonly claimCaseDbService: ClaimCaseDbService,
|
||||
private readonly branchDbService: BranchDbService,
|
||||
private readonly hashService: HashService,
|
||||
) {}
|
||||
|
||||
private getClientId(actorOrId: any): Types.ObjectId {
|
||||
@@ -124,6 +134,38 @@ export class ExpertInsurerService {
|
||||
};
|
||||
}
|
||||
|
||||
private buildPartiesPreview(parties: any[] | undefined) {
|
||||
if (!Array.isArray(parties) || !parties.length) return undefined;
|
||||
return parties.map((p: any) => ({
|
||||
role: p?.role,
|
||||
fullName: p?.person?.fullName,
|
||||
vehicle: {
|
||||
carName: p?.vehicle?.carName ?? p?.vehicle?.name,
|
||||
carModel: p?.vehicle?.carModel ?? p?.vehicle?.model,
|
||||
plate: p?.vehicle?.plate ?? p?.vehicle?.plateId,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
private sanitizeVehicleInquiry(vehicle: any) {
|
||||
if (!vehicle || typeof vehicle !== "object") return vehicle;
|
||||
const inquiry = vehicle.inquiry;
|
||||
if (!inquiry || typeof inquiry !== "object") return vehicle;
|
||||
const { raw, mapped, ...safeInquiry } = inquiry;
|
||||
return {
|
||||
...vehicle,
|
||||
inquiry: safeInquiry,
|
||||
};
|
||||
}
|
||||
|
||||
private sanitizePartyForInsurerView(party: any) {
|
||||
if (!party || typeof party !== "object") return party;
|
||||
return {
|
||||
...party,
|
||||
vehicle: this.sanitizeVehicleInquiry(party.vehicle),
|
||||
};
|
||||
}
|
||||
|
||||
private getCombinedFileScore(file: any): number | null {
|
||||
const insurerRating = file?.rating;
|
||||
const userRating = file?.userRating;
|
||||
@@ -155,11 +197,6 @@ export class ExpertInsurerService {
|
||||
.filter((f) =>
|
||||
(f?.parties || []).some((p) => String(p?.person?.clientId || "") === idStr),
|
||||
)
|
||||
.filter(
|
||||
(f) =>
|
||||
f?.status !== CaseStatus.OPEN &&
|
||||
f?.status !== CaseStatus.WAITING_FOR_SECOND_PARTY,
|
||||
)
|
||||
.map((f) => this.normalizeBlameCase(f));
|
||||
}
|
||||
|
||||
@@ -167,7 +204,7 @@ export class ExpertInsurerService {
|
||||
const all = (await this.claimCaseDbService.find({}, { lean: true })) as any[];
|
||||
const idStr = String(clientObjectId);
|
||||
return all
|
||||
.filter((f) => String(f?.owner?.userClientKey || "") === idStr)
|
||||
.filter((f) => claimCaseTouchesClient(f, idStr))
|
||||
.map((f) => this.normalizeClaimCase(f));
|
||||
}
|
||||
|
||||
@@ -395,7 +432,245 @@ export class ExpertInsurerService {
|
||||
this.getClientBlameFiles(id),
|
||||
this.getClientClaimFiles(id),
|
||||
]);
|
||||
return { blameFiles, claimFiles };
|
||||
|
||||
const byPublicId = new Map<
|
||||
string,
|
||||
{
|
||||
publicId: string;
|
||||
fileType?: string;
|
||||
parties?: Array<{
|
||||
role?: string;
|
||||
fullName?: string;
|
||||
vehicle?: {
|
||||
carName?: string;
|
||||
carModel?: string;
|
||||
plate?: string;
|
||||
};
|
||||
}>;
|
||||
blame?: {
|
||||
requestId?: string;
|
||||
requestNo?: string;
|
||||
status?: string;
|
||||
parties?: Array<{
|
||||
role?: string;
|
||||
fullName?: string;
|
||||
vehicle?: {
|
||||
carName?: string;
|
||||
carModel?: string;
|
||||
plate?: string;
|
||||
};
|
||||
}>;
|
||||
createdAt?: Date | string;
|
||||
updatedAt?: Date | string;
|
||||
};
|
||||
claim?: {
|
||||
requestId?: string;
|
||||
requestNo?: string;
|
||||
status?: string;
|
||||
claimStatus?: string;
|
||||
currentStep?: string;
|
||||
createdAt?: Date | string;
|
||||
updatedAt?: Date | string;
|
||||
};
|
||||
createdAt?: Date | string;
|
||||
updatedAt?: Date | string;
|
||||
}
|
||||
>();
|
||||
|
||||
for (const b of blameFiles) {
|
||||
const key = String((b as any).publicId || (b as any)._id || "");
|
||||
if (!key) continue;
|
||||
const prev = byPublicId.get(key) ?? { publicId: key };
|
||||
const partiesPreview = this.buildPartiesPreview((b as any).parties);
|
||||
prev.fileType = prev.fileType ?? (b as any).type;
|
||||
prev.blame = {
|
||||
requestId: (b as any)?._id?.toString?.() || String((b as any)?._id || ""),
|
||||
requestNo: (b as any).requestNo || String((b as any).requestNumber || ""),
|
||||
status: (b as any).status,
|
||||
parties: partiesPreview,
|
||||
createdAt: (b as any).createdAt,
|
||||
updatedAt: (b as any).updatedAt,
|
||||
};
|
||||
prev.parties = prev.parties ?? partiesPreview;
|
||||
prev.createdAt = prev.createdAt ?? (b as any).createdAt;
|
||||
prev.updatedAt = (b as any).updatedAt ?? prev.updatedAt;
|
||||
byPublicId.set(key, prev);
|
||||
}
|
||||
|
||||
for (const c of claimFiles) {
|
||||
const key = String((c as any).publicId || (c as any)._id || "");
|
||||
if (!key) continue;
|
||||
const prev = byPublicId.get(key) ?? { publicId: key };
|
||||
const claimPartiesPreview = this.buildPartiesPreview((c as any)?.snapshot?.parties);
|
||||
prev.fileType = prev.fileType ?? (c as any)?.snapshot?.accident?.type;
|
||||
prev.claim = {
|
||||
requestId: (c as any)?._id?.toString?.() || String((c as any)?._id || ""),
|
||||
requestNo: (c as any).requestNo || String((c as any).requestNumber || ""),
|
||||
status: (c as any).status,
|
||||
claimStatus: (c as any).claimStatus,
|
||||
currentStep: (c as any).currentStep,
|
||||
createdAt: (c as any).createdAt,
|
||||
updatedAt: (c as any).updatedAt,
|
||||
};
|
||||
prev.parties = prev.parties ?? claimPartiesPreview;
|
||||
prev.createdAt = prev.createdAt ?? (c as any).createdAt;
|
||||
prev.updatedAt = (c as any).updatedAt ?? prev.updatedAt;
|
||||
byPublicId.set(key, prev);
|
||||
}
|
||||
|
||||
const files = Array.from(byPublicId.values())
|
||||
.map((f) => ({
|
||||
publicId: f.publicId,
|
||||
fileType: f.fileType,
|
||||
hasClaim: !!f.claim,
|
||||
parties: f.parties,
|
||||
blame: f.blame,
|
||||
claim: f.claim,
|
||||
createdAt: f.createdAt,
|
||||
updatedAt: f.updatedAt,
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
const at = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
|
||||
const bt = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
|
||||
return bt - at;
|
||||
});
|
||||
|
||||
return {
|
||||
total: files.length,
|
||||
files,
|
||||
};
|
||||
}
|
||||
|
||||
async retrieveFileDetailsByPublicId(insurerId: string, publicId: string) {
|
||||
if (!publicId?.trim()) {
|
||||
throw new BadRequestException("publicId is required");
|
||||
}
|
||||
|
||||
const id = this.getClientId(insurerId);
|
||||
const [blameFiles, claimFiles] = await Promise.all([
|
||||
this.getClientBlameFiles(id),
|
||||
this.getClientClaimFiles(id),
|
||||
]);
|
||||
|
||||
const blame = blameFiles.find((b) => String((b as any).publicId) === publicId);
|
||||
const claim = claimFiles.find((c) => String((c as any).publicId) === publicId);
|
||||
|
||||
if (!blame && !claim) {
|
||||
throw new NotFoundException("File not found for this publicId");
|
||||
}
|
||||
|
||||
const parties = Array.isArray((blame as any)?.parties) ? (blame as any).parties : [];
|
||||
const firstParty = parties.find((p: any) => p?.role === "FIRST");
|
||||
const secondParty = parties.find((p: any) => p?.role === "SECOND");
|
||||
const claimSnapshotParties = Array.isArray((claim as any)?.snapshot?.parties)
|
||||
? (claim as any).snapshot.parties
|
||||
: undefined;
|
||||
|
||||
const overview = {
|
||||
publicId,
|
||||
requestNo:
|
||||
(blame as any)?.requestNo ||
|
||||
(claim as any)?.requestNo ||
|
||||
(blame as any)?.requestNumber ||
|
||||
(claim as any)?.requestNumber,
|
||||
type: (blame as any)?.type,
|
||||
hasClaim: !!claim,
|
||||
blameStatus: (blame as any)?.status,
|
||||
claimStatus: (claim as any)?.status,
|
||||
claimReviewStatus: (claim as any)?.claimStatus,
|
||||
createdAt: (blame as any)?.createdAt ?? (claim as any)?.createdAt,
|
||||
updatedAt: (claim as any)?.updatedAt ?? (blame as any)?.updatedAt,
|
||||
};
|
||||
|
||||
const blameDetails = blame
|
||||
? {
|
||||
requestId:
|
||||
(blame as any)?._id?.toString?.() || String((blame as any)?._id || ""),
|
||||
requestNo:
|
||||
(blame as any).requestNo || String((blame as any).requestNumber || ""),
|
||||
status: (blame as any).status,
|
||||
blameStatus: (blame as any).blameStatus,
|
||||
workflow: (blame as any).workflow,
|
||||
parties: {
|
||||
first: firstParty
|
||||
? {
|
||||
fullName: firstParty?.person?.fullName,
|
||||
phoneNumber: firstParty?.person?.phoneNumber,
|
||||
role: firstParty?.role,
|
||||
vehicle: this.sanitizeVehicleInquiry(firstParty?.vehicle),
|
||||
}
|
||||
: undefined,
|
||||
second: secondParty
|
||||
? {
|
||||
fullName: secondParty?.person?.fullName,
|
||||
phoneNumber: secondParty?.person?.phoneNumber,
|
||||
role: secondParty?.role,
|
||||
vehicle: this.sanitizeVehicleInquiry(secondParty?.vehicle),
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
expert: (blame as any).expert
|
||||
? {
|
||||
assignedExpertId: (blame as any).expert?.assignedExpertId,
|
||||
decision: (blame as any).expert?.decision,
|
||||
resend: (blame as any).expert?.resend,
|
||||
rating: (blame as any).expert?.rating,
|
||||
}
|
||||
: undefined,
|
||||
createdAt: (blame as any).createdAt,
|
||||
updatedAt: (blame as any).updatedAt,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
// Keep claim section claim-specific to avoid duplicating shared data already in blame/overview.
|
||||
const claimDetails = claim
|
||||
? {
|
||||
requestId:
|
||||
(claim as any)?._id?.toString?.() || String((claim as any)?._id || ""),
|
||||
requestNo:
|
||||
(claim as any).requestNo || String((claim as any).requestNumber || ""),
|
||||
status: (claim as any).status,
|
||||
claimStatus: (claim as any).claimStatus,
|
||||
owner: (claim as any).owner,
|
||||
vehicle: (claim as any).vehicle,
|
||||
money: (claim as any).money,
|
||||
damage: (claim as any).damage,
|
||||
media: (claim as any).media,
|
||||
requiredDocuments: (claim as any).requiredDocuments,
|
||||
snapshot: (claim as any).snapshot
|
||||
? {
|
||||
...(claim as any).snapshot,
|
||||
parties: claimSnapshotParties?.map((p: any) =>
|
||||
this.sanitizePartyForInsurerView(p),
|
||||
),
|
||||
}
|
||||
: undefined,
|
||||
workflow: {
|
||||
currentStep: (claim as any).currentStep,
|
||||
nextStep: (claim as any).nextStep,
|
||||
},
|
||||
evaluation: {
|
||||
damageExpertReply: (claim as any).damageExpertReply,
|
||||
damageExpertReplyFinal: (claim as any).damageExpertReplyFinal,
|
||||
damageExpertResend: (claim as any).damageExpertResend,
|
||||
objection: (claim as any).objection,
|
||||
priceDrop: (claim as any).priceDrop,
|
||||
visitLocation: (claim as any).visitLocation,
|
||||
factors: (claim as any).evaluation?.factors,
|
||||
},
|
||||
userResendDocuments: (claim as any).userResendDocuments,
|
||||
userRating: (claim as any).userRating,
|
||||
insurerRating: (claim as any).rating,
|
||||
createdAt: (claim as any).createdAt,
|
||||
updatedAt: (claim as any).updatedAt,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
overview,
|
||||
blame: blameDetails,
|
||||
claim: claimDetails,
|
||||
};
|
||||
}
|
||||
|
||||
async addBranch(clientKey: string, branchDto: CreateBranchDto) {
|
||||
@@ -420,6 +695,112 @@ export class ExpertInsurerService {
|
||||
return newBranch;
|
||||
}
|
||||
|
||||
private async assertBranchBelongsToClient(
|
||||
branchId: string,
|
||||
clientId: Types.ObjectId,
|
||||
) {
|
||||
const branch = await this.branchDbService.findById(branchId);
|
||||
if (!branch) {
|
||||
throw new NotFoundException("Branch not found");
|
||||
}
|
||||
if (String(branch.clientKey) !== String(clientId)) {
|
||||
throw new ForbiddenException(
|
||||
"Selected branch does not belong to your insurance company.",
|
||||
);
|
||||
}
|
||||
return branch;
|
||||
}
|
||||
|
||||
private sanitizeExpertResponse(doc: any) {
|
||||
const obj = typeof doc?.toObject === "function" ? doc.toObject() : doc;
|
||||
if (!obj) return obj;
|
||||
const { password, otp, ...rest } = obj;
|
||||
return rest;
|
||||
}
|
||||
|
||||
private async createExpertForInsurer(
|
||||
insurerClientKey: string,
|
||||
payload: CreateInsurerExpertDto,
|
||||
role: RoleEnum.EXPERT | RoleEnum.DAMAGE_EXPERT,
|
||||
) {
|
||||
const clientObjectId = this.getClientId(insurerClientKey);
|
||||
const branch = await this.assertBranchBelongsToClient(
|
||||
payload.branchId,
|
||||
clientObjectId,
|
||||
);
|
||||
|
||||
const email = payload.email.trim().toLowerCase();
|
||||
const existingByEmail = await this.expertDbService.findOne({ email });
|
||||
const existingDamageByEmail = await this.damageExpertDbService.findOne({ email });
|
||||
if (existingByEmail || existingDamageByEmail) {
|
||||
throw new ConflictException("An expert with this email already exists.");
|
||||
}
|
||||
|
||||
const nationalCode = payload.nationalCode.trim();
|
||||
const existingByNational = await this.expertDbService.findOne({ nationalCode });
|
||||
const existingDamageByNational = await this.damageExpertDbService.findOne({
|
||||
nationalCode,
|
||||
});
|
||||
if (existingByNational || existingDamageByNational) {
|
||||
throw new ConflictException(
|
||||
"An expert with this national code already exists.",
|
||||
);
|
||||
}
|
||||
|
||||
const hashedPassword = await this.hashService.hash(payload.password);
|
||||
const basePayload: any = {
|
||||
...payload,
|
||||
email,
|
||||
nationalCode,
|
||||
password: hashedPassword,
|
||||
role,
|
||||
userType: payload.userType ?? UserType.LEGAL,
|
||||
clientKey:
|
||||
role === RoleEnum.EXPERT ? String(clientObjectId) : clientObjectId,
|
||||
branchId: new Types.ObjectId(payload.branchId),
|
||||
insuActivityCo: String(clientObjectId),
|
||||
};
|
||||
|
||||
const created =
|
||||
role === RoleEnum.EXPERT
|
||||
? await this.expertDbService.create(basePayload)
|
||||
: await this.damageExpertDbService.create(basePayload);
|
||||
|
||||
return {
|
||||
expert: this.sanitizeExpertResponse(created),
|
||||
branch: {
|
||||
_id: (branch as any)._id,
|
||||
name: branch.name,
|
||||
code: branch.code,
|
||||
city: branch.city,
|
||||
state: branch.state,
|
||||
address: branch.address,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async addBlameExpert(
|
||||
insurerClientKey: string,
|
||||
payload: CreateBlameExpertByInsurerDto,
|
||||
) {
|
||||
return this.createExpertForInsurer(
|
||||
insurerClientKey,
|
||||
payload,
|
||||
RoleEnum.EXPERT,
|
||||
);
|
||||
}
|
||||
|
||||
async addClaimExpert(
|
||||
insurerClientKey: string,
|
||||
payload: CreateClaimExpertByInsurerDto,
|
||||
) {
|
||||
return this.createExpertForInsurer(
|
||||
insurerClientKey,
|
||||
payload,
|
||||
RoleEnum.DAMAGE_EXPERT,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comprehensive statistics for all experts of a client
|
||||
* Returns:
|
||||
@@ -549,4 +930,113 @@ export class ExpertInsurerService {
|
||||
async retrieveInsuranceBranches(insuranceId: string) {
|
||||
return this.branchDbService.findAll(insuranceId);
|
||||
}
|
||||
|
||||
private parseDateRange(from?: string, to?: string) {
|
||||
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 };
|
||||
}
|
||||
|
||||
private isInDateRange(value: any, fromDate?: Date, toDate?: Date): boolean {
|
||||
if (!fromDate && !toDate) return true;
|
||||
if (!value) return false;
|
||||
const d = new Date(value);
|
||||
if (Number.isNaN(d.getTime())) return false;
|
||||
if (fromDate && d < fromDate) return false;
|
||||
if (toDate && d > toDate) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
async getInsurerFileStatusCounts(
|
||||
actor: any,
|
||||
from?: string,
|
||||
to?: string,
|
||||
): Promise<{
|
||||
all: number;
|
||||
completed: number;
|
||||
under_review: number;
|
||||
waiting_for_documents_resend: number;
|
||||
}> {
|
||||
const clientObjectId = this.getClientId(actor);
|
||||
const { fromDate, toDate } = this.parseDateRange(from, to);
|
||||
|
||||
const [blameFilesRaw, claimFilesRaw] = await Promise.all([
|
||||
this.getClientBlameFiles(clientObjectId),
|
||||
this.getClientClaimFiles(clientObjectId),
|
||||
]);
|
||||
|
||||
const blameFiles = blameFilesRaw.filter((f) =>
|
||||
this.isInDateRange((f as any)?.createdAt, fromDate, toDate),
|
||||
);
|
||||
const claimFiles = claimFilesRaw.filter((f) =>
|
||||
this.isInDateRange((f as any)?.createdAt, fromDate, toDate),
|
||||
);
|
||||
|
||||
const fileMap = new Map<
|
||||
string,
|
||||
{ blame?: any; claim?: any }
|
||||
>();
|
||||
|
||||
for (const b of blameFiles) {
|
||||
const key = String((b as any).publicId || (b as any)._id || "");
|
||||
if (!key) continue;
|
||||
const prev = fileMap.get(key) ?? {};
|
||||
prev.blame = b;
|
||||
fileMap.set(key, prev);
|
||||
}
|
||||
for (const c of claimFiles) {
|
||||
const key = String((c as any).publicId || (c as any)._id || "");
|
||||
if (!key) continue;
|
||||
const prev = fileMap.get(key) ?? {};
|
||||
prev.claim = c;
|
||||
fileMap.set(key, prev);
|
||||
}
|
||||
|
||||
let completed = 0;
|
||||
let underReview = 0;
|
||||
let waitingForDocumentsResend = 0;
|
||||
|
||||
for (const entry of fileMap.values()) {
|
||||
const blameStatus = entry.blame?.status;
|
||||
const claimStatus = entry.claim?.status;
|
||||
|
||||
if (claimStatus === ClaimCaseStatus.COMPLETED) {
|
||||
completed += 1;
|
||||
}
|
||||
|
||||
const blameUnderReview =
|
||||
blameStatus === CaseStatus.WAITING_FOR_EXPERT;
|
||||
const claimUnderReview =
|
||||
claimStatus === ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT ||
|
||||
claimStatus === ClaimCaseStatus.EXPERT_REVIEWING;
|
||||
if (blameUnderReview || claimUnderReview) {
|
||||
underReview += 1;
|
||||
}
|
||||
|
||||
const blameResend =
|
||||
blameStatus === CaseStatus.WAITING_FOR_DOCUMENT_RESEND;
|
||||
const claimResend =
|
||||
claimStatus === ClaimCaseStatus.WAITING_FOR_USER_RESEND;
|
||||
if (blameResend || claimResend) {
|
||||
waitingForDocumentsResend += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
all: fileMap.size,
|
||||
completed,
|
||||
under_review: underReview,
|
||||
waiting_for_documents_resend: waitingForDocumentsResend,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
export enum PanelModels {
|
||||
BLAME = "blame",
|
||||
CLAIM = "claim",
|
||||
}
|
||||
@@ -32,7 +32,6 @@ async function bootstrap() {
|
||||
.setVersion("1.0.0")
|
||||
.addServer(process.env.URL)
|
||||
.addServer(process.env.BASE_URL + "/api")
|
||||
.addServer("http://192.168.20.249:9001")
|
||||
.addServer("http://192.168.20.170:9001")
|
||||
.addServer("http://localhost:9001")
|
||||
.addBearerAuth()
|
||||
|
||||
@@ -123,6 +123,9 @@ export class DamageExpertModel {
|
||||
@Prop({ type: Types.ObjectId })
|
||||
clientKey?: Types.ObjectId;
|
||||
|
||||
@Prop({ type: Types.ObjectId, index: true })
|
||||
branchId?: Types.ObjectId;
|
||||
|
||||
@Prop({ type: PersonalInfoSchema })
|
||||
personalInfo?: PersonalInfo;
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ export class ExpertFileActivity {
|
||||
@Prop({ type: Date, default: Date.now, index: true })
|
||||
occurredAt: Date;
|
||||
|
||||
@Prop({ type: String, required: false, index: true })
|
||||
@Prop({ type: String, required: false })
|
||||
idempotencyKey?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||
import { Types } from "mongoose";
|
||||
import { Degrees } from "src/Types&Enums/degrees.enum";
|
||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { UserType } from "src/Types&Enums/userType.enum";
|
||||
@@ -65,6 +66,9 @@ export class ExpertModel {
|
||||
@Prop({})
|
||||
clientKey?: string;
|
||||
|
||||
@Prop({ type: Types.ObjectId, index: true })
|
||||
branchId?: Types.ObjectId;
|
||||
|
||||
@Prop({ type: "string" })
|
||||
otp: string;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user