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 |
2846
package-lock.json
generated
2846
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -44,6 +44,7 @@
|
||||
"class-validator": "^0.14.1",
|
||||
"crypto": "^1.0.1",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.22.1",
|
||||
"express-basic-auth": "^1.2.1",
|
||||
"fastest-levenshtein": "^1.0.16",
|
||||
"form-data": "^4.0.2",
|
||||
|
||||
@@ -3,6 +3,9 @@ export enum WorkflowStep {
|
||||
|
||||
CREATED = "CREATED",
|
||||
|
||||
// ---------- CAR_BODY only (no confession; accident type form) ----------
|
||||
CAR_BODY_ACCIDENT_TYPE = "CAR_BODY_ACCIDENT_TYPE",
|
||||
|
||||
// ---------- First party ----------
|
||||
FIRST_BLAME_CONFESSION = "FIRST_BLAME_CONFESSION",
|
||||
FIRST_VIDEO = "FIRST_VIDEO",
|
||||
|
||||
@@ -2,6 +2,7 @@ export enum RoleEnum {
|
||||
EXPERT = "expert",
|
||||
DAMAGE_EXPERT = "damage_expert",
|
||||
FIELD_EXPERT = "field_expert",
|
||||
REGISTRAR = "registrar",
|
||||
COMPANY = "company",
|
||||
ADMIN = "admin",
|
||||
USER = "user",
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { LoginActorDto } from "src/auth/dto/actor/login.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 { CreateRegistrarDto } from "src/auth/dto/actor/create-registrar.actor.dto";
|
||||
import {
|
||||
GenuineRegisterDto,
|
||||
InsurerRegisterDto,
|
||||
@@ -64,6 +65,14 @@ export class ActorAuthController {
|
||||
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)
|
||||
@Post("login")
|
||||
@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 { ExpertDbService } from "src/users/entities/db-service/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 { MailService } from "src/utils/mail/mail.service";
|
||||
import { OtpService } from "src/utils/otp/otp.service";
|
||||
@@ -46,6 +47,7 @@ export class ActorAuthService {
|
||||
private readonly expertDbService: ExpertDbService,
|
||||
private readonly damageExpertDbService: DamageExpertDbService,
|
||||
private readonly fieldExpertDbService: FieldExpertDbService,
|
||||
private readonly registrarDbService: RegistrarDbService,
|
||||
private readonly insurerExpertDbService: InsurerExpertDbService,
|
||||
// private readonly mailService: MailService, // Mailer disabled – not used
|
||||
private readonly clientDbService: ClientDbService,
|
||||
@@ -79,6 +81,13 @@ export class ActorAuthService {
|
||||
else
|
||||
res = await this.fieldExpertDbService.findOne({ email: username });
|
||||
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:
|
||||
res = await this.insurerExpertDbService.findOne({ email: username });
|
||||
break;
|
||||
@@ -273,6 +282,10 @@ export class ActorAuthService {
|
||||
actor = await this.fieldExpertDbService.findOne(filter);
|
||||
dbServiceToUpdate = this.fieldExpertDbService as any;
|
||||
}
|
||||
if (!actor) {
|
||||
actor = await this.registrarDbService.findOne(filter);
|
||||
dbServiceToUpdate = this.registrarDbService as any;
|
||||
}
|
||||
|
||||
if (!actor) {
|
||||
throw new NotFoundException("actor not found");
|
||||
@@ -309,6 +322,10 @@ export class ActorAuthService {
|
||||
userExist = await this.fieldExpertDbService.findOne({ email });
|
||||
dbServiceToUpdate = this.fieldExpertDbService;
|
||||
}
|
||||
if (!userExist) {
|
||||
userExist = await this.registrarDbService.findOne({ email });
|
||||
dbServiceToUpdate = this.registrarDbService as any;
|
||||
}
|
||||
if (!userExist) throw new NotFoundException("user not found");
|
||||
const decodeOtp = await this.hashService.compare(otp, userExist.otp);
|
||||
if (!decodeOtp) throw new UnauthorizedException("otp invalid");
|
||||
@@ -383,6 +400,7 @@ export class ActorAuthService {
|
||||
"phone",
|
||||
"mobile",
|
||||
],
|
||||
registrar: ["email"],
|
||||
};
|
||||
|
||||
const allowedFields = allowedFieldsByRole[role];
|
||||
@@ -498,4 +516,34 @@ export class ActorAuthService {
|
||||
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 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) {
|
||||
await this.smsSender(otp, mobile);
|
||||
/// create otp request
|
||||
@@ -90,9 +96,7 @@ export class UserAuthService {
|
||||
city: "",
|
||||
address: "",
|
||||
state: "",
|
||||
otpExpire: new Date(
|
||||
new Date().getTime() + +process.env.EXP_OTP_TIME * 60 * 1000,
|
||||
).getTime(),
|
||||
otpExpire,
|
||||
});
|
||||
return new LoginDtoRs(newUser);
|
||||
}
|
||||
@@ -105,9 +109,7 @@ export class UserAuthService {
|
||||
},
|
||||
{
|
||||
otp: hashOtp,
|
||||
otpExpire: new Date(
|
||||
new Date().getTime() + +process.env.EXP_OTP_TIME * 60 * 1000,
|
||||
).getTime(),
|
||||
otpExpire,
|
||||
},
|
||||
);
|
||||
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.COMPANY,
|
||||
RoleEnum.FIELD_EXPERT,
|
||||
RoleEnum.REGISTRAR,
|
||||
].includes(payload.role)
|
||||
) {
|
||||
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 { ClaimRequestManagementController } from "./claim-request-management.controller";
|
||||
import { ClaimRequestManagementV2Controller } from "./claim-request-management.v2.controller";
|
||||
import { RegistrarClaimV1Controller } from "./registrar-claim.v1.controller";
|
||||
import { ClaimRequestManagementService } from "./claim-request-management.service";
|
||||
import { CarGreenCardDbService } from "./entites/db-service/car-green-card.db.service";
|
||||
import { ClaimRequestManagementDbService } from "./entites/db-service/claim-request-management.db.service";
|
||||
@@ -85,7 +86,11 @@ import { JwtModule } from "@nestjs/jwt";
|
||||
ClaimRequiredDocumentDbService,
|
||||
ClaimAccessGuard,
|
||||
],
|
||||
controllers: [ClaimRequestManagementController, ClaimRequestManagementV2Controller],
|
||||
controllers: [
|
||||
ClaimRequestManagementController,
|
||||
ClaimRequestManagementV2Controller,
|
||||
RegistrarClaimV1Controller,
|
||||
],
|
||||
exports: [
|
||||
ClaimRequestManagementService,
|
||||
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 { 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 { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||
import { ClaimSignDbService } from "./entites/db-service/claim-sign.db.service";
|
||||
import { DamageImageDbService } from "./entites/db-service/damage-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";
|
||||
|
||||
if (isCarBody) {
|
||||
// Check if accident is with another object (not a car)
|
||||
// This can be determined by:
|
||||
// 1. carBodyForm.carBodyForm.car === false (explicitly set to false)
|
||||
// 2. OR no secondPartyCarDetail exists (no second party car)
|
||||
// Accident with object (not another car): v2 uses parties[0].carBodyFirstForm.object, v1 uses carBodyForm.carBodyForm.car === false
|
||||
const firstPartyCarBody = claimRequest.blameFile?.parties?.[0]?.carBodyFirstForm;
|
||||
const isAccidentWithOtherObject =
|
||||
(firstPartyCarBody && firstPartyCarBody.object === true) ||
|
||||
claimRequest.blameFile?.carBodyForm?.carBodyForm?.car === false ||
|
||||
!claimRequest.blameFile?.secondPartyDetails?.secondPartyCarDetail;
|
||||
|
||||
@@ -2734,7 +2734,22 @@ export class ClaimRequestManagementService {
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Validate expert decision exists
|
||||
const parties = blameRequest.parties || [];
|
||||
const isCarBody = blameRequest.type === BlameRequestType.CAR_BODY;
|
||||
|
||||
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",
|
||||
@@ -2743,8 +2758,6 @@ export class ClaimRequestManagementService {
|
||||
|
||||
const guiltyPartyId = String(blameRequest.expert.decision.guiltyPartyId);
|
||||
|
||||
// 4. Validate both parties have signed and accepted
|
||||
const parties = blameRequest.parties || [];
|
||||
if (parties.length < 2) {
|
||||
throw new BadRequestException(
|
||||
"Both parties must be present in the blame request",
|
||||
@@ -2771,7 +2784,18 @@ export class ClaimRequestManagementService {
|
||||
);
|
||||
}
|
||||
|
||||
// 5. Validate current user is the DAMAGED party (NOT the guilty party)
|
||||
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",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Validate current user is a party (and for THIRD_PARTY not the guilty one)
|
||||
const currentUserParty = parties.find(
|
||||
(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
|
||||
const existingClaim = await this.claimCaseDbService.findOne({
|
||||
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)
|
||||
*
|
||||
@@ -2893,6 +3139,7 @@ export class ClaimRequestManagementService {
|
||||
claimRequestId: string,
|
||||
body: SelectOuterPartsV2Dto,
|
||||
currentUserId: string,
|
||||
actor?: { sub: string; role?: string },
|
||||
): Promise<SelectOuterPartsV2ResponseDto> {
|
||||
try {
|
||||
// 1. Validate claim exists
|
||||
@@ -2904,8 +3151,13 @@ export class ClaimRequestManagementService {
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Validate user is the claim owner (damaged party)
|
||||
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== currentUserId) {
|
||||
const effectiveUserId = await this.resolveClaimEffectiveUserId(
|
||||
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(
|
||||
'Only the claim owner (damaged party) can select damaged parts'
|
||||
);
|
||||
@@ -3007,6 +3259,7 @@ export class ClaimRequestManagementService {
|
||||
claimRequestId: string,
|
||||
body: SelectOtherPartsV2Dto,
|
||||
currentUserId: string,
|
||||
actor?: { sub: string; role?: string },
|
||||
): Promise<SelectOtherPartsV2ResponseDto> {
|
||||
try {
|
||||
// 1. Validate claim exists
|
||||
@@ -3018,8 +3271,13 @@ export class ClaimRequestManagementService {
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Validate user is the claim owner (damaged party)
|
||||
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== currentUserId) {
|
||||
const effectiveUserId = await this.resolveClaimEffectiveUserId(
|
||||
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(
|
||||
'Only the claim owner (damaged party) can submit other parts and bank information'
|
||||
);
|
||||
@@ -3120,6 +3378,7 @@ export class ClaimRequestManagementService {
|
||||
async getCaptureRequirementsV2(
|
||||
claimRequestId: string,
|
||||
currentUserId: string,
|
||||
actor?: { sub: string; role?: string },
|
||||
): Promise<GetCaptureRequirementsV2ResponseDto> {
|
||||
try {
|
||||
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||
@@ -3128,7 +3387,12 @@ export class ClaimRequestManagementService {
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -3152,7 +3416,11 @@ export class ClaimRequestManagementService {
|
||||
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 = [
|
||||
{ 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' },
|
||||
@@ -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_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' },
|
||||
...(isCarBody ? [] : [
|
||||
{ key: 'guilty_driving_license_front', label_fa: 'گواهینامه طرف مقصر - جلو', label_en: 'Guilty Party License - Front', category: 'guilty_party' },
|
||||
{ key: 'guilty_driving_license_back', label_fa: 'گواهینامه طرف مقصر - پشت', label_en: 'Guilty Party License - Back', category: 'guilty_party' },
|
||||
{ key: 'guilty_car_card_front', label_fa: 'کارت خودرو مقصر - جلو', label_en: 'Guilty Car Card - Front', 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 => {
|
||||
@@ -3250,6 +3520,7 @@ export class ClaimRequestManagementService {
|
||||
body: UploadRequiredDocumentV2Dto,
|
||||
file: Express.Multer.File,
|
||||
currentUserId: string,
|
||||
actor?: { sub: string; role?: string },
|
||||
): Promise<UploadRequiredDocumentV2ResponseDto> {
|
||||
try {
|
||||
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||
@@ -3258,7 +3529,12 @@ export class ClaimRequestManagementService {
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -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
|
||||
const existingDoc = (claimCase.requiredDocuments as any)?.get?.(body.documentKey);
|
||||
if (existingDoc?.uploaded) {
|
||||
@@ -3312,11 +3598,11 @@ export class ClaimRequestManagementService {
|
||||
},
|
||||
};
|
||||
|
||||
// Check if all documents are uploaded
|
||||
const totalDocs = 13;
|
||||
// Check if all documents are uploaded (7 for CAR_BODY, 13 for THIRD_PARTY)
|
||||
const totalDocsRequired = isCarBodyUpload ? 7 : 13;
|
||||
const currentDocs = claimCase.requiredDocuments || new Map();
|
||||
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 (allDocumentsUploaded) {
|
||||
@@ -3344,7 +3630,7 @@ export class ClaimRequestManagementService {
|
||||
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, updateData);
|
||||
|
||||
const remaining = totalDocs - uploadedCount;
|
||||
const remaining = totalDocsRequired - uploadedCount;
|
||||
const message = allDocumentsUploaded
|
||||
? 'All documents uploaded successfully. Please proceed to capture car angles and damaged parts.'
|
||||
: `Document uploaded successfully. ${remaining} documents remaining.`;
|
||||
@@ -3372,6 +3658,7 @@ export class ClaimRequestManagementService {
|
||||
body: CapturePartV2Dto,
|
||||
file: Express.Multer.File,
|
||||
currentUserId: string,
|
||||
actor?: { sub: string; role?: string },
|
||||
): Promise<CapturePartV2ResponseDto> {
|
||||
try {
|
||||
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||
@@ -3380,7 +3667,12 @@ export class ClaimRequestManagementService {
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
const claims = await this.claimCaseDbService.find(
|
||||
let claims: any[];
|
||||
if (actor?.role === RoleEnum.FIELD_EXPERT) {
|
||||
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) => ({
|
||||
claimRequestId: c._id.toString(),
|
||||
publicId: c.publicId,
|
||||
@@ -3524,13 +3844,19 @@ export class ClaimRequestManagementService {
|
||||
async getClaimDetailsV2(
|
||||
claimRequestId: string,
|
||||
currentUserId: string,
|
||||
actor?: { sub: string; role?: string },
|
||||
): Promise<ClaimDetailsV2ResponseDto> {
|
||||
try {
|
||||
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||
if (!claim) {
|
||||
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');
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ import { ClaimDetailsV2ResponseDto } from "./dto/claim-details-v2.dto";
|
||||
@Controller("v2/claim-request-management")
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(GlobalGuard, RolesGuard)
|
||||
@Roles(RoleEnum.USER)
|
||||
@Roles(RoleEnum.USER, RoleEnum.FIELD_EXPERT)
|
||||
export class ClaimRequestManagementV2Controller {
|
||||
constructor(
|
||||
private readonly claimRequestManagementService: ClaimRequestManagementService,
|
||||
@@ -51,7 +51,7 @@ export class ClaimRequestManagementV2Controller {
|
||||
})
|
||||
async getMyClaims(@CurrentUser() user: any): Promise<GetMyClaimsV2ResponseDto> {
|
||||
try {
|
||||
return await this.claimRequestManagementService.getMyClaimsV2(user.sub);
|
||||
return await this.claimRequestManagementService.getMyClaimsV2(user.sub, user);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
throw new InternalServerErrorException(
|
||||
@@ -91,6 +91,7 @@ export class ClaimRequestManagementV2Controller {
|
||||
return await this.claimRequestManagementService.getClaimDetailsV2(
|
||||
claimRequestId,
|
||||
user.sub,
|
||||
user,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
@@ -231,6 +232,7 @@ export class ClaimRequestManagementV2Controller {
|
||||
claimRequestId,
|
||||
body,
|
||||
user.sub,
|
||||
user,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
@@ -345,6 +347,7 @@ export class ClaimRequestManagementV2Controller {
|
||||
claimRequestId,
|
||||
body,
|
||||
user.sub,
|
||||
user,
|
||||
);
|
||||
} catch (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(
|
||||
claimRequestId,
|
||||
user.sub,
|
||||
user,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
@@ -434,16 +438,16 @@ Returns status of each item (uploaded/captured or not).
|
||||
description: `
|
||||
**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
|
||||
- damaged_driving_license_front/back
|
||||
- damaged_chassis_number, damaged_engine_photo
|
||||
- damaged_car_card_front/back, damaged_metal_plate
|
||||
- guilty_driving_license_front/back
|
||||
- guilty_car_card_front/back, guilty_metal_plate
|
||||
- guilty_driving_license_front/back, guilty_car_card_front/back, guilty_metal_plate (THIRD_PARTY)
|
||||
|
||||
**When all 13 documents are uploaded:**
|
||||
- Workflow automatically moves to: CAPTURE_PART_DAMAGES (Step 5)
|
||||
**When all required documents are uploaded:** Workflow 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({
|
||||
@@ -507,6 +511,7 @@ Returns status of each item (uploaded/captured or not).
|
||||
body,
|
||||
file,
|
||||
user.sub,
|
||||
user,
|
||||
);
|
||||
} catch (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
|
||||
|
||||
**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({
|
||||
@@ -598,6 +605,7 @@ Returns status of each item (uploaded/captured or not).
|
||||
body,
|
||||
file,
|
||||
user.sub,
|
||||
user,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
|
||||
@@ -133,6 +133,9 @@ export class ClaimCase {
|
||||
@Prop({ type: [HistoryEventSchema], default: [] })
|
||||
history?: HistoryEvent[];
|
||||
|
||||
@Prop({ type: Types.ObjectId, index: true })
|
||||
createdByRegistrarId?: Types.ObjectId;
|
||||
|
||||
/**
|
||||
* Legacy fields kept optional to simplify progressive migration.
|
||||
* 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:
|
||||
// 1. Fresh requests (WAITING_FOR_EXPERT and no decision)
|
||||
// 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 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 decision = doc.expert as any;
|
||||
const decidedByExpertId = decision?.decision?.decidedByExpertId;
|
||||
@@ -522,13 +533,20 @@ export class ExpertBlameService {
|
||||
);
|
||||
}
|
||||
|
||||
// Access control: Check if expert has permission to view this request
|
||||
const decision = (doc.expert as any)?.decision;
|
||||
const decidedByExpertId = decision?.decidedByExpertId;
|
||||
// Access control: Expert-initiated files only visible to the initiating field expert
|
||||
const expertInitiated = doc.expertInitiated === true;
|
||||
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:
|
||||
// 1. No decision yet (fresh request)
|
||||
// 2. Decision made by current expert
|
||||
const decision = (doc.expert as any)?.decision;
|
||||
const decidedByExpertId = decision?.decidedByExpertId;
|
||||
if (decidedByExpertId && String(decidedByExpertId) !== actorId) {
|
||||
throw new ForbiddenException(
|
||||
"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
|
||||
if (request.workflow?.locked) {
|
||||
const lockedAt = request.workflow.lockedAt;
|
||||
|
||||
@@ -22,7 +22,7 @@ import { ResendRequestDto } from "./dto/resend.dto";
|
||||
@Controller("v2/expert-blame")
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
||||
@Roles(RoleEnum.EXPERT)
|
||||
@Roles(RoleEnum.EXPERT, RoleEnum.FIELD_EXPERT)
|
||||
export class ExpertBlameV2Controller {
|
||||
constructor(private readonly expertBlameService: ExpertBlameService) {}
|
||||
|
||||
|
||||
@@ -48,44 +48,74 @@ export class FirstPartyFileDto {
|
||||
}
|
||||
|
||||
export class DescriptionDto {
|
||||
@ApiProperty()
|
||||
@ApiProperty({ description: "Accident description (required for all types)" })
|
||||
desc: string;
|
||||
|
||||
// CAR_BODY specific fields
|
||||
@ApiPropertyOptional({
|
||||
description: "Date of accident (for CAR_BODY type only)",
|
||||
example: "2025-12-08"
|
||||
description: "CAR_BODY only. Ignored for THIRD_PARTY.",
|
||||
example: "2025-12-08",
|
||||
})
|
||||
accidentDate?: Date;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: "Time of accident (for CAR_BODY type only)",
|
||||
example: "14:30"
|
||||
description: "CAR_BODY only. Ignored for THIRD_PARTY.",
|
||||
example: "14:30",
|
||||
})
|
||||
accidentTime?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
enum: WeatherCondition,
|
||||
description: "Weather condition at time of accident (for CAR_BODY type only)",
|
||||
example: WeatherCondition.CLEAR
|
||||
description: "CAR_BODY only. Ignored for THIRD_PARTY.",
|
||||
example: WeatherCondition.CLEAR,
|
||||
})
|
||||
weatherCondition?: WeatherCondition;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
enum: RoadCondition,
|
||||
description: "Road condition at time of accident (for CAR_BODY type only)",
|
||||
example: RoadCondition.DRY
|
||||
description: "CAR_BODY only. Ignored for THIRD_PARTY.",
|
||||
example: RoadCondition.DRY,
|
||||
})
|
||||
roadCondition?: RoadCondition;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
enum: LightCondition,
|
||||
description: "Light condition at time of accident (for CAR_BODY type only)",
|
||||
example: LightCondition.DAYLIGHT
|
||||
description: "CAR_BODY only. Ignored for THIRD_PARTY.",
|
||||
example: LightCondition.DAYLIGHT,
|
||||
})
|
||||
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 {
|
||||
// @ApiProperty({ type: String })
|
||||
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 })
|
||||
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);
|
||||
@@ -1,19 +1,27 @@
|
||||
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 { Party, PartySchema } from "./partyRole.enum";
|
||||
import { AccidentInfo, AccidentInfoSchema } from "./accidentInformation.type";
|
||||
import { ExpertSection, ExpertSectionSchema } from "./expert-section.type";
|
||||
import { Workflow, WorkflowSchema } from "./workflow.type";
|
||||
import { HistoryEvent, HistoryEventSchema } from "./historyEvent.type";
|
||||
import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.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 })
|
||||
export class BlameRequestSnapshot {
|
||||
@Prop({ type: AccidentInfoSchema })
|
||||
accident?: AccidentInfo;
|
||||
|
||||
@Prop({ type: [PartySchema], default: [] })
|
||||
parties?: Party[];
|
||||
}
|
||||
@@ -45,9 +53,6 @@ export class BlameRequest {
|
||||
@Prop({ type: WorkflowSchema })
|
||||
workflow?: Workflow;
|
||||
|
||||
@Prop({ type: AccidentInfoSchema })
|
||||
accident?: AccidentInfo;
|
||||
|
||||
@Prop({ type: [PartySchema], default: [] })
|
||||
parties: Party[];
|
||||
|
||||
@@ -57,12 +62,40 @@ export class BlameRequest {
|
||||
@Prop({ type: [HistoryEventSchema], default: [] })
|
||||
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).
|
||||
* Source of truth remains the top-level fields of this document.
|
||||
*/
|
||||
@Prop({ type: BlameRequestSnapshotSchema, required: false })
|
||||
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>;
|
||||
|
||||
@@ -70,6 +70,16 @@ export class Insurance {
|
||||
@Prop() financialCeiling?: string;
|
||||
@Prop({ type: [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);
|
||||
|
||||
@@ -87,6 +97,13 @@ export class PartyStatement {
|
||||
|
||||
@Prop({ type: 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);
|
||||
|
||||
@@ -130,6 +147,13 @@ export class Party {
|
||||
@Prop({ type: PersonSchema })
|
||||
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).
|
||||
*/
|
||||
|
||||
@@ -282,6 +282,7 @@ export enum CreationMethod {
|
||||
export enum FilledBy {
|
||||
CUSTOMER = "customer",
|
||||
EXPERT = "expert",
|
||||
REGISTRAR = "registrar",
|
||||
}
|
||||
|
||||
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 { SmsManagerModule } from "src/utils/sms-manager/sms-manager.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 { AuthModule } from "src/auth/auth.module";
|
||||
import { BlameDocumentDbService } from "./entities/db-service/blame-document.db.service";
|
||||
import { BlameVoiceDbService } from "./entities/db-service/blame.voice.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 { RequestManagementV2Controller } from "./request-management.v2.controller";
|
||||
import { ExpertInitiatedController } from "./expert-initiated.controller";
|
||||
import { ExpertInitiatedV2Controller } from "./expert-initiated.v2.controller";
|
||||
import { RegistrarInitiatedController } from "./registrar-initiated.controller";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -43,6 +47,7 @@ import { ExpertInitiatedController } from "./expert-initiated.controller";
|
||||
SandHubModule,
|
||||
SmsManagerModule,
|
||||
PublicIdModule,
|
||||
HashModule,
|
||||
WorkflowStepManagementModule,
|
||||
PlatesModule,
|
||||
MulterModule.register({
|
||||
@@ -58,11 +63,14 @@ import { ExpertInitiatedController } from "./expert-initiated.controller";
|
||||
]),
|
||||
forwardRef(() => ClaimRequestManagementModule),
|
||||
CronModule,
|
||||
AuthModule,
|
||||
],
|
||||
controllers: [
|
||||
RequestManagementController,
|
||||
RequestManagementV2Controller,
|
||||
ExpertInitiatedController,
|
||||
ExpertInitiatedV2Controller,
|
||||
RegistrarInitiatedController,
|
||||
],
|
||||
providers: [
|
||||
RequestManagementService,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,7 @@ import {
|
||||
CreateBlameRequestDtoV2,
|
||||
DescriptionDto,
|
||||
LocationDto,
|
||||
CarBodyFormDto,
|
||||
} from "./dto/create-request-management.dto";
|
||||
import { RequestManagementService } from "./request-management.service";
|
||||
|
||||
@@ -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")
|
||||
@ApiParam({ name: "requestId" })
|
||||
@ApiBody({ type: BlameConfessionDtoV2 })
|
||||
@@ -76,6 +98,19 @@ export class RequestManagementV2Controller {
|
||||
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({
|
||||
schema: {
|
||||
type: "object",
|
||||
@@ -185,7 +220,28 @@ export class RequestManagementV2Controller {
|
||||
|
||||
@Post("/add-detail-description/: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)
|
||||
async addDescriptionV2(
|
||||
@Param("requestId") requestId: string,
|
||||
|
||||
@@ -29,7 +29,144 @@ export class SandHubService {
|
||||
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> {
|
||||
if (!this.useLiveSandHubApis()) {
|
||||
return "mock-sandhub-access-token";
|
||||
}
|
||||
if (this.loginToken && this.tokenExpiry && this.tokenExpiry > new Date()) {
|
||||
return this.loginToken;
|
||||
}
|
||||
@@ -67,6 +204,9 @@ export class SandHubService {
|
||||
}
|
||||
|
||||
private async getTejaratAccessToken(): Promise<string> {
|
||||
if (!this.useLiveSandHubApis()) {
|
||||
return "mock-tejarat-access-token";
|
||||
}
|
||||
if (
|
||||
this.tejaratAccessToken &&
|
||||
this.tejaratTokenExpiry &&
|
||||
@@ -122,6 +262,10 @@ export class SandHubService {
|
||||
}
|
||||
|
||||
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 BACKOFF_FACTOR = 2;
|
||||
|
||||
@@ -183,7 +327,14 @@ export class SandHubService {
|
||||
};
|
||||
|
||||
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);
|
||||
return { raw, mapped };
|
||||
}
|
||||
@@ -193,6 +344,10 @@ export class SandHubService {
|
||||
}
|
||||
|
||||
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 INITIAL_DELAY = 1000;
|
||||
const BACKOFF_FACTOR = 2;
|
||||
@@ -293,16 +448,25 @@ export class SandHubService {
|
||||
rightTwoDigits: String(userDetail.plate.ir),
|
||||
nationalCode: userDetail.nationalCodeOfInsurer,
|
||||
};
|
||||
const requestUrl = `${process.env.SANDHUB_BASE_URL}/block-inquiry-tejarat`;
|
||||
const response = await this.makeSandHubRequest(requestUrl, requestPayload);
|
||||
const base = process.env.SANDHUB_BASE_URL ?? "";
|
||||
const requestUrl = `${base}/block-inquiry-tejarat`;
|
||||
|
||||
// Map the new API response format to the old format
|
||||
let result = this.mapNewApiResponseToOldFormat(response);
|
||||
|
||||
if (result.usgCod !== "8") {
|
||||
throw new Error("خودرو شما شخصی / سواری نمی باشد")
|
||||
let response: any;
|
||||
if (this.useLiveSandHubApis()) {
|
||||
response = await this.makeSandHubRequest(requestUrl, requestPayload);
|
||||
} else {
|
||||
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;
|
||||
} catch (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 { FieldExpertDbService } from "./entities/db-service/field-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 {
|
||||
DamageExpertDbSchema,
|
||||
@@ -21,6 +22,10 @@ import {
|
||||
InsurerExpertDbSchema,
|
||||
InsurerExpertModel,
|
||||
} from "./entities/schema/insurer-expert.schema";
|
||||
import {
|
||||
RegistrarDbSchema,
|
||||
RegistrarModel,
|
||||
} from "./entities/schema/registrar.schema";
|
||||
import { UserModel, UserDbSchema } from "./entities/schema/user.schema";
|
||||
|
||||
@Module({
|
||||
@@ -31,6 +36,7 @@ import { UserModel, UserDbSchema } from "./entities/schema/user.schema";
|
||||
{ name: ExpertModel.name, schema: ExpertDbSchema },
|
||||
{ name: FieldExpertModel.name, schema: FieldExpertDbSchema },
|
||||
{ name: InsurerExpertModel.name, schema: InsurerExpertDbSchema },
|
||||
{ name: RegistrarModel.name, schema: RegistrarDbSchema },
|
||||
]),
|
||||
OtpModule,
|
||||
HashModule,
|
||||
@@ -42,6 +48,7 @@ import { UserModel, UserDbSchema } from "./entities/schema/user.schema";
|
||||
DamageExpertDbService,
|
||||
FieldExpertDbService,
|
||||
InsurerExpertDbService,
|
||||
RegistrarDbService,
|
||||
],
|
||||
exports: [
|
||||
UserDbService,
|
||||
@@ -49,6 +56,7 @@ import { UserModel, UserDbSchema } from "./entities/schema/user.schema";
|
||||
DamageExpertDbService,
|
||||
FieldExpertDbService,
|
||||
InsurerExpertDbService,
|
||||
RegistrarDbService,
|
||||
],
|
||||
})
|
||||
export class UsersModule {}
|
||||
|
||||
@@ -128,9 +128,9 @@ export class CreateWorkflowStepDto {
|
||||
stepKey: WorkflowStep | ClaimWorkflowStep;
|
||||
|
||||
@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',
|
||||
enum: ['THIRD_PARTY', 'CLAIM']
|
||||
enum: ['THIRD_PARTY', 'CAR_BODY', 'CLAIM']
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
|
||||
@@ -1123,6 +1123,43 @@ export class WorkflowStepManagementService {
|
||||
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...
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user