forked from Yara724/api
Compare commits
17 Commits
integrate-
...
b216a363fb
| Author | SHA1 | Date | |
|---|---|---|---|
| b216a363fb | |||
|
|
b2391751f0 | ||
| df6d2bd04d | |||
| f277d87dfa | |||
| 672ec25438 | |||
| aa871c86f6 | |||
| 553a34054c | |||
| 8af152abc0 | |||
| 0446c83b36 | |||
| 255cfd3eb3 | |||
| b1be5b1e09 | |||
| 7d3565ec51 | |||
| fc599acf4a | |||
|
|
b40270f058 | ||
|
|
8d6fa3daac | ||
| 64dfd1ca8a | |||
| 665ed9e70f |
2848
package-lock.json
generated
2848
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -44,6 +44,7 @@
|
|||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
|
"express": "^4.22.1",
|
||||||
"express-basic-auth": "^1.2.1",
|
"express-basic-auth": "^1.2.1",
|
||||||
"fastest-levenshtein": "^1.0.16",
|
"fastest-levenshtein": "^1.0.16",
|
||||||
"form-data": "^4.0.2",
|
"form-data": "^4.0.2",
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ export enum WorkflowStep {
|
|||||||
|
|
||||||
CREATED = "CREATED",
|
CREATED = "CREATED",
|
||||||
|
|
||||||
|
// ---------- CAR_BODY only (no confession; accident type form) ----------
|
||||||
|
CAR_BODY_ACCIDENT_TYPE = "CAR_BODY_ACCIDENT_TYPE",
|
||||||
|
|
||||||
// ---------- First party ----------
|
// ---------- First party ----------
|
||||||
FIRST_BLAME_CONFESSION = "FIRST_BLAME_CONFESSION",
|
FIRST_BLAME_CONFESSION = "FIRST_BLAME_CONFESSION",
|
||||||
FIRST_VIDEO = "FIRST_VIDEO",
|
FIRST_VIDEO = "FIRST_VIDEO",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export enum RoleEnum {
|
|||||||
EXPERT = "expert",
|
EXPERT = "expert",
|
||||||
DAMAGE_EXPERT = "damage_expert",
|
DAMAGE_EXPERT = "damage_expert",
|
||||||
FIELD_EXPERT = "field_expert",
|
FIELD_EXPERT = "field_expert",
|
||||||
|
REGISTRAR = "registrar",
|
||||||
COMPANY = "company",
|
COMPANY = "company",
|
||||||
ADMIN = "admin",
|
ADMIN = "admin",
|
||||||
USER = "user",
|
USER = "user",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
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 { CreateFieldExpertDto } from "src/auth/dto/actor/create-field-expert.actor.dto";
|
||||||
|
import { CreateRegistrarDto } from "src/auth/dto/actor/create-registrar.actor.dto";
|
||||||
import {
|
import {
|
||||||
GenuineRegisterDto,
|
GenuineRegisterDto,
|
||||||
InsurerRegisterDto,
|
InsurerRegisterDto,
|
||||||
@@ -64,6 +65,14 @@ export class ActorAuthController {
|
|||||||
return await this.actorAuthService.createFieldExpertMock(body);
|
return await this.actorAuthService.createFieldExpertMock(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Mock: create a registrar for testing. Make private later. */
|
||||||
|
@Post("create-registrar")
|
||||||
|
@ApiBody({ type: CreateRegistrarDto })
|
||||||
|
@ApiAcceptedResponse()
|
||||||
|
async createRegistrar(@Body() body: CreateRegistrarDto) {
|
||||||
|
return await this.actorAuthService.createRegistrarMock(body);
|
||||||
|
}
|
||||||
|
|
||||||
@UseGuards(LocalActorAuthGuard)
|
@UseGuards(LocalActorAuthGuard)
|
||||||
@Post("login")
|
@Post("login")
|
||||||
@Roles()
|
@Roles()
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { InsurerExpertDbService } from "src/users/entities/db-service/insurer-ex
|
|||||||
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 { FieldExpertDbService } from "src/users/entities/db-service/field-expert.db.service";
|
||||||
|
import { RegistrarDbService } from "src/users/entities/db-service/registrar.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";
|
||||||
@@ -46,6 +47,7 @@ export class ActorAuthService {
|
|||||||
private readonly expertDbService: ExpertDbService,
|
private readonly expertDbService: ExpertDbService,
|
||||||
private readonly damageExpertDbService: DamageExpertDbService,
|
private readonly damageExpertDbService: DamageExpertDbService,
|
||||||
private readonly fieldExpertDbService: FieldExpertDbService,
|
private readonly fieldExpertDbService: FieldExpertDbService,
|
||||||
|
private readonly registrarDbService: RegistrarDbService,
|
||||||
private readonly insurerExpertDbService: InsurerExpertDbService,
|
private readonly insurerExpertDbService: InsurerExpertDbService,
|
||||||
// private readonly mailService: MailService, // Mailer disabled – not used
|
// private readonly mailService: MailService, // Mailer disabled – not used
|
||||||
private readonly clientDbService: ClientDbService,
|
private readonly clientDbService: ClientDbService,
|
||||||
@@ -79,6 +81,13 @@ export class ActorAuthService {
|
|||||||
else
|
else
|
||||||
res = await this.fieldExpertDbService.findOne({ email: username });
|
res = await this.fieldExpertDbService.findOne({ email: username });
|
||||||
break;
|
break;
|
||||||
|
case RoleEnum.REGISTRAR:
|
||||||
|
if (username == null && userId)
|
||||||
|
res = await this.registrarDbService.findOne({
|
||||||
|
_id: new Types.ObjectId(userId),
|
||||||
|
});
|
||||||
|
else res = await this.registrarDbService.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;
|
||||||
@@ -273,6 +282,10 @@ export class ActorAuthService {
|
|||||||
actor = await this.fieldExpertDbService.findOne(filter);
|
actor = await this.fieldExpertDbService.findOne(filter);
|
||||||
dbServiceToUpdate = this.fieldExpertDbService as any;
|
dbServiceToUpdate = this.fieldExpertDbService as any;
|
||||||
}
|
}
|
||||||
|
if (!actor) {
|
||||||
|
actor = await this.registrarDbService.findOne(filter);
|
||||||
|
dbServiceToUpdate = this.registrarDbService as any;
|
||||||
|
}
|
||||||
|
|
||||||
if (!actor) {
|
if (!actor) {
|
||||||
throw new NotFoundException("actor not found");
|
throw new NotFoundException("actor not found");
|
||||||
@@ -309,6 +322,10 @@ export class ActorAuthService {
|
|||||||
userExist = await this.fieldExpertDbService.findOne({ email });
|
userExist = await this.fieldExpertDbService.findOne({ email });
|
||||||
dbServiceToUpdate = this.fieldExpertDbService;
|
dbServiceToUpdate = this.fieldExpertDbService;
|
||||||
}
|
}
|
||||||
|
if (!userExist) {
|
||||||
|
userExist = await this.registrarDbService.findOne({ email });
|
||||||
|
dbServiceToUpdate = this.registrarDbService as any;
|
||||||
|
}
|
||||||
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");
|
||||||
@@ -383,6 +400,7 @@ export class ActorAuthService {
|
|||||||
"phone",
|
"phone",
|
||||||
"mobile",
|
"mobile",
|
||||||
],
|
],
|
||||||
|
registrar: ["email"],
|
||||||
};
|
};
|
||||||
|
|
||||||
const allowedFields = allowedFieldsByRole[role];
|
const allowedFields = allowedFieldsByRole[role];
|
||||||
@@ -498,4 +516,34 @@ export class ActorAuthService {
|
|||||||
throw er;
|
throw er;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createRegistrarMock(body: {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
clientId: string;
|
||||||
|
}) {
|
||||||
|
const hashPassword = await this.hashService.hash(body.password);
|
||||||
|
const payload = {
|
||||||
|
email: body.email.toLowerCase().trim(),
|
||||||
|
password: hashPassword,
|
||||||
|
clientKey: new Types.ObjectId(body.clientId),
|
||||||
|
role: RoleEnum.REGISTRAR,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const created = await this.registrarDbService.create(payload as any);
|
||||||
|
return {
|
||||||
|
id: (created as any)._id,
|
||||||
|
email: (created as any).email,
|
||||||
|
clientKey: (created as any).clientKey,
|
||||||
|
role: (created as any).role,
|
||||||
|
};
|
||||||
|
} catch (er) {
|
||||||
|
if (er.code === 11000) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"A registrar with this email already exists.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw er;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,12 @@ export class UserAuthService {
|
|||||||
});
|
});
|
||||||
const otp = this.otpCreator.create();
|
const otp = this.otpCreator.create();
|
||||||
const hashOtp = await this.hashService.hash(otp);
|
const hashOtp = await this.hashService.hash(otp);
|
||||||
|
const rawExpireMinutes = Number(process.env.EXP_OTP_TIME ?? "2");
|
||||||
|
const expireMinutes =
|
||||||
|
Number.isFinite(rawExpireMinutes) && rawExpireMinutes > 0
|
||||||
|
? rawExpireMinutes
|
||||||
|
: 2;
|
||||||
|
const otpExpire = Date.now() + expireMinutes * 60 * 1000;
|
||||||
if (!userExist) {
|
if (!userExist) {
|
||||||
await this.smsSender(otp, mobile);
|
await this.smsSender(otp, mobile);
|
||||||
/// create otp request
|
/// create otp request
|
||||||
@@ -90,9 +96,7 @@ export class UserAuthService {
|
|||||||
city: "",
|
city: "",
|
||||||
address: "",
|
address: "",
|
||||||
state: "",
|
state: "",
|
||||||
otpExpire: new Date(
|
otpExpire,
|
||||||
new Date().getTime() + +process.env.EXP_OTP_TIME * 60 * 1000,
|
|
||||||
).getTime(),
|
|
||||||
});
|
});
|
||||||
return new LoginDtoRs(newUser);
|
return new LoginDtoRs(newUser);
|
||||||
}
|
}
|
||||||
@@ -105,9 +109,7 @@ export class UserAuthService {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
otp: hashOtp,
|
otp: hashOtp,
|
||||||
otpExpire: new Date(
|
otpExpire,
|
||||||
new Date().getTime() + +process.env.EXP_OTP_TIME * 60 * 1000,
|
|
||||||
).getTime(),
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (updateTokens) return new LoginDtoRs(userExist);
|
if (updateTokens) return new LoginDtoRs(userExist);
|
||||||
|
|||||||
18
src/auth/dto/actor/create-registrar.actor.dto.ts
Normal file
18
src/auth/dto/actor/create-registrar.actor.dto.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsEmail, IsMongoId, IsString, MinLength } from "class-validator";
|
||||||
|
|
||||||
|
export class CreateRegistrarDto {
|
||||||
|
@ApiProperty({ example: "registrar@sample.com" })
|
||||||
|
@IsEmail()
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: "securePassword123", minLength: 6 })
|
||||||
|
@IsString()
|
||||||
|
@MinLength(6)
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: "507f1f77bcf86cd799439011" })
|
||||||
|
@IsMongoId()
|
||||||
|
clientId: string;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -44,6 +44,7 @@ export class LocalActorAuthGuard extends AuthGuard("actor") {
|
|||||||
RoleEnum.DAMAGE_EXPERT,
|
RoleEnum.DAMAGE_EXPERT,
|
||||||
RoleEnum.COMPANY,
|
RoleEnum.COMPANY,
|
||||||
RoleEnum.FIELD_EXPERT,
|
RoleEnum.FIELD_EXPERT,
|
||||||
|
RoleEnum.REGISTRAR,
|
||||||
].includes(payload.role)
|
].includes(payload.role)
|
||||||
) {
|
) {
|
||||||
throw new UnauthorizedException("User role is not authorized");
|
throw new UnauthorizedException("User role is not authorized");
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { RequestManagementModule } from "src/request-management/request-manageme
|
|||||||
import { UsersModule } from "src/users/users.module";
|
import { UsersModule } from "src/users/users.module";
|
||||||
import { ClaimRequestManagementController } from "./claim-request-management.controller";
|
import { ClaimRequestManagementController } from "./claim-request-management.controller";
|
||||||
import { ClaimRequestManagementV2Controller } from "./claim-request-management.v2.controller";
|
import { ClaimRequestManagementV2Controller } from "./claim-request-management.v2.controller";
|
||||||
|
import { RegistrarClaimV1Controller } from "./registrar-claim.v1.controller";
|
||||||
import { ClaimRequestManagementService } from "./claim-request-management.service";
|
import { ClaimRequestManagementService } from "./claim-request-management.service";
|
||||||
import { CarGreenCardDbService } from "./entites/db-service/car-green-card.db.service";
|
import { CarGreenCardDbService } from "./entites/db-service/car-green-card.db.service";
|
||||||
import { ClaimRequestManagementDbService } from "./entites/db-service/claim-request-management.db.service";
|
import { ClaimRequestManagementDbService } from "./entites/db-service/claim-request-management.db.service";
|
||||||
@@ -85,7 +86,11 @@ import { JwtModule } from "@nestjs/jwt";
|
|||||||
ClaimRequiredDocumentDbService,
|
ClaimRequiredDocumentDbService,
|
||||||
ClaimAccessGuard,
|
ClaimAccessGuard,
|
||||||
],
|
],
|
||||||
controllers: [ClaimRequestManagementController, ClaimRequestManagementV2Controller],
|
controllers: [
|
||||||
|
ClaimRequestManagementController,
|
||||||
|
ClaimRequestManagementV2Controller,
|
||||||
|
RegistrarClaimV1Controller,
|
||||||
|
],
|
||||||
exports: [
|
exports: [
|
||||||
ClaimRequestManagementService,
|
ClaimRequestManagementService,
|
||||||
ClaimRequestManagementDbService,
|
ClaimRequestManagementDbService,
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import { ClaimRequiredDocumentType, CarAngle } from "src/Types&Enums/claim-reque
|
|||||||
import { ClaimStatus } from "src/Types&Enums/claim-request-management/claimStatus.enum";
|
import { ClaimStatus } from "src/Types&Enums/claim-request-management/claimStatus.enum";
|
||||||
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
|
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
|
||||||
import { CaseStatus as BlameCaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
|
import { CaseStatus as BlameCaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
|
||||||
|
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||||
import { ClaimSignDbService } from "./entites/db-service/claim-sign.db.service";
|
import { ClaimSignDbService } from "./entites/db-service/claim-sign.db.service";
|
||||||
import { DamageImageDbService } from "./entites/db-service/damage-image.db.service";
|
import { DamageImageDbService } from "./entites/db-service/damage-image.db.service";
|
||||||
import { ClaimFactorsImageDbService } from "./entites/db-service/factor-image.db.service";
|
import { ClaimFactorsImageDbService } from "./entites/db-service/factor-image.db.service";
|
||||||
@@ -556,11 +557,10 @@ export class ClaimRequestManagementService {
|
|||||||
const isCarBody = claimRequest.blameFile?.type === "CAR_BODY";
|
const isCarBody = claimRequest.blameFile?.type === "CAR_BODY";
|
||||||
|
|
||||||
if (isCarBody) {
|
if (isCarBody) {
|
||||||
// Check if accident is with another object (not a car)
|
// Accident with object (not another car): v2 uses parties[0].carBodyFirstForm.object, v1 uses carBodyForm.carBodyForm.car === false
|
||||||
// This can be determined by:
|
const firstPartyCarBody = claimRequest.blameFile?.parties?.[0]?.carBodyFirstForm;
|
||||||
// 1. carBodyForm.carBodyForm.car === false (explicitly set to false)
|
|
||||||
// 2. OR no secondPartyCarDetail exists (no second party car)
|
|
||||||
const isAccidentWithOtherObject =
|
const isAccidentWithOtherObject =
|
||||||
|
(firstPartyCarBody && firstPartyCarBody.object === true) ||
|
||||||
claimRequest.blameFile?.carBodyForm?.carBodyForm?.car === false ||
|
claimRequest.blameFile?.carBodyForm?.carBodyForm?.car === false ||
|
||||||
!claimRequest.blameFile?.secondPartyDetails?.secondPartyCarDetail;
|
!claimRequest.blameFile?.secondPartyDetails?.secondPartyCarDetail;
|
||||||
|
|
||||||
@@ -2734,44 +2734,68 @@ export class ClaimRequestManagementService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Validate expert decision exists
|
|
||||||
if (!blameRequest.expert?.decision) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
"Cannot create claim until expert has made a decision",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const guiltyPartyId = String(blameRequest.expert.decision.guiltyPartyId);
|
|
||||||
|
|
||||||
// 4. Validate both parties have signed and accepted
|
|
||||||
const parties = blameRequest.parties || [];
|
const parties = blameRequest.parties || [];
|
||||||
if (parties.length < 2) {
|
const isCarBody = blameRequest.type === BlameRequestType.CAR_BODY;
|
||||||
throw new BadRequestException(
|
|
||||||
"Both parties must be present in the blame request",
|
if (isCarBody) {
|
||||||
|
// CAR_BODY: single party, no expert; first party is the damaged one who creates the claim
|
||||||
|
if (parties.length < 1) {
|
||||||
|
throw new BadRequestException("Blame request has no party");
|
||||||
|
}
|
||||||
|
const firstParty = parties.find((p) => p.role === "FIRST");
|
||||||
|
if (!firstParty || String(firstParty.person?.userId) !== currentUserId) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"Only the first party (damaged) can create a claim from this CAR_BODY blame request",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// THIRD_PARTY: require expert decision and both parties signed
|
||||||
|
if (!blameRequest.expert?.decision) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Cannot create claim until expert has made a decision",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const guiltyPartyId = String(blameRequest.expert.decision.guiltyPartyId);
|
||||||
|
|
||||||
|
if (parties.length < 2) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Both parties must be present in the blame request",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allPartiesSigned = parties.every(
|
||||||
|
(p) => p.confirmation !== undefined && p.confirmation !== null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!allPartiesSigned) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Both parties must sign the expert decision before creating a claim",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allPartiesAccepted = parties.every(
|
||||||
|
(p) => p.confirmation?.accepted === true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!allPartiesAccepted) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Both parties must accept the expert decision. If rejected, in-person resolution is required.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const guiltyParty = parties.find((p) => {
|
||||||
|
return String(p.person?.userId) === guiltyPartyId;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (guiltyParty && String(guiltyParty.person?.userId) === currentUserId) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"You cannot create a claim as you are the guilty party",
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const allPartiesSigned = parties.every(
|
// 5. Validate current user is a party (and for THIRD_PARTY not the guilty one)
|
||||||
(p) => p.confirmation !== undefined && p.confirmation !== null,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!allPartiesSigned) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
"Both parties must sign the expert decision before creating a claim",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const allPartiesAccepted = parties.every(
|
|
||||||
(p) => p.confirmation?.accepted === true,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!allPartiesAccepted) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
"Both parties must accept the expert decision. If rejected, in-person resolution is required.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Validate current user is the DAMAGED party (NOT the guilty party)
|
|
||||||
const currentUserParty = parties.find(
|
const currentUserParty = parties.find(
|
||||||
(p) => String(p.person?.userId) === currentUserId,
|
(p) => String(p.person?.userId) === currentUserId,
|
||||||
);
|
);
|
||||||
@@ -2782,24 +2806,6 @@ export class ClaimRequestManagementService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentUserIsGuilty = parties.some(
|
|
||||||
(p) =>
|
|
||||||
String(p.person?.userId) === currentUserId &&
|
|
||||||
String((p as any)._id || p.person?.userId) === guiltyPartyId,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Better check: compare guiltyPartyId with person.userId of each party
|
|
||||||
const guiltyParty = parties.find((p) => {
|
|
||||||
// guiltyPartyId could be userId or party identifier
|
|
||||||
return String(p.person?.userId) === guiltyPartyId;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (guiltyParty && String(guiltyParty.person?.userId) === currentUserId) {
|
|
||||||
throw new ForbiddenException(
|
|
||||||
"You cannot create a claim as you are the guilty party",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Validate no existing claim for this blame
|
// 6. Validate no existing claim for this blame
|
||||||
const existingClaim = await this.claimCaseDbService.findOne({
|
const existingClaim = await this.claimCaseDbService.findOne({
|
||||||
blameRequestId: new Types.ObjectId(blameRequestId),
|
blameRequestId: new Types.ObjectId(blameRequestId),
|
||||||
@@ -2874,6 +2880,246 @@ export class ClaimRequestManagementService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2: Field expert creates a claim from an expert-initiated IN_PERSON completed blame.
|
||||||
|
* The claim owner is set to the damaged party (first for CAR_BODY, non-guilty for THIRD_PARTY).
|
||||||
|
*/
|
||||||
|
async createClaimFromBlameForExpertV2(
|
||||||
|
blameRequestId: string,
|
||||||
|
expert: { sub: string; firstName?: string; lastName?: string },
|
||||||
|
): Promise<CreateClaimFromBlameResponseDto> {
|
||||||
|
const blameRequest = await this.blameRequestDbService.findById(blameRequestId);
|
||||||
|
if (!blameRequest) {
|
||||||
|
throw new NotFoundException("Blame request not found");
|
||||||
|
}
|
||||||
|
if (blameRequest.status !== BlameCaseStatus.COMPLETED) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Blame request must be COMPLETED. Current status: ${blameRequest.status}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!blameRequest.expertInitiated ||
|
||||||
|
blameRequest.creationMethod !== "IN_PERSON" ||
|
||||||
|
!blameRequest.initiatedByFieldExpertId
|
||||||
|
) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"This endpoint is only for expert-initiated IN_PERSON blame files.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (String(blameRequest.initiatedByFieldExpertId) !== String(expert.sub)) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"Only the field expert who created this blame file can create the claim.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const parties = blameRequest.parties || [];
|
||||||
|
const isCarBody = blameRequest.type === BlameRequestType.CAR_BODY;
|
||||||
|
let damagedUserId: string;
|
||||||
|
if (isCarBody) {
|
||||||
|
if (parties.length < 1) throw new BadRequestException("Blame request has no party");
|
||||||
|
const first = parties.find((p) => p.role === "FIRST");
|
||||||
|
if (!first?.person?.userId) throw new BadRequestException("First party has no userId");
|
||||||
|
damagedUserId = String(first.person.userId);
|
||||||
|
} else {
|
||||||
|
const guiltyPartyId = blameRequest.expert?.decision?.guiltyPartyId
|
||||||
|
? String(blameRequest.expert.decision.guiltyPartyId)
|
||||||
|
: null;
|
||||||
|
if (!guiltyPartyId) throw new BadRequestException("Blame request has no guilty party set");
|
||||||
|
const damagedParty = parties.find(
|
||||||
|
(p) => p.person?.userId && String(p.person.userId) !== guiltyPartyId,
|
||||||
|
);
|
||||||
|
if (!damagedParty?.person?.userId) {
|
||||||
|
throw new BadRequestException("Could not determine damaged party");
|
||||||
|
}
|
||||||
|
damagedUserId = String(damagedParty.person.userId);
|
||||||
|
}
|
||||||
|
const existingClaim = await this.claimCaseDbService.findOne({
|
||||||
|
blameRequestId: new Types.ObjectId(blameRequestId),
|
||||||
|
});
|
||||||
|
if (existingClaim) {
|
||||||
|
throw new ConflictException("A claim for this blame case already exists");
|
||||||
|
}
|
||||||
|
const claimNo = await this.generateUniqueClaimNumber();
|
||||||
|
const damagedParty = parties.find(
|
||||||
|
(p) => p.person?.userId && String(p.person.userId) === damagedUserId,
|
||||||
|
);
|
||||||
|
const newClaim = await this.claimCaseDbService.create({
|
||||||
|
requestNo: claimNo,
|
||||||
|
publicId: blameRequest.publicId,
|
||||||
|
blameRequestId: new Types.ObjectId(blameRequestId),
|
||||||
|
blameRequestNo: blameRequest.requestNo,
|
||||||
|
status: ClaimCaseStatus.SELECTING_OUTER_PARTS,
|
||||||
|
claimStatus: ClaimStatus.PENDING,
|
||||||
|
workflow: {
|
||||||
|
currentStep: ClaimWorkflowStep.SELECT_OUTER_PARTS,
|
||||||
|
nextStep: ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
||||||
|
completedSteps: [ClaimWorkflowStep.CLAIM_CREATED],
|
||||||
|
locked: false,
|
||||||
|
},
|
||||||
|
owner: {
|
||||||
|
userId: new Types.ObjectId(damagedUserId),
|
||||||
|
userRole: damagedParty?.role as any,
|
||||||
|
},
|
||||||
|
history: [
|
||||||
|
{
|
||||||
|
type: "CLAIM_CREATED",
|
||||||
|
actor: {
|
||||||
|
actorId: new Types.ObjectId(expert.sub),
|
||||||
|
actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(),
|
||||||
|
actorType: "field_expert",
|
||||||
|
},
|
||||||
|
timestamp: new Date(),
|
||||||
|
metadata: {
|
||||||
|
blameRequestId,
|
||||||
|
blamePublicId: blameRequest.publicId,
|
||||||
|
createdByExpert: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as any);
|
||||||
|
this.logger.log(
|
||||||
|
`Claim created by field expert: ${newClaim._id} from blame: ${blameRequestId}`,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
claimRequestId: String(newClaim._id),
|
||||||
|
publicId: blameRequest.publicId,
|
||||||
|
status: ClaimCaseStatus.SELECTING_OUTER_PARTS,
|
||||||
|
message: "Claim created successfully. You can now fill claim data on behalf of the damaged party.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve effective user id for claim operations.
|
||||||
|
* For FIELD_EXPERT acting on expert-initiated IN_PERSON claim, returns the claim owner's id; otherwise returns currentUserId.
|
||||||
|
*/
|
||||||
|
private async resolveClaimEffectiveUserId(
|
||||||
|
claimCase: any,
|
||||||
|
currentUserId: string,
|
||||||
|
actorRole?: string,
|
||||||
|
): Promise<string> {
|
||||||
|
if (
|
||||||
|
![RoleEnum.FIELD_EXPERT, RoleEnum.REGISTRAR].includes(actorRole as any) ||
|
||||||
|
!claimCase?.blameRequestId
|
||||||
|
) {
|
||||||
|
return currentUserId;
|
||||||
|
}
|
||||||
|
const blameRequest = await this.blameRequestDbService.findById(
|
||||||
|
claimCase.blameRequestId.toString(),
|
||||||
|
);
|
||||||
|
if (!blameRequest || blameRequest.creationMethod !== "IN_PERSON") return currentUserId;
|
||||||
|
if (actorRole === RoleEnum.FIELD_EXPERT) {
|
||||||
|
if (
|
||||||
|
!blameRequest.expertInitiated ||
|
||||||
|
!blameRequest.initiatedByFieldExpertId ||
|
||||||
|
String(blameRequest.initiatedByFieldExpertId) !== currentUserId
|
||||||
|
) {
|
||||||
|
return currentUserId;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
!blameRequest.registrarInitiated ||
|
||||||
|
!blameRequest.initiatedByRegistrarId ||
|
||||||
|
String(blameRequest.initiatedByRegistrarId) !== currentUserId
|
||||||
|
) {
|
||||||
|
return currentUserId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return claimCase.owner?.userId ? String(claimCase.owner.userId) : currentUserId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createClaimFromBlameForRegistrarV1(
|
||||||
|
blameRequestId: string,
|
||||||
|
registrar: { sub: string; firstName?: string; lastName?: string },
|
||||||
|
): Promise<CreateClaimFromBlameResponseDto> {
|
||||||
|
const blameRequest = await this.blameRequestDbService.findById(blameRequestId);
|
||||||
|
if (!blameRequest) throw new NotFoundException("Blame request not found");
|
||||||
|
if (blameRequest.status !== BlameCaseStatus.COMPLETED) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Blame request must be COMPLETED. Current status: ${blameRequest.status}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!blameRequest.registrarInitiated ||
|
||||||
|
blameRequest.creationMethod !== "IN_PERSON" ||
|
||||||
|
!blameRequest.initiatedByRegistrarId
|
||||||
|
) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"This endpoint is only for registrar-initiated IN_PERSON blame files.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (String(blameRequest.initiatedByRegistrarId) !== String(registrar.sub)) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"Only the registrar who created this blame file can create the claim.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const parties = blameRequest.parties || [];
|
||||||
|
const isCarBody = blameRequest.type === BlameRequestType.CAR_BODY;
|
||||||
|
let damagedUserId: string;
|
||||||
|
if (isCarBody) {
|
||||||
|
const first = parties.find((p) => p.role === "FIRST");
|
||||||
|
if (!first?.person?.userId) throw new BadRequestException("First party has no userId");
|
||||||
|
damagedUserId = String(first.person.userId);
|
||||||
|
} else {
|
||||||
|
const guiltyPartyId = blameRequest.expert?.decision?.guiltyPartyId
|
||||||
|
? String(blameRequest.expert.decision.guiltyPartyId)
|
||||||
|
: null;
|
||||||
|
if (!guiltyPartyId) throw new BadRequestException("Blame request has no guilty party set");
|
||||||
|
const damagedParty = parties.find(
|
||||||
|
(p) => p.person?.userId && String(p.person.userId) !== guiltyPartyId,
|
||||||
|
);
|
||||||
|
if (!damagedParty?.person?.userId) throw new BadRequestException("Could not determine damaged party");
|
||||||
|
damagedUserId = String(damagedParty.person.userId);
|
||||||
|
}
|
||||||
|
const existingClaim = await this.claimCaseDbService.findOne({
|
||||||
|
blameRequestId: new Types.ObjectId(blameRequestId),
|
||||||
|
});
|
||||||
|
if (existingClaim) throw new ConflictException("A claim for this blame case already exists");
|
||||||
|
const claimNo = await this.generateUniqueClaimNumber();
|
||||||
|
const damagedParty = parties.find(
|
||||||
|
(p) => p.person?.userId && String(p.person.userId) === damagedUserId,
|
||||||
|
);
|
||||||
|
const newClaim = await this.claimCaseDbService.create({
|
||||||
|
requestNo: claimNo,
|
||||||
|
publicId: blameRequest.publicId,
|
||||||
|
blameRequestId: new Types.ObjectId(blameRequestId),
|
||||||
|
blameRequestNo: blameRequest.requestNo,
|
||||||
|
status: ClaimCaseStatus.SELECTING_OUTER_PARTS,
|
||||||
|
claimStatus: ClaimStatus.PENDING,
|
||||||
|
workflow: {
|
||||||
|
currentStep: ClaimWorkflowStep.SELECT_OUTER_PARTS,
|
||||||
|
nextStep: ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
||||||
|
completedSteps: [ClaimWorkflowStep.CLAIM_CREATED],
|
||||||
|
locked: false,
|
||||||
|
},
|
||||||
|
owner: {
|
||||||
|
userId: new Types.ObjectId(damagedUserId),
|
||||||
|
userRole: damagedParty?.role as any,
|
||||||
|
},
|
||||||
|
createdByRegistrarId: new Types.ObjectId(registrar.sub),
|
||||||
|
history: [
|
||||||
|
{
|
||||||
|
type: "CLAIM_CREATED",
|
||||||
|
actor: {
|
||||||
|
actorId: new Types.ObjectId(registrar.sub),
|
||||||
|
actorName: `${registrar.firstName || ""} ${registrar.lastName || ""}`.trim(),
|
||||||
|
actorType: "registrar",
|
||||||
|
},
|
||||||
|
timestamp: new Date(),
|
||||||
|
metadata: {
|
||||||
|
blameRequestId,
|
||||||
|
blamePublicId: blameRequest.publicId,
|
||||||
|
createdByRegistrar: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as any);
|
||||||
|
return {
|
||||||
|
claimRequestId: String(newClaim._id),
|
||||||
|
publicId: blameRequest.publicId,
|
||||||
|
status: ClaimCaseStatus.SELECTING_OUTER_PARTS,
|
||||||
|
message: "Claim created successfully. Registrar can now fill claim data.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* V2 API: Select damaged outer car parts for claim (Step 2 of claim workflow)
|
* V2 API: Select damaged outer car parts for claim (Step 2 of claim workflow)
|
||||||
*
|
*
|
||||||
@@ -2893,6 +3139,7 @@ export class ClaimRequestManagementService {
|
|||||||
claimRequestId: string,
|
claimRequestId: string,
|
||||||
body: SelectOuterPartsV2Dto,
|
body: SelectOuterPartsV2Dto,
|
||||||
currentUserId: string,
|
currentUserId: string,
|
||||||
|
actor?: { sub: string; role?: string },
|
||||||
): Promise<SelectOuterPartsV2ResponseDto> {
|
): Promise<SelectOuterPartsV2ResponseDto> {
|
||||||
try {
|
try {
|
||||||
// 1. Validate claim exists
|
// 1. Validate claim exists
|
||||||
@@ -2904,8 +3151,13 @@ export class ClaimRequestManagementService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Validate user is the claim owner (damaged party)
|
const effectiveUserId = await this.resolveClaimEffectiveUserId(
|
||||||
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== currentUserId) {
|
claimCase,
|
||||||
|
currentUserId,
|
||||||
|
actor?.role,
|
||||||
|
);
|
||||||
|
// 2. Validate user is the claim owner (or field expert acting for IN_PERSON claim)
|
||||||
|
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== effectiveUserId) {
|
||||||
throw new ForbiddenException(
|
throw new ForbiddenException(
|
||||||
'Only the claim owner (damaged party) can select damaged parts'
|
'Only the claim owner (damaged party) can select damaged parts'
|
||||||
);
|
);
|
||||||
@@ -3007,6 +3259,7 @@ export class ClaimRequestManagementService {
|
|||||||
claimRequestId: string,
|
claimRequestId: string,
|
||||||
body: SelectOtherPartsV2Dto,
|
body: SelectOtherPartsV2Dto,
|
||||||
currentUserId: string,
|
currentUserId: string,
|
||||||
|
actor?: { sub: string; role?: string },
|
||||||
): Promise<SelectOtherPartsV2ResponseDto> {
|
): Promise<SelectOtherPartsV2ResponseDto> {
|
||||||
try {
|
try {
|
||||||
// 1. Validate claim exists
|
// 1. Validate claim exists
|
||||||
@@ -3018,8 +3271,13 @@ export class ClaimRequestManagementService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Validate user is the claim owner (damaged party)
|
const effectiveUserId = await this.resolveClaimEffectiveUserId(
|
||||||
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== currentUserId) {
|
claimCase,
|
||||||
|
currentUserId,
|
||||||
|
actor?.role,
|
||||||
|
);
|
||||||
|
// 2. Validate user is the claim owner (or field expert for IN_PERSON claim)
|
||||||
|
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== effectiveUserId) {
|
||||||
throw new ForbiddenException(
|
throw new ForbiddenException(
|
||||||
'Only the claim owner (damaged party) can submit other parts and bank information'
|
'Only the claim owner (damaged party) can submit other parts and bank information'
|
||||||
);
|
);
|
||||||
@@ -3120,6 +3378,7 @@ export class ClaimRequestManagementService {
|
|||||||
async getCaptureRequirementsV2(
|
async getCaptureRequirementsV2(
|
||||||
claimRequestId: string,
|
claimRequestId: string,
|
||||||
currentUserId: string,
|
currentUserId: string,
|
||||||
|
actor?: { sub: string; role?: string },
|
||||||
): Promise<GetCaptureRequirementsV2ResponseDto> {
|
): Promise<GetCaptureRequirementsV2ResponseDto> {
|
||||||
try {
|
try {
|
||||||
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
@@ -3128,7 +3387,12 @@ export class ClaimRequestManagementService {
|
|||||||
throw new NotFoundException(`Claim case with ID ${claimRequestId} not found`);
|
throw new NotFoundException(`Claim case with ID ${claimRequestId} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== currentUserId) {
|
const effectiveUserId = await this.resolveClaimEffectiveUserId(
|
||||||
|
claimCase,
|
||||||
|
currentUserId,
|
||||||
|
actor?.role,
|
||||||
|
);
|
||||||
|
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== effectiveUserId) {
|
||||||
throw new ForbiddenException('Only the claim owner can view capture requirements');
|
throw new ForbiddenException('Only the claim owner can view capture requirements');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3152,7 +3416,11 @@ export class ClaimRequestManagementService {
|
|||||||
return labels[key] || { fa: key, en: key };
|
return labels[key] || { fa: key, en: key };
|
||||||
};
|
};
|
||||||
|
|
||||||
// Required documents
|
// Required documents: CAR_BODY needs 7 (damaged + green card); THIRD_PARTY needs 13 (add guilty_*)
|
||||||
|
const blameRequest = claimCase.blameRequestId
|
||||||
|
? await this.blameRequestDbService.findById(claimCase.blameRequestId.toString())
|
||||||
|
: null;
|
||||||
|
const isCarBody = blameRequest?.type === BlameRequestType.CAR_BODY;
|
||||||
const requiredDocsDefinition = [
|
const requiredDocsDefinition = [
|
||||||
{ key: 'car_green_card', label_fa: 'کارت سبز خودرو', label_en: 'Car Green Card', category: 'general' },
|
{ key: 'car_green_card', label_fa: 'کارت سبز خودرو', label_en: 'Car Green Card', category: 'general' },
|
||||||
{ key: 'damaged_driving_license_front', label_fa: 'گواهینامه طرف آسیبدیده - جلو', label_en: 'Damaged Party License - Front', category: 'damaged_party' },
|
{ key: 'damaged_driving_license_front', label_fa: 'گواهینامه طرف آسیبدیده - جلو', label_en: 'Damaged Party License - Front', category: 'damaged_party' },
|
||||||
@@ -3162,11 +3430,13 @@ export class ClaimRequestManagementService {
|
|||||||
{ key: 'damaged_car_card_front', label_fa: 'کارت خودرو آسیبدیده - جلو', label_en: 'Damaged Car Card - Front', category: 'damaged_party' },
|
{ key: 'damaged_car_card_front', label_fa: 'کارت خودرو آسیبدیده - جلو', label_en: 'Damaged Car Card - Front', category: 'damaged_party' },
|
||||||
{ key: 'damaged_car_card_back', label_fa: 'کارت خودرو آسیبدیده - پشت', label_en: 'Damaged Car Card - Back', category: 'damaged_party' },
|
{ key: 'damaged_car_card_back', label_fa: 'کارت خودرو آسیبدیده - پشت', label_en: 'Damaged Car Card - Back', category: 'damaged_party' },
|
||||||
{ key: 'damaged_metal_plate', label_fa: 'پلاک فلزی آسیبدیده', label_en: 'Damaged Metal Plate', category: 'damaged_party' },
|
{ key: 'damaged_metal_plate', label_fa: 'پلاک فلزی آسیبدیده', label_en: 'Damaged Metal Plate', category: 'damaged_party' },
|
||||||
{ key: 'guilty_driving_license_front', label_fa: 'گواهینامه طرف مقصر - جلو', label_en: 'Guilty Party License - Front', category: 'guilty_party' },
|
...(isCarBody ? [] : [
|
||||||
{ key: 'guilty_driving_license_back', label_fa: 'گواهینامه طرف مقصر - پشت', label_en: 'Guilty Party License - Back', category: 'guilty_party' },
|
{ key: 'guilty_driving_license_front', label_fa: 'گواهینامه طرف مقصر - جلو', label_en: 'Guilty Party License - Front', category: 'guilty_party' },
|
||||||
{ key: 'guilty_car_card_front', label_fa: 'کارت خودرو مقصر - جلو', label_en: 'Guilty Car Card - Front', category: 'guilty_party' },
|
{ key: 'guilty_driving_license_back', label_fa: 'گواهینامه طرف مقصر - پشت', label_en: 'Guilty Party License - Back', category: 'guilty_party' },
|
||||||
{ key: 'guilty_car_card_back', label_fa: 'کارت خودرو مقصر - پشت', label_en: 'Guilty Car Card - Back', category: 'guilty_party' },
|
{ key: 'guilty_car_card_front', label_fa: 'کارت خودرو مقصر - جلو', label_en: 'Guilty Car Card - Front', category: 'guilty_party' },
|
||||||
{ key: 'guilty_metal_plate', label_fa: 'پلاک فلزی مقصر', label_en: 'Guilty Metal Plate', category: 'guilty_party' },
|
{ key: 'guilty_car_card_back', label_fa: 'کارت خودرو مقصر - پشت', label_en: 'Guilty Car Card - Back', category: 'guilty_party' },
|
||||||
|
{ key: 'guilty_metal_plate', label_fa: 'پلاک فلزی مقصر', label_en: 'Guilty Metal Plate', category: 'guilty_party' },
|
||||||
|
]),
|
||||||
];
|
];
|
||||||
|
|
||||||
const requiredDocuments = requiredDocsDefinition.map(doc => {
|
const requiredDocuments = requiredDocsDefinition.map(doc => {
|
||||||
@@ -3250,6 +3520,7 @@ export class ClaimRequestManagementService {
|
|||||||
body: UploadRequiredDocumentV2Dto,
|
body: UploadRequiredDocumentV2Dto,
|
||||||
file: Express.Multer.File,
|
file: Express.Multer.File,
|
||||||
currentUserId: string,
|
currentUserId: string,
|
||||||
|
actor?: { sub: string; role?: string },
|
||||||
): Promise<UploadRequiredDocumentV2ResponseDto> {
|
): Promise<UploadRequiredDocumentV2ResponseDto> {
|
||||||
try {
|
try {
|
||||||
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
@@ -3258,7 +3529,12 @@ export class ClaimRequestManagementService {
|
|||||||
throw new NotFoundException(`Claim case with ID ${claimRequestId} not found`);
|
throw new NotFoundException(`Claim case with ID ${claimRequestId} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== currentUserId) {
|
const effectiveUserId = await this.resolveClaimEffectiveUserId(
|
||||||
|
claimCase,
|
||||||
|
currentUserId,
|
||||||
|
actor?.role,
|
||||||
|
);
|
||||||
|
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== effectiveUserId) {
|
||||||
throw new ForbiddenException('Only the claim owner can upload documents');
|
throw new ForbiddenException('Only the claim owner can upload documents');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3268,6 +3544,16 @@ export class ClaimRequestManagementService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CAR_BODY claims only allow 7 document types (no guilty_*)
|
||||||
|
const blameForUpload = claimCase.blameRequestId
|
||||||
|
? await this.blameRequestDbService.findById(claimCase.blameRequestId.toString())
|
||||||
|
: null;
|
||||||
|
const isCarBodyUpload = blameForUpload?.type === BlameRequestType.CAR_BODY;
|
||||||
|
const guiltyKeys = ['guilty_driving_license_front', 'guilty_driving_license_back', 'guilty_car_card_front', 'guilty_car_card_back', 'guilty_metal_plate'];
|
||||||
|
if (isCarBodyUpload && guiltyKeys.includes(body.documentKey)) {
|
||||||
|
throw new BadRequestException(`Document type ${body.documentKey} is not required for CAR_BODY claims`);
|
||||||
|
}
|
||||||
|
|
||||||
// Check if document already uploaded
|
// Check if document already uploaded
|
||||||
const existingDoc = (claimCase.requiredDocuments as any)?.get?.(body.documentKey);
|
const existingDoc = (claimCase.requiredDocuments as any)?.get?.(body.documentKey);
|
||||||
if (existingDoc?.uploaded) {
|
if (existingDoc?.uploaded) {
|
||||||
@@ -3312,11 +3598,11 @@ export class ClaimRequestManagementService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if all documents are uploaded
|
// Check if all documents are uploaded (7 for CAR_BODY, 13 for THIRD_PARTY)
|
||||||
const totalDocs = 13;
|
const totalDocsRequired = isCarBodyUpload ? 7 : 13;
|
||||||
const currentDocs = claimCase.requiredDocuments || new Map();
|
const currentDocs = claimCase.requiredDocuments || new Map();
|
||||||
const uploadedCount = (currentDocs instanceof Map ? currentDocs.size : Object.keys(currentDocs).length) + 1;
|
const uploadedCount = (currentDocs instanceof Map ? currentDocs.size : Object.keys(currentDocs).length) + 1;
|
||||||
const allDocumentsUploaded = uploadedCount >= totalDocs;
|
const allDocumentsUploaded = uploadedCount >= totalDocsRequired;
|
||||||
|
|
||||||
// If all documents uploaded, move to next step
|
// If all documents uploaded, move to next step
|
||||||
if (allDocumentsUploaded) {
|
if (allDocumentsUploaded) {
|
||||||
@@ -3344,7 +3630,7 @@ export class ClaimRequestManagementService {
|
|||||||
|
|
||||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, updateData);
|
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, updateData);
|
||||||
|
|
||||||
const remaining = totalDocs - uploadedCount;
|
const remaining = totalDocsRequired - uploadedCount;
|
||||||
const message = allDocumentsUploaded
|
const message = allDocumentsUploaded
|
||||||
? 'All documents uploaded successfully. Please proceed to capture car angles and damaged parts.'
|
? 'All documents uploaded successfully. Please proceed to capture car angles and damaged parts.'
|
||||||
: `Document uploaded successfully. ${remaining} documents remaining.`;
|
: `Document uploaded successfully. ${remaining} documents remaining.`;
|
||||||
@@ -3372,6 +3658,7 @@ export class ClaimRequestManagementService {
|
|||||||
body: CapturePartV2Dto,
|
body: CapturePartV2Dto,
|
||||||
file: Express.Multer.File,
|
file: Express.Multer.File,
|
||||||
currentUserId: string,
|
currentUserId: string,
|
||||||
|
actor?: { sub: string; role?: string },
|
||||||
): Promise<CapturePartV2ResponseDto> {
|
): Promise<CapturePartV2ResponseDto> {
|
||||||
try {
|
try {
|
||||||
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
@@ -3380,7 +3667,12 @@ export class ClaimRequestManagementService {
|
|||||||
throw new NotFoundException(`Claim case with ID ${claimRequestId} not found`);
|
throw new NotFoundException(`Claim case with ID ${claimRequestId} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== currentUserId) {
|
const effectiveUserId = await this.resolveClaimEffectiveUserId(
|
||||||
|
claimCase,
|
||||||
|
currentUserId,
|
||||||
|
actor?.role,
|
||||||
|
);
|
||||||
|
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== effectiveUserId) {
|
||||||
throw new ForbiddenException('Only the claim owner can capture parts');
|
throw new ForbiddenException('Only the claim owner can capture parts');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3492,14 +3784,42 @@ export class ClaimRequestManagementService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* V2 API: Get list of claims for current user
|
* V2 API: Get list of claims for current user (or for FIELD_EXPERT: claims from their expert-initiated IN_PERSON blame files).
|
||||||
*/
|
*/
|
||||||
async getMyClaimsV2(currentUserId: string): Promise<GetMyClaimsV2ResponseDto> {
|
async getMyClaimsV2(
|
||||||
|
currentUserId: string,
|
||||||
|
actor?: { sub: string; role?: string },
|
||||||
|
): Promise<GetMyClaimsV2ResponseDto> {
|
||||||
try {
|
try {
|
||||||
const claims = await this.claimCaseDbService.find(
|
let claims: any[];
|
||||||
{ 'owner.userId': new Types.ObjectId(currentUserId) },
|
if (actor?.role === RoleEnum.FIELD_EXPERT) {
|
||||||
{ lean: true },
|
const expertBlameIds = await this.blameRequestDbService
|
||||||
);
|
.find(
|
||||||
|
{
|
||||||
|
expertInitiated: true,
|
||||||
|
creationMethod: 'IN_PERSON',
|
||||||
|
initiatedByFieldExpertId: new Types.ObjectId(currentUserId),
|
||||||
|
},
|
||||||
|
{ select: '_id', lean: true },
|
||||||
|
)
|
||||||
|
.then((docs) => docs.map((d) => (d as any)._id));
|
||||||
|
claims = await this.claimCaseDbService.find(
|
||||||
|
{
|
||||||
|
$or: [
|
||||||
|
{ 'owner.userId': new Types.ObjectId(currentUserId) },
|
||||||
|
...(expertBlameIds.length
|
||||||
|
? [{ blameRequestId: { $in: expertBlameIds } }]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ lean: true },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
claims = await this.claimCaseDbService.find(
|
||||||
|
{ 'owner.userId': new Types.ObjectId(currentUserId) },
|
||||||
|
{ lean: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
const list = (claims as any[]).map((c) => ({
|
const list = (claims as any[]).map((c) => ({
|
||||||
claimRequestId: c._id.toString(),
|
claimRequestId: c._id.toString(),
|
||||||
publicId: c.publicId,
|
publicId: c.publicId,
|
||||||
@@ -3524,13 +3844,19 @@ export class ClaimRequestManagementService {
|
|||||||
async getClaimDetailsV2(
|
async getClaimDetailsV2(
|
||||||
claimRequestId: string,
|
claimRequestId: string,
|
||||||
currentUserId: string,
|
currentUserId: string,
|
||||||
|
actor?: { sub: string; role?: string },
|
||||||
): Promise<ClaimDetailsV2ResponseDto> {
|
): Promise<ClaimDetailsV2ResponseDto> {
|
||||||
try {
|
try {
|
||||||
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
if (!claim) {
|
if (!claim) {
|
||||||
throw new NotFoundException('Claim request not found');
|
throw new NotFoundException('Claim request not found');
|
||||||
}
|
}
|
||||||
if (!claim.owner?.userId || claim.owner.userId.toString() !== currentUserId) {
|
const effectiveUserId = await this.resolveClaimEffectiveUserId(
|
||||||
|
claim,
|
||||||
|
currentUserId,
|
||||||
|
actor?.role,
|
||||||
|
);
|
||||||
|
if (!claim.owner?.userId || claim.owner.userId.toString() !== effectiveUserId) {
|
||||||
throw new ForbiddenException('You do not have access to this claim');
|
throw new ForbiddenException('You do not have access to this claim');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import { ClaimDetailsV2ResponseDto } from "./dto/claim-details-v2.dto";
|
|||||||
@Controller("v2/claim-request-management")
|
@Controller("v2/claim-request-management")
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@UseGuards(GlobalGuard, RolesGuard)
|
@UseGuards(GlobalGuard, RolesGuard)
|
||||||
@Roles(RoleEnum.USER)
|
@Roles(RoleEnum.USER, RoleEnum.FIELD_EXPERT)
|
||||||
export class ClaimRequestManagementV2Controller {
|
export class ClaimRequestManagementV2Controller {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly claimRequestManagementService: ClaimRequestManagementService,
|
private readonly claimRequestManagementService: ClaimRequestManagementService,
|
||||||
@@ -51,7 +51,7 @@ export class ClaimRequestManagementV2Controller {
|
|||||||
})
|
})
|
||||||
async getMyClaims(@CurrentUser() user: any): Promise<GetMyClaimsV2ResponseDto> {
|
async getMyClaims(@CurrentUser() user: any): Promise<GetMyClaimsV2ResponseDto> {
|
||||||
try {
|
try {
|
||||||
return await this.claimRequestManagementService.getMyClaimsV2(user.sub);
|
return await this.claimRequestManagementService.getMyClaimsV2(user.sub, user);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HttpException) throw error;
|
if (error instanceof HttpException) throw error;
|
||||||
throw new InternalServerErrorException(
|
throw new InternalServerErrorException(
|
||||||
@@ -91,6 +91,7 @@ export class ClaimRequestManagementV2Controller {
|
|||||||
return await this.claimRequestManagementService.getClaimDetailsV2(
|
return await this.claimRequestManagementService.getClaimDetailsV2(
|
||||||
claimRequestId,
|
claimRequestId,
|
||||||
user.sub,
|
user.sub,
|
||||||
|
user,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HttpException) throw error;
|
if (error instanceof HttpException) throw error;
|
||||||
@@ -231,6 +232,7 @@ export class ClaimRequestManagementV2Controller {
|
|||||||
claimRequestId,
|
claimRequestId,
|
||||||
body,
|
body,
|
||||||
user.sub,
|
user.sub,
|
||||||
|
user,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HttpException) throw error;
|
if (error instanceof HttpException) throw error;
|
||||||
@@ -345,6 +347,7 @@ export class ClaimRequestManagementV2Controller {
|
|||||||
claimRequestId,
|
claimRequestId,
|
||||||
body,
|
body,
|
||||||
user.sub,
|
user.sub,
|
||||||
|
user,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HttpException) throw error;
|
if (error instanceof HttpException) throw error;
|
||||||
@@ -397,6 +400,7 @@ Returns status of each item (uploaded/captured or not).
|
|||||||
return await this.claimRequestManagementService.getCaptureRequirementsV2(
|
return await this.claimRequestManagementService.getCaptureRequirementsV2(
|
||||||
claimRequestId,
|
claimRequestId,
|
||||||
user.sub,
|
user.sub,
|
||||||
|
user,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HttpException) throw error;
|
if (error instanceof HttpException) throw error;
|
||||||
@@ -434,16 +438,16 @@ Returns status of each item (uploaded/captured or not).
|
|||||||
description: `
|
description: `
|
||||||
**Workflow Step:** UPLOAD_REQUIRED_DOCUMENTS (Step 4 of Claim)
|
**Workflow Step:** UPLOAD_REQUIRED_DOCUMENTS (Step 4 of Claim)
|
||||||
|
|
||||||
**Upload one of the 13 required documents:**
|
**Upload one of the required documents** (13 for THIRD_PARTY; CAR_BODY may require fewer — see capture-requirements):
|
||||||
- car_green_card
|
- car_green_card
|
||||||
- damaged_driving_license_front/back
|
- damaged_driving_license_front/back
|
||||||
- damaged_chassis_number, damaged_engine_photo
|
- damaged_chassis_number, damaged_engine_photo
|
||||||
- damaged_car_card_front/back, damaged_metal_plate
|
- damaged_car_card_front/back, damaged_metal_plate
|
||||||
- guilty_driving_license_front/back
|
- guilty_driving_license_front/back, guilty_car_card_front/back, guilty_metal_plate (THIRD_PARTY)
|
||||||
- guilty_car_card_front/back, guilty_metal_plate
|
|
||||||
|
|
||||||
**When all 13 documents are uploaded:**
|
**When all required documents are uploaded:** Workflow moves to CAPTURE_PART_DAMAGES (Step 5).
|
||||||
- Workflow automatically moves to: CAPTURE_PART_DAMAGES (Step 5)
|
|
||||||
|
**Field expert IN_PERSON:** Same endpoint; use with claim created from expert-initiated IN_PERSON blame to upload documents on behalf of the damaged party.
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
@@ -507,6 +511,7 @@ Returns status of each item (uploaded/captured or not).
|
|||||||
body,
|
body,
|
||||||
file,
|
file,
|
||||||
user.sub,
|
user.sub,
|
||||||
|
user,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HttpException) throw error;
|
if (error instanceof HttpException) throw error;
|
||||||
@@ -547,6 +552,8 @@ Returns status of each item (uploaded/captured or not).
|
|||||||
2. **part**: Damaged parts based on selectedParts from Step 2
|
2. **part**: Damaged parts based on selectedParts from Step 2
|
||||||
|
|
||||||
**All captures must be completed before user can submit claim.**
|
**All captures must be completed before user can submit claim.**
|
||||||
|
|
||||||
|
**Field expert IN_PERSON:** Same endpoint; use with claim created from expert-initiated IN_PERSON blame to capture photos on behalf of the damaged party.
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
@@ -598,6 +605,7 @@ Returns status of each item (uploaded/captured or not).
|
|||||||
body,
|
body,
|
||||||
file,
|
file,
|
||||||
user.sub,
|
user.sub,
|
||||||
|
user,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HttpException) throw error;
|
if (error instanceof HttpException) throw error;
|
||||||
|
|||||||
@@ -133,6 +133,9 @@ export class ClaimCase {
|
|||||||
@Prop({ type: [HistoryEventSchema], default: [] })
|
@Prop({ type: [HistoryEventSchema], default: [] })
|
||||||
history?: HistoryEvent[];
|
history?: HistoryEvent[];
|
||||||
|
|
||||||
|
@Prop({ type: Types.ObjectId, index: true })
|
||||||
|
createdByRegistrarId?: Types.ObjectId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Legacy fields kept optional to simplify progressive migration.
|
* Legacy fields kept optional to simplify progressive migration.
|
||||||
* If you choose to migrate later, we can remove these.
|
* If you choose to migrate later, we can remove these.
|
||||||
|
|||||||
135
src/claim-request-management/registrar-claim.v1.controller.ts
Normal file
135
src/claim-request-management/registrar-claim.v1.controller.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { Body, Controller, Get, Param, Patch, Post, UploadedFile, UseGuards, UseInterceptors } from "@nestjs/common";
|
||||||
|
import { FileInterceptor } from "@nestjs/platform-express";
|
||||||
|
import { diskStorage } from "multer";
|
||||||
|
import { extname } from "path";
|
||||||
|
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiOperation, ApiParam, ApiTags } from "@nestjs/swagger";
|
||||||
|
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.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 { SelectOuterPartsV2Dto } from "./dto/select-outer-parts-v2.dto";
|
||||||
|
import { SelectOtherPartsV2Dto } from "./dto/select-other-parts-v2.dto";
|
||||||
|
import { UploadRequiredDocumentV2Dto } from "./dto/upload-document-v2.dto";
|
||||||
|
import { CapturePartV2Dto } from "./dto/capture-part-v2.dto";
|
||||||
|
import { ClaimRequestManagementService } from "./claim-request-management.service";
|
||||||
|
|
||||||
|
@ApiTags("registrar-claim (v1)")
|
||||||
|
@Controller("registrar-claim")
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
||||||
|
@Roles(RoleEnum.REGISTRAR)
|
||||||
|
export class RegistrarClaimV1Controller {
|
||||||
|
constructor(private readonly claimRequestManagementService: ClaimRequestManagementService) {}
|
||||||
|
|
||||||
|
@Post("create-from-blame/:blameRequestId")
|
||||||
|
@ApiParam({ name: "blameRequestId" })
|
||||||
|
createFromBlame(@Param("blameRequestId") blameRequestId: string, @CurrentUser() registrar: any) {
|
||||||
|
return this.claimRequestManagementService.createClaimFromBlameForRegistrarV1(
|
||||||
|
blameRequestId,
|
||||||
|
registrar,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("request/:claimRequestId")
|
||||||
|
getClaim(@Param("claimRequestId") claimRequestId: string, @CurrentUser() registrar: any) {
|
||||||
|
return this.claimRequestManagementService.getClaimDetailsV2(
|
||||||
|
claimRequestId,
|
||||||
|
registrar.sub,
|
||||||
|
registrar,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch("select-outer-parts/:claimRequestId")
|
||||||
|
@ApiBody({ type: SelectOuterPartsV2Dto })
|
||||||
|
selectOuter(
|
||||||
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
@Body() body: SelectOuterPartsV2Dto,
|
||||||
|
@CurrentUser() registrar: any,
|
||||||
|
) {
|
||||||
|
return this.claimRequestManagementService.selectOuterPartsV2(
|
||||||
|
claimRequestId,
|
||||||
|
body,
|
||||||
|
registrar.sub,
|
||||||
|
registrar,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch("select-other-parts/:claimRequestId")
|
||||||
|
@ApiBody({ type: SelectOtherPartsV2Dto })
|
||||||
|
selectOther(
|
||||||
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
@Body() body: SelectOtherPartsV2Dto,
|
||||||
|
@CurrentUser() registrar: any,
|
||||||
|
) {
|
||||||
|
return this.claimRequestManagementService.selectOtherPartsV2(
|
||||||
|
claimRequestId,
|
||||||
|
body,
|
||||||
|
registrar.sub,
|
||||||
|
registrar,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("capture-requirements/:claimRequestId")
|
||||||
|
getCapture(@Param("claimRequestId") claimRequestId: string, @CurrentUser() registrar: any) {
|
||||||
|
return this.claimRequestManagementService.getCaptureRequirementsV2(
|
||||||
|
claimRequestId,
|
||||||
|
registrar.sub,
|
||||||
|
registrar,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("upload-document/:claimRequestId")
|
||||||
|
@ApiConsumes("multipart/form-data")
|
||||||
|
@UseInterceptors(
|
||||||
|
FileInterceptor("file", {
|
||||||
|
limits: { fileSize: 10 * 1024 * 1024 },
|
||||||
|
storage: diskStorage({
|
||||||
|
destination: "./files/claim-documents",
|
||||||
|
filename: (req, file, cb) => cb(null, `${Date.now()}${extname(file.originalname)}`),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
@ApiOperation({ summary: "Registrar uploads required claim document" })
|
||||||
|
uploadDoc(
|
||||||
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
@Body() body: UploadRequiredDocumentV2Dto,
|
||||||
|
@UploadedFile() file: Express.Multer.File,
|
||||||
|
@CurrentUser() registrar: any,
|
||||||
|
) {
|
||||||
|
return this.claimRequestManagementService.uploadRequiredDocumentV2(
|
||||||
|
claimRequestId,
|
||||||
|
body,
|
||||||
|
file,
|
||||||
|
registrar.sub,
|
||||||
|
registrar,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("capture-part/:claimRequestId")
|
||||||
|
@ApiConsumes("multipart/form-data")
|
||||||
|
@UseInterceptors(
|
||||||
|
FileInterceptor("file", {
|
||||||
|
limits: { fileSize: 10 * 1024 * 1024 },
|
||||||
|
storage: diskStorage({
|
||||||
|
destination: "./files/claim-captures",
|
||||||
|
filename: (req, file, cb) => cb(null, `${Date.now()}${extname(file.originalname)}`),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
capturePart(
|
||||||
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
@Body() body: CapturePartV2Dto,
|
||||||
|
@UploadedFile() file: Express.Multer.File,
|
||||||
|
@CurrentUser() registrar: any,
|
||||||
|
) {
|
||||||
|
return this.claimRequestManagementService.capturePartV2(
|
||||||
|
claimRequestId,
|
||||||
|
body,
|
||||||
|
file,
|
||||||
|
registrar.sub,
|
||||||
|
registrar,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -198,7 +198,18 @@ export class ExpertBlameService {
|
|||||||
// Filter to show only:
|
// Filter to show only:
|
||||||
// 1. Fresh requests (WAITING_FOR_EXPERT and no decision)
|
// 1. Fresh requests (WAITING_FOR_EXPERT and no decision)
|
||||||
// 2. Requests decided by current expert
|
// 2. Requests decided by current expert
|
||||||
|
// 3. Expert-initiated: only the initiating field expert sees them
|
||||||
const visibleCases = (allCases as Record<string, unknown>[]).filter((doc) => {
|
const visibleCases = (allCases as Record<string, unknown>[]).filter((doc) => {
|
||||||
|
const expertInitiated = doc.expertInitiated === true;
|
||||||
|
const initiatedByFieldExpertId = doc.initiatedByFieldExpertId;
|
||||||
|
|
||||||
|
if (expertInitiated && initiatedByFieldExpertId) {
|
||||||
|
if (String(initiatedByFieldExpertId) !== expertId) {
|
||||||
|
return false; // Only the initiating field expert can see this file
|
||||||
|
}
|
||||||
|
return true; // Initiating expert can see their expert-initiated file
|
||||||
|
}
|
||||||
|
|
||||||
const status = doc.status as string;
|
const status = doc.status as string;
|
||||||
const decision = doc.expert as any;
|
const decision = doc.expert as any;
|
||||||
const decidedByExpertId = decision?.decision?.decidedByExpertId;
|
const decidedByExpertId = decision?.decision?.decidedByExpertId;
|
||||||
@@ -522,13 +533,20 @@ export class ExpertBlameService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Access control: Check if expert has permission to view this request
|
// Access control: Expert-initiated files only visible to the initiating field expert
|
||||||
const decision = (doc.expert as any)?.decision;
|
const expertInitiated = doc.expertInitiated === true;
|
||||||
const decidedByExpertId = decision?.decidedByExpertId;
|
const initiatedByFieldExpertId = doc.initiatedByFieldExpertId;
|
||||||
|
if (expertInitiated && initiatedByFieldExpertId && String(initiatedByFieldExpertId) !== actorId) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"Only the field expert who created this file can view and review it.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Allow if:
|
// Allow if:
|
||||||
// 1. No decision yet (fresh request)
|
// 1. No decision yet (fresh request)
|
||||||
// 2. Decision made by current expert
|
// 2. Decision made by current expert
|
||||||
|
const decision = (doc.expert as any)?.decision;
|
||||||
|
const decidedByExpertId = decision?.decidedByExpertId;
|
||||||
if (decidedByExpertId && String(decidedByExpertId) !== actorId) {
|
if (decidedByExpertId && String(decidedByExpertId) !== actorId) {
|
||||||
throw new ForbiddenException(
|
throw new ForbiddenException(
|
||||||
"You do not have permission to view this request. It has been handled by another expert.",
|
"You do not have permission to view this request. It has been handled by another expert.",
|
||||||
@@ -660,6 +678,17 @@ export class ExpertBlameService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Expert-initiated: only the initiating field expert can lock and review
|
||||||
|
if (
|
||||||
|
request.expertInitiated &&
|
||||||
|
request.initiatedByFieldExpertId &&
|
||||||
|
String(request.initiatedByFieldExpertId) !== actorDetail.sub
|
||||||
|
) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"Only the field expert who created this file can lock and review it.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Check if locked and not expired
|
// Check if locked and not expired
|
||||||
if (request.workflow?.locked) {
|
if (request.workflow?.locked) {
|
||||||
const lockedAt = request.workflow.lockedAt;
|
const lockedAt = request.workflow.lockedAt;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { ResendRequestDto } from "./dto/resend.dto";
|
|||||||
@Controller("v2/expert-blame")
|
@Controller("v2/expert-blame")
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
||||||
@Roles(RoleEnum.EXPERT)
|
@Roles(RoleEnum.EXPERT, RoleEnum.FIELD_EXPERT)
|
||||||
export class ExpertBlameV2Controller {
|
export class ExpertBlameV2Controller {
|
||||||
constructor(private readonly expertBlameService: ExpertBlameService) {}
|
constructor(private readonly expertBlameService: ExpertBlameService) {}
|
||||||
|
|
||||||
|
|||||||
@@ -48,44 +48,74 @@ export class FirstPartyFileDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class DescriptionDto {
|
export class DescriptionDto {
|
||||||
@ApiProperty()
|
@ApiProperty({ description: "Accident description (required for all types)" })
|
||||||
desc: string;
|
desc: string;
|
||||||
|
|
||||||
// CAR_BODY specific fields
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description: "Date of accident (for CAR_BODY type only)",
|
description: "CAR_BODY only. Ignored for THIRD_PARTY.",
|
||||||
example: "2025-12-08"
|
example: "2025-12-08",
|
||||||
})
|
})
|
||||||
accidentDate?: Date;
|
accidentDate?: Date;
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description: "Time of accident (for CAR_BODY type only)",
|
description: "CAR_BODY only. Ignored for THIRD_PARTY.",
|
||||||
example: "14:30"
|
example: "14:30",
|
||||||
})
|
})
|
||||||
accidentTime?: string;
|
accidentTime?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
enum: WeatherCondition,
|
enum: WeatherCondition,
|
||||||
description: "Weather condition at time of accident (for CAR_BODY type only)",
|
description: "CAR_BODY only. Ignored for THIRD_PARTY.",
|
||||||
example: WeatherCondition.CLEAR
|
example: WeatherCondition.CLEAR,
|
||||||
})
|
})
|
||||||
weatherCondition?: WeatherCondition;
|
weatherCondition?: WeatherCondition;
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
enum: RoadCondition,
|
enum: RoadCondition,
|
||||||
description: "Road condition at time of accident (for CAR_BODY type only)",
|
description: "CAR_BODY only. Ignored for THIRD_PARTY.",
|
||||||
example: RoadCondition.DRY
|
example: RoadCondition.DRY,
|
||||||
})
|
})
|
||||||
roadCondition?: RoadCondition;
|
roadCondition?: RoadCondition;
|
||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
enum: LightCondition,
|
enum: LightCondition,
|
||||||
description: "Light condition at time of accident (for CAR_BODY type only)",
|
description: "CAR_BODY only. Ignored for THIRD_PARTY.",
|
||||||
example: LightCondition.DAYLIGHT
|
example: LightCondition.DAYLIGHT,
|
||||||
})
|
})
|
||||||
lightCondition?: LightCondition;
|
lightCondition?: LightCondition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* THIRD_PARTY description step: only desc is accepted.
|
||||||
|
*/
|
||||||
|
export class ThirdPartyDescriptionDto {
|
||||||
|
@ApiProperty({ description: "Accident description" })
|
||||||
|
desc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CAR_BODY description step: desc + accident date/time and conditions required.
|
||||||
|
*/
|
||||||
|
export class CarBodyDescriptionDto {
|
||||||
|
@ApiProperty({ description: "Accident description" })
|
||||||
|
desc: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: "Date of accident", example: "2025-12-08" })
|
||||||
|
accidentDate: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: "Time of accident", example: "14:30" })
|
||||||
|
accidentTime: string;
|
||||||
|
|
||||||
|
@ApiProperty({ enum: WeatherCondition, example: WeatherCondition.CLEAR })
|
||||||
|
weatherCondition: WeatherCondition;
|
||||||
|
|
||||||
|
@ApiProperty({ enum: RoadCondition, example: RoadCondition.DRY })
|
||||||
|
roadCondition: RoadCondition;
|
||||||
|
|
||||||
|
@ApiProperty({ enum: LightCondition, example: LightCondition.DAYLIGHT })
|
||||||
|
lightCondition: LightCondition;
|
||||||
|
}
|
||||||
|
|
||||||
export class FirstPartyDetail {
|
export class FirstPartyDetail {
|
||||||
// @ApiProperty({ type: String })
|
// @ApiProperty({ type: String })
|
||||||
firstPartyId?: Types.ObjectId;
|
firstPartyId?: Types.ObjectId;
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Body for field expert uploading a party's signature for expert-initiated IN_PERSON blame.
|
||||||
|
*/
|
||||||
|
export class ExpertUploadPartySignatureDto {
|
||||||
|
@ApiProperty({
|
||||||
|
enum: ["FIRST", "SECOND"],
|
||||||
|
description: "Which party's signature is being uploaded. CAR_BODY: use FIRST only. THIRD_PARTY: FIRST and SECOND.",
|
||||||
|
})
|
||||||
|
partyRole: "FIRST" | "SECOND";
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
default: true,
|
||||||
|
description: "Party accepts the blame agreement (true). Set false if party rejects.",
|
||||||
|
})
|
||||||
|
isAccept: boolean;
|
||||||
|
}
|
||||||
10
src/request-management/dto/registrar-initiated.dto.ts
Normal file
10
src/request-management/dto/registrar-initiated.dto.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsIn, IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class CreateRegistrarInitiatedFileDto {
|
||||||
|
@ApiProperty({ enum: ["THIRD_PARTY", "CAR_BODY"], example: "THIRD_PARTY" })
|
||||||
|
@IsString()
|
||||||
|
@IsIn(["THIRD_PARTY", "CAR_BODY"])
|
||||||
|
type: "THIRD_PARTY" | "CAR_BODY";
|
||||||
|
}
|
||||||
|
|
||||||
19
src/request-management/dto/send-party-otps.dto.ts
Normal file
19
src/request-management/dto/send-party-otps.dto.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Body for field expert to request OTP send to one or two parties for expert-initiated IN_PERSON blame.
|
||||||
|
* Uses the same SMS flow as /user/send-otp. After this, parties receive SMS with OTP; expert collects and calls verify-party-otps.
|
||||||
|
*/
|
||||||
|
export class SendPartyOtpsDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: "First party phone number (e.g. 09123456789). SMS with OTP will be sent to this number.",
|
||||||
|
example: "09123456789",
|
||||||
|
})
|
||||||
|
firstPartyPhoneNumber: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Second party phone number. Required when blame type is THIRD_PARTY. SMS with OTP will be sent to this number.",
|
||||||
|
example: "09187654321",
|
||||||
|
})
|
||||||
|
secondPartyPhoneNumber?: string;
|
||||||
|
}
|
||||||
31
src/request-management/dto/verify-party-otps.dto.ts
Normal file
31
src/request-management/dto/verify-party-otps.dto.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Body for field expert to verify one or two party OTPs for expert-initiated IN_PERSON blame.
|
||||||
|
* First party must verify; second party required only when type is THIRD_PARTY.
|
||||||
|
*/
|
||||||
|
export class VerifyPartyOtpsDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: "First party phone number (must have requested OTP via /user/send-otp)",
|
||||||
|
example: "09123456789",
|
||||||
|
})
|
||||||
|
firstPartyPhoneNumber: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: "OTP received by first party",
|
||||||
|
example: "258567",
|
||||||
|
})
|
||||||
|
firstPartyOtp: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Second party phone number. Required when blame type is THIRD_PARTY.",
|
||||||
|
example: "09187654321",
|
||||||
|
})
|
||||||
|
secondPartyPhoneNumber?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "OTP received by second party. Required when secondPartyPhoneNumber is provided.",
|
||||||
|
example: "123456",
|
||||||
|
})
|
||||||
|
secondPartyOtp?: string;
|
||||||
|
}
|
||||||
@@ -50,5 +50,10 @@ export class AccidentInfo {
|
|||||||
|
|
||||||
@Prop({ type: AccidentClassificationSchema })
|
@Prop({ type: AccidentClassificationSchema })
|
||||||
classification?: AccidentClassification;
|
classification?: AccidentClassification;
|
||||||
|
|
||||||
|
/** CAR_BODY: weather / road / light at time of accident */
|
||||||
|
@Prop() weatherCondition?: string;
|
||||||
|
@Prop() roadCondition?: string;
|
||||||
|
@Prop() lightCondition?: string;
|
||||||
}
|
}
|
||||||
export const AccidentInfoSchema = SchemaFactory.createForClass(AccidentInfo);
|
export const AccidentInfoSchema = SchemaFactory.createForClass(AccidentInfo);
|
||||||
@@ -1,19 +1,27 @@
|
|||||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
import { HydratedDocument } from "mongoose";
|
import { HydratedDocument, Types } from "mongoose";
|
||||||
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||||
import { Party, PartySchema } from "./partyRole.enum";
|
import { Party, PartySchema } from "./partyRole.enum";
|
||||||
import { AccidentInfo, AccidentInfoSchema } from "./accidentInformation.type";
|
|
||||||
import { ExpertSection, ExpertSectionSchema } from "./expert-section.type";
|
import { ExpertSection, ExpertSectionSchema } from "./expert-section.type";
|
||||||
import { Workflow, WorkflowSchema } from "./workflow.type";
|
import { Workflow, WorkflowSchema } from "./workflow.type";
|
||||||
import { HistoryEvent, HistoryEventSchema } from "./historyEvent.type";
|
import { HistoryEvent, HistoryEventSchema } from "./historyEvent.type";
|
||||||
import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
|
import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
|
||||||
import { BlameStatus } from "src/Types&Enums/blame-request-management/blameStatus.enum";
|
import { BlameStatus } from "src/Types&Enums/blame-request-management/blameStatus.enum";
|
||||||
|
import { CreationMethod, FilledBy } from "./request-management.schema";
|
||||||
|
|
||||||
|
/** CAR_BODY only: mocked insurance inquiry result (legacy top-level; prefer parties[].insurance.carBodyInsurance) */
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class CarBodyInsuranceDetail {
|
||||||
|
@Prop() policyNumber?: string;
|
||||||
|
@Prop() startDate?: string;
|
||||||
|
@Prop() endDate?: string;
|
||||||
|
@Prop() insurerCompany?: string;
|
||||||
|
@Prop({ type: [String] }) coverages?: string[];
|
||||||
|
}
|
||||||
|
export const CarBodyInsuranceDetailSchema = SchemaFactory.createForClass(CarBodyInsuranceDetail);
|
||||||
|
|
||||||
@Schema({ _id: false })
|
@Schema({ _id: false })
|
||||||
export class BlameRequestSnapshot {
|
export class BlameRequestSnapshot {
|
||||||
@Prop({ type: AccidentInfoSchema })
|
|
||||||
accident?: AccidentInfo;
|
|
||||||
|
|
||||||
@Prop({ type: [PartySchema], default: [] })
|
@Prop({ type: [PartySchema], default: [] })
|
||||||
parties?: Party[];
|
parties?: Party[];
|
||||||
}
|
}
|
||||||
@@ -45,9 +53,6 @@ export class BlameRequest {
|
|||||||
@Prop({ type: WorkflowSchema })
|
@Prop({ type: WorkflowSchema })
|
||||||
workflow?: Workflow;
|
workflow?: Workflow;
|
||||||
|
|
||||||
@Prop({ type: AccidentInfoSchema })
|
|
||||||
accident?: AccidentInfo;
|
|
||||||
|
|
||||||
@Prop({ type: [PartySchema], default: [] })
|
@Prop({ type: [PartySchema], default: [] })
|
||||||
parties: Party[];
|
parties: Party[];
|
||||||
|
|
||||||
@@ -57,12 +62,40 @@ export class BlameRequest {
|
|||||||
@Prop({ type: [HistoryEventSchema], default: [] })
|
@Prop({ type: [HistoryEventSchema], default: [] })
|
||||||
history: HistoryEvent[];
|
history: HistoryEvent[];
|
||||||
|
|
||||||
|
/** CAR_BODY only: first party car-body insurance (mocked inquiry) – legacy; prefer parties[0].insurance.carBodyInsurance */
|
||||||
|
@Prop({ type: CarBodyInsuranceDetailSchema })
|
||||||
|
carBodyInsuranceDetail?: CarBodyInsuranceDetail;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional snapshot structure (kept for future use / read-optimized copies).
|
* Optional snapshot structure (kept for future use / read-optimized copies).
|
||||||
* Source of truth remains the top-level fields of this document.
|
* Source of truth remains the top-level fields of this document.
|
||||||
*/
|
*/
|
||||||
@Prop({ type: BlameRequestSnapshotSchema, required: false })
|
@Prop({ type: BlameRequestSnapshotSchema, required: false })
|
||||||
snapshot?: BlameRequestSnapshot;
|
snapshot?: BlameRequestSnapshot;
|
||||||
|
|
||||||
|
/** True when this blame was created by a field expert (independent expert); only that expert can see/review it. */
|
||||||
|
@Prop({ default: false })
|
||||||
|
expertInitiated?: boolean;
|
||||||
|
|
||||||
|
/** Field expert who created this file (for expert-initiated flows). */
|
||||||
|
@Prop({ type: Types.ObjectId })
|
||||||
|
initiatedByFieldExpertId?: Types.ObjectId;
|
||||||
|
|
||||||
|
/** True when this blame was created by a registrar. */
|
||||||
|
@Prop({ default: false })
|
||||||
|
registrarInitiated?: boolean;
|
||||||
|
|
||||||
|
/** Registrar who created this file (for registrar-initiated flows). */
|
||||||
|
@Prop({ type: Types.ObjectId })
|
||||||
|
initiatedByRegistrarId?: Types.ObjectId;
|
||||||
|
|
||||||
|
/** LINK = expert sends link to user(s); IN_PERSON = expert fills form on-site. */
|
||||||
|
@Prop({ type: String, enum: CreationMethod })
|
||||||
|
creationMethod?: CreationMethod;
|
||||||
|
|
||||||
|
/** Who fills the blame data: CUSTOMER (LINK) or EXPERT (IN_PERSON). */
|
||||||
|
@Prop({ type: String, enum: FilledBy })
|
||||||
|
filledBy?: FilledBy;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BlameRequestDocument = HydratedDocument<BlameRequest>;
|
export type BlameRequestDocument = HydratedDocument<BlameRequest>;
|
||||||
|
|||||||
@@ -70,6 +70,16 @@ export class Insurance {
|
|||||||
@Prop() financialCeiling?: string;
|
@Prop() financialCeiling?: string;
|
||||||
@Prop({ type: [String] })
|
@Prop({ type: [String] })
|
||||||
coverages?: string[];
|
coverages?: string[];
|
||||||
|
|
||||||
|
/** CAR_BODY only: mocked car-body insurance inquiry result */
|
||||||
|
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||||
|
carBodyInsurance?: {
|
||||||
|
policyNumber?: string;
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
insurerCompany?: string;
|
||||||
|
coverages?: string[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
export const InsuranceSchema = SchemaFactory.createForClass(Insurance);
|
export const InsuranceSchema = SchemaFactory.createForClass(Insurance);
|
||||||
|
|
||||||
@@ -87,6 +97,13 @@ export class PartyStatement {
|
|||||||
|
|
||||||
@Prop({ type: String })
|
@Prop({ type: String })
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
|
/** CAR_BODY: accident conditions from description step */
|
||||||
|
@Prop() accidentDate?: Date;
|
||||||
|
@Prop() accidentTime?: string;
|
||||||
|
@Prop() weatherCondition?: string;
|
||||||
|
@Prop() roadCondition?: string;
|
||||||
|
@Prop() lightCondition?: string;
|
||||||
}
|
}
|
||||||
export const PartyStatementSchema = SchemaFactory.createForClass(PartyStatement);
|
export const PartyStatementSchema = SchemaFactory.createForClass(PartyStatement);
|
||||||
|
|
||||||
@@ -130,6 +147,13 @@ export class Party {
|
|||||||
@Prop({ type: PersonSchema })
|
@Prop({ type: PersonSchema })
|
||||||
person: Person;
|
person: Person;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CAR_BODY only: first form – accident with car vs object.
|
||||||
|
* Second form (guilty/damaged) may be added later as carBodySecondForm.
|
||||||
|
*/
|
||||||
|
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||||
|
carBodyFirstForm?: { car?: boolean; object?: boolean };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Party-submitted location (step-driven: FIRST_LOCATION / SECOND_LOCATION).
|
* Party-submitted location (step-driven: FIRST_LOCATION / SECOND_LOCATION).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -282,6 +282,7 @@ export enum CreationMethod {
|
|||||||
export enum FilledBy {
|
export enum FilledBy {
|
||||||
CUSTOMER = "customer",
|
CUSTOMER = "customer",
|
||||||
EXPERT = "expert",
|
EXPERT = "expert",
|
||||||
|
REGISTRAR = "registrar",
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExpertLinkInfo {
|
export class ExpertLinkInfo {
|
||||||
|
|||||||
517
src/request-management/expert-initiated.v2.controller.ts
Normal file
517
src/request-management/expert-initiated.v2.controller.ts
Normal file
@@ -0,0 +1,517 @@
|
|||||||
|
import { extname } from "node:path";
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Param,
|
||||||
|
UseGuards,
|
||||||
|
Get,
|
||||||
|
UseInterceptors,
|
||||||
|
UploadedFile,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { FileInterceptor } from "@nestjs/platform-express";
|
||||||
|
import { diskStorage } from "multer";
|
||||||
|
import {
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiBody,
|
||||||
|
ApiParam,
|
||||||
|
ApiTags,
|
||||||
|
ApiOperation,
|
||||||
|
ApiResponse,
|
||||||
|
ApiConsumes,
|
||||||
|
} from "@nestjs/swagger";
|
||||||
|
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
||||||
|
import { RolesGuard } from "src/auth/guards/role.guard";
|
||||||
|
import { CurrentUser } from "src/decorators/user.decorator";
|
||||||
|
import { Roles } from "src/decorators/roles.decorator";
|
||||||
|
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||||
|
import { RequestManagementService } from "./request-management.service";
|
||||||
|
import { CreateExpertInitiatedFileDto } from "./dto/expert-initiated.dto";
|
||||||
|
import { ExpertCompleteThirdPartyFormDto } from "./dto/expert-complete-third-party-form.dto";
|
||||||
|
import { ExpertCompleteCarBodyFormDto } from "./dto/expert-complete-car-body-form.dto";
|
||||||
|
import { ExpertAccidentFieldsDto } from "./dto/expert-accident-fields.dto";
|
||||||
|
import { ExpertCompleteClaimDataDto } from "./dto/expert-complete-claim-data.dto";
|
||||||
|
import { ExpertUploadPartySignatureDto } from "./dto/expert-upload-party-signature.dto";
|
||||||
|
import { VerifyPartyOtpsDto } from "./dto/verify-party-otps.dto";
|
||||||
|
import { SendPartyOtpsDto } from "./dto/send-party-otps.dto";
|
||||||
|
import { ClaimRequestManagementService } from "src/claim-request-management/claim-request-management.service";
|
||||||
|
import { PartyRole } from "./entities/schema/partyRole.enum";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2 expert-initiated blame API: uses BlameRequest (workflow) model.
|
||||||
|
* Field experts create files via LINK (send link to user) or IN_PERSON (expert fills on-site).
|
||||||
|
* Only the initiating field expert can see/review these files.
|
||||||
|
*/
|
||||||
|
@ApiTags("expert-initiated-blame (v2)")
|
||||||
|
@Controller("v2/expert-initiated-blame")
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
||||||
|
@Roles(RoleEnum.FIELD_EXPERT)
|
||||||
|
export class ExpertInitiatedV2Controller {
|
||||||
|
constructor(
|
||||||
|
private readonly requestManagementService: RequestManagementService,
|
||||||
|
private readonly claimRequestManagementService: ClaimRequestManagementService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Get("my-files")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "[V2] List my expert-initiated blame files",
|
||||||
|
description:
|
||||||
|
"Returns all BlameRequest (workflow) files created by the current field expert.",
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: "List of expert-initiated files" })
|
||||||
|
async getMyFilesV2(@CurrentUser() expert: any) {
|
||||||
|
return this.requestManagementService.getMyExpertInitiatedFilesV2(expert);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("blame/:requestId")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "[V2] Get one expert-initiated blame request",
|
||||||
|
description: "Returns a single blame request. Only the initiating field expert can access.",
|
||||||
|
})
|
||||||
|
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
|
@ApiResponse({ status: 200, description: "Blame request" })
|
||||||
|
async getBlameRequestV2(
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.getBlameRequestV2(requestId, expert);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("create")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "[V2] Create expert-initiated blame file",
|
||||||
|
description:
|
||||||
|
"Creates a BlameRequest with workflow. LINK: parties identified by phone, expert sends link. IN_PERSON: expert fills form later.",
|
||||||
|
})
|
||||||
|
@ApiBody({
|
||||||
|
type: CreateExpertInitiatedFileDto,
|
||||||
|
examples: {
|
||||||
|
thirdPartyInPerson: {
|
||||||
|
summary: "Third-party, IN_PERSON (both parties)",
|
||||||
|
description:
|
||||||
|
"Expert fills form on-site for both parties.",
|
||||||
|
value: {
|
||||||
|
type: "THIRD_PARTY",
|
||||||
|
creationMethod: "IN_PERSON",
|
||||||
|
firstPartyPhoneNumber: "09123456789",
|
||||||
|
secondPartyPhoneNumber: "09187654321",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
thirdPartyLink: {
|
||||||
|
summary: "Third-party, LINK",
|
||||||
|
description:
|
||||||
|
"Expert sends link to first and second party by phone.",
|
||||||
|
value: {
|
||||||
|
type: "THIRD_PARTY",
|
||||||
|
creationMethod: "LINK",
|
||||||
|
firstPartyPhoneNumber: "09123456789",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
carBodyInPerson: {
|
||||||
|
summary: "Car-body, IN_PERSON",
|
||||||
|
description: "Expert fills form on-site for single party (car body).",
|
||||||
|
value: {
|
||||||
|
type: "CAR_BODY",
|
||||||
|
creationMethod: "IN_PERSON",
|
||||||
|
firstPartyPhoneNumber: "09123456789",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
carBodyLink: {
|
||||||
|
summary: "Car-body, LINK",
|
||||||
|
description:
|
||||||
|
"Expert sends link to party by phone. Only first party phone needed.",
|
||||||
|
value: {
|
||||||
|
type: "CAR_BODY",
|
||||||
|
creationMethod: "LINK",
|
||||||
|
firstPartyPhoneNumber: "09123456789",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 201,
|
||||||
|
description: "File created",
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
requestId: { type: "string" },
|
||||||
|
publicId: { type: "string" },
|
||||||
|
linkUrl: { type: "string", description: "Present for LINK method" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
async createV2(
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
@Body() dto: CreateExpertInitiatedFileDto,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.createExpertInitiatedBlameV2(
|
||||||
|
expert,
|
||||||
|
dto,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("send-link/:requestId")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "[V2] Send blame link to party/parties (LINK)",
|
||||||
|
description:
|
||||||
|
"For expert-initiated LINK files only. Sends the blame link to the first party (and second party for THIRD_PARTY). SMS delivery is mocked for now; first party opens the link and fills the form via the normal flow. Call after create when creationMethod is LINK.",
|
||||||
|
})
|
||||||
|
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: "Link sent (mocked); recipients can open the link to fill the form",
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
sent: { type: "boolean" },
|
||||||
|
linkUrl: { type: "string" },
|
||||||
|
sentTo: {
|
||||||
|
type: "array",
|
||||||
|
items: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
role: { type: "string", enum: ["FIRST", "SECOND"] },
|
||||||
|
phoneNumber: { type: "string" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
async sendLinkV2(
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.sendLinkV2(expert, requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("send-party-otps/:requestId")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "[V2] Send OTP to party/parties (IN_PERSON)",
|
||||||
|
description:
|
||||||
|
"Sends OTP via SMS to first party (and second party for THIRD_PARTY) using the same flow as /user/send-otp. Parties receive the code; collect it from them and call verify-party-otps. Call this before filling the blame form.",
|
||||||
|
})
|
||||||
|
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
|
@ApiBody({ type: SendPartyOtpsDto })
|
||||||
|
@ApiResponse({ status: 200, description: "OTP(s) sent; collect codes and call verify-party-otps" })
|
||||||
|
async sendPartyOtpsV2(
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@Body() dto: SendPartyOtpsDto,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.sendPartyOtpsV2(expert, requestId, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("verify-party-otps/:requestId")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "[V2] Verify party OTPs (IN_PERSON)",
|
||||||
|
description:
|
||||||
|
"After send-party-otps, parties receive SMS. They tell you the code; submit it here. Required before complete-blame-data.",
|
||||||
|
})
|
||||||
|
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
|
@ApiBody({ type: VerifyPartyOtpsDto })
|
||||||
|
@ApiResponse({ status: 200, description: "OTPs verified; expert can proceed to complete-blame-data" })
|
||||||
|
async verifyPartyOtpsV2(
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@Body() dto: VerifyPartyOtpsDto,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.verifyPartyOtpsV2(expert, requestId, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("complete-blame-data/:requestId")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "[V2] Submit all blame data (IN_PERSON)",
|
||||||
|
description:
|
||||||
|
"For IN_PERSON files only. Send THIRD_PARTY or CAR_BODY form. After this, status becomes WAITING_FOR_SIGNATURES; use upload-party-signature to collect party signatures.",
|
||||||
|
})
|
||||||
|
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
|
@ApiBody({
|
||||||
|
description: "Choose THIRD_PARTY or CAR_BODY example according to the file type. All nested fields are listed so you can fill or test without guessing property names.",
|
||||||
|
examples: {
|
||||||
|
THIRD_PARTY: {
|
||||||
|
summary: "THIRD_PARTY",
|
||||||
|
description: "Use when the blame file type is THIRD_PARTY",
|
||||||
|
value: {
|
||||||
|
firstPartyPhoneNumber: "09123456789",
|
||||||
|
firstPartyInitialForm: {
|
||||||
|
expertOpinion: false,
|
||||||
|
imDamaged: false,
|
||||||
|
imGuilty: true,
|
||||||
|
},
|
||||||
|
firstPartyPlate: {
|
||||||
|
nationalCodeOfInsurer: "",
|
||||||
|
nationalCodeOfDriver: "",
|
||||||
|
insurerLicense: "",
|
||||||
|
driverLicense: "",
|
||||||
|
plate: { leftDigits: 12, centerAlphabet: "الف", centerDigits: 345, ir: 22 },
|
||||||
|
driverIsInsurer: true,
|
||||||
|
isNewCar: false,
|
||||||
|
userNoCertificate: false,
|
||||||
|
insurerBirthday: 13770624,
|
||||||
|
driverBirthday: "1370-01-01",
|
||||||
|
},
|
||||||
|
firstPartyLocation: { lat: 35.6892, lon: 51.389 },
|
||||||
|
firstPartyDescription: { desc: "توضیح حادثه طرف اول" },
|
||||||
|
secondParty: {
|
||||||
|
phoneNumber: "09187654321",
|
||||||
|
initialForm: {
|
||||||
|
expertOpinion: false,
|
||||||
|
imDamaged: true,
|
||||||
|
imGuilty: false,
|
||||||
|
},
|
||||||
|
plate: {
|
||||||
|
nationalCodeOfInsurer: "",
|
||||||
|
nationalCodeOfDriver: "",
|
||||||
|
insurerLicense: "",
|
||||||
|
driverLicense: "",
|
||||||
|
plate: { leftDigits: 91, centerAlphabet: "ن", centerDigits: 174, ir: 79 },
|
||||||
|
driverIsInsurer: true,
|
||||||
|
isNewCar: false,
|
||||||
|
userNoCertificate: false,
|
||||||
|
insurerBirthday: 13700720,
|
||||||
|
driverBirthday: "1370-01-01",
|
||||||
|
},
|
||||||
|
location: { lat: 35.6892, lon: 51.389 },
|
||||||
|
description: { desc: "توضیح حادثه طرف دوم" },
|
||||||
|
},
|
||||||
|
guiltyPartyPhoneNumber: "09123456789",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CAR_BODY: {
|
||||||
|
summary: "CAR_BODY",
|
||||||
|
description: "Use when the blame file type is CAR_BODY",
|
||||||
|
value: {
|
||||||
|
firstPartyPhoneNumber: "09123456789",
|
||||||
|
firstPartyInitialForm: {
|
||||||
|
expertOpinion: false,
|
||||||
|
imDamaged: false,
|
||||||
|
imGuilty: true,
|
||||||
|
},
|
||||||
|
carBodyForm: { car: true, object: false },
|
||||||
|
firstPartyPlate: {
|
||||||
|
plateId: "",
|
||||||
|
nationalCodeOfInsurer: "",
|
||||||
|
nationalCodeOfDriver: "",
|
||||||
|
insurerLicense: "",
|
||||||
|
driverLicense: "",
|
||||||
|
plate: { leftDigits: 12, centerAlphabet: "الف", centerDigits: 345, ir: 22 },
|
||||||
|
driverIsInsurer: true,
|
||||||
|
isNewCar: false,
|
||||||
|
userNoCertificate: false,
|
||||||
|
insurerBirthday: 1370,
|
||||||
|
driverBirthday: "1370-01-01",
|
||||||
|
},
|
||||||
|
firstPartyLocation: { lat: 35.6892, lon: 51.389 },
|
||||||
|
firstPartyDescription: {
|
||||||
|
desc: "توضیح حادثه",
|
||||||
|
accidentDate: "2025-01-15",
|
||||||
|
accidentTime: "14:30",
|
||||||
|
weatherCondition: "صاف",
|
||||||
|
roadCondition: "خشک",
|
||||||
|
lightCondition: "روز",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
schema: { type: "object" },
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: "Blame form completed; next: upload party signature(s)" })
|
||||||
|
async completeBlameDataV2(
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@Body() formData: any,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.expertCompleteBlameDataV2(
|
||||||
|
expert,
|
||||||
|
requestId,
|
||||||
|
formData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("upload-video/:requestId")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "[V2] Expert uploads video for expert-initiated BlameRequest",
|
||||||
|
})
|
||||||
|
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
|
@ApiConsumes("multipart/form-data")
|
||||||
|
@ApiBody({
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
properties: { file: { type: "string", format: "binary" } },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@UseInterceptors(
|
||||||
|
FileInterceptor("file", {
|
||||||
|
limits: { fileSize: 20 * 1024 * 1024 },
|
||||||
|
storage: diskStorage({
|
||||||
|
destination: "./files/video",
|
||||||
|
filename: (req, file, callback) => {
|
||||||
|
const unique = Date.now();
|
||||||
|
const ex = extname(file.originalname);
|
||||||
|
callback(null, `expert-${file.originalname}-${unique}${ex}`);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
@ApiResponse({ status: 200, description: "Video uploaded" })
|
||||||
|
async uploadVideoV2(
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@UploadedFile() file?: Express.Multer.File,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.expertUploadVideoForBlameV2(
|
||||||
|
expert,
|
||||||
|
requestId,
|
||||||
|
file,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("upload-voice/:requestId")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "[V2] Expert uploads voice for expert-initiated BlameRequest",
|
||||||
|
})
|
||||||
|
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
|
@ApiConsumes("multipart/form-data")
|
||||||
|
@ApiBody({
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
properties: { voice: { type: "string", format: "binary" } },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@UseInterceptors(
|
||||||
|
FileInterceptor("voice", {
|
||||||
|
limits: { fileSize: 10 * 1024 * 1024 },
|
||||||
|
storage: diskStorage({
|
||||||
|
destination: "./files/voice",
|
||||||
|
filename: (req, file, callback) => {
|
||||||
|
const unique = Date.now();
|
||||||
|
const ex = extname(file.originalname);
|
||||||
|
const flname = file.originalname.split(".")[0];
|
||||||
|
callback(null, `expert-${flname}-${unique}${ex}`);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
@ApiResponse({ status: 200, description: "Voice uploaded" })
|
||||||
|
async uploadVoiceV2(
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@UploadedFile() voice?: Express.Multer.File,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.expertUploadVoiceForBlameV2(
|
||||||
|
expert,
|
||||||
|
requestId,
|
||||||
|
voice,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("add-accident-fields/:requestId")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "[V2] Expert adds accident fields to expert-initiated BlameRequest",
|
||||||
|
})
|
||||||
|
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
|
@ApiBody({ type: ExpertAccidentFieldsDto })
|
||||||
|
@ApiResponse({ status: 200, description: "Accident fields added" })
|
||||||
|
async addAccidentFieldsV2(
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@Body() fields: ExpertAccidentFieldsDto,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.expertAddAccidentFieldsForBlameV2(
|
||||||
|
expert,
|
||||||
|
requestId,
|
||||||
|
fields,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("upload-party-signature/:requestId")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "[V2] Expert uploads party signature (IN_PERSON)",
|
||||||
|
description:
|
||||||
|
"For IN_PERSON only. Upload a party's signature collected on-site. CAR_BODY: use partyRole FIRST once. THIRD_PARTY: upload FIRST then SECOND. When all required parties have signed, blame case completes.",
|
||||||
|
})
|
||||||
|
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
|
@ApiConsumes("multipart/form-data")
|
||||||
|
@ApiBody({
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
required: ["partyRole", "sign"],
|
||||||
|
properties: {
|
||||||
|
partyRole: { type: "string", enum: ["FIRST", "SECOND"] },
|
||||||
|
isAccept: { type: "boolean", default: true },
|
||||||
|
sign: { type: "string", format: "binary" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@UseInterceptors(
|
||||||
|
FileInterceptor("sign", {
|
||||||
|
limits: { fileSize: 10 * 1024 * 1024 },
|
||||||
|
storage: diskStorage({
|
||||||
|
destination: "./files/signs",
|
||||||
|
filename: (req, file, callback) => {
|
||||||
|
const unique = Date.now();
|
||||||
|
const ex = extname(file.originalname);
|
||||||
|
callback(null, `expert-party-${unique}${ex}`);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
@ApiResponse({ status: 200, description: "Signature recorded" })
|
||||||
|
async uploadPartySignatureV2(
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@Body() body: { partyRole?: string; isAccept?: string | boolean },
|
||||||
|
@UploadedFile() sign?: Express.Multer.File,
|
||||||
|
) {
|
||||||
|
const partyRole = (body.partyRole === "FIRST" || body.partyRole === "SECOND")
|
||||||
|
? body.partyRole
|
||||||
|
: ("FIRST" as const);
|
||||||
|
const isAccept = body.isAccept === false || body.isAccept === "false" ? false : true;
|
||||||
|
return this.requestManagementService.expertUploadPartySignatureV2(
|
||||||
|
expert,
|
||||||
|
requestId,
|
||||||
|
partyRole as PartyRole,
|
||||||
|
isAccept,
|
||||||
|
sign!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("create-claim-from-blame/:blameRequestId")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "[V2] Create claim from expert-initiated IN_PERSON blame",
|
||||||
|
description:
|
||||||
|
"Field expert creates a claim on behalf of the damaged party. Blame must be COMPLETED (signatures collected). Then use claim v2 endpoints to fill parts, documents, and captures.",
|
||||||
|
})
|
||||||
|
@ApiParam({ name: "blameRequestId", description: "Completed blame request ID" })
|
||||||
|
@ApiResponse({ status: 201, description: "Claim created" })
|
||||||
|
async createClaimFromBlame(
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
@Param("blameRequestId") blameRequestId: string,
|
||||||
|
) {
|
||||||
|
return this.claimRequestManagementService.createClaimFromBlameForExpertV2(
|
||||||
|
blameRequestId,
|
||||||
|
expert,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("complete-claim-data/:claimRequestId")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Submit claim-needed data (expert-initiated IN_PERSON)",
|
||||||
|
})
|
||||||
|
@ApiParam({ name: "claimRequestId", description: "Claim file ID" })
|
||||||
|
@ApiBody({ type: ExpertCompleteClaimDataDto })
|
||||||
|
@ApiResponse({ status: 200, description: "Claim data updated" })
|
||||||
|
async completeClaimData(
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
@Body() dto: ExpertCompleteClaimDataDto,
|
||||||
|
) {
|
||||||
|
return this.claimRequestManagementService.expertCompleteClaimData(
|
||||||
|
claimRequestId,
|
||||||
|
expert,
|
||||||
|
dto,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
155
src/request-management/registrar-initiated.controller.ts
Normal file
155
src/request-management/registrar-initiated.controller.ts
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import { extname } from "node:path";
|
||||||
|
import { Body, Controller, Get, Param, Post, UploadedFile, UseGuards, UseInterceptors } from "@nestjs/common";
|
||||||
|
import { FileInterceptor } from "@nestjs/platform-express";
|
||||||
|
import { diskStorage } from "multer";
|
||||||
|
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiOperation, ApiParam, ApiTags } from "@nestjs/swagger";
|
||||||
|
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
||||||
|
import { RolesGuard } from "src/auth/guards/role.guard";
|
||||||
|
import { CurrentUser } from "src/decorators/user.decorator";
|
||||||
|
import { Roles } from "src/decorators/roles.decorator";
|
||||||
|
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||||
|
import { RequestManagementService } from "./request-management.service";
|
||||||
|
import { CreateRegistrarInitiatedFileDto } from "./dto/registrar-initiated.dto";
|
||||||
|
import { SendPartyOtpsDto } from "./dto/send-party-otps.dto";
|
||||||
|
import { VerifyPartyOtpsDto } from "./dto/verify-party-otps.dto";
|
||||||
|
import { ExpertAccidentFieldsDto } from "./dto/expert-accident-fields.dto";
|
||||||
|
import { PartyRole } from "./entities/schema/partyRole.enum";
|
||||||
|
|
||||||
|
@ApiTags("registrar-initiated-blame (v1)")
|
||||||
|
@Controller("registrar-initiated-blame")
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
||||||
|
@Roles(RoleEnum.REGISTRAR)
|
||||||
|
export class RegistrarInitiatedController {
|
||||||
|
constructor(private readonly requestManagementService: RequestManagementService) {}
|
||||||
|
|
||||||
|
@Post("create")
|
||||||
|
@ApiOperation({ summary: "Registrar creates IN_PERSON blame file" })
|
||||||
|
@ApiBody({ type: CreateRegistrarInitiatedFileDto })
|
||||||
|
create(@CurrentUser() registrar: any, @Body() dto: CreateRegistrarInitiatedFileDto) {
|
||||||
|
return this.requestManagementService.createRegistrarInitiatedBlame(registrar, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("my-files")
|
||||||
|
myFiles(@CurrentUser() registrar: any) {
|
||||||
|
return this.requestManagementService.getMyExpertInitiatedFilesV2(registrar);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("blame/:requestId")
|
||||||
|
getBlame(@Param("requestId") requestId: string, @CurrentUser() registrar: any) {
|
||||||
|
return this.requestManagementService.getBlameRequestV2(requestId, registrar);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("send-party-otps/:requestId")
|
||||||
|
@ApiParam({ name: "requestId" })
|
||||||
|
@ApiBody({ type: SendPartyOtpsDto })
|
||||||
|
sendPartyOtps(
|
||||||
|
@CurrentUser() registrar: any,
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@Body() dto: SendPartyOtpsDto,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.sendPartyOtpsV2(registrar, requestId, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("verify-party-otps/:requestId")
|
||||||
|
@ApiParam({ name: "requestId" })
|
||||||
|
@ApiBody({ type: VerifyPartyOtpsDto })
|
||||||
|
verifyPartyOtps(
|
||||||
|
@CurrentUser() registrar: any,
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@Body() dto: VerifyPartyOtpsDto,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.verifyPartyOtpsV2(registrar, requestId, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("complete-blame-data/:requestId")
|
||||||
|
completeBlameData(
|
||||||
|
@CurrentUser() registrar: any,
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@Body() body: any,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.expertCompleteBlameDataV2(registrar, requestId, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("upload-video/:requestId")
|
||||||
|
@ApiConsumes("multipart/form-data")
|
||||||
|
@UseInterceptors(
|
||||||
|
FileInterceptor("file", {
|
||||||
|
limits: { fileSize: 20 * 1024 * 1024 },
|
||||||
|
storage: diskStorage({
|
||||||
|
destination: "./files/video",
|
||||||
|
filename: (req, file, cb) => cb(null, `registrar-${Date.now()}${extname(file.originalname)}`),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
uploadVideo(
|
||||||
|
@CurrentUser() registrar: any,
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@UploadedFile() file?: Express.Multer.File,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.expertUploadVideoForBlameV2(registrar, requestId, file);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("upload-voice/:requestId")
|
||||||
|
@ApiConsumes("multipart/form-data")
|
||||||
|
@UseInterceptors(
|
||||||
|
FileInterceptor("voice", {
|
||||||
|
limits: { fileSize: 10 * 1024 * 1024 },
|
||||||
|
storage: diskStorage({
|
||||||
|
destination: "./files/voice",
|
||||||
|
filename: (req, file, cb) => cb(null, `registrar-${Date.now()}${extname(file.originalname)}`),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
uploadVoice(
|
||||||
|
@CurrentUser() registrar: any,
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@UploadedFile() voice?: Express.Multer.File,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.expertUploadVoiceForBlameV2(registrar, requestId, voice);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("add-accident-fields/:requestId")
|
||||||
|
@ApiBody({ type: ExpertAccidentFieldsDto })
|
||||||
|
addAccidentFields(
|
||||||
|
@CurrentUser() registrar: any,
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@Body() fields: ExpertAccidentFieldsDto,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.expertAddAccidentFieldsForBlameV2(
|
||||||
|
registrar,
|
||||||
|
requestId,
|
||||||
|
fields,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("upload-party-signature/:requestId")
|
||||||
|
@ApiConsumes("multipart/form-data")
|
||||||
|
@UseInterceptors(
|
||||||
|
FileInterceptor("sign", {
|
||||||
|
limits: { fileSize: 10 * 1024 * 1024 },
|
||||||
|
storage: diskStorage({
|
||||||
|
destination: "./files/signs",
|
||||||
|
filename: (req, file, cb) => cb(null, `registrar-party-${Date.now()}${extname(file.originalname)}`),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
uploadPartySignature(
|
||||||
|
@CurrentUser() registrar: any,
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@Body() body: { partyRole?: string; isAccept?: boolean | string },
|
||||||
|
@UploadedFile() sign?: Express.Multer.File,
|
||||||
|
) {
|
||||||
|
const role =
|
||||||
|
body.partyRole === "SECOND" ? PartyRole.SECOND : PartyRole.FIRST;
|
||||||
|
const isAccept = !(body.isAccept === false || body.isAccept === "false");
|
||||||
|
return this.requestManagementService.expertUploadPartySignatureV2(
|
||||||
|
registrar,
|
||||||
|
requestId,
|
||||||
|
role,
|
||||||
|
isAccept,
|
||||||
|
sign!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -12,7 +12,9 @@ import { UsersModule } from "src/users/users.module";
|
|||||||
import { CronModule } from "src/utils/cron/cron.module";
|
import { CronModule } from "src/utils/cron/cron.module";
|
||||||
import { SmsManagerModule } from "src/utils/sms-manager/sms-manager.module";
|
import { SmsManagerModule } from "src/utils/sms-manager/sms-manager.module";
|
||||||
import { PublicIdModule } from "src/utils/public-id/public-id.module";
|
import { PublicIdModule } from "src/utils/public-id/public-id.module";
|
||||||
|
import { HashModule } from "src/utils/hash/hash.module";
|
||||||
import { WorkflowStepManagementModule } from "src/workflow-step-management/workflow-step-management.module";
|
import { WorkflowStepManagementModule } from "src/workflow-step-management/workflow-step-management.module";
|
||||||
|
import { AuthModule } from "src/auth/auth.module";
|
||||||
import { BlameDocumentDbService } from "./entities/db-service/blame-document.db.service";
|
import { BlameDocumentDbService } from "./entities/db-service/blame-document.db.service";
|
||||||
import { BlameVoiceDbService } from "./entities/db-service/blame.voice.db.service";
|
import { BlameVoiceDbService } from "./entities/db-service/blame.voice.db.service";
|
||||||
import { UserSignDbService } from "./entities/db-service/sign.db.service";
|
import { UserSignDbService } from "./entities/db-service/sign.db.service";
|
||||||
@@ -35,6 +37,8 @@ import { RequestManagementService } from "./request-management.service";
|
|||||||
import { RequestManagementController } from "./request-management.controller";
|
import { RequestManagementController } from "./request-management.controller";
|
||||||
import { RequestManagementV2Controller } from "./request-management.v2.controller";
|
import { RequestManagementV2Controller } from "./request-management.v2.controller";
|
||||||
import { ExpertInitiatedController } from "./expert-initiated.controller";
|
import { ExpertInitiatedController } from "./expert-initiated.controller";
|
||||||
|
import { ExpertInitiatedV2Controller } from "./expert-initiated.v2.controller";
|
||||||
|
import { RegistrarInitiatedController } from "./registrar-initiated.controller";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -43,6 +47,7 @@ import { ExpertInitiatedController } from "./expert-initiated.controller";
|
|||||||
SandHubModule,
|
SandHubModule,
|
||||||
SmsManagerModule,
|
SmsManagerModule,
|
||||||
PublicIdModule,
|
PublicIdModule,
|
||||||
|
HashModule,
|
||||||
WorkflowStepManagementModule,
|
WorkflowStepManagementModule,
|
||||||
PlatesModule,
|
PlatesModule,
|
||||||
MulterModule.register({
|
MulterModule.register({
|
||||||
@@ -58,11 +63,14 @@ import { ExpertInitiatedController } from "./expert-initiated.controller";
|
|||||||
]),
|
]),
|
||||||
forwardRef(() => ClaimRequestManagementModule),
|
forwardRef(() => ClaimRequestManagementModule),
|
||||||
CronModule,
|
CronModule,
|
||||||
|
AuthModule,
|
||||||
],
|
],
|
||||||
controllers: [
|
controllers: [
|
||||||
RequestManagementController,
|
RequestManagementController,
|
||||||
RequestManagementV2Controller,
|
RequestManagementV2Controller,
|
||||||
ExpertInitiatedController,
|
ExpertInitiatedController,
|
||||||
|
ExpertInitiatedV2Controller,
|
||||||
|
RegistrarInitiatedController,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
RequestManagementService,
|
RequestManagementService,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,7 @@ import {
|
|||||||
CreateBlameRequestDtoV2,
|
CreateBlameRequestDtoV2,
|
||||||
DescriptionDto,
|
DescriptionDto,
|
||||||
LocationDto,
|
LocationDto,
|
||||||
|
CarBodyFormDto,
|
||||||
} from "./dto/create-request-management.dto";
|
} from "./dto/create-request-management.dto";
|
||||||
import { RequestManagementService } from "./request-management.service";
|
import { RequestManagementService } from "./request-management.service";
|
||||||
|
|
||||||
@@ -50,7 +51,7 @@ import { RequestManagementService } from "./request-management.service";
|
|||||||
export class RequestManagementV2Controller {
|
export class RequestManagementV2Controller {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly requestManagementService: RequestManagementService,
|
private readonly requestManagementService: RequestManagementService,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@UseGuards(GlobalGuard)
|
@UseGuards(GlobalGuard)
|
||||||
@@ -64,6 +65,27 @@ export class RequestManagementV2Controller {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@Roles(RoleEnum.USER, RoleEnum.FIELD_EXPERT)
|
||||||
|
async getAllBlameRequestsV2(
|
||||||
|
@CurrentUser() user: any,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.getAllBlameRequestsV2(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get one blame request by id. Allowed for the request owner (party) or the initiating field expert.
|
||||||
|
*/
|
||||||
|
@Get(":requestId")
|
||||||
|
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
|
@Roles(RoleEnum.USER, RoleEnum.FIELD_EXPERT)
|
||||||
|
async getBlameRequestV2(
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@CurrentUser() user: any,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.getBlameRequestV2(requestId, user);
|
||||||
|
}
|
||||||
|
|
||||||
@Post("/blame-confession/:requestId")
|
@Post("/blame-confession/:requestId")
|
||||||
@ApiParam({ name: "requestId" })
|
@ApiParam({ name: "requestId" })
|
||||||
@ApiBody({ type: BlameConfessionDtoV2 })
|
@ApiBody({ type: BlameConfessionDtoV2 })
|
||||||
@@ -76,6 +98,19 @@ export class RequestManagementV2Controller {
|
|||||||
return this.requestManagementService.blameConfessionV2(requestId, body, user);
|
return this.requestManagementService.blameConfessionV2(requestId, body, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** CAR_BODY only: submit accident type (car vs object). Call this when nextStep is CAR_BODY_ACCIDENT_TYPE. */
|
||||||
|
@Post("/car-body-form/:requestId")
|
||||||
|
@ApiParam({ name: "requestId" })
|
||||||
|
@ApiBody({ type: CarBodyFormDto })
|
||||||
|
@UseGuards(GlobalGuard)
|
||||||
|
async carBodyFormV2(
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@Body() body: CarBodyFormDto,
|
||||||
|
@CurrentUser() user,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.carBodyAccidentTypeFormV2(requestId, body, user);
|
||||||
|
}
|
||||||
|
|
||||||
@ApiBody({
|
@ApiBody({
|
||||||
schema: {
|
schema: {
|
||||||
type: "object",
|
type: "object",
|
||||||
@@ -185,7 +220,28 @@ export class RequestManagementV2Controller {
|
|||||||
|
|
||||||
@Post("/add-detail-description/:requestId")
|
@Post("/add-detail-description/:requestId")
|
||||||
@ApiParam({ name: "requestId" })
|
@ApiParam({ name: "requestId" })
|
||||||
@ApiBody({ type: DescriptionDto })
|
@ApiBody({
|
||||||
|
description:
|
||||||
|
"THIRD_PARTY: send only desc. CAR_BODY: send desc + accidentDate, accidentTime, weatherCondition, roadCondition, lightCondition (all required).",
|
||||||
|
type: DescriptionDto,
|
||||||
|
examples: {
|
||||||
|
thirdParty: {
|
||||||
|
summary: "THIRD_PARTY (only desc)",
|
||||||
|
value: { desc: "Accident description text" },
|
||||||
|
},
|
||||||
|
carBody: {
|
||||||
|
summary: "CAR_BODY (desc + conditions)",
|
||||||
|
value: {
|
||||||
|
desc: "Accident description",
|
||||||
|
accidentDate: "2025-12-08",
|
||||||
|
accidentTime: "14:30",
|
||||||
|
weatherCondition: "صاف",
|
||||||
|
roadCondition: "خشک",
|
||||||
|
lightCondition: "روز",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@UseGuards(GlobalGuard)
|
@UseGuards(GlobalGuard)
|
||||||
async addDescriptionV2(
|
async addDescriptionV2(
|
||||||
@Param("requestId") requestId: string,
|
@Param("requestId") requestId: string,
|
||||||
|
|||||||
@@ -29,7 +29,144 @@ export class SandHubService {
|
|||||||
private readonly sandHubDbService: SandHubDbService,
|
private readonly sandHubDbService: SandHubDbService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When false (default), no outbound HTTP to SandHub/Tejarat gateways — all inquiries return mocks.
|
||||||
|
* Set `SANDHUB_USE_REAL_API=true` to restore live calls.
|
||||||
|
*/
|
||||||
|
private useLiveSandHubApis(): boolean {
|
||||||
|
return process.env.SANDHUB_USE_REAL_API === "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fixed plate/insurance inquiry payload used everywhere we mock block-inquiry style APIs. */
|
||||||
|
private getDefaultMockPlateInquiryRaw(): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
PrntPlcyCmpDocNo: "1403/1143-70591/200/35",
|
||||||
|
MapTypNam: "پرايد هاچ بک -111",
|
||||||
|
MtrNum: "5215907",
|
||||||
|
ShsNum: "NAS431100E5798656",
|
||||||
|
DisFnYrNum: "0",
|
||||||
|
DisLfYrNum: "0",
|
||||||
|
DisPrsnYrNum: "0",
|
||||||
|
DisPrsnYrPrcnt: "0",
|
||||||
|
DisFnYrPrcnt: "0",
|
||||||
|
DisLfYrPrcnt: "0",
|
||||||
|
vin: "IRPC941V2BD798656",
|
||||||
|
MapVehicleSystemName: "ثبت نشده",
|
||||||
|
LfCvrCptl: 0,
|
||||||
|
FnCvrCptl: 0,
|
||||||
|
PrsnCvrCptl: 0,
|
||||||
|
VehicleSystemCode: 1,
|
||||||
|
EdrsJson: '[{"id":1,"Dsc":" الحاقيه توضيحات ندارد"}]',
|
||||||
|
PersonCvrCptl: 12000000000,
|
||||||
|
LifeCvrCptl: 16000000000,
|
||||||
|
FinancialCvrCptl: 4000000000,
|
||||||
|
CarGroupCode: 3,
|
||||||
|
CylCnt: 4,
|
||||||
|
LastCompanyDocumentNumber: "03/1031/2835/1001/662",
|
||||||
|
UsageCode: "8",
|
||||||
|
MapUsageCode: 1,
|
||||||
|
MapUsageName: "شخصي",
|
||||||
|
Plk1: 59,
|
||||||
|
Plk2: 16,
|
||||||
|
Plk3: 419,
|
||||||
|
PlkSrl: 78,
|
||||||
|
PrintEndorsCompanyDocumentNumber: "بيمه نامه الحاقيه ندارد.",
|
||||||
|
EndorseDate: null,
|
||||||
|
InsuranceFullName: null,
|
||||||
|
SystemField: "سايپا",
|
||||||
|
TypeField: "111SE",
|
||||||
|
UsageField: "سواري",
|
||||||
|
MainColorField: "سفيد",
|
||||||
|
SecondColorField: "سفيد شيري",
|
||||||
|
ModelField: "1394",
|
||||||
|
CapacityField: "جمعا 4 نفر",
|
||||||
|
CylinderNumberField: "4",
|
||||||
|
EngineNumberField: "5215907",
|
||||||
|
ChassisNumberField: "NAS431100E5798656",
|
||||||
|
VinNumberField: "IRPC941V2BD798656",
|
||||||
|
InstallDateField: "1403/03/01",
|
||||||
|
AxelNumberField: "2",
|
||||||
|
WheelNumberField: "4",
|
||||||
|
CompanyName: "بیمه سامان",
|
||||||
|
CompanyCode: "15",
|
||||||
|
IssueDate: "1403/04/06",
|
||||||
|
SatrtDate: "1403/04/06",
|
||||||
|
EndDate: "1404/04/06",
|
||||||
|
Thrname: "",
|
||||||
|
EndorseText: null,
|
||||||
|
PolicyHealthLossCount: 0,
|
||||||
|
PolicyFinancialLossCount: 0,
|
||||||
|
PolicyPersonLossCount: 0,
|
||||||
|
Tonage: 0,
|
||||||
|
ThirdPolicyCode: 10502330579,
|
||||||
|
DiscountPersonPercent: null,
|
||||||
|
DiscountThirdPercent: null,
|
||||||
|
SystemCodeCii: 0,
|
||||||
|
SystemNameCii: "ثبت نشده",
|
||||||
|
TypeCodeCii: 12189,
|
||||||
|
TypeNameCii: "پرايد هاچ بک -111",
|
||||||
|
UsageNameCii: "سواري",
|
||||||
|
UsageCodeCii: 1,
|
||||||
|
ModelCii: 1394,
|
||||||
|
damageTypes: [],
|
||||||
|
StatusTypeCode: 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bodies returned from `makeSandHubRequest` in mock mode (same shape callers expect from real API).
|
||||||
|
*/
|
||||||
|
private buildMockSandHubResponseForUrl(url: string): any {
|
||||||
|
const u = (url || "").toLowerCase();
|
||||||
|
if (u.includes("personal-inquiry")) {
|
||||||
|
return {
|
||||||
|
message: "success",
|
||||||
|
data: {
|
||||||
|
firstName: "نام",
|
||||||
|
lastName: "خانوادگی",
|
||||||
|
fatherName: "-",
|
||||||
|
birthCertificateNumber: "-",
|
||||||
|
nin: "-",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (u.includes("driver-license-check")) {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
IsSucceed: true,
|
||||||
|
Message: "mock",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (u.includes("ownership")) {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
IsSuccess: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (u.includes("sheba")) {
|
||||||
|
return {
|
||||||
|
ReturnValue: true,
|
||||||
|
HasError: false,
|
||||||
|
Message: "mock-sheba-ok",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (u.includes("block-inquiry") || u.includes("tejarat")) {
|
||||||
|
return this.mapNewApiResponseToOldFormat(
|
||||||
|
this.getDefaultMockPlateInquiryRaw(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.logger.warn(
|
||||||
|
`[MOCK] Unrecognized SandHub URL; returning generic OK: ${url}`,
|
||||||
|
);
|
||||||
|
return { data: {}, message: "mock-ok" };
|
||||||
|
}
|
||||||
|
|
||||||
private async getAccessToken(): Promise<string> {
|
private async getAccessToken(): Promise<string> {
|
||||||
|
if (!this.useLiveSandHubApis()) {
|
||||||
|
return "mock-sandhub-access-token";
|
||||||
|
}
|
||||||
if (this.loginToken && this.tokenExpiry && this.tokenExpiry > new Date()) {
|
if (this.loginToken && this.tokenExpiry && this.tokenExpiry > new Date()) {
|
||||||
return this.loginToken;
|
return this.loginToken;
|
||||||
}
|
}
|
||||||
@@ -67,6 +204,9 @@ export class SandHubService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getTejaratAccessToken(): Promise<string> {
|
private async getTejaratAccessToken(): Promise<string> {
|
||||||
|
if (!this.useLiveSandHubApis()) {
|
||||||
|
return "mock-tejarat-access-token";
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
this.tejaratAccessToken &&
|
this.tejaratAccessToken &&
|
||||||
this.tejaratTokenExpiry &&
|
this.tejaratTokenExpiry &&
|
||||||
@@ -122,6 +262,10 @@ export class SandHubService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async makeTejaratRequest(url: string, payload: any, maxRetries = 2) {
|
private async makeTejaratRequest(url: string, payload: any, maxRetries = 2) {
|
||||||
|
if (!this.useLiveSandHubApis()) {
|
||||||
|
this.logger.log(`[MOCK] Tejarat POST skipped: ${url}`);
|
||||||
|
return this.getDefaultMockPlateInquiryRaw();
|
||||||
|
}
|
||||||
const INITIAL_DELAY = 500;
|
const INITIAL_DELAY = 500;
|
||||||
const BACKOFF_FACTOR = 2;
|
const BACKOFF_FACTOR = 2;
|
||||||
|
|
||||||
@@ -183,7 +327,14 @@ export class SandHubService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const requestUrl = `${baseUrl}/block-inquiry-tejarat`;
|
const requestUrl = `${baseUrl}/block-inquiry-tejarat`;
|
||||||
const raw = await this.makeTejaratRequest(requestUrl, requestPayload);
|
const raw = this.useLiveSandHubApis()
|
||||||
|
? await this.makeTejaratRequest(requestUrl, requestPayload)
|
||||||
|
: this.getDefaultMockPlateInquiryRaw();
|
||||||
|
if (!this.useLiveSandHubApis()) {
|
||||||
|
this.logger.debug(
|
||||||
|
`[MOCK] getTejaratBlockInquiry plate=${JSON.stringify(requestPayload)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
const mapped = this.mapNewApiResponseToOldFormat(raw);
|
const mapped = this.mapNewApiResponseToOldFormat(raw);
|
||||||
return { raw, mapped };
|
return { raw, mapped };
|
||||||
}
|
}
|
||||||
@@ -193,6 +344,10 @@ export class SandHubService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async makeSandHubRequest(url: string, payload: any, maxRetries = 3) {
|
private async makeSandHubRequest(url: string, payload: any, maxRetries = 3) {
|
||||||
|
if (!this.useLiveSandHubApis()) {
|
||||||
|
this.logger.log(`[MOCK] SandHub POST skipped: ${url}`);
|
||||||
|
return this.buildMockSandHubResponseForUrl(url);
|
||||||
|
}
|
||||||
const token = await this.getAccessToken();
|
const token = await this.getAccessToken();
|
||||||
const INITIAL_DELAY = 1000;
|
const INITIAL_DELAY = 1000;
|
||||||
const BACKOFF_FACTOR = 2;
|
const BACKOFF_FACTOR = 2;
|
||||||
@@ -293,16 +448,25 @@ export class SandHubService {
|
|||||||
rightTwoDigits: String(userDetail.plate.ir),
|
rightTwoDigits: String(userDetail.plate.ir),
|
||||||
nationalCode: userDetail.nationalCodeOfInsurer,
|
nationalCode: userDetail.nationalCodeOfInsurer,
|
||||||
};
|
};
|
||||||
const requestUrl = `${process.env.SANDHUB_BASE_URL}/block-inquiry-tejarat`;
|
const base = process.env.SANDHUB_BASE_URL ?? "";
|
||||||
const response = await this.makeSandHubRequest(requestUrl, requestPayload);
|
const requestUrl = `${base}/block-inquiry-tejarat`;
|
||||||
|
|
||||||
// Map the new API response format to the old format
|
let response: any;
|
||||||
let result = this.mapNewApiResponseToOldFormat(response);
|
if (this.useLiveSandHubApis()) {
|
||||||
|
response = await this.makeSandHubRequest(requestUrl, requestPayload);
|
||||||
if (result.usgCod !== "8") {
|
} else {
|
||||||
throw new Error("خودرو شما شخصی / سواری نمی باشد")
|
this.logger.debug(
|
||||||
|
`[MOCK] getSandHubResponse plate=${JSON.stringify(requestPayload)}`,
|
||||||
|
);
|
||||||
|
response = this.getDefaultMockPlateInquiryRaw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const result = this.mapNewApiResponseToOldFormat(response);
|
||||||
|
|
||||||
|
// if (result.usgCod !== "8") {
|
||||||
|
// throw new Error("خودرو شما شخصی / سواری نمی باشد")
|
||||||
|
// }
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error(err);
|
throw new Error(err);
|
||||||
|
|||||||
40
src/users/entities/db-service/registrar.db.service.ts
Normal file
40
src/users/entities/db-service/registrar.db.service.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { InjectModel } from "@nestjs/mongoose";
|
||||||
|
import { FilterQuery, Model, Types } from "mongoose";
|
||||||
|
import { RegistrarModel } from "../schema/registrar.schema";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RegistrarDbService {
|
||||||
|
constructor(
|
||||||
|
@InjectModel(RegistrarModel.name)
|
||||||
|
private readonly registrarModel: Model<RegistrarModel>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async create(
|
||||||
|
user: Partial<RegistrarModel> & { password: string },
|
||||||
|
): Promise<RegistrarModel> {
|
||||||
|
return this.registrarModel.create(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(
|
||||||
|
filter: FilterQuery<RegistrarModel>,
|
||||||
|
): Promise<RegistrarModel | null> {
|
||||||
|
return this.registrarModel.findOne(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOneAndUpdate(
|
||||||
|
filter: FilterQuery<RegistrarModel>,
|
||||||
|
update: any,
|
||||||
|
): Promise<RegistrarModel | null> {
|
||||||
|
return this.registrarModel.findOneAndUpdate(filter, update, { new: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateOne(filter: FilterQuery<RegistrarModel>, update: any) {
|
||||||
|
return this.registrarModel.updateOne(filter, update);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(userId: string): Promise<RegistrarModel | null> {
|
||||||
|
return this.registrarModel.findOne({ _id: new Types.ObjectId(userId) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
38
src/users/entities/schema/registrar.schema.ts
Normal file
38
src/users/entities/schema/registrar.schema.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
|
import { Types } from "mongoose";
|
||||||
|
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||||
|
|
||||||
|
@Schema({
|
||||||
|
collection: "registrar",
|
||||||
|
versionKey: false,
|
||||||
|
timestamps: true,
|
||||||
|
})
|
||||||
|
export class RegistrarModel {
|
||||||
|
_id?: Types.ObjectId;
|
||||||
|
|
||||||
|
@Prop({ type: "string", unique: true })
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@Prop({ type: "string" })
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@Prop({ required: true })
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
@Prop({ type: Types.ObjectId, required: true, index: true })
|
||||||
|
clientKey: Types.ObjectId;
|
||||||
|
|
||||||
|
@Prop({ default: RoleEnum.REGISTRAR })
|
||||||
|
role: RoleEnum;
|
||||||
|
|
||||||
|
@Prop({ type: "string", default: "" })
|
||||||
|
otp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RegistrarDbSchema = SchemaFactory.createForClass(RegistrarModel);
|
||||||
|
|
||||||
|
RegistrarDbSchema.pre("save", function (next) {
|
||||||
|
if (!this.username) this.username = this.email;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@ 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 { 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 { RegistrarDbService } from "./entities/db-service/registrar.db.service";
|
||||||
import { UserDbService } from "./entities/db-service/user.db.service";
|
import { UserDbService } from "./entities/db-service/user.db.service";
|
||||||
import {
|
import {
|
||||||
DamageExpertDbSchema,
|
DamageExpertDbSchema,
|
||||||
@@ -21,6 +22,10 @@ import {
|
|||||||
InsurerExpertDbSchema,
|
InsurerExpertDbSchema,
|
||||||
InsurerExpertModel,
|
InsurerExpertModel,
|
||||||
} from "./entities/schema/insurer-expert.schema";
|
} from "./entities/schema/insurer-expert.schema";
|
||||||
|
import {
|
||||||
|
RegistrarDbSchema,
|
||||||
|
RegistrarModel,
|
||||||
|
} from "./entities/schema/registrar.schema";
|
||||||
import { UserModel, UserDbSchema } from "./entities/schema/user.schema";
|
import { UserModel, UserDbSchema } from "./entities/schema/user.schema";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -31,6 +36,7 @@ import { UserModel, UserDbSchema } from "./entities/schema/user.schema";
|
|||||||
{ name: ExpertModel.name, schema: ExpertDbSchema },
|
{ name: ExpertModel.name, schema: ExpertDbSchema },
|
||||||
{ name: FieldExpertModel.name, schema: FieldExpertDbSchema },
|
{ name: FieldExpertModel.name, schema: FieldExpertDbSchema },
|
||||||
{ name: InsurerExpertModel.name, schema: InsurerExpertDbSchema },
|
{ name: InsurerExpertModel.name, schema: InsurerExpertDbSchema },
|
||||||
|
{ name: RegistrarModel.name, schema: RegistrarDbSchema },
|
||||||
]),
|
]),
|
||||||
OtpModule,
|
OtpModule,
|
||||||
HashModule,
|
HashModule,
|
||||||
@@ -42,6 +48,7 @@ import { UserModel, UserDbSchema } from "./entities/schema/user.schema";
|
|||||||
DamageExpertDbService,
|
DamageExpertDbService,
|
||||||
FieldExpertDbService,
|
FieldExpertDbService,
|
||||||
InsurerExpertDbService,
|
InsurerExpertDbService,
|
||||||
|
RegistrarDbService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
UserDbService,
|
UserDbService,
|
||||||
@@ -49,6 +56,7 @@ import { UserModel, UserDbSchema } from "./entities/schema/user.schema";
|
|||||||
DamageExpertDbService,
|
DamageExpertDbService,
|
||||||
FieldExpertDbService,
|
FieldExpertDbService,
|
||||||
InsurerExpertDbService,
|
InsurerExpertDbService,
|
||||||
|
RegistrarDbService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class UsersModule {}
|
export class UsersModule {}
|
||||||
|
|||||||
@@ -128,9 +128,9 @@ export class CreateWorkflowStepDto {
|
|||||||
stepKey: WorkflowStep | ClaimWorkflowStep;
|
stepKey: WorkflowStep | ClaimWorkflowStep;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'Workflow type: THIRD_PARTY (blame) or CLAIM (claim workflow)',
|
description: 'Workflow type: THIRD_PARTY (blame), CAR_BODY (blame), or CLAIM (claim workflow)',
|
||||||
example: 'THIRD_PARTY',
|
example: 'THIRD_PARTY',
|
||||||
enum: ['THIRD_PARTY', 'CLAIM']
|
enum: ['THIRD_PARTY', 'CAR_BODY', 'CLAIM']
|
||||||
})
|
})
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|||||||
@@ -1123,6 +1123,43 @@ export class WorkflowStepManagementService {
|
|||||||
party: 'second'
|
party: 'second'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// CAR_BODY workflow: accident type form (replaces confession for CAR_BODY)
|
||||||
|
{
|
||||||
|
stepKey: WorkflowStep.CAR_BODY_ACCIDENT_TYPE,
|
||||||
|
type: 'CAR_BODY',
|
||||||
|
stepNumber: 2,
|
||||||
|
isActive: true,
|
||||||
|
stepName_fa: 'نوع حادثه بدنه',
|
||||||
|
stepName_en: 'Car Body Accident Type',
|
||||||
|
description_fa: 'انتخاب اینکه حادثه با خودرو بوده یا با شیء',
|
||||||
|
description_en: 'Select whether accident was with another car or with an object',
|
||||||
|
category: 'FIRST_PARTY',
|
||||||
|
requiredPreviousSteps: [WorkflowStep.CREATED],
|
||||||
|
nextPossibleSteps: [WorkflowStep.FIRST_VIDEO],
|
||||||
|
allowedRoles: ['user'],
|
||||||
|
estimatedDuration: 1,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
id: '507f1f77bcf86cd7994390cb' as any,
|
||||||
|
name_fa: 'نوع حادثه',
|
||||||
|
name_en: 'accidentType',
|
||||||
|
label_fa: 'نوع حادثه',
|
||||||
|
label_en: 'Accident Type',
|
||||||
|
placeholder_fa: 'حادثه با خودرو یا شیء؟',
|
||||||
|
placeholder_en: 'Accident with car or object?',
|
||||||
|
value: [
|
||||||
|
{ label_fa: 'با خودرو', label_en: 'With Car', value: 'car' },
|
||||||
|
{ label_fa: 'با شیء', label_en: 'With Object', value: 'object' }
|
||||||
|
],
|
||||||
|
dataType: 'select',
|
||||||
|
required: true,
|
||||||
|
order: 1,
|
||||||
|
isActive: true,
|
||||||
|
validation: { enum: ['car', 'object'] }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
metadata: { icon: 'car', color: '#10B981' }
|
||||||
|
},
|
||||||
// Add more default steps as needed...
|
// Add more default steps as needed...
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user