forked from Yara724/api
Added in-person expert
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
export enum RoleEnum {
|
export enum RoleEnum {
|
||||||
EXPERT = "expert",
|
EXPERT = "expert",
|
||||||
DAMAGE_EXPERT = "damage_expert",
|
DAMAGE_EXPERT = "damage_expert",
|
||||||
|
FIELD_EXPERT = "field_expert",
|
||||||
COMPANY = "company",
|
COMPANY = "company",
|
||||||
ADMIN = "admin",
|
ADMIN = "admin",
|
||||||
USER = "user",
|
USER = "user",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
} from "src/auth/dto/actor/forget-password.actor.dto";
|
} from "src/auth/dto/actor/forget-password.actor.dto";
|
||||||
import { LoginActorDto } from "src/auth/dto/actor/login.actor.dto";
|
import { LoginActorDto } from "src/auth/dto/actor/login.actor.dto";
|
||||||
import { ActorEditUserProfileDto } from "src/auth/dto/actor/profile.actor.dto";
|
import { ActorEditUserProfileDto } from "src/auth/dto/actor/profile.actor.dto";
|
||||||
|
import { CreateFieldExpertDto } from "src/auth/dto/actor/create-field-expert.actor.dto";
|
||||||
import {
|
import {
|
||||||
GenuineRegisterDto,
|
GenuineRegisterDto,
|
||||||
InsurerRegisterDto,
|
InsurerRegisterDto,
|
||||||
@@ -55,6 +56,14 @@ export class ActorAuthController {
|
|||||||
return await this.actorAuthService.insurerRegister(body);
|
return await this.actorAuthService.insurerRegister(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Mock: create a field expert for testing. Make private later. */
|
||||||
|
@Post("create-field-expert")
|
||||||
|
@ApiBody({ type: CreateFieldExpertDto })
|
||||||
|
@ApiAcceptedResponse()
|
||||||
|
async createFieldExpert(@Body() body: CreateFieldExpertDto) {
|
||||||
|
return await this.actorAuthService.createFieldExpertMock(body);
|
||||||
|
}
|
||||||
|
|
||||||
@UseGuards(LocalActorAuthGuard)
|
@UseGuards(LocalActorAuthGuard)
|
||||||
@Post("login")
|
@Post("login")
|
||||||
@Roles()
|
@Roles()
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { UserType } from "src/Types&Enums/userType.enum";
|
|||||||
import { InsurerExpertDbService } from "src/users/entities/db-service/insurer-expert.db.service";
|
import { InsurerExpertDbService } from "src/users/entities/db-service/insurer-expert.db.service";
|
||||||
import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service";
|
import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service";
|
||||||
import { ExpertDbService } from "src/users/entities/db-service/expert.db.service";
|
import { ExpertDbService } from "src/users/entities/db-service/expert.db.service";
|
||||||
|
import { FieldExpertDbService } from "src/users/entities/db-service/field-expert.db.service";
|
||||||
import { HashService } from "src/utils/hash/hash.service";
|
import { HashService } from "src/utils/hash/hash.service";
|
||||||
import { MailService } from "src/utils/mail/mail.service";
|
import { MailService } from "src/utils/mail/mail.service";
|
||||||
import { OtpService } from "src/utils/otp/otp.service";
|
import { OtpService } from "src/utils/otp/otp.service";
|
||||||
@@ -44,6 +45,7 @@ export class ActorAuthService {
|
|||||||
private readonly hashService: HashService,
|
private readonly hashService: HashService,
|
||||||
private readonly expertDbService: ExpertDbService,
|
private readonly expertDbService: ExpertDbService,
|
||||||
private readonly damageExpertDbService: DamageExpertDbService,
|
private readonly damageExpertDbService: DamageExpertDbService,
|
||||||
|
private readonly fieldExpertDbService: FieldExpertDbService,
|
||||||
private readonly insurerExpertDbService: InsurerExpertDbService,
|
private readonly insurerExpertDbService: InsurerExpertDbService,
|
||||||
private readonly mailService: MailService,
|
private readonly mailService: MailService,
|
||||||
private readonly clientDbService: ClientDbService,
|
private readonly clientDbService: ClientDbService,
|
||||||
@@ -69,6 +71,14 @@ export class ActorAuthService {
|
|||||||
else
|
else
|
||||||
res = await this.damageExpertDbService.findOne({ email: username });
|
res = await this.damageExpertDbService.findOne({ email: username });
|
||||||
break;
|
break;
|
||||||
|
case RoleEnum.FIELD_EXPERT:
|
||||||
|
if (username == null && userId)
|
||||||
|
res = await this.fieldExpertDbService.findOne({
|
||||||
|
_id: new Types.ObjectId(userId),
|
||||||
|
});
|
||||||
|
else
|
||||||
|
res = await this.fieldExpertDbService.findOne({ email: username });
|
||||||
|
break;
|
||||||
case RoleEnum.COMPANY:
|
case RoleEnum.COMPANY:
|
||||||
res = await this.insurerExpertDbService.findOne({ email: username });
|
res = await this.insurerExpertDbService.findOne({ email: username });
|
||||||
break;
|
break;
|
||||||
@@ -259,6 +269,11 @@ export class ActorAuthService {
|
|||||||
dbServiceToUpdate = this.insurerExpertDbService as any;
|
dbServiceToUpdate = this.insurerExpertDbService as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!actor) {
|
||||||
|
actor = await this.fieldExpertDbService.findOne(filter);
|
||||||
|
dbServiceToUpdate = this.fieldExpertDbService as any;
|
||||||
|
}
|
||||||
|
|
||||||
if (!actor) {
|
if (!actor) {
|
||||||
throw new NotFoundException("actor not found");
|
throw new NotFoundException("actor not found");
|
||||||
}
|
}
|
||||||
@@ -278,18 +293,29 @@ export class ActorAuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async forgetPasswordVerify(email, otp, newPassword) {
|
async forgetPasswordVerify(email, otp, newPassword) {
|
||||||
const userExist = await this.expertDbService.findOne({ email });
|
let userExist: any = await this.expertDbService.findOne({ email });
|
||||||
|
let dbServiceToUpdate: any = this.expertDbService;
|
||||||
|
if (!userExist) {
|
||||||
|
userExist = await this.damageExpertDbService.findOne({ email });
|
||||||
|
dbServiceToUpdate = this.damageExpertDbService;
|
||||||
|
}
|
||||||
|
if (!userExist) {
|
||||||
|
userExist = await this.insurerExpertDbService.findOne({ email });
|
||||||
|
dbServiceToUpdate = this.insurerExpertDbService;
|
||||||
|
}
|
||||||
|
if (!userExist) {
|
||||||
|
userExist = await this.fieldExpertDbService.findOne({ email });
|
||||||
|
dbServiceToUpdate = this.fieldExpertDbService;
|
||||||
|
}
|
||||||
if (!userExist) throw new NotFoundException("user not found");
|
if (!userExist) throw new NotFoundException("user not found");
|
||||||
const decodeOtp = await this.hashService.compare(otp, userExist.otp);
|
const decodeOtp = await this.hashService.compare(otp, userExist.otp);
|
||||||
if (!decodeOtp) throw new UnauthorizedException("otp invalid");
|
if (!decodeOtp) throw new UnauthorizedException("otp invalid");
|
||||||
if (decodeOtp) {
|
const hashNewPassword = await this.hashService.hash(newPassword);
|
||||||
const hashNewPassword = await this.hashService.hash(newPassword);
|
await dbServiceToUpdate.findOneAndUpdate(
|
||||||
await this.expertDbService.findOneAndUpdate(
|
{ email },
|
||||||
{ email },
|
{ password: hashNewPassword },
|
||||||
{ password: hashNewPassword },
|
);
|
||||||
);
|
return new ForgetPasswordVerifyCodeDtoRs(userExist, "update password");
|
||||||
return new ForgetPasswordVerifyCodeDtoRs(userExist, "update password");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProfiles(currentUser) {
|
async getProfiles(currentUser) {
|
||||||
@@ -347,7 +373,14 @@ export class ActorAuthService {
|
|||||||
"city",
|
"city",
|
||||||
"state",
|
"state",
|
||||||
"address",
|
"address",
|
||||||
]
|
],
|
||||||
|
field_expert: [
|
||||||
|
"firstName",
|
||||||
|
"lastName",
|
||||||
|
"email",
|
||||||
|
"phone",
|
||||||
|
"mobile",
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const allowedFields = allowedFieldsByRole[role];
|
const allowedFields = allowedFieldsByRole[role];
|
||||||
@@ -407,6 +440,13 @@ export class ActorAuthService {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "field_expert":
|
||||||
|
await this.fieldExpertDbService.updateOne(
|
||||||
|
{ _id: new Types.ObjectId(userId) },
|
||||||
|
{ $set: sanitized },
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new ForbiddenException("Unsupported role for update");
|
throw new ForbiddenException("Unsupported role for update");
|
||||||
}
|
}
|
||||||
@@ -417,4 +457,43 @@ export class ActorAuthService {
|
|||||||
throw new InternalServerErrorException("Failed to update profile");
|
throw new InternalServerErrorException("Failed to update profile");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Mock endpoint: create a field expert for testing. Make private later. */
|
||||||
|
async createFieldExpertMock(body: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
mobile: string;
|
||||||
|
phone?: string;
|
||||||
|
}) {
|
||||||
|
const hashPassword = await this.hashService.hash(body.password);
|
||||||
|
const payload = {
|
||||||
|
email: body.email.toLowerCase().trim(),
|
||||||
|
password: hashPassword,
|
||||||
|
firstName: body.firstName,
|
||||||
|
lastName: body.lastName,
|
||||||
|
mobile: body.mobile,
|
||||||
|
phone: body.phone,
|
||||||
|
role: RoleEnum.FIELD_EXPERT,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const created = await this.fieldExpertDbService.create(payload);
|
||||||
|
return {
|
||||||
|
id: created._id,
|
||||||
|
email: created.email,
|
||||||
|
firstName: created.firstName,
|
||||||
|
lastName: created.lastName,
|
||||||
|
mobile: created.mobile,
|
||||||
|
role: created.role,
|
||||||
|
};
|
||||||
|
} catch (er) {
|
||||||
|
if (er.code === 11000) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"A field expert with this email already exists.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw er;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
30
src/auth/dto/actor/create-field-expert.actor.dto.ts
Normal file
30
src/auth/dto/actor/create-field-expert.actor.dto.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsEmail, IsOptional, IsString, MinLength } from "class-validator";
|
||||||
|
|
||||||
|
export class CreateFieldExpertDto {
|
||||||
|
@ApiProperty({ example: "field.expert@example.com" })
|
||||||
|
@IsEmail()
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: "securePassword123", minLength: 6 })
|
||||||
|
@IsString()
|
||||||
|
@MinLength(6)
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: "علی" })
|
||||||
|
@IsString()
|
||||||
|
firstName: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: "محمدی" })
|
||||||
|
@IsString()
|
||||||
|
lastName: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: "09121234567" })
|
||||||
|
@IsString()
|
||||||
|
mobile: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: "02188776655", required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
phone?: string;
|
||||||
|
}
|
||||||
@@ -39,9 +39,12 @@ export class LocalActorAuthGuard extends AuthGuard("actor") {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
![RoleEnum.EXPERT, RoleEnum.DAMAGE_EXPERT, RoleEnum.COMPANY].includes(
|
![
|
||||||
payload.role,
|
RoleEnum.EXPERT,
|
||||||
)
|
RoleEnum.DAMAGE_EXPERT,
|
||||||
|
RoleEnum.COMPANY,
|
||||||
|
RoleEnum.FIELD_EXPERT,
|
||||||
|
].includes(payload.role)
|
||||||
) {
|
) {
|
||||||
throw new UnauthorizedException("User role is not authorized");
|
throw new UnauthorizedException("User role is not authorized");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ import { JwtModule } from "@nestjs/jwt";
|
|||||||
],
|
],
|
||||||
controllers: [ClaimRequestManagementController],
|
controllers: [ClaimRequestManagementController],
|
||||||
exports: [
|
exports: [
|
||||||
|
ClaimRequestManagementService,
|
||||||
ClaimRequestManagementDbService,
|
ClaimRequestManagementDbService,
|
||||||
DamageImageDbService,
|
DamageImageDbService,
|
||||||
VideoCaptureDbService,
|
VideoCaptureDbService,
|
||||||
|
|||||||
@@ -118,7 +118,12 @@ export class ClaimRequestManagementService {
|
|||||||
|
|
||||||
// If actor is an expert, verify they're accessing an IN_PERSON claim file
|
// If actor is an expert, verify they're accessing an IN_PERSON claim file
|
||||||
// and return the claim file's userId
|
// and return the claim file's userId
|
||||||
if (actor.role === RoleEnum.EXPERT || actor.role === RoleEnum.DAMAGE_EXPERT) {
|
const expertRoles = [
|
||||||
|
RoleEnum.EXPERT,
|
||||||
|
RoleEnum.DAMAGE_EXPERT,
|
||||||
|
RoleEnum.FIELD_EXPERT,
|
||||||
|
];
|
||||||
|
if (expertRoles.includes(actor.role)) {
|
||||||
const claim = await this.claimDbService.findOne(claimRequestId);
|
const claim = await this.claimDbService.findOne(claimRequestId);
|
||||||
if (!claim) {
|
if (!claim) {
|
||||||
throw new NotFoundException("Claim file not found");
|
throw new NotFoundException("Claim file not found");
|
||||||
@@ -184,11 +189,16 @@ export class ClaimRequestManagementService {
|
|||||||
|
|
||||||
// For IN_PERSON expert-initiated files, expert creates the claim
|
// For IN_PERSON expert-initiated files, expert creates the claim
|
||||||
// For LINK method or regular files, user creates their own claim
|
// For LINK method or regular files, user creates their own claim
|
||||||
|
const expertRoles = [
|
||||||
|
RoleEnum.EXPERT,
|
||||||
|
RoleEnum.DAMAGE_EXPERT,
|
||||||
|
RoleEnum.FIELD_EXPERT,
|
||||||
|
];
|
||||||
if (
|
if (
|
||||||
blameRequest.expertInitiated &&
|
blameRequest.expertInitiated &&
|
||||||
blameRequest.creationMethod === "IN_PERSON" &&
|
blameRequest.creationMethod === "IN_PERSON" &&
|
||||||
actor &&
|
actor &&
|
||||||
(actor.role === "EXPERT" || actor.role === "DAMAGE_EXPERT")
|
expertRoles.includes(actor.role)
|
||||||
) {
|
) {
|
||||||
// Expert is creating claim for IN_PERSON file
|
// Expert is creating claim for IN_PERSON file
|
||||||
// Verify expert is the initiating expert
|
// Verify expert is the initiating expert
|
||||||
@@ -343,6 +353,69 @@ export class ClaimRequestManagementService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Field expert completes claim-needed data for expert-initiated IN_PERSON files.
|
||||||
|
* Only the initiating expert can call this. Accepts car part damage and/or sheba, nationalCodeOfInsurer, otherParts.
|
||||||
|
*/
|
||||||
|
async expertCompleteClaimData(
|
||||||
|
claimRequestId: string,
|
||||||
|
expert: any,
|
||||||
|
dto: {
|
||||||
|
carPartDamage?: CarDamagePartDto;
|
||||||
|
sheba?: string;
|
||||||
|
nationalCodeOfInsurer?: string;
|
||||||
|
otherParts?: any[];
|
||||||
|
},
|
||||||
|
): Promise<{ success: boolean; claimRequestId: string }> {
|
||||||
|
const claim = await this.claimDbService.findOne(claimRequestId);
|
||||||
|
if (!claim) {
|
||||||
|
throw new NotFoundException("Claim file not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const blameFile = claim.blameFile as any;
|
||||||
|
if (!blameFile?.expertInitiated || blameFile?.creationMethod !== "IN_PERSON") {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"This endpoint is only for expert-initiated IN_PERSON claim files.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (String(blameFile.initiatedBy) !== expert.sub) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"Only the initiating field expert can complete claim data for this file.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dto.carPartDamage) {
|
||||||
|
if (claim.carPartDamage && (claim.carPartDamage as any[]).length > 0) {
|
||||||
|
throw new BadRequestException("Car part damage is already set.");
|
||||||
|
}
|
||||||
|
const trueItems = this.findTrueItems(dto.carPartDamage);
|
||||||
|
const claimDoc = (await this.claimDbService.findOne(
|
||||||
|
claimRequestId,
|
||||||
|
)) as any;
|
||||||
|
if (claimDoc) {
|
||||||
|
await this.setCarPartDamageAndImage(claimDoc, trueItems);
|
||||||
|
}
|
||||||
|
await this.claimDbService.findAndUpdate(claimRequestId, {
|
||||||
|
currentStep: ClaimStepsEnum.SelectDamagePart,
|
||||||
|
nextStep: ClaimStepsEnum.SelectOtherParts,
|
||||||
|
$push: { steps: ClaimStepsEnum.SelectDamagePart },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates: Record<string, any> = {};
|
||||||
|
if (dto.sheba != null) updates.sheba = dto.sheba;
|
||||||
|
if (dto.nationalCodeOfInsurer != null)
|
||||||
|
updates.nationalCodeOfInsurer = dto.nationalCodeOfInsurer;
|
||||||
|
if (dto.otherParts != null) updates.otherParts = dto.otherParts;
|
||||||
|
if (Object.keys(updates).length > 0) {
|
||||||
|
await this.claimDbService.findAndUpdate(claimRequestId, {
|
||||||
|
$set: updates,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, claimRequestId };
|
||||||
|
}
|
||||||
|
|
||||||
async selectCarOtherPartDamage(
|
async selectCarOtherPartDamage(
|
||||||
requestId: string,
|
requestId: string,
|
||||||
body: any, // Use your DTO for better type safety
|
body: any, // Use your DTO for better type safety
|
||||||
@@ -1186,10 +1259,12 @@ export class ClaimRequestManagementService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For experts, return IN_PERSON claim files they initiated
|
// For experts, return IN_PERSON claim files they initiated
|
||||||
if (
|
const expertRoles = [
|
||||||
currentUser.role === RoleEnum.EXPERT ||
|
RoleEnum.EXPERT,
|
||||||
currentUser.role === RoleEnum.DAMAGE_EXPERT
|
RoleEnum.DAMAGE_EXPERT,
|
||||||
) {
|
RoleEnum.FIELD_EXPERT,
|
||||||
|
];
|
||||||
|
if (expertRoles.includes(currentUser.role)) {
|
||||||
const allClaims = await this.claimDbService.findAllByAnyFilter({});
|
const allClaims = await this.claimDbService.findAllByAnyFilter({});
|
||||||
const expertClaims = allClaims.filter((claim) => {
|
const expertClaims = allClaims.filter((claim) => {
|
||||||
const blameFile = claim.blameFile;
|
const blameFile = claim.blameFile;
|
||||||
|
|||||||
32
src/request-management/dto/expert-complete-claim-data.dto.ts
Normal file
32
src/request-management/dto/expert-complete-claim-data.dto.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { CarDamagePartDto } from "src/claim-request-management/dto/car-part.dto";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO for field expert to submit claim-needed data in one go (IN_PERSON flow).
|
||||||
|
* All fields optional so expert can submit in one or two steps (e.g. car parts first, then sheba/other).
|
||||||
|
*/
|
||||||
|
export class ExpertCompleteClaimDataDto {
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Car part damage selection (same as selectCarPartDamage). Required for first claim data step.",
|
||||||
|
type: CarDamagePartDto,
|
||||||
|
})
|
||||||
|
carPartDamage?: CarDamagePartDto;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Sheba number",
|
||||||
|
example: "IR123456789012345678901234",
|
||||||
|
})
|
||||||
|
sheba?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "National code of insurer",
|
||||||
|
example: "1234567890",
|
||||||
|
})
|
||||||
|
nationalCodeOfInsurer?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Other parts / extra damage items",
|
||||||
|
example: [{ "حسگر درها": true }],
|
||||||
|
})
|
||||||
|
otherParts?: any[];
|
||||||
|
}
|
||||||
@@ -34,17 +34,83 @@ import { CreateExpertInitiatedFileDto } from "./dto/expert-initiated.dto";
|
|||||||
import { ExpertCompleteThirdPartyFormDto } from "./dto/expert-complete-third-party-form.dto";
|
import { ExpertCompleteThirdPartyFormDto } from "./dto/expert-complete-third-party-form.dto";
|
||||||
import { ExpertCompleteCarBodyFormDto } from "./dto/expert-complete-car-body-form.dto";
|
import { ExpertCompleteCarBodyFormDto } from "./dto/expert-complete-car-body-form.dto";
|
||||||
import { ExpertAccidentFieldsDto } from "./dto/expert-accident-fields.dto";
|
import { ExpertAccidentFieldsDto } from "./dto/expert-accident-fields.dto";
|
||||||
|
import { ExpertCompleteClaimDataDto } from "./dto/expert-complete-claim-data.dto";
|
||||||
|
import { ClaimRequestManagementService } from "src/claim-request-management/claim-request-management.service";
|
||||||
|
|
||||||
@ApiTags("expert-initiated-blame")
|
@ApiTags("expert-initiated-blame")
|
||||||
@Controller("expert-initiated-blame")
|
@Controller("expert-initiated-blame")
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
||||||
@Roles(RoleEnum.EXPERT, RoleEnum.DAMAGE_EXPERT)
|
@Roles(RoleEnum.FIELD_EXPERT)
|
||||||
export class ExpertInitiatedController {
|
export class ExpertInitiatedController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly requestManagementService: RequestManagementService,
|
private readonly requestManagementService: RequestManagementService,
|
||||||
|
private readonly claimRequestManagementService: ClaimRequestManagementService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@Get("my-files")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "List my expert-initiated files",
|
||||||
|
description:
|
||||||
|
"Returns all blame files created by the current field expert (their panel).",
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: "List of expert-initiated files" })
|
||||||
|
async getMyFiles(@CurrentUser() expert: any) {
|
||||||
|
return await this.requestManagementService.getMyExpertInitiatedFiles(
|
||||||
|
expert,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("complete-blame-data/:requestId")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Submit all blame-needed data",
|
||||||
|
description:
|
||||||
|
"Single endpoint to complete blame form: send THIRD_PARTY or CAR_BODY form data according to file type. " +
|
||||||
|
"Video and voice can be uploaded separately.",
|
||||||
|
})
|
||||||
|
@ApiParam({ name: "requestId", description: "ID of the expert-initiated file" })
|
||||||
|
@ApiBody({
|
||||||
|
description:
|
||||||
|
"Send ExpertCompleteThirdPartyFormDto for THIRD_PARTY files or ExpertCompleteCarBodyFormDto for CAR_BODY files (according to file type).",
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: "Blame form completed successfully" })
|
||||||
|
async completeBlameData(
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@Body() formData: any,
|
||||||
|
) {
|
||||||
|
return await this.requestManagementService.completeBlameData(
|
||||||
|
expert,
|
||||||
|
requestId,
|
||||||
|
formData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("complete-claim-data/:claimRequestId")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Submit claim-needed data",
|
||||||
|
description:
|
||||||
|
"Field expert fills claim data for expert-initiated IN_PERSON files (car part damage, sheba, national code, other parts). " +
|
||||||
|
"Only the initiating expert can call this.",
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: "claimRequestId",
|
||||||
|
description: "ID of the claim file (created from the expert-initiated blame file)",
|
||||||
|
})
|
||||||
|
@ApiBody({ type: ExpertCompleteClaimDataDto })
|
||||||
|
@ApiResponse({ status: 200, description: "Claim data updated successfully" })
|
||||||
|
async completeClaimData(
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
@Body() dto: ExpertCompleteClaimDataDto,
|
||||||
|
) {
|
||||||
|
return await this.claimRequestManagementService.expertCompleteClaimData(
|
||||||
|
claimRequestId,
|
||||||
|
expert,
|
||||||
|
dto,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Post("create")
|
@Post("create")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Create expert-initiated blame file",
|
summary: "Create expert-initiated blame file",
|
||||||
|
|||||||
@@ -2005,8 +2005,13 @@ export class RequestManagementService {
|
|||||||
request: RequestManagementModel,
|
request: RequestManagementModel,
|
||||||
user: any,
|
user: any,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// If user is not an expert, no verification needed (normal flow)
|
// If user is not an expert type, no verification needed (normal flow)
|
||||||
if (user.role !== RoleEnum.EXPERT && user.role !== RoleEnum.DAMAGE_EXPERT) {
|
const expertRoles = [
|
||||||
|
RoleEnum.EXPERT,
|
||||||
|
RoleEnum.DAMAGE_EXPERT,
|
||||||
|
RoleEnum.FIELD_EXPERT,
|
||||||
|
];
|
||||||
|
if (!expertRoles.includes(user.role)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2155,6 +2160,60 @@ export class RequestManagementService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all expert-initiated blame files for the current field expert (their panel).
|
||||||
|
* Only files where initiatedBy === current user are returned.
|
||||||
|
*/
|
||||||
|
async getMyExpertInitiatedFiles(expert: any): Promise<any[]> {
|
||||||
|
const expertId = new Types.ObjectId(expert.sub);
|
||||||
|
const files = await this.requestManagementDbService.findAll({
|
||||||
|
expertInitiated: true,
|
||||||
|
initiatedBy: expertId,
|
||||||
|
});
|
||||||
|
return (files || []).map((f) => ({
|
||||||
|
_id: f._id,
|
||||||
|
requestNumber: f.requestNumber,
|
||||||
|
type: f.type,
|
||||||
|
creationMethod: f.creationMethod,
|
||||||
|
blameStatus: f.blameStatus,
|
||||||
|
currentStep: f.currentStep,
|
||||||
|
nextStep: f.nextStep,
|
||||||
|
createdAt: f.createdAt,
|
||||||
|
steps: f.steps,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single endpoint handler: complete all blame-needed data.
|
||||||
|
* Routes to THIRD_PARTY or CAR_BODY completion based on request type.
|
||||||
|
*/
|
||||||
|
async completeBlameData(
|
||||||
|
expert: any,
|
||||||
|
requestId: string,
|
||||||
|
formData: any,
|
||||||
|
): Promise<RequestManagementDtoRs> {
|
||||||
|
const request = await this.requestManagementDbService.findOne(requestId);
|
||||||
|
if (!request) {
|
||||||
|
throw new NotFoundException("Request not found");
|
||||||
|
}
|
||||||
|
if (!request.expertInitiated) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"This endpoint is only for expert-initiated files",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await this.verifyExpertAccess(request, expert);
|
||||||
|
|
||||||
|
if (request.type === "THIRD_PARTY") {
|
||||||
|
return this.expertCompleteThirdPartyForm(expert, requestId, formData);
|
||||||
|
}
|
||||||
|
if (request.type === "CAR_BODY") {
|
||||||
|
return this.expertCompleteCarBodyForm(expert, requestId, formData);
|
||||||
|
}
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Unknown file type. Must be THIRD_PARTY or CAR_BODY.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expert completes all information for IN_PERSON THIRD_PARTY file at once
|
* Expert completes all information for IN_PERSON THIRD_PARTY file at once
|
||||||
* This replaces the step-by-step flow for experts filling files on-site
|
* This replaces the step-by-step flow for experts filling files on-site
|
||||||
|
|||||||
49
src/users/entities/db-service/field-expert.db.service.ts
Normal file
49
src/users/entities/db-service/field-expert.db.service.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { InjectModel } from "@nestjs/mongoose";
|
||||||
|
import { FilterQuery, Model, Types } from "mongoose";
|
||||||
|
import { FieldExpertModel } from "../schema/field-expert.schema";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FieldExpertDbService {
|
||||||
|
constructor(
|
||||||
|
@InjectModel(FieldExpertModel.name)
|
||||||
|
private readonly fieldExpertModel: Model<FieldExpertModel>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async create(
|
||||||
|
user: Partial<FieldExpertModel> & { password: string },
|
||||||
|
): Promise<FieldExpertModel> {
|
||||||
|
return await this.fieldExpertModel.create(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(
|
||||||
|
filter: FilterQuery<FieldExpertModel>,
|
||||||
|
): Promise<FieldExpertModel | null> {
|
||||||
|
return await this.fieldExpertModel.findOne(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(userId: string): Promise<FieldExpertModel | null> {
|
||||||
|
return await this.fieldExpertModel.findOne({
|
||||||
|
_id: new Types.ObjectId(userId),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneAndUpdate(
|
||||||
|
filter: FilterQuery<FieldExpertModel>,
|
||||||
|
update: FilterQuery<FieldExpertModel>,
|
||||||
|
) {
|
||||||
|
return await this.fieldExpertModel.findOneAndUpdate(filter, update, {
|
||||||
|
new: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(
|
||||||
|
filter: FilterQuery<FieldExpertModel>,
|
||||||
|
): Promise<(FieldExpertModel & { _id: Types.ObjectId })[]> {
|
||||||
|
return await this.fieldExpertModel.find(filter).lean();
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateOne(filter: FilterQuery<FieldExpertModel>, update: any) {
|
||||||
|
return this.fieldExpertModel.updateOne(filter, update);
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/users/entities/schema/field-expert.schema.ts
Normal file
78
src/users/entities/schema/field-expert.schema.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
|
import { Types } from "mongoose";
|
||||||
|
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class RequestStats {
|
||||||
|
@Prop({ default: 0 })
|
||||||
|
totalHandled: number;
|
||||||
|
|
||||||
|
@Prop({ default: 0 })
|
||||||
|
totalChecked: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RequestStatsSchema = SchemaFactory.createForClass(RequestStats);
|
||||||
|
|
||||||
|
@Schema({
|
||||||
|
collection: "field-expert",
|
||||||
|
versionKey: false,
|
||||||
|
timestamps: true,
|
||||||
|
})
|
||||||
|
export class FieldExpertModel {
|
||||||
|
_id?: Types.ObjectId;
|
||||||
|
|
||||||
|
@Prop({ required: true })
|
||||||
|
firstName: string;
|
||||||
|
|
||||||
|
@Prop({ required: true })
|
||||||
|
lastName: string;
|
||||||
|
|
||||||
|
@Prop({ type: "string", unique: true })
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@Prop({ type: "string" })
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@Prop({ required: true })
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
@Prop({ required: true })
|
||||||
|
mobile: string;
|
||||||
|
|
||||||
|
@Prop({ required: false })
|
||||||
|
phone?: string;
|
||||||
|
|
||||||
|
@Prop({ default: RoleEnum.FIELD_EXPERT })
|
||||||
|
role: RoleEnum;
|
||||||
|
|
||||||
|
@Prop({ type: "string", default: "" })
|
||||||
|
otp: string;
|
||||||
|
|
||||||
|
@Prop({ type: RequestStatsSchema, default: () => ({}) })
|
||||||
|
requestStats: RequestStats;
|
||||||
|
|
||||||
|
@Prop({
|
||||||
|
type: [{ type: String }],
|
||||||
|
default: [],
|
||||||
|
validate: {
|
||||||
|
validator: (arr: any[]) => arr.every((val) => typeof val === "string"),
|
||||||
|
message: "All countedRequests must be strings",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
countedRequests?: string[];
|
||||||
|
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FieldExpertDbSchema =
|
||||||
|
SchemaFactory.createForClass(FieldExpertModel);
|
||||||
|
|
||||||
|
FieldExpertDbSchema.pre("save", function (next) {
|
||||||
|
if (!this.username) {
|
||||||
|
this.username = this.email;
|
||||||
|
}
|
||||||
|
if (!this.requestStats) {
|
||||||
|
this.requestStats = { totalChecked: 0, totalHandled: 0 };
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
@@ -5,6 +5,7 @@ import { ExpertModel } from "src/users/entities/schema/expert.schema";
|
|||||||
import { HashModule } from "src/utils/hash/hash.module";
|
import { HashModule } from "src/utils/hash/hash.module";
|
||||||
import { OtpModule } from "src/utils/otp/otp.module";
|
import { OtpModule } from "src/utils/otp/otp.module";
|
||||||
import { ExpertDbService } from "./entities/db-service/expert.db.service";
|
import { ExpertDbService } from "./entities/db-service/expert.db.service";
|
||||||
|
import { FieldExpertDbService } from "./entities/db-service/field-expert.db.service";
|
||||||
import { InsurerExpertDbService } from "./entities/db-service/insurer-expert.db.service";
|
import { InsurerExpertDbService } from "./entities/db-service/insurer-expert.db.service";
|
||||||
import { UserDbService } from "./entities/db-service/user.db.service";
|
import { UserDbService } from "./entities/db-service/user.db.service";
|
||||||
import {
|
import {
|
||||||
@@ -12,6 +13,10 @@ import {
|
|||||||
DamageExpertModel,
|
DamageExpertModel,
|
||||||
} from "./entities/schema/damage-expert.schema";
|
} from "./entities/schema/damage-expert.schema";
|
||||||
import { ExpertDbSchema } from "./entities/schema/expert.schema";
|
import { ExpertDbSchema } from "./entities/schema/expert.schema";
|
||||||
|
import {
|
||||||
|
FieldExpertDbSchema,
|
||||||
|
FieldExpertModel,
|
||||||
|
} from "./entities/schema/field-expert.schema";
|
||||||
import {
|
import {
|
||||||
InsurerExpertDbSchema,
|
InsurerExpertDbSchema,
|
||||||
InsurerExpertModel,
|
InsurerExpertModel,
|
||||||
@@ -24,6 +29,7 @@ import { UserModel, UserDbSchema } from "./entities/schema/user.schema";
|
|||||||
{ name: UserModel.name, schema: UserDbSchema },
|
{ name: UserModel.name, schema: UserDbSchema },
|
||||||
{ name: DamageExpertModel.name, schema: DamageExpertDbSchema },
|
{ name: DamageExpertModel.name, schema: DamageExpertDbSchema },
|
||||||
{ name: ExpertModel.name, schema: ExpertDbSchema },
|
{ name: ExpertModel.name, schema: ExpertDbSchema },
|
||||||
|
{ name: FieldExpertModel.name, schema: FieldExpertDbSchema },
|
||||||
{ name: InsurerExpertModel.name, schema: InsurerExpertDbSchema },
|
{ name: InsurerExpertModel.name, schema: InsurerExpertDbSchema },
|
||||||
]),
|
]),
|
||||||
OtpModule,
|
OtpModule,
|
||||||
@@ -34,12 +40,14 @@ import { UserModel, UserDbSchema } from "./entities/schema/user.schema";
|
|||||||
UserDbService,
|
UserDbService,
|
||||||
ExpertDbService,
|
ExpertDbService,
|
||||||
DamageExpertDbService,
|
DamageExpertDbService,
|
||||||
|
FieldExpertDbService,
|
||||||
InsurerExpertDbService,
|
InsurerExpertDbService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
UserDbService,
|
UserDbService,
|
||||||
ExpertDbService,
|
ExpertDbService,
|
||||||
DamageExpertDbService,
|
DamageExpertDbService,
|
||||||
|
FieldExpertDbService,
|
||||||
InsurerExpertDbService,
|
InsurerExpertDbService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user