forked from Yara724/api
blame and claim refactored #1
@@ -22,7 +22,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@arashioz/errjson-talieh": "^2.2.5",
|
"@arashioz/errjson-talieh": "^2.2.5",
|
||||||
"@fraybabak/kavenegar_nest": "^1.0.5",
|
"@fraybabak/kavenegar_nest": "^1.0.5",
|
||||||
"@nestjs-modules/mailer": "^2.0.2",
|
"@nestjs-modules/mailer": "^1.8.1",
|
||||||
"@nestjs/axios": "^3.1.3",
|
"@nestjs/axios": "^3.1.3",
|
||||||
"@nestjs/common": "^10.4.15",
|
"@nestjs/common": "^10.4.15",
|
||||||
"@nestjs/core": "^10.4.15",
|
"@nestjs/core": "^10.4.15",
|
||||||
@@ -34,8 +34,8 @@
|
|||||||
"@nestjs/platform-fastify": "^10.4.15",
|
"@nestjs/platform-fastify": "^10.4.15",
|
||||||
"@nestjs/platform-socket.io": "^10.4.15",
|
"@nestjs/platform-socket.io": "^10.4.15",
|
||||||
"@nestjs/schedule": "^4.1.2",
|
"@nestjs/schedule": "^4.1.2",
|
||||||
"@nestjs/serve-static": "^5.0.3",
|
"@nestjs/serve-static": "^4.0.2",
|
||||||
"@nestjs/swagger": "^8.1.0",
|
"@nestjs/swagger": "^7.4.2",
|
||||||
"@nestjs/websockets": "^10.4.15",
|
"@nestjs/websockets": "^10.4.15",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.4.9",
|
"@nestjs/cli": "^11.0.14",
|
||||||
"@nestjs/schematics": "^10.2.3",
|
"@nestjs/schematics": "^10.2.3",
|
||||||
"@nestjs/testing": "^10.4.15",
|
"@nestjs/testing": "^10.4.15",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
//! NEW
|
||||||
|
export enum BlameRequestType {
|
||||||
|
THIRD_PARTY = "THIRD_PARTY",
|
||||||
|
CAR_BODY = "CAR_BODY",
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
//! NEW
|
||||||
|
export enum BlameStatus {
|
||||||
|
UNKNOWN = "UNKNOWN",
|
||||||
|
|
||||||
|
AGREED = "AGREED",
|
||||||
|
|
||||||
|
DISAGREEMENT = "DISAGREEMENT"
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
//! NEW
|
||||||
|
export enum WorkflowStep {
|
||||||
|
|
||||||
|
CREATED = "CREATED",
|
||||||
|
|
||||||
|
// ---------- First party ----------
|
||||||
|
FIRST_BLAME_CONFESSION = "FIRST_BLAME_CONFESSION",
|
||||||
|
FIRST_VIDEO = "FIRST_VIDEO",
|
||||||
|
FIRST_INITIAL_FORM = "FIRST_INITIAL_FORM",
|
||||||
|
FIRST_LOCATION = "FIRST_LOCATION",
|
||||||
|
FIRST_VOICE = "FIRST_VOICE",
|
||||||
|
FIRST_DESCRIPTION = "FIRST_DESCRIPTION",
|
||||||
|
FIRST_INVITE_SECOND = "FIRST_INVITE_SECOND",
|
||||||
|
FIRST_SIGN = "FIRST_SIGN",
|
||||||
|
FIRST_COMPLETED = "FIRST_COMPLETED",
|
||||||
|
|
||||||
|
// ---------- Second party ----------
|
||||||
|
SECOND_INITIAL_FORM = "SECOND_INITIAL_FORM",
|
||||||
|
SECOND_LOCATION = "SECOND_LOCATION",
|
||||||
|
SECOND_VOICE = "SECOND_VOICE",
|
||||||
|
SECOND_DESCRIPTION = "SECOND_DESCRIPTION",
|
||||||
|
SECOND_SIGN = "SECOND_SIGN",
|
||||||
|
SECOND_COMPLETED = "SECOND_COMPLETED",
|
||||||
|
|
||||||
|
// ---------- Resolution ----------
|
||||||
|
WAITING_FOR_GUILT_DECISION = "WAITING_FOR_GUILT_DECISION",
|
||||||
|
WAITING_FOR_DOCUMENT_RESEND = "WAITING_FOR_DOCUMENT_RESEND",
|
||||||
|
WAITING_FOR_SIGNATURES = "WAITING_FOR_SIGNATURES",
|
||||||
|
|
||||||
|
COMPLETED = "COMPLETED"
|
||||||
|
}
|
||||||
19
src/Types&Enums/blame-request-management/caseStatus.enum.ts
Normal file
19
src/Types&Enums/blame-request-management/caseStatus.enum.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
//! NEW
|
||||||
|
export enum CaseStatus {
|
||||||
|
|
||||||
|
OPEN = "OPEN",
|
||||||
|
|
||||||
|
WAITING_FOR_SECOND_PARTY = "WAITING_FOR_SECOND_PARTY",
|
||||||
|
|
||||||
|
WAITING_FOR_EXPERT = "WAITING_FOR_EXPERT",
|
||||||
|
|
||||||
|
WAITING_FOR_DOCUMENT_RESEND = "WAITING_FOR_DOCUMENT_RESEND",
|
||||||
|
|
||||||
|
WAITING_FOR_SIGNATURES = "WAITING_FOR_SIGNATURES",
|
||||||
|
|
||||||
|
COMPLETED = "COMPLETED",
|
||||||
|
|
||||||
|
CANCELLED = "CANCELLED",
|
||||||
|
|
||||||
|
AUTO_CLOSED = "AUTO_CLOSED"
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
//! NEW - Items that expert can request parties to resend
|
||||||
|
export enum ResendItemType {
|
||||||
|
// Documents
|
||||||
|
NATIONAL_CERTIFICATE = "nationalCertificate",
|
||||||
|
CAR_CERTIFICATE = "carCertificate",
|
||||||
|
DRIVING_LICENSE = "drivingLicense",
|
||||||
|
CAR_GREEN_CARD = "carGreenCard",
|
||||||
|
|
||||||
|
// Media evidence
|
||||||
|
VOICE = "voice",
|
||||||
|
VIDEO = "video",
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
//! Overall claim case status (user flow + expert flow)
|
||||||
|
export enum ClaimCaseStatus {
|
||||||
|
// User flow - damage selection
|
||||||
|
CREATED = "CREATED",
|
||||||
|
SELECTING_OUTER_PARTS = "SELECTING_OUTER_PARTS",
|
||||||
|
SELECTING_OTHER_PARTS = "SELECTING_OTHER_PARTS",
|
||||||
|
|
||||||
|
// User flow - documentation
|
||||||
|
UPLOADING_REQUIRED_DOCUMENTS = "UPLOADING_REQUIRED_DOCUMENTS",
|
||||||
|
|
||||||
|
// User flow - damage capture
|
||||||
|
CAPTURING_PART_DAMAGES = "CAPTURING_PART_DAMAGES",
|
||||||
|
|
||||||
|
// Expert flow
|
||||||
|
WAITING_FOR_DAMAGE_EXPERT = "WAITING_FOR_DAMAGE_EXPERT",
|
||||||
|
EXPERT_REVIEWING = "EXPERT_REVIEWING",
|
||||||
|
WAITING_FOR_INSURER_APPROVAL = "WAITING_FOR_INSURER_APPROVAL",
|
||||||
|
|
||||||
|
// Final states
|
||||||
|
COMPLETED = "COMPLETED",
|
||||||
|
CANCELLED = "CANCELLED",
|
||||||
|
REJECTED = "REJECTED",
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
//! Claim workflow steps
|
||||||
|
export enum ClaimWorkflowStep {
|
||||||
|
// Initial creation
|
||||||
|
CLAIM_CREATED = "CLAIM_CREATED",
|
||||||
|
|
||||||
|
// User: Damage selection phase
|
||||||
|
SELECT_OUTER_PARTS = "SELECT_OUTER_PARTS",
|
||||||
|
SELECT_OTHER_PARTS = "SELECT_OTHER_PARTS",
|
||||||
|
|
||||||
|
// User: Documentation phase
|
||||||
|
UPLOAD_REQUIRED_DOCUMENTS = "UPLOAD_REQUIRED_DOCUMENTS",
|
||||||
|
|
||||||
|
// User: Damage capture phase (per part)
|
||||||
|
CAPTURE_PART_DAMAGES = "CAPTURE_PART_DAMAGES",
|
||||||
|
|
||||||
|
// User submission complete
|
||||||
|
USER_SUBMISSION_COMPLETE = "USER_SUBMISSION_COMPLETE",
|
||||||
|
|
||||||
|
// Expert: Damage assessment
|
||||||
|
EXPERT_DAMAGE_ASSESSMENT = "EXPERT_DAMAGE_ASSESSMENT",
|
||||||
|
EXPERT_COST_EVALUATION = "EXPERT_COST_EVALUATION",
|
||||||
|
|
||||||
|
// Insurer approval
|
||||||
|
INSURER_REVIEW = "INSURER_REVIEW",
|
||||||
|
|
||||||
|
// Final
|
||||||
|
CLAIM_COMPLETED = "CLAIM_COMPLETED",
|
||||||
|
}
|
||||||
13
src/Types&Enums/claim-request-management/claimStatus.enum.ts
Normal file
13
src/Types&Enums/claim-request-management/claimStatus.enum.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
//! Claim damage determination status
|
||||||
|
export enum ClaimStatus {
|
||||||
|
// Initial state
|
||||||
|
PENDING = "PENDING",
|
||||||
|
|
||||||
|
// Expert assessment
|
||||||
|
UNDER_REVIEW = "UNDER_REVIEW",
|
||||||
|
|
||||||
|
// Final states
|
||||||
|
APPROVED = "APPROVED",
|
||||||
|
REJECTED = "REJECTED",
|
||||||
|
NEEDS_REVISION = "NEEDS_REVISION",
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
export enum ClaimRequiredDocumentType {
|
export enum ClaimRequiredDocumentType {
|
||||||
|
// Car green card
|
||||||
|
CAR_GREEN_CARD = "car_green_card",
|
||||||
|
|
||||||
// Damaged party documents
|
// Damaged party documents
|
||||||
DAMAGED_DRIVING_LICENSE_BACK = "damaged_driving_license_back",
|
DAMAGED_DRIVING_LICENSE_BACK = "damaged_driving_license_back",
|
||||||
DAMAGED_DRIVING_LICENSE_FRONT = "damaged_driving_license_front",
|
DAMAGED_DRIVING_LICENSE_FRONT = "damaged_driving_license_front",
|
||||||
@@ -16,3 +19,10 @@ export enum ClaimRequiredDocumentType {
|
|||||||
GUILTY_METAL_PLATE = "guilty_metal_plate",
|
GUILTY_METAL_PLATE = "guilty_metal_plate",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CarAngle {
|
||||||
|
FRONT = "front",
|
||||||
|
BACK = "back",
|
||||||
|
LEFT = "left",
|
||||||
|
RIGHT = "right",
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,3 +30,11 @@ export enum SkillEnum {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { ReportsModule } from "./reports/reports.module";
|
|||||||
import { RequestManagementModule } from "./request-management/request-management.module";
|
import { RequestManagementModule } from "./request-management/request-management.module";
|
||||||
import { UsersModule } from "./users/users.module";
|
import { UsersModule } from "./users/users.module";
|
||||||
import { CronModule } from "./utils/cron/cron.module";
|
import { CronModule } from "./utils/cron/cron.module";
|
||||||
|
import { WorkflowStepManagementModule } from "./workflow-step-management/workflow-step-management.module";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
dotenv.config({ path: `.${process.env.NODE_ENV}.env` });
|
dotenv.config({ path: `.${process.env.NODE_ENV}.env` });
|
||||||
@@ -59,6 +60,7 @@ dotenv.config({ path: `.${process.env.NODE_ENV}.env` });
|
|||||||
ReportsModule,
|
ReportsModule,
|
||||||
ExpertInsurerModule,
|
ExpertInsurerModule,
|
||||||
LookupsModule,
|
LookupsModule,
|
||||||
|
WorkflowStepManagementModule,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [],
|
providers: [],
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import { InsurerExpertDbService } from "src/users/entities/db-service/insurer-ex
|
|||||||
import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service";
|
import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service";
|
||||||
import { ExpertDbService } from "src/users/entities/db-service/expert.db.service";
|
import { ExpertDbService } from "src/users/entities/db-service/expert.db.service";
|
||||||
import { HashService } from "src/utils/hash/hash.service";
|
import { HashService } from "src/utils/hash/hash.service";
|
||||||
import { MailService } from "src/utils/mail/mail.service";
|
// import { MailService } from "src/utils/mail/mail.service";
|
||||||
import { OtpService } from "src/utils/otp/otp.service";
|
import { OtpService } from "src/utils/otp/otp.service";
|
||||||
|
|
||||||
function pick(obj: Record<string, any>, keys: string[]) {
|
function pick(obj: Record<string, any>, keys: string[]) {
|
||||||
@@ -45,7 +45,7 @@ export class ActorAuthService {
|
|||||||
private readonly expertDbService: ExpertDbService,
|
private readonly expertDbService: ExpertDbService,
|
||||||
private readonly damageExpertDbService: DamageExpertDbService,
|
private readonly damageExpertDbService: DamageExpertDbService,
|
||||||
private readonly insurerExpertDbService: InsurerExpertDbService,
|
private readonly insurerExpertDbService: InsurerExpertDbService,
|
||||||
private readonly mailService: MailService,
|
// private readonly mailService: MailService, // Mailer disabled – not used
|
||||||
private readonly clientDbService: ClientDbService,
|
private readonly clientDbService: ClientDbService,
|
||||||
private readonly otpService: OtpService,
|
private readonly otpService: OtpService,
|
||||||
) {}
|
) {}
|
||||||
@@ -264,17 +264,19 @@ export class ActorAuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const otp = this.otpService.create();
|
const otp = this.otpService.create();
|
||||||
const sendEmail = await this.mailService.sendMail(normalizedEmail, otp);
|
// Mailer disabled – not used
|
||||||
|
// const sendEmail = await this.mailService.sendMail(normalizedEmail, otp);
|
||||||
if (sendEmail) {
|
// if (sendEmail) {
|
||||||
const hashOtp = await this.hashService.hash(otp);
|
const hashOtp = await this.hashService.hash(otp);
|
||||||
|
|
||||||
await dbServiceToUpdate.findOneAndUpdate(filter, { otp: hashOtp });
|
await dbServiceToUpdate.findOneAndUpdate(filter, { otp: hashOtp });
|
||||||
|
return {
|
||||||
return sendEmail;
|
message: "OTP generated (mail sending disabled)",
|
||||||
} else {
|
code: 200,
|
||||||
throw new BadGatewayException("mailServer in unavailable");
|
emailSent: false,
|
||||||
}
|
};
|
||||||
|
// } else {
|
||||||
|
// throw new BadGatewayException("mailServer in unavailable");
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
async forgetPasswordVerify(email, otp, newPassword) {
|
async forgetPasswordVerify(email, otp, newPassword) {
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ import { UserAuthService } from "src/auth/auth-services/user.auth.service";
|
|||||||
import { ClientModule } from "src/client/client.module";
|
import { ClientModule } from "src/client/client.module";
|
||||||
import { UsersModule } from "src/users/users.module";
|
import { UsersModule } from "src/users/users.module";
|
||||||
import { HashModule } from "src/utils/hash/hash.module";
|
import { HashModule } from "src/utils/hash/hash.module";
|
||||||
import { MailModule } from "src/utils/mail/mail.module";
|
// import { MailModule } from "src/utils/mail/mail.module";
|
||||||
import { OtpModule } from "src/utils/otp/otp.module";
|
import { OtpModule } from "src/utils/otp/otp.module";
|
||||||
import { SmsManagerModule } from "src/utils/sms-manager/sms-manager.module";
|
import { SmsManagerModule } from "src/utils/sms-manager/sms-manager.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
MailModule,
|
// MailModule, // Mailer disabled – not used
|
||||||
UsersModule,
|
UsersModule,
|
||||||
ClientModule,
|
ClientModule,
|
||||||
HashModule,
|
HashModule,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ApiProperty, ApiSchema } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { IsMobilePhone, IsString, Length } from "class-validator";
|
import { IsMobilePhone, IsString, Length } from "class-validator";
|
||||||
import { DamageExpertModel } from "src/users/entities/schema/damage-expert.schema";
|
import { DamageExpertModel } from "src/users/entities/schema/damage-expert.schema";
|
||||||
|
|
||||||
@@ -28,10 +28,6 @@ export class ProfileActor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiSchema({
|
|
||||||
name: "Edit Actor Profile",
|
|
||||||
description: "Body of the request to edit the actor's profile",
|
|
||||||
})
|
|
||||||
export class ActorEditUserProfileDto {
|
export class ActorEditUserProfileDto {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
type: "string",
|
type: "string",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { Roles } from "src/decorators/roles.decorator";
|
|||||||
import { CurrentUser } from "src/decorators/user.decorator";
|
import { CurrentUser } from "src/decorators/user.decorator";
|
||||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||||
import { ClaimRequestManagementService } from "./claim-request-management.service";
|
import { ClaimRequestManagementService } from "./claim-request-management.service";
|
||||||
|
import { ClaimRequiredDocumentType } from "src/Types&Enums/claim-request-management/required-document-type.enum";
|
||||||
import { CarDamagePartDto, OtherCarDamagePartDto } from "./dto/car-part.dto";
|
import { CarDamagePartDto, OtherCarDamagePartDto } from "./dto/car-part.dto";
|
||||||
import { UserCommentDto } from "./dto/user-comment.dto";
|
import { UserCommentDto } from "./dto/user-comment.dto";
|
||||||
import { UserObjectionDto } from "./dto/user-objection.dto";
|
import { UserObjectionDto } from "./dto/user-objection.dto";
|
||||||
@@ -167,20 +168,7 @@ export class ClaimRequestManagementController {
|
|||||||
@ApiParam({ name: "claimRequestID" })
|
@ApiParam({ name: "claimRequestID" })
|
||||||
@ApiQuery({
|
@ApiQuery({
|
||||||
name: "documentType",
|
name: "documentType",
|
||||||
enum: [
|
enum: ClaimRequiredDocumentType,
|
||||||
"damaged_driving_license_back",
|
|
||||||
"damaged_driving_license_front",
|
|
||||||
"damaged_chassis_number",
|
|
||||||
"damaged_engine_photo",
|
|
||||||
"damaged_car_card_front",
|
|
||||||
"damaged_car_card_back",
|
|
||||||
"damaged_metal_plate",
|
|
||||||
"guilty_driving_license_front",
|
|
||||||
"guilty_driving_license_back",
|
|
||||||
"guilty_car_card_front",
|
|
||||||
"guilty_car_card_back",
|
|
||||||
"guilty_metal_plate",
|
|
||||||
],
|
|
||||||
description: "Type of required document to upload",
|
description: "Type of required document to upload",
|
||||||
})
|
})
|
||||||
@Patch("upload-required-document/:claimRequestID")
|
@Patch("upload-required-document/:claimRequestID")
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ import { SandHubModule } from "src/sand-hub/sand-hub.module";
|
|||||||
import { RequestManagementModule } from "src/request-management/request-management.module";
|
import { RequestManagementModule } from "src/request-management/request-management.module";
|
||||||
import { UsersModule } from "src/users/users.module";
|
import { UsersModule } from "src/users/users.module";
|
||||||
import { ClaimRequestManagementController } from "./claim-request-management.controller";
|
import { ClaimRequestManagementController } from "./claim-request-management.controller";
|
||||||
|
import { ClaimRequestManagementV2Controller } from "./claim-request-management.v2.controller";
|
||||||
import { ClaimRequestManagementService } from "./claim-request-management.service";
|
import { ClaimRequestManagementService } from "./claim-request-management.service";
|
||||||
import { CarGreenCardDbService } from "./entites/db-service/car-green-card.db.service";
|
import { CarGreenCardDbService } from "./entites/db-service/car-green-card.db.service";
|
||||||
import { ClaimRequestManagementDbService } from "./entites/db-service/claim-request-management.db.service";
|
import { ClaimRequestManagementDbService } from "./entites/db-service/claim-request-management.db.service";
|
||||||
|
import { ClaimCaseDbService } from "./entites/db-service/claim-case.db.service";
|
||||||
|
import { ClaimCase, ClaimCaseSchema } from "./entites/schema/claim-cases.schema";
|
||||||
import { ClaimSignDbService } from "./entites/db-service/claim-sign.db.service";
|
import { ClaimSignDbService } from "./entites/db-service/claim-sign.db.service";
|
||||||
import { DamageImageDbService } from "./entites/db-service/damage-image.db.service";
|
import { DamageImageDbService } from "./entites/db-service/damage-image.db.service";
|
||||||
import { ClaimFactorsImageDbService } from "./entites/db-service/factor-image.db.service";
|
import { ClaimFactorsImageDbService } from "./entites/db-service/factor-image.db.service";
|
||||||
@@ -38,12 +41,14 @@ import {
|
|||||||
VideoCaptureModel,
|
VideoCaptureModel,
|
||||||
VideoCaptureSchema,
|
VideoCaptureSchema,
|
||||||
} from "./entites/schema/video-capture.schema";
|
} from "./entites/schema/video-capture.schema";
|
||||||
|
import { PublicIdModule } from "src/utils/public-id/public-id.module";
|
||||||
import { ClientModule } from "src/client/client.module";
|
import { ClientModule } from "src/client/client.module";
|
||||||
import { ClaimAccessGuard } from "src/auth/guards/claim-access.guard";
|
import { ClaimAccessGuard } from "src/auth/guards/claim-access.guard";
|
||||||
import { JwtModule } from "@nestjs/jwt";
|
import { JwtModule } from "@nestjs/jwt";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
PublicIdModule,
|
||||||
UsersModule,
|
UsersModule,
|
||||||
RequestManagementModule,
|
RequestManagementModule,
|
||||||
AiModule,
|
AiModule,
|
||||||
@@ -51,6 +56,7 @@ import { JwtModule } from "@nestjs/jwt";
|
|||||||
ClientModule,
|
ClientModule,
|
||||||
JwtModule.register({}),
|
JwtModule.register({}),
|
||||||
MongooseModule.forFeature([
|
MongooseModule.forFeature([
|
||||||
|
{ name: ClaimCase.name, schema: ClaimCaseSchema },
|
||||||
{
|
{
|
||||||
name: ClaimRequestManagementModel.name,
|
name: ClaimRequestManagementModel.name,
|
||||||
schema: ClaimRequestManagementSchema,
|
schema: ClaimRequestManagementSchema,
|
||||||
@@ -69,7 +75,9 @@ import { JwtModule } from "@nestjs/jwt";
|
|||||||
providers: [
|
providers: [
|
||||||
ClaimRequestManagementService,
|
ClaimRequestManagementService,
|
||||||
ClaimRequestManagementDbService,
|
ClaimRequestManagementDbService,
|
||||||
|
ClaimCaseDbService,
|
||||||
CarGreenCardDbService,
|
CarGreenCardDbService,
|
||||||
|
ClaimCaseDbService,
|
||||||
DamageImageDbService,
|
DamageImageDbService,
|
||||||
ClaimSignDbService,
|
ClaimSignDbService,
|
||||||
ClaimFactorsImageDbService,
|
ClaimFactorsImageDbService,
|
||||||
@@ -77,9 +85,10 @@ import { JwtModule } from "@nestjs/jwt";
|
|||||||
ClaimRequiredDocumentDbService,
|
ClaimRequiredDocumentDbService,
|
||||||
ClaimAccessGuard,
|
ClaimAccessGuard,
|
||||||
],
|
],
|
||||||
controllers: [ClaimRequestManagementController],
|
controllers: [ClaimRequestManagementController, ClaimRequestManagementV2Controller],
|
||||||
exports: [
|
exports: [
|
||||||
ClaimRequestManagementDbService,
|
ClaimRequestManagementDbService,
|
||||||
|
ClaimCaseDbService,
|
||||||
DamageImageDbService,
|
DamageImageDbService,
|
||||||
VideoCaptureDbService,
|
VideoCaptureDbService,
|
||||||
ClaimRequiredDocumentDbService,
|
ClaimRequiredDocumentDbService,
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
Injectable,
|
Injectable,
|
||||||
Logger,
|
Logger,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
|
ConflictException,
|
||||||
|
InternalServerErrorException,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
@@ -32,6 +34,21 @@ import { UserCommentDto } from "./dto/user-comment.dto";
|
|||||||
import { UserObjectionDto } from "./dto/user-objection.dto";
|
import { UserObjectionDto } from "./dto/user-objection.dto";
|
||||||
import { CarGreenCardDbService } from "./entites/db-service/car-green-card.db.service";
|
import { CarGreenCardDbService } from "./entites/db-service/car-green-card.db.service";
|
||||||
import { ClaimRequestManagementDbService } from "./entites/db-service/claim-request-management.db.service";
|
import { ClaimRequestManagementDbService } from "./entites/db-service/claim-request-management.db.service";
|
||||||
|
import { ClaimCaseDbService } from "./entites/db-service/claim-case.db.service";
|
||||||
|
import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service";
|
||||||
|
import { CreateClaimFromBlameResponseDto } from "./dto/create-claim-v2.dto";
|
||||||
|
import { SelectOuterPartsV2Dto, SelectOuterPartsV2ResponseDto } from "./dto/select-outer-parts-v2.dto";
|
||||||
|
import { SelectOtherPartsV2Dto, SelectOtherPartsV2ResponseDto } from "./dto/select-other-parts-v2.dto";
|
||||||
|
import { GetCaptureRequirementsV2ResponseDto } from "./dto/capture-requirements-v2.dto";
|
||||||
|
import { UploadRequiredDocumentV2Dto, UploadRequiredDocumentV2ResponseDto } from "./dto/upload-document-v2.dto";
|
||||||
|
import { CapturePartV2Dto, CapturePartV2ResponseDto } from "./dto/capture-part-v2.dto";
|
||||||
|
import { GetMyClaimsV2ResponseDto, ClaimListItemV2Dto } from "./dto/my-claims-v2.dto";
|
||||||
|
import { ClaimDetailsV2ResponseDto } from "./dto/claim-details-v2.dto";
|
||||||
|
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
|
||||||
|
import { ClaimRequiredDocumentType, CarAngle } from "src/Types&Enums/claim-request-management/required-document-type.enum";
|
||||||
|
import { ClaimStatus } from "src/Types&Enums/claim-request-management/claimStatus.enum";
|
||||||
|
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
|
||||||
|
import { CaseStatus as BlameCaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
|
||||||
import { ClaimSignDbService } from "./entites/db-service/claim-sign.db.service";
|
import { ClaimSignDbService } from "./entites/db-service/claim-sign.db.service";
|
||||||
import { DamageImageDbService } from "./entites/db-service/damage-image.db.service";
|
import { DamageImageDbService } from "./entites/db-service/damage-image.db.service";
|
||||||
import { ClaimFactorsImageDbService } from "./entites/db-service/factor-image.db.service";
|
import { ClaimFactorsImageDbService } from "./entites/db-service/factor-image.db.service";
|
||||||
@@ -41,12 +58,11 @@ import {
|
|||||||
ClaimRequestManagementModel,
|
ClaimRequestManagementModel,
|
||||||
UserClaimRating,
|
UserClaimRating,
|
||||||
} from "./entites/schema/claim-request-management.schema";
|
} from "./entites/schema/claim-request-management.schema";
|
||||||
|
import { PublicIdService } from "src/utils/public-id/public-id.service";
|
||||||
import { ImageRequiredModel } from "./entites/schema/image-required.schema";
|
import { ImageRequiredModel } from "./entites/schema/image-required.schema";
|
||||||
import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service";
|
import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service";
|
||||||
import { FactorStatus } from "src/Types&Enums/claim-request-management/factor-status.enum";
|
import { FactorStatus } from "src/Types&Enums/claim-request-management/factor-status.enum";
|
||||||
import { BranchDbService } from "src/client/entities/db-service/branch.db.service";
|
import { BranchDbService } from "src/client/entities/db-service/branch.db.service";import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||||
import { ClaimRequiredDocumentType } from "src/Types&Enums/claim-request-management/required-document-type.enum";
|
|
||||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
|
||||||
import { UserRatingDto } from "./dto/user-rating.dto";
|
import { UserRatingDto } from "./dto/user-rating.dto";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -75,6 +91,8 @@ export class ClaimRequestManagementService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly blameDbService: RequestManagementDbService,
|
private readonly blameDbService: RequestManagementDbService,
|
||||||
private readonly claimDbService: ClaimRequestManagementDbService,
|
private readonly claimDbService: ClaimRequestManagementDbService,
|
||||||
|
private readonly claimCaseDbService: ClaimCaseDbService,
|
||||||
|
private readonly blameRequestDbService: BlameRequestDbService,
|
||||||
private readonly carGreenCardDbService: CarGreenCardDbService,
|
private readonly carGreenCardDbService: CarGreenCardDbService,
|
||||||
private readonly damageImageDbService: DamageImageDbService,
|
private readonly damageImageDbService: DamageImageDbService,
|
||||||
private readonly userDbService: UserDbService,
|
private readonly userDbService: UserDbService,
|
||||||
@@ -86,6 +104,7 @@ export class ClaimRequestManagementService {
|
|||||||
private readonly damageExpertDbService: DamageExpertDbService,
|
private readonly damageExpertDbService: DamageExpertDbService,
|
||||||
private readonly branchDbService: BranchDbService,
|
private readonly branchDbService: BranchDbService,
|
||||||
private readonly claimRequiredDocumentDbService: ClaimRequiredDocumentDbService,
|
private readonly claimRequiredDocumentDbService: ClaimRequiredDocumentDbService,
|
||||||
|
private readonly publicIdService: PublicIdService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private delay(ms: number): Promise<void> {
|
private delay(ms: number): Promise<void> {
|
||||||
@@ -152,8 +171,22 @@ export class ClaimRequestManagementService {
|
|||||||
if (!blameRequest) {
|
if (!blameRequest) {
|
||||||
throw new NotFoundException("Source blame request not found.");
|
throw new NotFoundException("Source blame request not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure the blame request has a shared human-friendly id.
|
||||||
|
// For legacy records, generate it lazily exactly once.
|
||||||
|
let publicId = (blameRequest as any).publicId as string | undefined;
|
||||||
|
if (!publicId) {
|
||||||
|
publicId = await this.publicIdService.generateRequestPublicId();
|
||||||
|
await this.blameDbService.findAndUpdate(
|
||||||
|
{ _id: new Types.ObjectId(requestId) },
|
||||||
|
{ $set: { publicId } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const existingClaimCount = await this.claimDbService.countByFilter({
|
const existingClaimCount = await this.claimDbService.countByFilter({
|
||||||
"blameFile._id": new Types.ObjectId(requestId),
|
"blameFile._id": new Types.ObjectId(requestId),
|
||||||
|
blameRequestId: new Types.ObjectId(requestId),
|
||||||
|
publicId,
|
||||||
});
|
});
|
||||||
if (existingClaimCount > 0) {
|
if (existingClaimCount > 0) {
|
||||||
throw new ForbiddenException(
|
throw new ForbiddenException(
|
||||||
@@ -2603,4 +2636,911 @@ export class ClaimRequestManagementService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2: Create claim request from completed blame case
|
||||||
|
* Only damaged party can create claim after both parties accept expert decision
|
||||||
|
*/
|
||||||
|
async createClaimFromBlameV2(
|
||||||
|
blameRequestId: string,
|
||||||
|
currentUserId: string,
|
||||||
|
): Promise<CreateClaimFromBlameResponseDto> {
|
||||||
|
try {
|
||||||
|
// 1. Fetch blame request from new blameCases collection
|
||||||
|
const blameRequest = await this.blameRequestDbService.findById(blameRequestId);
|
||||||
|
if (!blameRequest) {
|
||||||
|
throw new NotFoundException("Blame request not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Validate blame status is COMPLETED
|
||||||
|
if (blameRequest.status !== BlameCaseStatus.COMPLETED) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Blame request must be COMPLETED. Current status: ${blameRequest.status}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Validate expert decision exists
|
||||||
|
if (!blameRequest.expert?.decision) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Cannot create claim until expert has made a decision",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const guiltyPartyId = String(blameRequest.expert.decision.guiltyPartyId);
|
||||||
|
|
||||||
|
// 4. Validate both parties have signed and accepted
|
||||||
|
const parties = blameRequest.parties || [];
|
||||||
|
if (parties.length < 2) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Both parties must be present in the blame request",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allPartiesSigned = parties.every(
|
||||||
|
(p) => p.confirmation !== undefined && p.confirmation !== null,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!allPartiesSigned) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Both parties must sign the expert decision before creating a claim",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allPartiesAccepted = parties.every(
|
||||||
|
(p) => p.confirmation?.accepted === true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!allPartiesAccepted) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Both parties must accept the expert decision. If rejected, in-person resolution is required.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Validate current user is the DAMAGED party (NOT the guilty party)
|
||||||
|
const currentUserParty = parties.find(
|
||||||
|
(p) => String(p.person?.userId) === currentUserId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!currentUserParty) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"You are not a party in this blame request",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingClaim) {
|
||||||
|
throw new ConflictException(
|
||||||
|
"A claim request for this blame case already exists",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Generate claim number
|
||||||
|
const claimNo = await this.generateUniqueClaimNumber();
|
||||||
|
|
||||||
|
// 8. Create claim case
|
||||||
|
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(currentUserId),
|
||||||
|
userRole: currentUserParty.role as any,
|
||||||
|
},
|
||||||
|
history: [
|
||||||
|
{
|
||||||
|
type: "CLAIM_CREATED",
|
||||||
|
actor: {
|
||||||
|
actorId: new Types.ObjectId(currentUserId),
|
||||||
|
actorName: currentUserParty.person?.fullName || "User",
|
||||||
|
actorType: "user",
|
||||||
|
},
|
||||||
|
timestamp: new Date(),
|
||||||
|
metadata: {
|
||||||
|
blameRequestId,
|
||||||
|
blamePublicId: blameRequest.publicId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Claim created: ${newClaim._id} from blame: ${blameRequestId}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
claimRequestId: String(newClaim._id),
|
||||||
|
publicId: blameRequest.publicId,
|
||||||
|
status: ClaimCaseStatus.SELECTING_OUTER_PARTS,
|
||||||
|
message: "Claim request created successfully",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
this.logger.error(
|
||||||
|
"createClaimFromBlameV2 failed",
|
||||||
|
blameRequestId,
|
||||||
|
error instanceof Error ? error.stack : String(error),
|
||||||
|
);
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Failed to create claim request",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2 API: Select damaged outer car parts for claim (Step 2 of claim workflow)
|
||||||
|
*
|
||||||
|
* @param claimRequestId - The claim case ID
|
||||||
|
* @param body - Selected outer parts
|
||||||
|
* @param currentUserId - The current user's ID
|
||||||
|
* @returns Response with updated claim information
|
||||||
|
*
|
||||||
|
* Validations:
|
||||||
|
* - Claim case must exist
|
||||||
|
* - User must be the claim owner (damaged party)
|
||||||
|
* - Current workflow step must be CLAIM_CREATED (step 1)
|
||||||
|
* - Selected parts array must not be empty
|
||||||
|
* - Parts must not have been selected previously
|
||||||
|
*/
|
||||||
|
async selectOuterPartsV2(
|
||||||
|
claimRequestId: string,
|
||||||
|
body: SelectOuterPartsV2Dto,
|
||||||
|
currentUserId: string,
|
||||||
|
): Promise<SelectOuterPartsV2ResponseDto> {
|
||||||
|
try {
|
||||||
|
// 1. Validate claim exists
|
||||||
|
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
|
|
||||||
|
if (!claimCase) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
`Claim case with ID ${claimRequestId} not found`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Validate user is the claim owner (damaged party)
|
||||||
|
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== currentUserId) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
'Only the claim owner (damaged party) can select damaged parts'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Validate workflow step is correct (should be at SELECT_OUTER_PARTS)
|
||||||
|
if (claimCase.workflow?.currentStep !== ClaimWorkflowStep.SELECT_OUTER_PARTS) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Invalid workflow step. Expected ${ClaimWorkflowStep.SELECT_OUTER_PARTS}, but current step is ${claimCase.workflow?.currentStep}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Validate outer parts haven't been selected yet
|
||||||
|
if (claimCase.damage?.selectedParts && claimCase.damage.selectedParts.length > 0) {
|
||||||
|
throw new ConflictException(
|
||||||
|
'Outer parts have already been selected for this claim'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Convert selected parts to storage format (simple array of strings)
|
||||||
|
const selectedParts = body.selectedParts;
|
||||||
|
|
||||||
|
// 6. Update claim case with selected parts and move to next step
|
||||||
|
const updatedClaim = await this.claimCaseDbService.findByIdAndUpdate(
|
||||||
|
claimRequestId,
|
||||||
|
{
|
||||||
|
'damage.selectedParts': selectedParts,
|
||||||
|
'status': ClaimCaseStatus.SELECTING_OTHER_PARTS,
|
||||||
|
'workflow.currentStep': ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
||||||
|
'workflow.nextStep': ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||||
|
$push: {
|
||||||
|
'workflow.completedSteps': ClaimWorkflowStep.SELECT_OUTER_PARTS,
|
||||||
|
history: {
|
||||||
|
type: 'STEP_COMPLETED',
|
||||||
|
actor: {
|
||||||
|
actorId: new Types.ObjectId(currentUserId),
|
||||||
|
actorName: claimCase.owner?.fullName || 'User',
|
||||||
|
actorType: 'user',
|
||||||
|
},
|
||||||
|
timestamp: new Date(),
|
||||||
|
metadata: {
|
||||||
|
stepKey: ClaimWorkflowStep.SELECT_OUTER_PARTS,
|
||||||
|
selectedParts: selectedParts,
|
||||||
|
partsCount: selectedParts.length,
|
||||||
|
description: `User selected ${selectedParts.length} damaged outer parts`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!updatedClaim) {
|
||||||
|
throw new InternalServerErrorException('Failed to update claim case');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Outer parts selected for claim ${claimRequestId}: ${selectedParts.length} parts`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 7. Return response
|
||||||
|
return {
|
||||||
|
claimRequestId: updatedClaim._id.toString(),
|
||||||
|
publicId: updatedClaim.publicId,
|
||||||
|
selectedParts: selectedParts,
|
||||||
|
currentStep: ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
||||||
|
nextStep: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||||
|
message: 'Outer parts selected successfully. Please proceed to select other parts and provide bank information.',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
this.logger.error(
|
||||||
|
'selectOuterPartsV2 failed',
|
||||||
|
claimRequestId,
|
||||||
|
error instanceof Error ? error.stack : String(error),
|
||||||
|
);
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: 'Failed to select outer parts',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2 API: Select other damaged parts and provide bank information (Step 3 of claim workflow)
|
||||||
|
*
|
||||||
|
* @param claimRequestId - The claim case ID
|
||||||
|
* @param body - Other parts, Sheba number, and national code
|
||||||
|
* @param currentUserId - The current user's ID
|
||||||
|
* @returns Response with updated claim information
|
||||||
|
*
|
||||||
|
* Validations:
|
||||||
|
* - Claim case must exist
|
||||||
|
* - User must be the claim owner (damaged party)
|
||||||
|
* - Current workflow step must be SELECT_OTHER_PARTS (step 3)
|
||||||
|
* - Other parts and bank info must not have been submitted previously
|
||||||
|
*/
|
||||||
|
async selectOtherPartsV2(
|
||||||
|
claimRequestId: string,
|
||||||
|
body: SelectOtherPartsV2Dto,
|
||||||
|
currentUserId: string,
|
||||||
|
): Promise<SelectOtherPartsV2ResponseDto> {
|
||||||
|
try {
|
||||||
|
// 1. Validate claim exists
|
||||||
|
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
|
|
||||||
|
if (!claimCase) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
`Claim case with ID ${claimRequestId} not found`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Validate user is the claim owner (damaged party)
|
||||||
|
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== currentUserId) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
'Only the claim owner (damaged party) can submit other parts and bank information'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Validate workflow step is correct (should be at SELECT_OTHER_PARTS)
|
||||||
|
if (claimCase.workflow?.currentStep !== ClaimWorkflowStep.SELECT_OTHER_PARTS) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Invalid workflow step. Expected ${ClaimWorkflowStep.SELECT_OTHER_PARTS}, but current step is ${claimCase.workflow?.currentStep}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Validate other parts and bank info haven't been submitted yet
|
||||||
|
if (claimCase.money?.sheba || claimCase.money?.nationalCodeOfInsurer) {
|
||||||
|
throw new ConflictException(
|
||||||
|
'Bank information has already been submitted for this claim'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Prepare data
|
||||||
|
const otherParts = body.otherParts || [];
|
||||||
|
const shebaNumber = body.shebaNumber;
|
||||||
|
const nationalCode = body.nationalCodeOfOwner;
|
||||||
|
|
||||||
|
// 6. Update claim case with other parts, bank info and move to next step
|
||||||
|
const updatedClaim = await this.claimCaseDbService.findByIdAndUpdate(
|
||||||
|
claimRequestId,
|
||||||
|
{
|
||||||
|
'damage.otherParts': otherParts.length > 0 ? otherParts : undefined,
|
||||||
|
'money.sheba': shebaNumber,
|
||||||
|
'money.nationalCodeOfInsurer': nationalCode,
|
||||||
|
'status': ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS,
|
||||||
|
'workflow.currentStep': ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||||
|
'workflow.nextStep': ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
||||||
|
$push: {
|
||||||
|
'workflow.completedSteps': ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
||||||
|
history: {
|
||||||
|
type: 'STEP_COMPLETED',
|
||||||
|
actor: {
|
||||||
|
actorId: new Types.ObjectId(currentUserId),
|
||||||
|
actorName: claimCase.owner?.fullName || 'User',
|
||||||
|
actorType: 'user',
|
||||||
|
},
|
||||||
|
timestamp: new Date(),
|
||||||
|
metadata: {
|
||||||
|
stepKey: ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
||||||
|
otherParts: otherParts,
|
||||||
|
otherPartsCount: otherParts.length,
|
||||||
|
hasBankInfo: true,
|
||||||
|
description: `User selected ${otherParts.length} other damaged parts and provided bank information`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!updatedClaim) {
|
||||||
|
throw new InternalServerErrorException('Failed to update claim case');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Other parts and bank info submitted for claim ${claimRequestId}: ${otherParts.length} parts`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 7. Mask sensitive data for response
|
||||||
|
const maskedSheba = shebaNumber.replace(/^(.{4})(.*)(.{4})$/, 'IR$1************$3');
|
||||||
|
const maskedNationalCode = nationalCode.replace(/^(.{2})(.*)(.{2})$/, '$1******$3');
|
||||||
|
|
||||||
|
// 8. Return response
|
||||||
|
return {
|
||||||
|
claimRequestId: updatedClaim._id.toString(),
|
||||||
|
publicId: updatedClaim.publicId,
|
||||||
|
otherParts: otherParts,
|
||||||
|
shebaNumber: maskedSheba,
|
||||||
|
nationalCodeOfOwner: maskedNationalCode,
|
||||||
|
currentStep: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||||
|
nextStep: ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
||||||
|
message: 'Other parts and bank information saved successfully. Please proceed to upload required documents.',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
this.logger.error(
|
||||||
|
'selectOtherPartsV2 failed',
|
||||||
|
claimRequestId,
|
||||||
|
error instanceof Error ? error.stack : String(error),
|
||||||
|
);
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: 'Failed to select other parts',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2 API: Get capture requirements for claim (documents, angles, parts)
|
||||||
|
*/
|
||||||
|
async getCaptureRequirementsV2(
|
||||||
|
claimRequestId: string,
|
||||||
|
currentUserId: string,
|
||||||
|
): Promise<GetCaptureRequirementsV2ResponseDto> {
|
||||||
|
try {
|
||||||
|
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
|
|
||||||
|
if (!claimCase) {
|
||||||
|
throw new NotFoundException(`Claim case with ID ${claimRequestId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== currentUserId) {
|
||||||
|
throw new ForbiddenException('Only the claim owner can view capture requirements');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to get part labels
|
||||||
|
const getPartLabel = (key: string): { fa: string; en: string } => {
|
||||||
|
const labels = {
|
||||||
|
hood: { fa: 'کاپوت', en: 'Hood' },
|
||||||
|
trunk: { fa: 'صندوق عقب', en: 'Trunk' },
|
||||||
|
roof: { fa: 'سقف', en: 'Roof' },
|
||||||
|
front_right_door: { fa: 'درب جلو راست', en: 'Front Right Door' },
|
||||||
|
front_left_door: { fa: 'درب جلو چپ', en: 'Front Left Door' },
|
||||||
|
rear_right_door: { fa: 'درب عقب راست', en: 'Rear Right Door' },
|
||||||
|
rear_left_door: { fa: 'درب عقب چپ', en: 'Rear Left Door' },
|
||||||
|
front_bumper: { fa: 'سپر جلو', en: 'Front Bumper' },
|
||||||
|
rear_bumper: { fa: 'سپر عقب', en: 'Rear Bumper' },
|
||||||
|
front_right_fender: { fa: 'گلگیر جلو راست', en: 'Front Right Fender' },
|
||||||
|
front_left_fender: { fa: 'گلگیر جلو چپ', en: 'Front Left Fender' },
|
||||||
|
rear_right_fender: { fa: 'گلگیر عقب راست', en: 'Rear Right Fender' },
|
||||||
|
rear_left_fender: { fa: 'گلگیر عقب چپ', en: 'Rear Left Fender' },
|
||||||
|
};
|
||||||
|
return labels[key] || { fa: key, en: key };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Required documents
|
||||||
|
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' },
|
||||||
|
{ key: 'damaged_driving_license_back', label_fa: 'گواهینامه طرف آسیبدیده - پشت', label_en: 'Damaged Party License - Back', category: 'damaged_party' },
|
||||||
|
{ key: 'damaged_chassis_number', label_fa: 'شماره شاسی', label_en: 'Chassis Number', category: 'damaged_party' },
|
||||||
|
{ key: 'damaged_engine_photo', label_fa: 'عکس موتور', label_en: 'Engine Photo', category: 'damaged_party' },
|
||||||
|
{ key: 'damaged_car_card_front', label_fa: 'کارت خودرو آسیبدیده - جلو', label_en: 'Damaged Car Card - Front', category: 'damaged_party' },
|
||||||
|
{ key: 'damaged_car_card_back', label_fa: 'کارت خودرو آسیبدیده - پشت', label_en: 'Damaged Car Card - Back', category: 'damaged_party' },
|
||||||
|
{ key: 'damaged_metal_plate', label_fa: 'پلاک فلزی آسیبدیده', label_en: 'Damaged Metal Plate', category: 'damaged_party' },
|
||||||
|
{ key: 'guilty_driving_license_front', label_fa: 'گواهینامه طرف مقصر - جلو', label_en: 'Guilty Party License - Front', category: 'guilty_party' },
|
||||||
|
{ 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 => {
|
||||||
|
const docData = (claimCase.requiredDocuments as any)?.get?.(doc.key);
|
||||||
|
return {
|
||||||
|
key: doc.key,
|
||||||
|
label_fa: doc.label_fa,
|
||||||
|
label_en: doc.label_en,
|
||||||
|
category: doc.category,
|
||||||
|
uploaded: docData?.uploaded || false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasCapture = (data: any, key: string) =>
|
||||||
|
data && (data instanceof Map ? data.get(key) : data[key]);
|
||||||
|
|
||||||
|
// Car angles
|
||||||
|
const carAngles = [
|
||||||
|
{ key: 'front', label_fa: 'جلو', label_en: 'Front' },
|
||||||
|
{ key: 'back', label_fa: 'عقب', label_en: 'Back' },
|
||||||
|
{ key: 'left', label_fa: 'چپ', label_en: 'Left' },
|
||||||
|
{ key: 'right', label_fa: 'راست', label_en: 'Right' },
|
||||||
|
].map(angle => ({
|
||||||
|
key: angle.key,
|
||||||
|
label_fa: angle.label_fa,
|
||||||
|
label_en: angle.label_en,
|
||||||
|
captured: !!hasCapture(claimCase.media?.carAngles as any, angle.key),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Damaged parts from selected parts
|
||||||
|
const selectedParts = claimCase.damage?.selectedParts || [];
|
||||||
|
const damagedParts = selectedParts.map(partKey => {
|
||||||
|
const label = getPartLabel(partKey);
|
||||||
|
return {
|
||||||
|
key: partKey,
|
||||||
|
label_fa: label.fa,
|
||||||
|
label_en: label.en,
|
||||||
|
captured: !!hasCapture(claimCase.media?.damagedParts as any, partKey),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate progress
|
||||||
|
const documentsUploaded = requiredDocuments.filter(d => d.uploaded).length;
|
||||||
|
const anglesCaptured = carAngles.filter(a => a.captured).length;
|
||||||
|
const partsCaptured = damagedParts.filter(p => p.captured).length;
|
||||||
|
|
||||||
|
const totalRemaining =
|
||||||
|
(requiredDocuments.length - documentsUploaded) +
|
||||||
|
(carAngles.length - anglesCaptured) +
|
||||||
|
(damagedParts.length - partsCaptured);
|
||||||
|
|
||||||
|
return {
|
||||||
|
claimRequestId: claimCase._id.toString(),
|
||||||
|
publicId: claimCase.publicId,
|
||||||
|
currentStep: claimCase.workflow?.currentStep || '',
|
||||||
|
requiredDocuments,
|
||||||
|
carAngles,
|
||||||
|
damagedParts,
|
||||||
|
totalRemaining,
|
||||||
|
progress: {
|
||||||
|
documentsUploaded,
|
||||||
|
documentsTotal: requiredDocuments.length,
|
||||||
|
anglesCaptured,
|
||||||
|
anglesTotal: carAngles.length,
|
||||||
|
partsCaptured,
|
||||||
|
partsTotal: damagedParts.length,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
this.logger.error('getCaptureRequirementsV2 failed', error);
|
||||||
|
throw new InternalServerErrorException('Failed to get capture requirements');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2 API: Upload required document
|
||||||
|
*/
|
||||||
|
async uploadRequiredDocumentV2(
|
||||||
|
claimRequestId: string,
|
||||||
|
body: UploadRequiredDocumentV2Dto,
|
||||||
|
file: Express.Multer.File,
|
||||||
|
currentUserId: string,
|
||||||
|
): Promise<UploadRequiredDocumentV2ResponseDto> {
|
||||||
|
try {
|
||||||
|
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
|
|
||||||
|
if (!claimCase) {
|
||||||
|
throw new NotFoundException(`Claim case with ID ${claimRequestId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== currentUserId) {
|
||||||
|
throw new ForbiddenException('Only the claim owner can upload documents');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (claimCase.workflow?.currentStep !== ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Invalid workflow step. Expected ${ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS}, but current step is ${claimCase.workflow?.currentStep}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if document already uploaded
|
||||||
|
const existingDoc = (claimCase.requiredDocuments as any)?.get?.(body.documentKey);
|
||||||
|
if (existingDoc?.uploaded) {
|
||||||
|
throw new ConflictException(`Document ${body.documentKey} has already been uploaded`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileUrl = buildFileLink(file.path);
|
||||||
|
|
||||||
|
// Create document reference in claim-required-documents collection
|
||||||
|
const docRef = await this.claimRequiredDocumentDbService.create({
|
||||||
|
path: file.path,
|
||||||
|
fileName: file.filename,
|
||||||
|
claimId: new Types.ObjectId(claimRequestId),
|
||||||
|
documentType: body.documentKey,
|
||||||
|
uploadedAt: new Date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update claim case
|
||||||
|
const updateData: any = {
|
||||||
|
[`requiredDocuments.${body.documentKey}`]: {
|
||||||
|
fileId: docRef._id,
|
||||||
|
filePath: file.path,
|
||||||
|
fileName: file.filename,
|
||||||
|
uploaded: true,
|
||||||
|
uploadedAt: new Date(),
|
||||||
|
},
|
||||||
|
$push: {
|
||||||
|
history: {
|
||||||
|
type: 'DOCUMENT_UPLOADED',
|
||||||
|
actor: {
|
||||||
|
actorId: new Types.ObjectId(currentUserId),
|
||||||
|
actorName: claimCase.owner?.fullName || 'User',
|
||||||
|
actorType: 'user',
|
||||||
|
},
|
||||||
|
timestamp: new Date(),
|
||||||
|
metadata: {
|
||||||
|
documentKey: body.documentKey,
|
||||||
|
fileName: file.filename,
|
||||||
|
fileId: docRef._id.toString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if all documents are uploaded
|
||||||
|
const totalDocs = 13;
|
||||||
|
const currentDocs = claimCase.requiredDocuments || new Map();
|
||||||
|
const uploadedCount = (currentDocs instanceof Map ? currentDocs.size : Object.keys(currentDocs).length) + 1;
|
||||||
|
const allDocumentsUploaded = uploadedCount >= totalDocs;
|
||||||
|
|
||||||
|
// If all documents uploaded, move to next step
|
||||||
|
if (allDocumentsUploaded) {
|
||||||
|
updateData['status'] = ClaimCaseStatus.CAPTURING_PART_DAMAGES;
|
||||||
|
updateData['workflow.currentStep'] = ClaimWorkflowStep.CAPTURE_PART_DAMAGES;
|
||||||
|
updateData['workflow.nextStep'] = ClaimWorkflowStep.USER_SUBMISSION_COMPLETE;
|
||||||
|
updateData.$push.history = [
|
||||||
|
updateData.$push.history,
|
||||||
|
{
|
||||||
|
type: 'STEP_COMPLETED',
|
||||||
|
actor: {
|
||||||
|
actorId: new Types.ObjectId(currentUserId),
|
||||||
|
actorName: claimCase.owner?.fullName || 'User',
|
||||||
|
actorType: 'user',
|
||||||
|
},
|
||||||
|
timestamp: new Date(),
|
||||||
|
metadata: {
|
||||||
|
stepKey: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||||
|
description: 'All required documents uploaded',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
updateData.$push['workflow.completedSteps'] = ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, updateData);
|
||||||
|
|
||||||
|
const remaining = totalDocs - uploadedCount;
|
||||||
|
const message = allDocumentsUploaded
|
||||||
|
? 'All documents uploaded successfully. Please proceed to capture car angles and damaged parts.'
|
||||||
|
: `Document uploaded successfully. ${remaining} documents remaining.`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
claimRequestId: claimCase._id.toString(),
|
||||||
|
documentKey: body.documentKey,
|
||||||
|
fileUrl,
|
||||||
|
allDocumentsUploaded,
|
||||||
|
currentStep: allDocumentsUploaded ? ClaimWorkflowStep.CAPTURE_PART_DAMAGES : ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
this.logger.error('uploadRequiredDocumentV2 failed', error);
|
||||||
|
throw new InternalServerErrorException('Failed to upload document');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2 API: Capture car angle or damaged part
|
||||||
|
*/
|
||||||
|
async capturePartV2(
|
||||||
|
claimRequestId: string,
|
||||||
|
body: CapturePartV2Dto,
|
||||||
|
file: Express.Multer.File,
|
||||||
|
currentUserId: string,
|
||||||
|
): Promise<CapturePartV2ResponseDto> {
|
||||||
|
try {
|
||||||
|
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
|
|
||||||
|
if (!claimCase) {
|
||||||
|
throw new NotFoundException(`Claim case with ID ${claimRequestId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!claimCase.owner?.userId || claimCase.owner.userId.toString() !== currentUserId) {
|
||||||
|
throw new ForbiddenException('Only the claim owner can capture parts');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (claimCase.workflow?.currentStep !== ClaimWorkflowStep.CAPTURE_PART_DAMAGES) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Invalid workflow step. Expected ${ClaimWorkflowStep.CAPTURE_PART_DAMAGES}, but current step is ${claimCase.workflow?.currentStep}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileUrl = buildFileLink(file.path);
|
||||||
|
const captureData = {
|
||||||
|
path: file.path,
|
||||||
|
fileName: file.filename,
|
||||||
|
url: fileUrl,
|
||||||
|
capturedAt: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateData: any = {
|
||||||
|
$push: {
|
||||||
|
history: {
|
||||||
|
type: body.captureType === 'angle' ? 'ANGLE_CAPTURED' : 'PART_CAPTURED',
|
||||||
|
actor: {
|
||||||
|
actorId: new Types.ObjectId(currentUserId),
|
||||||
|
actorName: claimCase.owner?.fullName || 'User',
|
||||||
|
actorType: 'user',
|
||||||
|
},
|
||||||
|
timestamp: new Date(),
|
||||||
|
metadata: {
|
||||||
|
captureType: body.captureType,
|
||||||
|
captureKey: body.captureKey,
|
||||||
|
fileName: file.filename,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (body.captureType === 'angle') {
|
||||||
|
updateData[`media.carAngles.${body.captureKey}`] = captureData;
|
||||||
|
} else {
|
||||||
|
updateData[`media.damagedParts.${body.captureKey}`] = captureData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedClaim = await this.claimCaseDbService.findByIdAndUpdate(
|
||||||
|
claimRequestId,
|
||||||
|
updateData,
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasCapture = (data: any, key: string) =>
|
||||||
|
data && (data instanceof Map ? data.get(key) : data[key]);
|
||||||
|
|
||||||
|
// Use same logic as getCaptureRequirementsV2 to determine completion
|
||||||
|
const anglesKeys = ['front', 'back', 'left', 'right'];
|
||||||
|
const carAnglesData = updatedClaim?.media?.carAngles as any;
|
||||||
|
const damagedPartsData = updatedClaim?.media?.damagedParts as any;
|
||||||
|
const selectedParts = updatedClaim?.damage?.selectedParts || [];
|
||||||
|
|
||||||
|
const anglesCaptured = anglesKeys.filter(k => hasCapture(carAnglesData, k)).length;
|
||||||
|
const partsCaptured = selectedParts.filter(p => hasCapture(damagedPartsData, p)).length;
|
||||||
|
|
||||||
|
const allCapturesComplete =
|
||||||
|
anglesCaptured >= 4 && partsCaptured >= selectedParts.length;
|
||||||
|
|
||||||
|
if (allCapturesComplete) {
|
||||||
|
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||||
|
status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT,
|
||||||
|
claimStatus: ClaimStatus.PENDING,
|
||||||
|
'workflow.currentStep': ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
||||||
|
'workflow.nextStep': ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
||||||
|
$push: {
|
||||||
|
'workflow.completedSteps': ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
||||||
|
history: {
|
||||||
|
type: 'STEP_COMPLETED',
|
||||||
|
actor: {
|
||||||
|
actorId: new Types.ObjectId(currentUserId),
|
||||||
|
actorName: claimCase.owner?.fullName || 'User',
|
||||||
|
actorType: 'user',
|
||||||
|
},
|
||||||
|
timestamp: new Date(),
|
||||||
|
metadata: {
|
||||||
|
stepKey: ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
||||||
|
description: 'User submission complete. Claim ready for damage expert review.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const remaining = (4 - anglesCaptured) + (selectedParts.length - partsCaptured);
|
||||||
|
const message = allCapturesComplete
|
||||||
|
? 'All captures complete. Your claim is now ready for damage expert review.'
|
||||||
|
: `${body.captureType === 'angle' ? 'Angle' : 'Part'} captured successfully. ${remaining} captures remaining.`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
claimRequestId: claimCase._id.toString(),
|
||||||
|
captureType: body.captureType,
|
||||||
|
captureKey: body.captureKey,
|
||||||
|
fileUrl,
|
||||||
|
allCapturesComplete,
|
||||||
|
currentStep: allCapturesComplete
|
||||||
|
? ClaimWorkflowStep.USER_SUBMISSION_COMPLETE
|
||||||
|
: ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
this.logger.error('capturePartV2 failed', error);
|
||||||
|
throw new InternalServerErrorException('Failed to capture part');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2 API: Get list of claims for current user
|
||||||
|
*/
|
||||||
|
async getMyClaimsV2(currentUserId: string): Promise<GetMyClaimsV2ResponseDto> {
|
||||||
|
try {
|
||||||
|
const 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,
|
||||||
|
requestNo: c.requestNo,
|
||||||
|
status: c.status,
|
||||||
|
claimStatus: c.claimStatus || 'PENDING',
|
||||||
|
currentStep: c.workflow?.currentStep || '',
|
||||||
|
createdAt: c.createdAt,
|
||||||
|
blameRequestId: c.blameRequestId?.toString(),
|
||||||
|
})) as ClaimListItemV2Dto[];
|
||||||
|
return { list, total: list.length };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
this.logger.error('getMyClaimsV2 failed', error);
|
||||||
|
throw new InternalServerErrorException('Failed to get claims list');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2 API: Get claim details by ID (owner only)
|
||||||
|
*/
|
||||||
|
async getClaimDetailsV2(
|
||||||
|
claimRequestId: string,
|
||||||
|
currentUserId: 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) {
|
||||||
|
throw new ForbiddenException('You do not have access to this claim');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasCapture = (data: any, key: string) =>
|
||||||
|
data && (data instanceof Map ? data.get(key) : data[key]);
|
||||||
|
|
||||||
|
const requiredDocs = claim.requiredDocuments as any;
|
||||||
|
const requiredDocumentsStatus: Record<string, { uploaded: boolean; fileId?: string }> = {};
|
||||||
|
if (requiredDocs) {
|
||||||
|
const keys = requiredDocs instanceof Map ? Array.from(requiredDocs.keys()) : Object.keys(requiredDocs);
|
||||||
|
for (const k of keys) {
|
||||||
|
const doc = requiredDocs instanceof Map ? requiredDocs.get(k) : requiredDocs[k];
|
||||||
|
requiredDocumentsStatus[k] = {
|
||||||
|
uploaded: !!doc?.uploaded,
|
||||||
|
fileId: doc?.fileId?.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const carAnglesData = claim.media?.carAngles as any;
|
||||||
|
const carAngles: Record<string, { captured: boolean; url?: string }> = {};
|
||||||
|
for (const k of ['front', 'back', 'left', 'right']) {
|
||||||
|
const cap = hasCapture(carAnglesData, k);
|
||||||
|
carAngles[k] = {
|
||||||
|
captured: !!cap,
|
||||||
|
url: cap?.url || (cap?.path ? buildFileLink(cap.path) : undefined),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const damagedPartsData = claim.media?.damagedParts as any;
|
||||||
|
const damagedParts: Record<string, { captured: boolean; url?: string }> = {};
|
||||||
|
for (const p of claim.damage?.selectedParts || []) {
|
||||||
|
const cap = hasCapture(damagedPartsData, p);
|
||||||
|
damagedParts[p] = {
|
||||||
|
captured: !!cap,
|
||||||
|
url: cap?.url || (cap?.path ? buildFileLink(cap.path) : undefined),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const maskSheba = (s?: string) =>
|
||||||
|
s ? s.replace(/^(.{4})(.*)(.{4})$/, 'IR$1************$3') : undefined;
|
||||||
|
const maskNationalCode = (s?: string) =>
|
||||||
|
s ? s.replace(/^(.{2})(.*)(.{2})$/, '$1******$3') : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
claimRequestId: claim._id.toString(),
|
||||||
|
publicId: claim.publicId,
|
||||||
|
requestNo: claim.requestNo,
|
||||||
|
status: claim.status,
|
||||||
|
claimStatus: claim.claimStatus || 'PENDING',
|
||||||
|
currentStep: claim.workflow?.currentStep || '',
|
||||||
|
nextStep: claim.workflow?.nextStep,
|
||||||
|
blameRequestId: claim.blameRequestId?.toString(),
|
||||||
|
blameRequestNo: claim.blameRequestNo,
|
||||||
|
owner: claim.owner
|
||||||
|
? {
|
||||||
|
userId: claim.owner.userId?.toString(),
|
||||||
|
fullName: claim.owner.fullName,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
vehicle: claim.vehicle,
|
||||||
|
selectedParts: claim.damage?.selectedParts,
|
||||||
|
otherParts: claim.damage?.otherParts,
|
||||||
|
money: claim.money
|
||||||
|
? {
|
||||||
|
sheba: maskSheba(claim.money.sheba),
|
||||||
|
nationalCodeOfOwner: maskNationalCode(claim.money.nationalCodeOfInsurer),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
requiredDocuments: Object.keys(requiredDocumentsStatus).length > 0 ? requiredDocumentsStatus : undefined,
|
||||||
|
carAngles,
|
||||||
|
damagedParts,
|
||||||
|
createdAt: (claim as any).createdAt,
|
||||||
|
updatedAt: (claim as any).updatedAt,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
this.logger.error('getClaimDetailsV2 failed', error);
|
||||||
|
throw new InternalServerErrorException('Failed to get claim details');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generateUniqueClaimNumber(): Promise<string> {
|
||||||
|
const prefix = "CL";
|
||||||
|
const randomPart = Math.floor(10000 + Math.random() * 90000); // 5 digits
|
||||||
|
return `${prefix}${randomPart}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,609 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
HttpException,
|
||||||
|
InternalServerErrorException,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
Patch,
|
||||||
|
Body,
|
||||||
|
UseGuards,
|
||||||
|
Get,
|
||||||
|
UseInterceptors,
|
||||||
|
UploadedFile,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { ApiBearerAuth, ApiParam, ApiTags, ApiOperation, ApiResponse, ApiBody, ApiConsumes } from "@nestjs/swagger";
|
||||||
|
import { FileInterceptor } from "@nestjs/platform-express";
|
||||||
|
import { diskStorage } from "multer";
|
||||||
|
import { extname } from "path";
|
||||||
|
import { GlobalGuard } from "src/auth/guards/global.guard";
|
||||||
|
import { RolesGuard } from "src/auth/guards/role.guard";
|
||||||
|
import { Roles } from "src/decorators/roles.decorator";
|
||||||
|
import { CurrentUser } from "src/decorators/user.decorator";
|
||||||
|
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||||
|
import { ClaimRequestManagementService } from "./claim-request-management.service";
|
||||||
|
import { SelectOuterPartsV2Dto, SelectOuterPartsV2ResponseDto } from "./dto/select-outer-parts-v2.dto";
|
||||||
|
import { SelectOtherPartsV2Dto, SelectOtherPartsV2ResponseDto } from "./dto/select-other-parts-v2.dto";
|
||||||
|
import { GetCaptureRequirementsV2ResponseDto } from "./dto/capture-requirements-v2.dto";
|
||||||
|
import { UploadRequiredDocumentV2Dto, UploadRequiredDocumentV2ResponseDto } from "./dto/upload-document-v2.dto";
|
||||||
|
import { CapturePartV2Dto, CapturePartV2ResponseDto } from "./dto/capture-part-v2.dto";
|
||||||
|
import { GetMyClaimsV2ResponseDto } from "./dto/my-claims-v2.dto";
|
||||||
|
import { ClaimDetailsV2ResponseDto } from "./dto/claim-details-v2.dto";
|
||||||
|
|
||||||
|
@ApiTags("claim-request-management (v2)")
|
||||||
|
@Controller("v2/claim-request-management")
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(GlobalGuard, RolesGuard)
|
||||||
|
@Roles(RoleEnum.USER)
|
||||||
|
export class ClaimRequestManagementV2Controller {
|
||||||
|
constructor(
|
||||||
|
private readonly claimRequestManagementService: ClaimRequestManagementService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Get("requests")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get My Claims (V2)",
|
||||||
|
description: "Get list of all claim requests for the current user.",
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: "List of user claims",
|
||||||
|
type: GetMyClaimsV2ResponseDto,
|
||||||
|
})
|
||||||
|
async getMyClaims(@CurrentUser() user: any): Promise<GetMyClaimsV2ResponseDto> {
|
||||||
|
try {
|
||||||
|
return await this.claimRequestManagementService.getMyClaimsV2(user.sub);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
error instanceof Error ? error.message : "Failed to get claims list",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("request/:claimRequestId")
|
||||||
|
@ApiParam({
|
||||||
|
name: "claimRequestId",
|
||||||
|
description: "The claim case ID (MongoDB ObjectId)",
|
||||||
|
example: "507f1f77bcf86cd799439011",
|
||||||
|
})
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get Claim Details (V2)",
|
||||||
|
description: "Get full details of a claim request. Only the claim owner can access.",
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: "Claim details",
|
||||||
|
type: ClaimDetailsV2ResponseDto,
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 403,
|
||||||
|
description: "User is not the claim owner",
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 404,
|
||||||
|
description: "Claim not found",
|
||||||
|
})
|
||||||
|
async getClaimDetails(
|
||||||
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
@CurrentUser() user: any,
|
||||||
|
): Promise<ClaimDetailsV2ResponseDto> {
|
||||||
|
try {
|
||||||
|
return await this.claimRequestManagementService.getClaimDetailsV2(
|
||||||
|
claimRequestId,
|
||||||
|
user.sub,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
error instanceof Error ? error.message : "Failed to get claim details",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("create-from-blame/:blameRequestId")
|
||||||
|
@ApiParam({
|
||||||
|
name: "blameRequestId",
|
||||||
|
description: "ID of the completed blame case to create claim from",
|
||||||
|
})
|
||||||
|
async createClaimFromBlame(
|
||||||
|
@Param("blameRequestId") blameRequestId: string,
|
||||||
|
@CurrentUser() user: any,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
return await this.claimRequestManagementService.createClaimFromBlameV2(
|
||||||
|
blameRequestId,
|
||||||
|
user.sub,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Failed to create claim request",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2 API: Select damaged outer car parts (Step 2 of claim workflow)
|
||||||
|
*/
|
||||||
|
@Patch("select-outer-parts/:claimRequestId")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Select Damaged Outer Car Parts (V2 - Step 2)",
|
||||||
|
description: `
|
||||||
|
**Workflow Step:** SELECT_OUTER_PARTS (Step 2 of Claim)
|
||||||
|
|
||||||
|
**Purpose:** User selects which outer car parts (body parts) were damaged in the accident.
|
||||||
|
|
||||||
|
**Validations:**
|
||||||
|
- Claim must exist
|
||||||
|
- User must be the claim owner (damaged party from blame case)
|
||||||
|
- Current workflow step must be CLAIM_CREATED
|
||||||
|
- Parts array must contain at least 1 part
|
||||||
|
- No duplicate parts allowed
|
||||||
|
- Parts cannot be re-selected once submitted
|
||||||
|
|
||||||
|
**Valid Outer Parts:**
|
||||||
|
- hood, trunk, roof
|
||||||
|
- front_right_door, front_left_door, rear_right_door, rear_left_door
|
||||||
|
- front_bumper, rear_bumper
|
||||||
|
- front_right_fender, front_left_fender, rear_right_fender, rear_left_fender
|
||||||
|
|
||||||
|
**After Success:**
|
||||||
|
- Workflow moves to: SELECT_OTHER_PARTS (Step 3)
|
||||||
|
- User can proceed to select non-body parts and provide bank info
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: "claimRequestId",
|
||||||
|
description: "The claim case ID (MongoDB ObjectId)",
|
||||||
|
example: "507f1f77bcf86cd799439011",
|
||||||
|
})
|
||||||
|
@ApiBody({
|
||||||
|
type: SelectOuterPartsV2Dto,
|
||||||
|
description: "Array of selected damaged outer parts",
|
||||||
|
examples: {
|
||||||
|
example1: {
|
||||||
|
summary: "Minor front damage",
|
||||||
|
value: {
|
||||||
|
selectedParts: ["hood", "front_bumper", "front_right_fender"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
example2: {
|
||||||
|
summary: "Side impact damage",
|
||||||
|
value: {
|
||||||
|
selectedParts: [
|
||||||
|
"front_left_door",
|
||||||
|
"rear_left_door",
|
||||||
|
"front_left_fender",
|
||||||
|
"rear_left_fender",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
example3: {
|
||||||
|
summary: "Rear-end collision",
|
||||||
|
value: {
|
||||||
|
selectedParts: ["rear_bumper", "trunk", "rear_right_fender"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
example4: {
|
||||||
|
summary: "Multiple damage areas",
|
||||||
|
value: {
|
||||||
|
selectedParts: [
|
||||||
|
"hood",
|
||||||
|
"front_bumper",
|
||||||
|
"front_right_door",
|
||||||
|
"rear_bumper",
|
||||||
|
"trunk",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: "Outer parts selected successfully",
|
||||||
|
type: SelectOuterPartsV2ResponseDto,
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 400,
|
||||||
|
description: "Invalid workflow step or validation failed",
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 403,
|
||||||
|
description: "User is not the claim owner",
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 404,
|
||||||
|
description: "Claim case not found",
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 409,
|
||||||
|
description: "Outer parts already selected",
|
||||||
|
})
|
||||||
|
async selectOuterParts(
|
||||||
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
@Body() body: SelectOuterPartsV2Dto,
|
||||||
|
@CurrentUser() user: any,
|
||||||
|
): Promise<SelectOuterPartsV2ResponseDto> {
|
||||||
|
try {
|
||||||
|
return await this.claimRequestManagementService.selectOuterPartsV2(
|
||||||
|
claimRequestId,
|
||||||
|
body,
|
||||||
|
user.sub,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Failed to select outer parts",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2 API: Select other damaged parts and provide bank information (Step 3 of claim workflow)
|
||||||
|
*/
|
||||||
|
@Patch("select-other-parts/:claimRequestId")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Select Other Parts & Bank Information (V2 - Step 3)",
|
||||||
|
description: `
|
||||||
|
**Workflow Step:** SELECT_OTHER_PARTS (Step 3 of Claim)
|
||||||
|
|
||||||
|
**Purpose:** User selects non-body damaged parts and provides bank information for payment.
|
||||||
|
|
||||||
|
**Validations:**
|
||||||
|
- Claim must exist
|
||||||
|
- User must be the claim owner (damaged party from blame case)
|
||||||
|
- Current workflow step must be SELECT_OTHER_PARTS
|
||||||
|
- Bank information must not have been submitted previously
|
||||||
|
- Sheba number must be exactly 24 digits
|
||||||
|
- National code must be exactly 10 digits
|
||||||
|
|
||||||
|
**Valid Other Parts (Optional):**
|
||||||
|
- engine, suspension, brake_system, electrical
|
||||||
|
- radiator, transmission, exhaust
|
||||||
|
- headlight, taillight, mirror, glass
|
||||||
|
|
||||||
|
**After Success:**
|
||||||
|
- Workflow moves to: UPLOAD_REQUIRED_DOCUMENTS (Step 4)
|
||||||
|
- User can proceed to upload required documents
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: "claimRequestId",
|
||||||
|
description: "The claim case ID (MongoDB ObjectId)",
|
||||||
|
example: "507f1f77bcf86cd799439011",
|
||||||
|
})
|
||||||
|
@ApiBody({
|
||||||
|
type: SelectOtherPartsV2Dto,
|
||||||
|
description: "Other parts selection and bank information",
|
||||||
|
examples: {
|
||||||
|
example1: {
|
||||||
|
summary: "Only bank info (no other parts damaged)",
|
||||||
|
value: {
|
||||||
|
otherParts: [],
|
||||||
|
shebaNumber: "123456789012345678901234",
|
||||||
|
nationalCodeOfOwner: "1234567890",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
example2: {
|
||||||
|
summary: "Engine and suspension damage",
|
||||||
|
value: {
|
||||||
|
otherParts: ["engine", "suspension"],
|
||||||
|
shebaNumber: "123456789012345678901234",
|
||||||
|
nationalCodeOfOwner: "1234567890",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
example3: {
|
||||||
|
summary: "Multiple systems damaged",
|
||||||
|
value: {
|
||||||
|
otherParts: ["engine", "brake_system", "electrical", "headlight"],
|
||||||
|
shebaNumber: "123456789012345678901234",
|
||||||
|
nationalCodeOfOwner: "1234567890",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
example4: {
|
||||||
|
summary: "Lighting and glass damage",
|
||||||
|
value: {
|
||||||
|
otherParts: ["headlight", "taillight", "mirror", "glass"],
|
||||||
|
shebaNumber: "123456789012345678901234",
|
||||||
|
nationalCodeOfOwner: "1234567890",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: "Other parts and bank information saved successfully",
|
||||||
|
type: SelectOtherPartsV2ResponseDto,
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 400,
|
||||||
|
description: "Invalid workflow step or validation failed",
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 403,
|
||||||
|
description: "User is not the claim owner",
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 404,
|
||||||
|
description: "Claim case not found",
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 409,
|
||||||
|
description: "Bank information already submitted",
|
||||||
|
})
|
||||||
|
async selectOtherParts(
|
||||||
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
@Body() body: SelectOtherPartsV2Dto,
|
||||||
|
@CurrentUser() user: any,
|
||||||
|
): Promise<SelectOtherPartsV2ResponseDto> {
|
||||||
|
try {
|
||||||
|
return await this.claimRequestManagementService.selectOtherPartsV2(
|
||||||
|
claimRequestId,
|
||||||
|
body,
|
||||||
|
user.sub,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Failed to select other parts",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2 API: Get capture requirements (documents, angles, parts)
|
||||||
|
*/
|
||||||
|
@Get("capture-requirements/:claimRequestId")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get Capture Requirements (V2)",
|
||||||
|
description: `
|
||||||
|
**Get list of what needs to be captured:**
|
||||||
|
- Required documents (13 items)
|
||||||
|
- Car angles (4 items: front, back, left, right)
|
||||||
|
- Damaged parts (based on selected outer parts)
|
||||||
|
|
||||||
|
Returns status of each item (uploaded/captured or not).
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: "claimRequestId",
|
||||||
|
description: "The claim case ID (MongoDB ObjectId)",
|
||||||
|
example: "507f1f77bcf86cd799439011",
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: "Capture requirements retrieved successfully",
|
||||||
|
type: GetCaptureRequirementsV2ResponseDto,
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 403,
|
||||||
|
description: "User is not the claim owner",
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 404,
|
||||||
|
description: "Claim case not found",
|
||||||
|
})
|
||||||
|
async getCaptureRequirements(
|
||||||
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
@CurrentUser() user: any,
|
||||||
|
): Promise<GetCaptureRequirementsV2ResponseDto> {
|
||||||
|
try {
|
||||||
|
return await this.claimRequestManagementService.getCaptureRequirementsV2(
|
||||||
|
claimRequestId,
|
||||||
|
user.sub,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Failed to get capture requirements",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2 API: Upload required document
|
||||||
|
*/
|
||||||
|
@Post("upload-document/:claimRequestId")
|
||||||
|
@UseInterceptors(
|
||||||
|
FileInterceptor("file", {
|
||||||
|
limits: {
|
||||||
|
fileSize: 10 * 1024 * 1024, // 10MB
|
||||||
|
},
|
||||||
|
storage: diskStorage({
|
||||||
|
destination: "./files/claim-documents",
|
||||||
|
filename: (req, file, callback) => {
|
||||||
|
const unique = Date.now();
|
||||||
|
const ex = extname(file.originalname);
|
||||||
|
const filename = `${file.originalname.split(".")[0]}-${unique}${ex}`;
|
||||||
|
callback(null, filename);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
@ApiConsumes("multipart/form-data")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Upload Required Document (V2 - Step 4)",
|
||||||
|
description: `
|
||||||
|
**Workflow Step:** UPLOAD_REQUIRED_DOCUMENTS (Step 4 of Claim)
|
||||||
|
|
||||||
|
**Upload one of the 13 required documents:**
|
||||||
|
- 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
|
||||||
|
|
||||||
|
**When all 13 documents are uploaded:**
|
||||||
|
- Workflow automatically moves to: CAPTURE_PART_DAMAGES (Step 5)
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: "claimRequestId",
|
||||||
|
description: "The claim case ID",
|
||||||
|
example: "507f1f77bcf86cd799439011",
|
||||||
|
})
|
||||||
|
@ApiBody({
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
required: ["documentKey", "file"],
|
||||||
|
properties: {
|
||||||
|
documentKey: {
|
||||||
|
type: "string",
|
||||||
|
enum: [
|
||||||
|
"car_green_card",
|
||||||
|
"damaged_driving_license_front",
|
||||||
|
"damaged_driving_license_back",
|
||||||
|
"damaged_chassis_number",
|
||||||
|
"damaged_engine_photo",
|
||||||
|
"damaged_car_card_front",
|
||||||
|
"damaged_car_card_back",
|
||||||
|
"damaged_metal_plate",
|
||||||
|
"guilty_driving_license_front",
|
||||||
|
"guilty_driving_license_back",
|
||||||
|
"guilty_car_card_front",
|
||||||
|
"guilty_car_card_back",
|
||||||
|
"guilty_metal_plate",
|
||||||
|
],
|
||||||
|
example: "car_green_card",
|
||||||
|
},
|
||||||
|
file: {
|
||||||
|
type: "string",
|
||||||
|
format: "binary",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: "Document uploaded successfully",
|
||||||
|
type: UploadRequiredDocumentV2ResponseDto,
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 400,
|
||||||
|
description: "Invalid workflow step",
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 409,
|
||||||
|
description: "Document already uploaded",
|
||||||
|
})
|
||||||
|
async uploadDocument(
|
||||||
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
@Body() body: UploadRequiredDocumentV2Dto,
|
||||||
|
@UploadedFile() file: Express.Multer.File,
|
||||||
|
@CurrentUser() user: any,
|
||||||
|
): Promise<UploadRequiredDocumentV2ResponseDto> {
|
||||||
|
try {
|
||||||
|
return await this.claimRequestManagementService.uploadRequiredDocumentV2(
|
||||||
|
claimRequestId,
|
||||||
|
body,
|
||||||
|
file,
|
||||||
|
user.sub,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
error instanceof Error ? error.message : "Failed to upload document",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2 API: Capture car angle or damaged part
|
||||||
|
*/
|
||||||
|
@Post("capture-part/:claimRequestId")
|
||||||
|
@UseInterceptors(
|
||||||
|
FileInterceptor("file", {
|
||||||
|
limits: {
|
||||||
|
fileSize: 10 * 1024 * 1024, // 10MB
|
||||||
|
},
|
||||||
|
storage: diskStorage({
|
||||||
|
destination: "./files/claim-captures",
|
||||||
|
filename: (req, file, callback) => {
|
||||||
|
const unique = Date.now();
|
||||||
|
const ex = extname(file.originalname);
|
||||||
|
const filename = `${file.originalname.split(".")[0]}-${unique}${ex}`;
|
||||||
|
callback(null, filename);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
@ApiConsumes("multipart/form-data")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Capture Car Angle or Damaged Part (V2 - Step 5)",
|
||||||
|
description: `
|
||||||
|
**Workflow Step:** CAPTURE_PART_DAMAGES (Step 5 of Claim)
|
||||||
|
|
||||||
|
**Capture types:**
|
||||||
|
1. **angle**: Car angles (front, back, left, right) - 4 required
|
||||||
|
2. **part**: Damaged parts based on selectedParts from Step 2
|
||||||
|
|
||||||
|
**All captures must be completed before user can submit claim.**
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: "claimRequestId",
|
||||||
|
description: "The claim case ID",
|
||||||
|
example: "507f1f77bcf86cd799439011",
|
||||||
|
})
|
||||||
|
@ApiBody({
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
required: ["captureType", "captureKey", "file"],
|
||||||
|
properties: {
|
||||||
|
captureType: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["angle", "part"],
|
||||||
|
example: "angle",
|
||||||
|
},
|
||||||
|
captureKey: {
|
||||||
|
type: "string",
|
||||||
|
example: "front",
|
||||||
|
description:
|
||||||
|
'For angle: front/back/left/right. For part: hood/front_bumper/etc.',
|
||||||
|
},
|
||||||
|
file: {
|
||||||
|
type: "string",
|
||||||
|
format: "binary",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: "Capture saved successfully",
|
||||||
|
type: CapturePartV2ResponseDto,
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 400,
|
||||||
|
description: "Invalid workflow step",
|
||||||
|
})
|
||||||
|
async capturePart(
|
||||||
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
@Body() body: CapturePartV2Dto,
|
||||||
|
@UploadedFile() file: Express.Multer.File,
|
||||||
|
@CurrentUser() user: any,
|
||||||
|
): Promise<CapturePartV2ResponseDto> {
|
||||||
|
try {
|
||||||
|
return await this.claimRequestManagementService.capturePartV2(
|
||||||
|
claimRequestId,
|
||||||
|
body,
|
||||||
|
file,
|
||||||
|
user.sub,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
error instanceof Error ? error.message : "Failed to capture part",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/claim-request-management/dto/capture-part-v2.dto.ts
Normal file
81
src/claim-request-management/dto/capture-part-v2.dto.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
import { CarAngle } from 'src/Types&Enums/claim-request-management/required-document-type.enum';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2 DTO for capturing car angle or damaged part
|
||||||
|
*/
|
||||||
|
export class CapturePartV2Dto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Type of capture: angle or part',
|
||||||
|
example: 'angle',
|
||||||
|
enum: ['angle', 'part'],
|
||||||
|
})
|
||||||
|
@IsNotEmpty({ message: 'Capture type is required' })
|
||||||
|
@IsEnum(['angle', 'part'], {
|
||||||
|
message: 'Capture type must be either "angle" or "part"',
|
||||||
|
})
|
||||||
|
captureType: 'angle' | 'part';
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Key of the angle or part being captured',
|
||||||
|
example: 'front',
|
||||||
|
})
|
||||||
|
@IsNotEmpty({ message: 'Capture key is required' })
|
||||||
|
@IsString({ message: 'Capture key must be a string' })
|
||||||
|
captureKey: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: 'string',
|
||||||
|
format: 'binary',
|
||||||
|
description: 'Image file (JPG, PNG)',
|
||||||
|
})
|
||||||
|
file: Express.Multer.File;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response DTO for capture part V2
|
||||||
|
*/
|
||||||
|
export class CapturePartV2ResponseDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Claim request ID',
|
||||||
|
example: '507f1f77bcf86cd799439011',
|
||||||
|
})
|
||||||
|
claimRequestId: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Type of capture',
|
||||||
|
example: 'angle',
|
||||||
|
})
|
||||||
|
captureType: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Key of what was captured',
|
||||||
|
example: 'front',
|
||||||
|
})
|
||||||
|
captureKey: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'File URL',
|
||||||
|
example: 'http://localhost:3000/files/captures/front-1234567890.jpg',
|
||||||
|
})
|
||||||
|
fileUrl: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Whether all captures are now complete',
|
||||||
|
example: false,
|
||||||
|
})
|
||||||
|
allCapturesComplete: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Current workflow step',
|
||||||
|
example: 'CAPTURE_PART_DAMAGES',
|
||||||
|
})
|
||||||
|
currentStep: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Success message',
|
||||||
|
example: 'Angle captured successfully. 6 captures remaining.',
|
||||||
|
})
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
163
src/claim-request-management/dto/capture-requirements-v2.dto.ts
Normal file
163
src/claim-request-management/dto/capture-requirements-v2.dto.ts
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO for required document item
|
||||||
|
*/
|
||||||
|
export class RequiredDocumentItem {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Document key/type',
|
||||||
|
example: 'car_green_card',
|
||||||
|
})
|
||||||
|
key: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Display label in Farsi',
|
||||||
|
example: 'کارت سبز خودرو',
|
||||||
|
})
|
||||||
|
label_fa: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Display label in English',
|
||||||
|
example: 'Car Green Card',
|
||||||
|
})
|
||||||
|
label_en: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Whether this document has been uploaded',
|
||||||
|
example: false,
|
||||||
|
})
|
||||||
|
uploaded: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Category of the document',
|
||||||
|
example: 'general',
|
||||||
|
enum: ['general', 'damaged_party', 'guilty_party'],
|
||||||
|
})
|
||||||
|
category: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO for car angle item
|
||||||
|
*/
|
||||||
|
export class CarAngleItem {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Angle key',
|
||||||
|
example: 'front',
|
||||||
|
enum: ['front', 'back', 'left', 'right'],
|
||||||
|
})
|
||||||
|
key: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Display label in Farsi',
|
||||||
|
example: 'جلو',
|
||||||
|
})
|
||||||
|
label_fa: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Display label in English',
|
||||||
|
example: 'Front',
|
||||||
|
})
|
||||||
|
label_en: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Whether this angle has been captured',
|
||||||
|
example: false,
|
||||||
|
})
|
||||||
|
captured: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO for damaged part item
|
||||||
|
*/
|
||||||
|
export class DamagedPartItem {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Part key',
|
||||||
|
example: 'hood',
|
||||||
|
})
|
||||||
|
key: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Display label in Farsi',
|
||||||
|
example: 'کاپوت',
|
||||||
|
})
|
||||||
|
label_fa: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Display label in English',
|
||||||
|
example: 'Hood',
|
||||||
|
})
|
||||||
|
label_en: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Whether this part has been captured',
|
||||||
|
example: false,
|
||||||
|
})
|
||||||
|
captured: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response DTO for capture requirements V2
|
||||||
|
*/
|
||||||
|
export class GetCaptureRequirementsV2ResponseDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Claim request ID',
|
||||||
|
example: '507f1f77bcf86cd799439011',
|
||||||
|
})
|
||||||
|
claimRequestId: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Public ID',
|
||||||
|
example: 'A14235',
|
||||||
|
})
|
||||||
|
publicId: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Current workflow step',
|
||||||
|
example: 'UPLOAD_REQUIRED_DOCUMENTS',
|
||||||
|
})
|
||||||
|
currentStep: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'List of required documents to upload',
|
||||||
|
type: [RequiredDocumentItem],
|
||||||
|
})
|
||||||
|
requiredDocuments: RequiredDocumentItem[];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'List of car angles to capture',
|
||||||
|
type: [CarAngleItem],
|
||||||
|
})
|
||||||
|
carAngles: CarAngleItem[];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'List of damaged parts to capture',
|
||||||
|
type: [DamagedPartItem],
|
||||||
|
})
|
||||||
|
damagedParts: DamagedPartItem[];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Total number of items remaining',
|
||||||
|
example: 18,
|
||||||
|
})
|
||||||
|
totalRemaining: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Summary of progress',
|
||||||
|
example: {
|
||||||
|
documentsUploaded: 2,
|
||||||
|
documentsTotal: 13,
|
||||||
|
anglesCapture: 0,
|
||||||
|
anglesTotal: 4,
|
||||||
|
partsCapture: 0,
|
||||||
|
partsTotal: 3,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
progress: {
|
||||||
|
documentsUploaded: number;
|
||||||
|
documentsTotal: number;
|
||||||
|
anglesCaptured: number;
|
||||||
|
anglesTotal: number;
|
||||||
|
partsCaptured: number;
|
||||||
|
partsTotal: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
71
src/claim-request-management/dto/claim-details-v2.dto.ts
Normal file
71
src/claim-request-management/dto/claim-details-v2.dto.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class ClaimDetailsV2ResponseDto {
|
||||||
|
@ApiProperty({ description: 'Claim case ID' })
|
||||||
|
claimRequestId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Public ID' })
|
||||||
|
publicId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Request number' })
|
||||||
|
requestNo: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Overall case status' })
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Claim damage status' })
|
||||||
|
claimStatus: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Current workflow step' })
|
||||||
|
currentStep: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Next workflow step' })
|
||||||
|
nextStep?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Blame request ID' })
|
||||||
|
blameRequestId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Blame request number' })
|
||||||
|
blameRequestNo?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Owner info' })
|
||||||
|
owner?: {
|
||||||
|
userId: string;
|
||||||
|
fullName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Vehicle snapshot' })
|
||||||
|
vehicle?: {
|
||||||
|
carName?: string;
|
||||||
|
carModel?: string;
|
||||||
|
carType?: string;
|
||||||
|
plate?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Selected outer damaged parts' })
|
||||||
|
selectedParts?: string[];
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Selected other damaged parts' })
|
||||||
|
otherParts?: string[];
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Bank info (masked)' })
|
||||||
|
money?: {
|
||||||
|
sheba?: string;
|
||||||
|
nationalCodeOfOwner?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Required documents status' })
|
||||||
|
requiredDocuments?: Record<string, { uploaded: boolean; fileId?: string }>;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Car angles captured' })
|
||||||
|
carAngles?: Record<string, { captured: boolean; url?: string }>;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Damaged parts captured' })
|
||||||
|
damagedParts?: Record<string, { captured: boolean; url?: string }>;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Created at' })
|
||||||
|
createdAt: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Updated at' })
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
26
src/claim-request-management/dto/create-claim-v2.dto.ts
Normal file
26
src/claim-request-management/dto/create-claim-v2.dto.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
export class CreateClaimFromBlameResponseDto {
|
||||||
|
@ApiProperty({
|
||||||
|
example: "67fa10c259e15f231a2d1aae",
|
||||||
|
description: "Created claim request ID",
|
||||||
|
})
|
||||||
|
claimRequestId: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: "A14235",
|
||||||
|
description: "Shared public ID from blame case",
|
||||||
|
})
|
||||||
|
publicId: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: "SELECTING_OUTER_PARTS",
|
||||||
|
description: "Current status after creation",
|
||||||
|
})
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: "Claim request created successfully",
|
||||||
|
})
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
35
src/claim-request-management/dto/my-claims-v2.dto.ts
Normal file
35
src/claim-request-management/dto/my-claims-v2.dto.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class ClaimListItemV2Dto {
|
||||||
|
@ApiProperty({ description: 'Claim case ID', example: '507f1f77bcf86cd799439011' })
|
||||||
|
claimRequestId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Public ID shared across blame and claim', example: 'A14235' })
|
||||||
|
publicId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Claim request number', example: 'CL12345' })
|
||||||
|
requestNo: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Overall case status', example: 'WAITING_FOR_DAMAGE_EXPERT' })
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Claim damage determination status', example: 'PENDING' })
|
||||||
|
claimStatus: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Current workflow step', example: 'USER_SUBMISSION_COMPLETE' })
|
||||||
|
currentStep: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Creation date', example: '2026-02-22T10:00:00.000Z' })
|
||||||
|
createdAt: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Blame request ID this claim originated from' })
|
||||||
|
blameRequestId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GetMyClaimsV2ResponseDto {
|
||||||
|
@ApiProperty({ description: 'List of user claims', type: [ClaimListItemV2Dto] })
|
||||||
|
list: ClaimListItemV2Dto[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Total count', example: 5 })
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
126
src/claim-request-management/dto/select-other-parts-v2.dto.ts
Normal file
126
src/claim-request-management/dto/select-other-parts-v2.dto.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsArray, IsEnum, IsNotEmpty, IsOptional, IsString, Matches, Length, ArrayMaxSize } from 'class-validator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum for valid other (non-body) car parts that can be damaged
|
||||||
|
*/
|
||||||
|
export enum OtherCarPart {
|
||||||
|
ENGINE = 'engine',
|
||||||
|
SUSPENSION = 'suspension',
|
||||||
|
BRAKE_SYSTEM = 'brake_system',
|
||||||
|
ELECTRICAL = 'electrical',
|
||||||
|
RADIATOR = 'radiator',
|
||||||
|
TRANSMISSION = 'transmission',
|
||||||
|
EXHAUST = 'exhaust',
|
||||||
|
HEADLIGHT = 'headlight',
|
||||||
|
TAILLIGHT = 'taillight',
|
||||||
|
MIRROR = 'mirror',
|
||||||
|
GLASS = 'glass',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2 DTO for selecting other damaged parts and bank information
|
||||||
|
* Step 3 of claim workflow
|
||||||
|
*/
|
||||||
|
export class SelectOtherPartsV2Dto {
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Array of selected other damaged parts (non-body parts)',
|
||||||
|
example: ['engine', 'suspension', 'headlight'],
|
||||||
|
enum: OtherCarPart,
|
||||||
|
isArray: true,
|
||||||
|
maxItems: 11,
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray({ message: 'otherParts must be an array' })
|
||||||
|
@ArrayMaxSize(11, { message: 'Maximum 11 other parts can be selected' })
|
||||||
|
@IsEnum(OtherCarPart, {
|
||||||
|
each: true,
|
||||||
|
message: 'Invalid part name. Must be one of the valid other car parts',
|
||||||
|
})
|
||||||
|
otherParts?: OtherCarPart[];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Sheba number (IBAN) for payment - 24 digits without IR prefix',
|
||||||
|
example: '123456789012345678901234',
|
||||||
|
pattern: '^[0-9]{24}$',
|
||||||
|
minLength: 24,
|
||||||
|
maxLength: 24,
|
||||||
|
})
|
||||||
|
@IsNotEmpty({ message: 'Sheba number is required' })
|
||||||
|
@IsString({ message: 'Sheba number must be a string' })
|
||||||
|
@Length(24, 24, { message: 'Sheba number must be exactly 24 digits' })
|
||||||
|
@Matches(/^[0-9]{24}$/, {
|
||||||
|
message: 'Sheba number must contain exactly 24 digits (without IR prefix)',
|
||||||
|
})
|
||||||
|
shebaNumber: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'National code of the owner - 10 digits',
|
||||||
|
example: '1234567890',
|
||||||
|
pattern: '^[0-9]{10}$',
|
||||||
|
minLength: 10,
|
||||||
|
maxLength: 10,
|
||||||
|
})
|
||||||
|
@IsNotEmpty({ message: 'National code of owner is required' })
|
||||||
|
@IsString({ message: 'National code must be a string' })
|
||||||
|
@Length(10, 10, { message: 'National code must be exactly 10 digits' })
|
||||||
|
@Matches(/^[0-9]{10}$/, {
|
||||||
|
message: 'National code must contain exactly 10 digits',
|
||||||
|
})
|
||||||
|
nationalCodeOfOwner: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response DTO for select other parts V2
|
||||||
|
*/
|
||||||
|
export class SelectOtherPartsV2ResponseDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Claim request ID',
|
||||||
|
example: '507f1f77bcf86cd799439011',
|
||||||
|
})
|
||||||
|
claimRequestId: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Public ID shared across blame and claim',
|
||||||
|
example: 'A14235',
|
||||||
|
})
|
||||||
|
publicId: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Array of selected other damaged parts',
|
||||||
|
example: ['engine', 'suspension'],
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
otherParts?: string[];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Sheba number (masked for security)',
|
||||||
|
example: 'IR12************1234',
|
||||||
|
})
|
||||||
|
shebaNumber: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'National code of owner (masked)',
|
||||||
|
example: '12******90',
|
||||||
|
})
|
||||||
|
nationalCodeOfOwner: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Current workflow step',
|
||||||
|
example: 'UPLOAD_REQUIRED_DOCUMENTS',
|
||||||
|
})
|
||||||
|
currentStep: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Next possible workflow step',
|
||||||
|
example: 'CAPTURE_PART_DAMAGES',
|
||||||
|
})
|
||||||
|
nextStep: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Success message',
|
||||||
|
example: 'Other parts and bank information saved successfully. Please proceed to upload required documents.',
|
||||||
|
})
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsArray, IsEnum, IsNotEmpty, ArrayMinSize, ArrayUnique } from 'class-validator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum for valid outer car parts that can be damaged
|
||||||
|
* Follows the naming convention from workflow step definition
|
||||||
|
*/
|
||||||
|
export enum OuterCarPart {
|
||||||
|
// Hood
|
||||||
|
HOOD = 'hood',
|
||||||
|
|
||||||
|
// Doors
|
||||||
|
FRONT_RIGHT_DOOR = 'front_right_door',
|
||||||
|
FRONT_LEFT_DOOR = 'front_left_door',
|
||||||
|
REAR_RIGHT_DOOR = 'rear_right_door',
|
||||||
|
REAR_LEFT_DOOR = 'rear_left_door',
|
||||||
|
|
||||||
|
// Bumpers
|
||||||
|
FRONT_BUMPER = 'front_bumper',
|
||||||
|
REAR_BUMPER = 'rear_bumper',
|
||||||
|
|
||||||
|
// Fenders
|
||||||
|
FRONT_RIGHT_FENDER = 'front_right_fender',
|
||||||
|
FRONT_LEFT_FENDER = 'front_left_fender',
|
||||||
|
REAR_RIGHT_FENDER = 'rear_right_fender',
|
||||||
|
REAR_LEFT_FENDER = 'rear_left_fender',
|
||||||
|
|
||||||
|
// Trunk & Roof
|
||||||
|
TRUNK = 'trunk',
|
||||||
|
ROOF = 'roof',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2 DTO for selecting damaged outer car parts
|
||||||
|
* Much cleaner than the nested boolean structure
|
||||||
|
*/
|
||||||
|
export class SelectOuterPartsV2Dto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Array of selected damaged outer car parts',
|
||||||
|
example: ['hood', 'front_right_door', 'rear_bumper', 'roof'],
|
||||||
|
enum: OuterCarPart,
|
||||||
|
isArray: true,
|
||||||
|
minItems: 1,
|
||||||
|
maxItems: 13,
|
||||||
|
})
|
||||||
|
@IsNotEmpty({ message: 'Selected parts array cannot be empty' })
|
||||||
|
@IsArray({ message: 'selectedParts must be an array' })
|
||||||
|
@ArrayMinSize(1, { message: 'At least one damaged part must be selected' })
|
||||||
|
@ArrayUnique({ message: 'Duplicate parts are not allowed' })
|
||||||
|
@IsEnum(OuterCarPart, {
|
||||||
|
each: true,
|
||||||
|
message: 'Invalid part name. Must be one of the valid outer car parts',
|
||||||
|
})
|
||||||
|
selectedParts: OuterCarPart[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response DTO for select outer parts V2
|
||||||
|
*/
|
||||||
|
export class SelectOuterPartsV2ResponseDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Claim request ID',
|
||||||
|
example: '507f1f77bcf86cd799439011',
|
||||||
|
})
|
||||||
|
claimRequestId: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Public ID shared across blame and claim',
|
||||||
|
example: 'A14235',
|
||||||
|
})
|
||||||
|
publicId: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Array of selected damaged parts',
|
||||||
|
example: ['hood', 'front_right_door', 'rear_bumper'],
|
||||||
|
})
|
||||||
|
selectedParts: string[];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Current workflow step',
|
||||||
|
example: 'SELECT_OUTER_PARTS',
|
||||||
|
})
|
||||||
|
currentStep: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Next possible workflow step',
|
||||||
|
example: 'SELECT_OTHER_PARTS',
|
||||||
|
})
|
||||||
|
nextStep: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Success message',
|
||||||
|
example: 'Outer parts selected successfully. Please proceed to select other parts.',
|
||||||
|
})
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
67
src/claim-request-management/dto/upload-document-v2.dto.ts
Normal file
67
src/claim-request-management/dto/upload-document-v2.dto.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
import { ClaimRequiredDocumentType } from 'src/Types&Enums/claim-request-management/required-document-type.enum';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2 DTO for uploading required document
|
||||||
|
*/
|
||||||
|
export class UploadRequiredDocumentV2Dto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Document type/key',
|
||||||
|
example: 'car_green_card',
|
||||||
|
enum: ClaimRequiredDocumentType,
|
||||||
|
})
|
||||||
|
@IsNotEmpty({ message: 'Document key is required' })
|
||||||
|
@IsEnum(ClaimRequiredDocumentType, {
|
||||||
|
message: 'Invalid document type',
|
||||||
|
})
|
||||||
|
documentKey: ClaimRequiredDocumentType;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
type: 'string',
|
||||||
|
format: 'binary',
|
||||||
|
description: 'Image file (JPG, PNG, PDF)',
|
||||||
|
})
|
||||||
|
file: Express.Multer.File;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response DTO for upload required document V2
|
||||||
|
*/
|
||||||
|
export class UploadRequiredDocumentV2ResponseDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Claim request ID',
|
||||||
|
example: '507f1f77bcf86cd799439011',
|
||||||
|
})
|
||||||
|
claimRequestId: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Document key that was uploaded',
|
||||||
|
example: 'car_green_card',
|
||||||
|
})
|
||||||
|
documentKey: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'File URL',
|
||||||
|
example: 'http://localhost:3000/files/documents/car-green-card-1234567890.jpg',
|
||||||
|
})
|
||||||
|
fileUrl: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Whether all required documents are now uploaded',
|
||||||
|
example: false,
|
||||||
|
})
|
||||||
|
allDocumentsUploaded: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Current workflow step',
|
||||||
|
example: 'UPLOAD_REQUIRED_DOCUMENTS',
|
||||||
|
})
|
||||||
|
currentStep: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Success message',
|
||||||
|
example: 'Document uploaded successfully. 12 documents remaining.',
|
||||||
|
})
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { InjectModel } from "@nestjs/mongoose";
|
||||||
|
import { FilterQuery, Model, Types } from "mongoose";
|
||||||
|
import { ClaimCase, ClaimCaseDocument } from "../schema/claim-cases.schema";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ClaimCaseDbService {
|
||||||
|
constructor(
|
||||||
|
@InjectModel(ClaimCase.name)
|
||||||
|
private readonly claimCaseModel: Model<ClaimCaseDocument>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async create(payload: Partial<ClaimCase>): Promise<ClaimCaseDocument> {
|
||||||
|
return this.claimCaseModel.create(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: string | Types.ObjectId): Promise<ClaimCaseDocument | null> {
|
||||||
|
return this.claimCaseModel.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(filter: FilterQuery<ClaimCase>): Promise<ClaimCaseDocument | null> {
|
||||||
|
return this.claimCaseModel.findOne(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByIdAndUpdate(
|
||||||
|
id: string | Types.ObjectId,
|
||||||
|
update: any,
|
||||||
|
): Promise<ClaimCaseDocument | null> {
|
||||||
|
return this.claimCaseModel.findByIdAndUpdate(id, update, { new: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async find(
|
||||||
|
filter: FilterQuery<ClaimCase>,
|
||||||
|
options?: { lean?: boolean; select?: string },
|
||||||
|
): Promise<ClaimCaseDocument[] | Record<string, unknown>[]> {
|
||||||
|
if (options?.lean) {
|
||||||
|
const query = options?.select
|
||||||
|
? this.claimCaseModel.find(filter).select(options.select).lean()
|
||||||
|
: this.claimCaseModel.find(filter).lean();
|
||||||
|
return query.exec() as Promise<Record<string, unknown>[]>;
|
||||||
|
}
|
||||||
|
const query = options?.select
|
||||||
|
? this.claimCaseModel.find(filter).select(options.select)
|
||||||
|
: this.claimCaseModel.find(filter);
|
||||||
|
return query.exec() as Promise<ClaimCaseDocument[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async countByFilter(filter: FilterQuery<ClaimCase>): Promise<number> {
|
||||||
|
return this.claimCaseModel.countDocuments(filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,6 @@ export class VideoCaptureDbService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async findById(id: string): Promise<VideoCaptureModel | null> {
|
async findById(id: string): Promise<VideoCaptureModel | null> {
|
||||||
return this.videoCapture.findById(id).lean();
|
return this.videoCapture.findById(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
|
import { CarDamagePartModel, CarDamagePartOtherModel } from "./car-parts.schema";
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ClaimDamageSelection {
|
||||||
|
/**
|
||||||
|
* V2: Array of selected damaged outer car part names
|
||||||
|
* Examples: ['hood', 'front_right_door', 'rear_bumper']
|
||||||
|
*/
|
||||||
|
@Prop({ type: [String], default: [] })
|
||||||
|
selectedParts?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2: Array of selected other (non-body) damaged parts
|
||||||
|
* Examples: ['engine', 'suspension', 'headlight']
|
||||||
|
*/
|
||||||
|
@Prop({ type: [String], default: [] })
|
||||||
|
otherParts?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy fields - kept for backward compatibility
|
||||||
|
*/
|
||||||
|
@Prop({ type: [CarDamagePartModel] })
|
||||||
|
legacyParts?: CarDamagePartModel[];
|
||||||
|
|
||||||
|
@Prop({ type: CarDamagePartOtherModel })
|
||||||
|
legacyOtherParts?: CarDamagePartOtherModel;
|
||||||
|
}
|
||||||
|
export const ClaimDamageSelectionSchema =
|
||||||
|
SchemaFactory.createForClass(ClaimDamageSelection);
|
||||||
|
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
|
import { Schema as MongooseSchema, Types } from "mongoose";
|
||||||
|
import { FactorStatus } from "src/Types&Enums/claim-request-management/factor-status.enum";
|
||||||
|
import { UserReplyEnum } from "src/Types&Enums/claim-request-management/userReply.enum";
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ClaimPartPricing {
|
||||||
|
@Prop({ type: String })
|
||||||
|
partId: string;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
carPartDamage?: string;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
typeOfDamage?: string;
|
||||||
|
|
||||||
|
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||||
|
price?: string | number;
|
||||||
|
|
||||||
|
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||||
|
salary?: string | number;
|
||||||
|
|
||||||
|
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||||
|
totalPayment?: string | number;
|
||||||
|
|
||||||
|
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||||
|
daghi?: string | number;
|
||||||
|
|
||||||
|
@Prop({ type: Boolean, default: false })
|
||||||
|
factorNeeded?: boolean;
|
||||||
|
|
||||||
|
@Prop({ type: Types.ObjectId })
|
||||||
|
factorLink?: Types.ObjectId;
|
||||||
|
|
||||||
|
@Prop({ type: String, enum: FactorStatus, default: null })
|
||||||
|
factorStatus?: FactorStatus | null;
|
||||||
|
|
||||||
|
@Prop({ type: String, default: null })
|
||||||
|
rejectionReason?: string | null;
|
||||||
|
}
|
||||||
|
export const ClaimPartPricingSchema =
|
||||||
|
SchemaFactory.createForClass(ClaimPartPricing);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ClaimExpertActor {
|
||||||
|
@Prop({ type: String })
|
||||||
|
actorName?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kept as string to support existing queries like:
|
||||||
|
* `"damageExpertReply.actorDetail.actorId": actor.sub`
|
||||||
|
*/
|
||||||
|
@Prop({ type: String })
|
||||||
|
actorId?: string;
|
||||||
|
}
|
||||||
|
export const ClaimExpertActorSchema =
|
||||||
|
SchemaFactory.createForClass(ClaimExpertActor);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ClaimUserComment {
|
||||||
|
@Prop({ type: Boolean, default: null })
|
||||||
|
isAccept?: boolean | null;
|
||||||
|
|
||||||
|
@Prop({ type: Types.ObjectId })
|
||||||
|
signDetailId?: Types.ObjectId;
|
||||||
|
}
|
||||||
|
export const ClaimUserCommentSchema =
|
||||||
|
SchemaFactory.createForClass(ClaimUserComment);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ClaimExpertReply {
|
||||||
|
@Prop({ type: String })
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@Prop({ type: ClaimExpertActorSchema })
|
||||||
|
actorDetail?: ClaimExpertActor;
|
||||||
|
|
||||||
|
@Prop({ type: [ClaimPartPricingSchema], default: [] })
|
||||||
|
parts?: ClaimPartPricing[];
|
||||||
|
|
||||||
|
@Prop({ type: Date, default: () => new Date() })
|
||||||
|
submittedAt?: Date;
|
||||||
|
|
||||||
|
@Prop({ type: ClaimUserCommentSchema })
|
||||||
|
userComment?: ClaimUserComment;
|
||||||
|
}
|
||||||
|
export const ClaimExpertReplySchema =
|
||||||
|
SchemaFactory.createForClass(ClaimExpertReply);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ClaimUserObjection {
|
||||||
|
@Prop({ type: String, required: true })
|
||||||
|
partId: string;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
reason?: string;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
partPrice?: string;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
partSalary?: string;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
typeOfDamage?: string;
|
||||||
|
}
|
||||||
|
export const ClaimUserObjectionSchema =
|
||||||
|
SchemaFactory.createForClass(ClaimUserObjection);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ClaimResendRequest {
|
||||||
|
@Prop({ type: String })
|
||||||
|
resendDescription?: string;
|
||||||
|
|
||||||
|
@Prop({ type: [MongooseSchema.Types.Mixed], default: [] })
|
||||||
|
resendDocuments?: any[];
|
||||||
|
|
||||||
|
@Prop({ type: [MongooseSchema.Types.Mixed], default: [] })
|
||||||
|
resendCarParts?: any[];
|
||||||
|
}
|
||||||
|
export const ClaimResendRequestSchema =
|
||||||
|
SchemaFactory.createForClass(ClaimResendRequest);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ClaimUserResendPayload {
|
||||||
|
@Prop({ type: [MongooseSchema.Types.Mixed], default: [] })
|
||||||
|
documents?: any[];
|
||||||
|
|
||||||
|
@Prop({ type: [MongooseSchema.Types.Mixed], default: [] })
|
||||||
|
carParts?: any[];
|
||||||
|
}
|
||||||
|
export const ClaimUserResendPayloadSchema =
|
||||||
|
SchemaFactory.createForClass(ClaimUserResendPayload);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ClaimFileRating {
|
||||||
|
@Prop({ type: Number, min: 0, max: 5 })
|
||||||
|
collisionMethodAccuracy?: number;
|
||||||
|
|
||||||
|
@Prop({ type: Number, min: 0, max: 5 })
|
||||||
|
evaluationTimeliness?: number;
|
||||||
|
|
||||||
|
@Prop({ type: Number, min: 0, max: 5 })
|
||||||
|
accidentCauseAccuracy?: number;
|
||||||
|
|
||||||
|
@Prop({ type: Number, min: 0, max: 5 })
|
||||||
|
guiltyVehicleIdentification?: number;
|
||||||
|
}
|
||||||
|
export const ClaimFileRatingSchema =
|
||||||
|
SchemaFactory.createForClass(ClaimFileRating);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ClaimPriceDrop {
|
||||||
|
@Prop({ type: Number })
|
||||||
|
total?: number;
|
||||||
|
|
||||||
|
@Prop({ type: Number })
|
||||||
|
carPrice?: number;
|
||||||
|
|
||||||
|
@Prop({ type: Number })
|
||||||
|
carModel?: number;
|
||||||
|
|
||||||
|
@Prop({ type: [Number], default: [] })
|
||||||
|
carValue?: number[];
|
||||||
|
|
||||||
|
@Prop({ type: Number })
|
||||||
|
sumOfSeverity?: number;
|
||||||
|
}
|
||||||
|
export const ClaimPriceDropSchema = SchemaFactory.createForClass(ClaimPriceDrop);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ClaimEvaluation {
|
||||||
|
@Prop({ type: String, enum: UserReplyEnum, default: null })
|
||||||
|
effectedUserReply?: UserReplyEnum | null;
|
||||||
|
|
||||||
|
@Prop({ type: ClaimExpertReplySchema })
|
||||||
|
damageExpertReply?: ClaimExpertReply | null;
|
||||||
|
|
||||||
|
@Prop({ type: ClaimExpertReplySchema })
|
||||||
|
damageExpertReplyFinal?: ClaimExpertReply | null;
|
||||||
|
|
||||||
|
@Prop({ type: ClaimResendRequestSchema })
|
||||||
|
damageExpertResend?: ClaimResendRequest | null;
|
||||||
|
|
||||||
|
@Prop({ type: ClaimUserObjectionSchema })
|
||||||
|
objection?: ClaimUserObjection;
|
||||||
|
|
||||||
|
@Prop({ type: ClaimUserResendPayloadSchema })
|
||||||
|
userResendDocuments?: ClaimUserResendPayload;
|
||||||
|
|
||||||
|
@Prop({ type: ClaimFileRatingSchema })
|
||||||
|
rating?: ClaimFileRating;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
visitLocation?: string;
|
||||||
|
|
||||||
|
@Prop({ type: ClaimPriceDropSchema })
|
||||||
|
priceDrop?: ClaimPriceDrop;
|
||||||
|
}
|
||||||
|
export const ClaimEvaluationSchema =
|
||||||
|
SchemaFactory.createForClass(ClaimEvaluation);
|
||||||
|
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
|
import { Types } from "mongoose";
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ClaimPlate {
|
||||||
|
@Prop({ type: Number })
|
||||||
|
leftDigits?: number;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
centerAlphabet?: string;
|
||||||
|
|
||||||
|
@Prop({ type: Number })
|
||||||
|
centerDigits?: number;
|
||||||
|
|
||||||
|
@Prop({ type: Number })
|
||||||
|
ir?: number;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
nationalCode?: string;
|
||||||
|
}
|
||||||
|
export const ClaimPlateSchema = SchemaFactory.createForClass(ClaimPlate);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ClaimVehicleSnapshot {
|
||||||
|
@Prop({ type: String })
|
||||||
|
carName?: string;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
carModel?: string;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
carType?: string;
|
||||||
|
|
||||||
|
@Prop({ type: Boolean })
|
||||||
|
isNewCar?: boolean;
|
||||||
|
|
||||||
|
@Prop({ type: ClaimPlateSchema })
|
||||||
|
plate?: ClaimPlate;
|
||||||
|
}
|
||||||
|
export const ClaimVehicleSnapshotSchema =
|
||||||
|
SchemaFactory.createForClass(ClaimVehicleSnapshot);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ClaimOwner {
|
||||||
|
@Prop({ type: Types.ObjectId })
|
||||||
|
userId?: Types.ObjectId;
|
||||||
|
|
||||||
|
@Prop({ type: Types.ObjectId })
|
||||||
|
userClientKey?: Types.ObjectId;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
fullName?: string;
|
||||||
|
}
|
||||||
|
export const ClaimOwnerSchema = SchemaFactory.createForClass(ClaimOwner);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ClaimMoney {
|
||||||
|
@Prop({ type: String, trim: true })
|
||||||
|
sheba?: string;
|
||||||
|
|
||||||
|
@Prop({ type: String, trim: true })
|
||||||
|
nationalCodeOfInsurer?: string;
|
||||||
|
}
|
||||||
|
export const ClaimMoneySchema = SchemaFactory.createForClass(ClaimMoney);
|
||||||
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
|
import { Types } from "mongoose";
|
||||||
|
import { AiImagesModel } from "./ai-image.schema";
|
||||||
|
import { CarGreenCardModel } from "./car-green-card.schema";
|
||||||
|
import { ImageRequiredModel } from "./image-required.schema";
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class CapturedImage {
|
||||||
|
@Prop({ type: String })
|
||||||
|
path?: string;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
fileName?: string;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
url?: string;
|
||||||
|
|
||||||
|
@Prop({ type: Date })
|
||||||
|
capturedAt?: Date;
|
||||||
|
}
|
||||||
|
export const CapturedImageSchema = SchemaFactory.createForClass(CapturedImage);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ClaimMedia {
|
||||||
|
@Prop({ type: CarGreenCardModel })
|
||||||
|
carGreenCard?: CarGreenCardModel;
|
||||||
|
|
||||||
|
@Prop({ type: ImageRequiredModel })
|
||||||
|
imageRequired?: ImageRequiredModel;
|
||||||
|
|
||||||
|
@Prop({ type: AiImagesModel })
|
||||||
|
aiImages?: AiImagesModel;
|
||||||
|
|
||||||
|
@Prop({ type: Types.ObjectId })
|
||||||
|
videoCaptureId?: Types.ObjectId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2: Car angles captures (front, back, left, right)
|
||||||
|
* Map of angle key to captured image
|
||||||
|
*/
|
||||||
|
@Prop({
|
||||||
|
type: Map,
|
||||||
|
of: CapturedImageSchema,
|
||||||
|
default: () => ({}),
|
||||||
|
})
|
||||||
|
carAngles?: Map<string, CapturedImage>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2: Damaged parts captures
|
||||||
|
* Map of part key to captured image
|
||||||
|
*/
|
||||||
|
@Prop({
|
||||||
|
type: Map,
|
||||||
|
of: CapturedImageSchema,
|
||||||
|
default: () => ({}),
|
||||||
|
})
|
||||||
|
damagedParts?: Map<string, CapturedImage>;
|
||||||
|
}
|
||||||
|
export const ClaimMediaSchema = SchemaFactory.createForClass(ClaimMedia);
|
||||||
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
|
import { AccidentInfo, AccidentInfoSchema } from "src/request-management/entities/schema/accidentInformation.type";
|
||||||
|
import { Party, PartySchema } from "src/request-management/entities/schema/partyRole.enum";
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ClaimCaseSnapshot {
|
||||||
|
@Prop({ type: AccidentInfoSchema })
|
||||||
|
accident?: AccidentInfo;
|
||||||
|
|
||||||
|
@Prop({ type: [PartySchema], default: [] })
|
||||||
|
parties?: Party[];
|
||||||
|
}
|
||||||
|
export const ClaimCaseSnapshotSchema =
|
||||||
|
SchemaFactory.createForClass(ClaimCaseSnapshot);
|
||||||
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
|
import { Types } from "mongoose";
|
||||||
|
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ClaimActorLock {
|
||||||
|
@Prop({ type: Types.ObjectId })
|
||||||
|
actorId: Types.ObjectId;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
actorName: string;
|
||||||
|
|
||||||
|
@Prop({ type: String, default: "damage_expert" })
|
||||||
|
actorRole: "damage_expert" | "expert" | "admin";
|
||||||
|
}
|
||||||
|
export const ClaimActorLockSchema = SchemaFactory.createForClass(ClaimActorLock);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ClaimWorkflow {
|
||||||
|
@Prop({ type: String, enum: ClaimWorkflowStep })
|
||||||
|
currentStep: ClaimWorkflowStep;
|
||||||
|
|
||||||
|
@Prop({ type: String, enum: ClaimWorkflowStep })
|
||||||
|
nextStep: ClaimWorkflowStep;
|
||||||
|
|
||||||
|
@Prop({ type: [String], enum: ClaimWorkflowStep, default: [] })
|
||||||
|
completedSteps?: ClaimWorkflowStep[];
|
||||||
|
|
||||||
|
@Prop({ type: Boolean, default: false })
|
||||||
|
locked?: boolean;
|
||||||
|
|
||||||
|
@Prop({ type: Date })
|
||||||
|
lockedAt?: Date;
|
||||||
|
|
||||||
|
@Prop({ type: ClaimActorLockSchema })
|
||||||
|
lockedBy?: ClaimActorLock;
|
||||||
|
}
|
||||||
|
export const ClaimWorkflowSchema = SchemaFactory.createForClass(ClaimWorkflow);
|
||||||
|
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
|
import { HydratedDocument, Schema as MongooseSchema, Types } from "mongoose";
|
||||||
|
import { ClaimRequiredDocumentType } from "src/Types&Enums/claim-request-management/required-document-type.enum";
|
||||||
|
import { ClaimStatus } from "src/Types&Enums/claim-request-management/claimStatus.enum";
|
||||||
|
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
|
||||||
|
import { HistoryEvent, HistoryEventSchema } from "src/request-management/entities/schema/historyEvent.type";
|
||||||
|
import {
|
||||||
|
ClaimDamageSelection,
|
||||||
|
ClaimDamageSelectionSchema,
|
||||||
|
} from "./claim-case.damage.schema";
|
||||||
|
import {
|
||||||
|
ClaimMoney,
|
||||||
|
ClaimMoneySchema,
|
||||||
|
ClaimOwner,
|
||||||
|
ClaimOwnerSchema,
|
||||||
|
ClaimVehicleSnapshot,
|
||||||
|
ClaimVehicleSnapshotSchema,
|
||||||
|
} from "./claim-case.identity.schema";
|
||||||
|
import { ClaimMedia, ClaimMediaSchema } from "./claim-case.media.schema";
|
||||||
|
import {
|
||||||
|
ClaimEvaluation,
|
||||||
|
ClaimEvaluationSchema,
|
||||||
|
} from "./claim-case.evaluation.schema";
|
||||||
|
import {
|
||||||
|
ClaimCaseSnapshot,
|
||||||
|
ClaimCaseSnapshotSchema,
|
||||||
|
} from "./claim-case.snapshot.schema";
|
||||||
|
import { ClaimWorkflow, ClaimWorkflowSchema } from "./claim-case.workflow.schema";
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class RequiredDocumentRef {
|
||||||
|
@Prop({ type: Types.ObjectId })
|
||||||
|
fileId?: Types.ObjectId;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
filePath?: string;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
fileName?: string;
|
||||||
|
|
||||||
|
@Prop({ type: Boolean, default: true })
|
||||||
|
uploaded?: boolean;
|
||||||
|
|
||||||
|
@Prop({ type: Date })
|
||||||
|
uploadedAt?: Date;
|
||||||
|
}
|
||||||
|
export const RequiredDocumentRefSchema = SchemaFactory.createForClass(RequiredDocumentRef);
|
||||||
|
|
||||||
|
@Schema({
|
||||||
|
collection: "claimCases",
|
||||||
|
timestamps: true,
|
||||||
|
id: true,
|
||||||
|
versionKey: false,
|
||||||
|
})
|
||||||
|
export class ClaimCase {
|
||||||
|
@Prop({ required: true, unique: true, index: true, trim: true })
|
||||||
|
requestNo: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Human-friendly shared id across blame+claim (e.g. A14235).
|
||||||
|
* Generated once on blame creation and copied into claim.
|
||||||
|
*/
|
||||||
|
@Prop({ required: true, unique: true, index: true, trim: true })
|
||||||
|
publicId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overall case status (user flow + expert flow progression)
|
||||||
|
*/
|
||||||
|
@Prop({ required: true, type: String, enum: ClaimCaseStatus, default: ClaimCaseStatus.CREATED })
|
||||||
|
status: ClaimCaseStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claim damage determination status (expert assessment)
|
||||||
|
*/
|
||||||
|
@Prop({ type: String, enum: ClaimStatus, default: ClaimStatus.PENDING })
|
||||||
|
claimStatus: ClaimStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Link to the blame case that originated this claim.
|
||||||
|
* IMPORTANT: this is intentionally ONLY an id reference (no embedded blame file).
|
||||||
|
*/
|
||||||
|
@Prop({ type: Types.ObjectId, index: true })
|
||||||
|
blameRequestId?: Types.ObjectId;
|
||||||
|
|
||||||
|
@Prop({ type: String, index: true })
|
||||||
|
blameRequestNo?: string;
|
||||||
|
|
||||||
|
@Prop({ type: ClaimWorkflowSchema, default: () => ({}) })
|
||||||
|
workflow?: ClaimWorkflow;
|
||||||
|
|
||||||
|
@Prop({ type: ClaimOwnerSchema, default: () => ({}) })
|
||||||
|
owner?: ClaimOwner;
|
||||||
|
|
||||||
|
@Prop({ type: ClaimVehicleSnapshotSchema, default: () => ({}) })
|
||||||
|
vehicle?: ClaimVehicleSnapshot;
|
||||||
|
|
||||||
|
@Prop({ type: ClaimMoneySchema })
|
||||||
|
money?: ClaimMoney;
|
||||||
|
|
||||||
|
@Prop({ type: Number })
|
||||||
|
claimNo?: number;
|
||||||
|
|
||||||
|
@Prop({ type: Number })
|
||||||
|
claimId?: number;
|
||||||
|
|
||||||
|
@Prop({ type: ClaimDamageSelectionSchema, default: () => ({}) })
|
||||||
|
damage?: ClaimDamageSelection;
|
||||||
|
|
||||||
|
@Prop({ type: ClaimMediaSchema, default: () => ({}) })
|
||||||
|
media?: ClaimMedia;
|
||||||
|
|
||||||
|
@Prop({ type: ClaimEvaluationSchema, default: () => ({}) })
|
||||||
|
evaluation?: ClaimEvaluation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional “read-optimized” copy of fields from blame/request side.
|
||||||
|
* Source of truth remains `blameRequestId`.
|
||||||
|
*/
|
||||||
|
@Prop({ type: ClaimCaseSnapshotSchema })
|
||||||
|
snapshot?: ClaimCaseSnapshot;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For quick “has user uploaded X?” checks. Values point to docs in the
|
||||||
|
* `claim-required-documents` collection.
|
||||||
|
*/
|
||||||
|
@Prop({
|
||||||
|
type: Map,
|
||||||
|
of: RequiredDocumentRefSchema,
|
||||||
|
default: () => ({}),
|
||||||
|
})
|
||||||
|
requiredDocuments?: Map<string, RequiredDocumentRef>;
|
||||||
|
|
||||||
|
@Prop({ type: [HistoryEventSchema], default: [] })
|
||||||
|
history?: HistoryEvent[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy fields kept optional to simplify progressive migration.
|
||||||
|
* If you choose to migrate later, we can remove these.
|
||||||
|
*/
|
||||||
|
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||||
|
legacy?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClaimCaseDocument = HydratedDocument<ClaimCase>;
|
||||||
|
export const ClaimCaseSchema = SchemaFactory.createForClass(ClaimCase);
|
||||||
|
|
||||||
|
|
||||||
@@ -241,6 +241,22 @@ export class ClaimRequestManagementModel {
|
|||||||
@Prop({ type: RequestManagementModel, unique: true })
|
@Prop({ type: RequestManagementModel, unique: true })
|
||||||
blameFile: RequestManagementModel;
|
blameFile: RequestManagementModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Human-friendly shared id across blame+claim (e.g. A14235).
|
||||||
|
* Copied from the source blame request.
|
||||||
|
*
|
||||||
|
* NOTE: `sparse` keeps existing documents without this field valid.
|
||||||
|
*/
|
||||||
|
@Prop({ type: String, unique: true, index: true, sparse: true, trim: true })
|
||||||
|
publicId?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Explicit reference to the blame request id (in addition to embedded blameFile).
|
||||||
|
* This will be the preferred reference going forward.
|
||||||
|
*/
|
||||||
|
@Prop({ type: Types.ObjectId, index: true })
|
||||||
|
blameRequestId?: Types.ObjectId;
|
||||||
|
|
||||||
@Prop({ type: Number, default: 100 })
|
@Prop({ type: Number, default: 100 })
|
||||||
requestNumber?: number;
|
requestNumber?: number;
|
||||||
|
|
||||||
|
|||||||
@@ -101,3 +101,27 @@ export class AllRequestDtoRs {
|
|||||||
this.data = requests.map((r) => new AllRequestDto(r));
|
this.data = requests.map((r) => new AllRequestDto(r));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** V2: list item from blameCases (BlameRequest) for expert panel */
|
||||||
|
export class AllRequestDtoV2 {
|
||||||
|
requestId: string;
|
||||||
|
status: string;
|
||||||
|
userComment: null;
|
||||||
|
requestCode: string;
|
||||||
|
date: string;
|
||||||
|
time: string;
|
||||||
|
updatedAtDate: string;
|
||||||
|
updatedAtTime: string;
|
||||||
|
lockFile: boolean;
|
||||||
|
lockTime: string | null;
|
||||||
|
type: string;
|
||||||
|
blameStatus: string;
|
||||||
|
partiesInitialForms: { firstParty: string; secondParty: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AllRequestDtoRsV2 {
|
||||||
|
data: AllRequestDtoV2[];
|
||||||
|
constructor(items: AllRequestDtoV2[]) {
|
||||||
|
this.data = items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,42 +3,81 @@ import { Types } from "mongoose";
|
|||||||
import { BlameDocumentType } from "src/request-management/entities/schema/blame-document.schema";
|
import { BlameDocumentType } from "src/request-management/entities/schema/blame-document.schema";
|
||||||
|
|
||||||
export class AccidentWayIF {
|
export class AccidentWayIF {
|
||||||
@ApiProperty({ required: true })
|
@ApiProperty({ required: true, example: "9" })
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
@ApiProperty({ required: true })
|
@ApiProperty({ required: true, example: "جلو به پهلو" })
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
export class AccidentReasonIF {
|
export class AccidentReasonIF {
|
||||||
@ApiProperty({ required: true })
|
@ApiProperty({ required: true, example: "23" })
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
@ApiProperty({ required: true })
|
@ApiProperty({ required: true, example: "عدم رعايت فاصله جانبی" })
|
||||||
label: string;
|
label: string;
|
||||||
|
|
||||||
@ApiProperty({ required: true })
|
@ApiProperty({ required: true, example: 3 })
|
||||||
fanavaran: number;
|
fanavaran: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FieldsInterface {
|
export class FieldsInterface {
|
||||||
@ApiProperty({ required: true })
|
@ApiProperty({
|
||||||
|
required: true,
|
||||||
|
example: { id: "9", label: "جلو به پهلو" },
|
||||||
|
})
|
||||||
accidentWay: AccidentWayIF;
|
accidentWay: AccidentWayIF;
|
||||||
|
|
||||||
@ApiProperty({ required: true })
|
@ApiProperty({
|
||||||
|
required: true,
|
||||||
|
example: {
|
||||||
|
id: "23",
|
||||||
|
label: "عدم رعايت فاصله جانبی",
|
||||||
|
fanavaran: 3,
|
||||||
|
},
|
||||||
|
})
|
||||||
accidentReason: AccidentReasonIF;
|
accidentReason: AccidentReasonIF;
|
||||||
|
|
||||||
@ApiProperty({ required: true })
|
@ApiProperty({
|
||||||
|
required: true,
|
||||||
|
example: {
|
||||||
|
id: "3",
|
||||||
|
label: "برخورد یک وسیله نقلیه با وسیله نقلیه پارک شده",
|
||||||
|
},
|
||||||
|
})
|
||||||
accidentType: AccidentWayIF;
|
accidentType: AccidentWayIF;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SubmitReplyDto {
|
export class SubmitReplyDto {
|
||||||
@ApiProperty({ required: true })
|
@ApiProperty({
|
||||||
|
required: true,
|
||||||
|
example: "Expert's decision explanation",
|
||||||
|
description: "Detailed explanation of the expert's decision",
|
||||||
|
})
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
@ApiProperty({ required: true, type: Types.ObjectId })
|
@ApiProperty({
|
||||||
|
required: true,
|
||||||
|
type: String,
|
||||||
|
example: "507f1f77bcf86cd799439011",
|
||||||
|
description: "ObjectId of the guilty party",
|
||||||
|
})
|
||||||
guiltyUserId: Types.ObjectId;
|
guiltyUserId: Types.ObjectId;
|
||||||
|
|
||||||
@ApiProperty({ required: true })
|
@ApiProperty({
|
||||||
|
required: true,
|
||||||
|
example: {
|
||||||
|
accidentWay: { id: "9", label: "جلو به پهلو" },
|
||||||
|
accidentReason: {
|
||||||
|
id: "23",
|
||||||
|
label: "عدم رعايت فاصله جانبی",
|
||||||
|
fanavaran: 3,
|
||||||
|
},
|
||||||
|
accidentType: {
|
||||||
|
id: "3",
|
||||||
|
label: "برخورد یک وسیله نقلیه با وسیله نقلیه پارک شده",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
fields: FieldsInterface;
|
fields: FieldsInterface;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
45
src/expert-blame/dto/resend.dto.ts
Normal file
45
src/expert-blame/dto/resend.dto.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { Types } from "mongoose";
|
||||||
|
import { ResendItemType } from "src/Types&Enums/blame-request-management/resendItemType.enum";
|
||||||
|
|
||||||
|
export class PartyResendRequestDto {
|
||||||
|
@ApiProperty({
|
||||||
|
required: true,
|
||||||
|
type: String,
|
||||||
|
example: "507f1f77bcf86cd799439011",
|
||||||
|
description: "userId (person.userId) of the party member to request documents from",
|
||||||
|
})
|
||||||
|
partyId: Types.ObjectId;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
required: true,
|
||||||
|
isArray: true,
|
||||||
|
enum: ResendItemType,
|
||||||
|
example: [ResendItemType.DRIVING_LICENSE, ResendItemType.CAR_CERTIFICATE],
|
||||||
|
description: "Array of items to request from this party",
|
||||||
|
})
|
||||||
|
requestedItems: ResendItemType[];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
required: false,
|
||||||
|
example: "Please resend your driving license and car certificate with better quality",
|
||||||
|
description: "Optional explanation for why these items are needed",
|
||||||
|
})
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ResendRequestDto {
|
||||||
|
@ApiProperty({
|
||||||
|
required: true,
|
||||||
|
type: [PartyResendRequestDto],
|
||||||
|
description: "Array of resend requests (can include first party, second party, or both)",
|
||||||
|
example: [
|
||||||
|
{
|
||||||
|
partyId: "507f1f77bcf86cd799439011",
|
||||||
|
requestedItems: ["drivingLicense", "carCertificate"],
|
||||||
|
description: "Please resend with better quality",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
parties: PartyResendRequestDto[];
|
||||||
|
}
|
||||||
@@ -3,12 +3,13 @@ import { ClientModule } from "src/client/client.module";
|
|||||||
import { PlatesModule } from "src/plates/plates.module";
|
import { PlatesModule } from "src/plates/plates.module";
|
||||||
import { UsersModule } from "src/users/users.module";
|
import { UsersModule } from "src/users/users.module";
|
||||||
import { ExpertBlameController } from "./expert-blame.controller";
|
import { ExpertBlameController } from "./expert-blame.controller";
|
||||||
|
import { ExpertBlameV2Controller } from "./expert-blame.v2.controller";
|
||||||
import { ExpertBlameService } from "./expert-blame.service";
|
import { ExpertBlameService } from "./expert-blame.service";
|
||||||
import { RequestManagementModule } from "src/request-management/request-management.module";
|
import { RequestManagementModule } from "src/request-management/request-management.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [UsersModule, ClientModule, RequestManagementModule, PlatesModule],
|
imports: [UsersModule, ClientModule, RequestManagementModule, PlatesModule],
|
||||||
controllers: [ExpertBlameController],
|
controllers: [ExpertBlameController, ExpertBlameV2Controller],
|
||||||
providers: [ExpertBlameService],
|
providers: [ExpertBlameService],
|
||||||
exports: [ExpertBlameService],
|
exports: [ExpertBlameService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import {
|
import {
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
ForbiddenException,
|
ForbiddenException,
|
||||||
|
HttpException,
|
||||||
Injectable,
|
Injectable,
|
||||||
|
InternalServerErrorException,
|
||||||
Logger,
|
Logger,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { RequestManagementDbService } from "src/request-management/entities/db-service/request-management.db.service";
|
import { RequestManagementDbService } from "src/request-management/entities/db-service/request-management.db.service";
|
||||||
import { AllRequestDtoRs } from "./dto/all-request.dto";
|
import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service";
|
||||||
|
import {
|
||||||
|
AllRequestDtoRs,
|
||||||
|
AllRequestDtoV2,
|
||||||
|
AllRequestDtoRsV2,
|
||||||
|
} from "./dto/all-request.dto";
|
||||||
import { UserType } from "src/Types&Enums/userType.enum";
|
import { UserType } from "src/Types&Enums/userType.enum";
|
||||||
import { SubmitReplyDto } from "./dto/reply.dto";
|
import { SubmitReplyDto } from "./dto/reply.dto";
|
||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
@@ -14,7 +21,13 @@ import { BlameVideoDbService } from "src/request-management/entities/db-service/
|
|||||||
import { BlameVoiceDbService } from "src/request-management/entities/db-service/blame.voice.db.service";
|
import { BlameVoiceDbService } from "src/request-management/entities/db-service/blame.voice.db.service";
|
||||||
import { ClientDbService } from "src/client/entities/db-service/client.db.service";
|
import { ClientDbService } from "src/client/entities/db-service/client.db.service";
|
||||||
import { ReqBlameStatus } from "src/Types&Enums/blame-request-management/status.enum";
|
import { ReqBlameStatus } from "src/Types&Enums/blame-request-management/status.enum";
|
||||||
|
import { BlameStatus } from "src/Types&Enums/blame-request-management/blameStatus.enum";
|
||||||
|
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||||
|
import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
|
||||||
|
import { ResendItemType } from "src/Types&Enums/blame-request-management/resendItemType.enum";
|
||||||
import { buildFileLink } from "src/helpers/urlCreator";
|
import { buildFileLink } from "src/helpers/urlCreator";
|
||||||
|
import { toJalaliDateAndTime } from "src/helpers/date-jalali";
|
||||||
|
import { ResendRequestDto } from "./dto/resend.dto";
|
||||||
import { readFile } from "fs/promises";
|
import { readFile } from "fs/promises";
|
||||||
import { ExpertDbService } from "src/users/entities/db-service/expert.db.service";
|
import { ExpertDbService } from "src/users/entities/db-service/expert.db.service";
|
||||||
import { BlameDocumentDbService } from "src/request-management/entities/db-service/blame-document.db.service";
|
import { BlameDocumentDbService } from "src/request-management/entities/db-service/blame-document.db.service";
|
||||||
@@ -28,11 +41,26 @@ interface CheckedRequestEntry {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function statementToFormKey(
|
||||||
|
statement?: {
|
||||||
|
admitsGuilt?: boolean;
|
||||||
|
claimsDamage?: boolean;
|
||||||
|
acceptsExpertOpinion?: boolean;
|
||||||
|
},
|
||||||
|
): string | null {
|
||||||
|
if (!statement) return null;
|
||||||
|
if (statement.admitsGuilt) return "imGuilty";
|
||||||
|
if (statement.claimsDamage) return "imDamaged";
|
||||||
|
if (statement.acceptsExpertOpinion) return "expertOpinion";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExpertBlameService {
|
export class ExpertBlameService {
|
||||||
private readonly logger = new Logger(ExpertBlameService.name);
|
private readonly logger = new Logger(ExpertBlameService.name);
|
||||||
constructor(
|
constructor(
|
||||||
private readonly requestManagementDbService: RequestManagementDbService,
|
private readonly requestManagementDbService: RequestManagementDbService,
|
||||||
|
private readonly blameRequestDbService: BlameRequestDbService,
|
||||||
private readonly clientDbService: ClientDbService,
|
private readonly clientDbService: ClientDbService,
|
||||||
private readonly blameVideoDbService: BlameVideoDbService,
|
private readonly blameVideoDbService: BlameVideoDbService,
|
||||||
private readonly blameVoiceDbService: BlameVoiceDbService,
|
private readonly blameVoiceDbService: BlameVoiceDbService,
|
||||||
@@ -148,6 +176,108 @@ export class ExpertBlameService {
|
|||||||
return new AllRequestDtoRs(visibleRequests);
|
return new AllRequestDtoRs(visibleRequests);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2: List blame cases for current expert.
|
||||||
|
* Shows:
|
||||||
|
* 1. Fresh requests (WAITING_FOR_EXPERT with no decision)
|
||||||
|
* 2. Requests where current expert made the decision
|
||||||
|
* Does NOT show requests decided by other experts.
|
||||||
|
*/
|
||||||
|
async findAllV2(actor: any): Promise<AllRequestDtoRsV2> {
|
||||||
|
try {
|
||||||
|
const expertId = actor.sub;
|
||||||
|
|
||||||
|
// Fetch all DISAGREEMENT cases
|
||||||
|
const allCases = await this.blameRequestDbService.find(
|
||||||
|
{
|
||||||
|
blameStatus: BlameStatus.DISAGREEMENT,
|
||||||
|
},
|
||||||
|
{ lean: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filter to show only:
|
||||||
|
// 1. Fresh requests (WAITING_FOR_EXPERT and no decision)
|
||||||
|
// 2. Requests decided by current expert
|
||||||
|
const visibleCases = (allCases as Record<string, unknown>[]).filter((doc) => {
|
||||||
|
const status = doc.status as string;
|
||||||
|
const decision = doc.expert as any;
|
||||||
|
const decidedByExpertId = decision?.decision?.decidedByExpertId;
|
||||||
|
const hasDecision = !!decision?.decision;
|
||||||
|
|
||||||
|
// Fresh request (no decision yet)
|
||||||
|
if (status === CaseStatus.WAITING_FOR_EXPERT && !hasDecision) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request decided by current expert
|
||||||
|
if (decidedByExpertId && String(decidedByExpertId) === expertId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Locked by current expert but no decision yet
|
||||||
|
const lockedBy = decision?.resend?.requestedByExpertId || (doc.workflow as any)?.lockedBy?.actorId;
|
||||||
|
if (status === CaseStatus.WAITING_FOR_EXPERT && lockedBy && String(lockedBy) === expertId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const items: AllRequestDtoV2[] = visibleCases.map(
|
||||||
|
(doc) => this.mapBlameRequestToListItemV2(doc),
|
||||||
|
);
|
||||||
|
return new AllRequestDtoRsV2(items);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
this.logger.error("findAllV2 failed", error instanceof Error ? error.stack : String(error));
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
error instanceof Error ? error.message : "Failed to list blame cases",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapBlameRequestToListItemV2(
|
||||||
|
doc: Record<string, unknown>,
|
||||||
|
): AllRequestDtoV2 {
|
||||||
|
const createdAt = doc.createdAt ? new Date(doc.createdAt as string) : new Date();
|
||||||
|
const updatedAt = doc.updatedAt ? new Date(doc.updatedAt as string) : new Date();
|
||||||
|
const [date, time] = toJalaliDateAndTime(createdAt);
|
||||||
|
const [updatedAtDate, updatedAtTime] = toJalaliDateAndTime(updatedAt);
|
||||||
|
const workflow = (doc.workflow ?? {}) as Record<string, unknown>;
|
||||||
|
const parties = (doc.parties ?? []) as Array<{
|
||||||
|
role?: string;
|
||||||
|
statement?: {
|
||||||
|
admitsGuilt?: boolean;
|
||||||
|
claimsDamage?: boolean;
|
||||||
|
acceptsExpertOpinion?: boolean;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const firstParty = parties.find((p) => p.role === "FIRST");
|
||||||
|
const secondParty = parties.find((p) => p.role === "SECOND");
|
||||||
|
|
||||||
|
return {
|
||||||
|
requestId: String(doc._id),
|
||||||
|
status: String(doc.status ?? ""),
|
||||||
|
userComment: null,
|
||||||
|
requestCode: String(doc.publicId ?? ""),
|
||||||
|
date,
|
||||||
|
time,
|
||||||
|
updatedAtDate,
|
||||||
|
updatedAtTime,
|
||||||
|
lockFile: Boolean(workflow.locked),
|
||||||
|
lockTime: workflow.lockedAt
|
||||||
|
? new Date(workflow.lockedAt as string).toISOString()
|
||||||
|
: null,
|
||||||
|
type: String(doc.type ?? "THIRD_PARTY"),
|
||||||
|
blameStatus: String(doc.blameStatus ?? ""),
|
||||||
|
partiesInitialForms: {
|
||||||
|
firstParty: statementToFormKey(firstParty?.statement) ?? "",
|
||||||
|
secondParty: statementToFormKey(secondParty?.statement) ?? "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public unlockApi(request, timer) {
|
public unlockApi(request, timer) {
|
||||||
return setTimeout(async () => {
|
return setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -374,6 +504,92 @@ export class ExpertBlameService {
|
|||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2: Get blame case details by id from blameCases collection.
|
||||||
|
* Excludes history. Returns only non–CAR_BODY types. Builds file links for all evidence.
|
||||||
|
* Access control: Only allows viewing fresh requests or requests decided by current expert.
|
||||||
|
*/
|
||||||
|
async findOneV2(requestId: string, actorId: string): Promise<Record<string, unknown>> {
|
||||||
|
try {
|
||||||
|
const doc = await this.blameRequestDbService.findByIdWithoutHistory(requestId);
|
||||||
|
if (!doc) {
|
||||||
|
throw new NotFoundException("Request not found");
|
||||||
|
}
|
||||||
|
const type = doc.type as string;
|
||||||
|
if (type === BlameRequestType.CAR_BODY) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"CAR_BODY type requests are automatically handled and do not require expert review.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access control: Check if expert has permission to view this request
|
||||||
|
const decision = (doc.expert as any)?.decision;
|
||||||
|
const decidedByExpertId = decision?.decidedByExpertId;
|
||||||
|
|
||||||
|
// Allow if:
|
||||||
|
// 1. No decision yet (fresh request)
|
||||||
|
// 2. Decision made by current expert
|
||||||
|
if (decidedByExpertId && String(decidedByExpertId) !== actorId) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"You do not have permission to view this request. It has been handled by another expert.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parties = (doc.parties ?? []) as Array<{
|
||||||
|
evidence?: { videoId?: string; voices?: string[] };
|
||||||
|
[key: string]: unknown;
|
||||||
|
}>;
|
||||||
|
for (const party of parties) {
|
||||||
|
if (!party.evidence) continue;
|
||||||
|
const evidence = party.evidence as Record<string, unknown>;
|
||||||
|
if (evidence.videoId) {
|
||||||
|
const videoDoc = await this.blameVideoDbService.findById(
|
||||||
|
String(evidence.videoId),
|
||||||
|
);
|
||||||
|
if (videoDoc?.path) {
|
||||||
|
evidence.videoUrl = buildFileLink(videoDoc.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (evidence.voices && Array.isArray(evidence.voices)) {
|
||||||
|
const voiceUrls: string[] = [];
|
||||||
|
for (const voiceId of evidence.voices) {
|
||||||
|
const voiceDoc = await this.blameVoiceDbService.findById(
|
||||||
|
String(voiceId),
|
||||||
|
);
|
||||||
|
if (voiceDoc?.path) {
|
||||||
|
voiceUrls.push(buildFileLink(voiceDoc.path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
evidence.voiceUrls = voiceUrls;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createdAt = doc.createdAt ? new Date(doc.createdAt as string | Date) : new Date();
|
||||||
|
const updatedAt = doc.updatedAt ? new Date(doc.updatedAt as string | Date) : new Date();
|
||||||
|
const [createdDate, createdTime] = toJalaliDateAndTime(createdAt);
|
||||||
|
const [updatedDate, updatedTime] = toJalaliDateAndTime(updatedAt);
|
||||||
|
doc.createdAtFormatted = `${createdDate} ${createdTime}`;
|
||||||
|
doc.updatedAtFormatted = `${updatedDate} ${updatedTime}`;
|
||||||
|
|
||||||
|
// Exclude mapped inquiry vehicle for both parties from response
|
||||||
|
for (const party of parties) {
|
||||||
|
delete party.vehicle;
|
||||||
|
}
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
this.logger.error(
|
||||||
|
"findOneV2 failed",
|
||||||
|
requestId,
|
||||||
|
error instanceof Error ? error.stack : String(error),
|
||||||
|
);
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
error instanceof Error ? error.message : "Failed to get blame case details",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async lockRequest(requestId: string, actorDetail) {
|
async lockRequest(requestId: string, actorDetail) {
|
||||||
const fifteenMinutes = new Date(Date.now() + 15 * 60 * 1000);
|
const fifteenMinutes = new Date(Date.now() + 15 * 60 * 1000);
|
||||||
|
|
||||||
@@ -418,6 +634,340 @@ export class ExpertBlameService {
|
|||||||
return { _id: requestId, lock: true };
|
return { _id: requestId, lock: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2: Lock blame case for 15 minutes (blameCases collection).
|
||||||
|
* Sets workflow.locked, workflow.lockedBy, workflow.lockedAt.
|
||||||
|
* Checks if existing lock is expired and allows re-locking.
|
||||||
|
*/
|
||||||
|
async lockRequestV2(requestId: string, actorDetail: any): Promise<{ _id: string; lock: boolean }> {
|
||||||
|
try {
|
||||||
|
const now = new Date();
|
||||||
|
const fifteenMinutesAgo = new Date(now.getTime() - 15 * 60 * 1000);
|
||||||
|
|
||||||
|
// First, check the current state
|
||||||
|
const request = await this.blameRequestDbService.findById(requestId);
|
||||||
|
if (!request) {
|
||||||
|
throw new NotFoundException("Request not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate request is available for expert review
|
||||||
|
if (
|
||||||
|
request.status !== CaseStatus.WAITING_FOR_EXPERT ||
|
||||||
|
request.blameStatus !== BlameStatus.DISAGREEMENT
|
||||||
|
) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Request is not available for expert review",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if locked and not expired
|
||||||
|
if (request.workflow?.locked) {
|
||||||
|
const lockedAt = request.workflow.lockedAt;
|
||||||
|
if (lockedAt) {
|
||||||
|
const lockExpiryTime = new Date(lockedAt).getTime() + 15 * 60 * 1000;
|
||||||
|
if (Date.now() < lockExpiryTime) {
|
||||||
|
// Lock is still valid
|
||||||
|
const lockedByActorId = String(request.workflow.lockedBy?.actorId);
|
||||||
|
if (lockedByActorId === actorDetail.sub) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"You have already locked this request",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Request is currently locked by another expert",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Lock expired, allow re-locking (continue below)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lock the request (either unlocked or expired lock)
|
||||||
|
const updateResult = await this.blameRequestDbService.findByIdAndUpdate(
|
||||||
|
requestId,
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
"workflow.locked": true,
|
||||||
|
"workflow.lockedAt": now,
|
||||||
|
"workflow.lockedBy": {
|
||||||
|
actorId: new Types.ObjectId(actorDetail.sub),
|
||||||
|
actorName: actorDetail.fullName || "Unknown Expert",
|
||||||
|
actorRole: "expert",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!updateResult) {
|
||||||
|
throw new InternalServerErrorException("Failed to lock the request");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update expert stats (reusing existing helper)
|
||||||
|
await this.updateDamageExpertStats(actorDetail.sub, requestId, "checked");
|
||||||
|
|
||||||
|
return { _id: requestId, lock: true };
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
this.logger.error(
|
||||||
|
"lockRequestV2 failed",
|
||||||
|
requestId,
|
||||||
|
error instanceof Error ? error.stack : String(error),
|
||||||
|
);
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
error instanceof Error ? error.message : "Failed to lock blame case",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2: Request parties to resend documents/evidence (blameCases collection).
|
||||||
|
* Expert can request one or both parties to provide additional or better quality evidence.
|
||||||
|
*/
|
||||||
|
async resendRequestV2(
|
||||||
|
requestId: string,
|
||||||
|
resendDto: ResendRequestDto,
|
||||||
|
actorId: string,
|
||||||
|
): Promise<{ requestId: string; status: string }> {
|
||||||
|
try {
|
||||||
|
const request = await this.blameRequestDbService.findById(requestId);
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
throw new NotFoundException("Request not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate request is locked by current expert
|
||||||
|
if (!request.workflow?.locked) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"You must lock the request before requesting document resend.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lockedByActorId = String(request.workflow?.lockedBy?.actorId || "");
|
||||||
|
if (lockedByActorId !== actorId) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"Access denied. You are not the locked expert for this request.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate lock hasn't expired
|
||||||
|
const lockedAt = request.workflow?.lockedAt;
|
||||||
|
if (lockedAt) {
|
||||||
|
const lockExpiryTime = new Date(lockedAt).getTime() + 15 * 60 * 1000;
|
||||||
|
if (Date.now() > lockExpiryTime) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"Your lock time has expired. Please lock the request again.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate at least one party is specified
|
||||||
|
if (!resendDto.parties || resendDto.parties.length === 0) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"At least one party must be specified for resend request",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate no existing expert decision
|
||||||
|
if (request.expert?.decision) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"Cannot request resend after expert decision has been made.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// Build resend requests for parties
|
||||||
|
const partyResendRequests = resendDto.parties.map((party) => ({
|
||||||
|
partyId: new Types.ObjectId(String(party.partyId)),
|
||||||
|
requestedItems: party.requestedItems,
|
||||||
|
description: party.description || "",
|
||||||
|
requestedAt: now,
|
||||||
|
completed: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const updatePayload = {
|
||||||
|
$set: {
|
||||||
|
"workflow.locked": false,
|
||||||
|
"workflow.currentStep": "WAITING_FOR_DOCUMENT_RESEND",
|
||||||
|
"workflow.nextStep": "WAITING_FOR_GUILT_DECISION",
|
||||||
|
"expert.resend": {
|
||||||
|
parties: partyResendRequests,
|
||||||
|
requestedAt: now,
|
||||||
|
requestedByExpertId: new Types.ObjectId(actorId),
|
||||||
|
},
|
||||||
|
status: CaseStatus.WAITING_FOR_DOCUMENT_RESEND,
|
||||||
|
},
|
||||||
|
$push: {
|
||||||
|
"workflow.completedSteps": "WAITING_FOR_GUILT_DECISION",
|
||||||
|
},
|
||||||
|
$unset: {
|
||||||
|
"workflow.lockedAt": "",
|
||||||
|
"workflow.lockedBy": "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateResult = await this.blameRequestDbService.findByIdAndUpdate(
|
||||||
|
requestId,
|
||||||
|
updatePayload,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!updateResult) {
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
"Failed to update request with resend request",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Send notifications to parties (SMS/Push) about required documents
|
||||||
|
|
||||||
|
return {
|
||||||
|
requestId: String(request._id),
|
||||||
|
status: CaseStatus.WAITING_FOR_DOCUMENT_RESEND,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
this.logger.error(
|
||||||
|
"resendRequestV2 failed",
|
||||||
|
requestId,
|
||||||
|
error instanceof Error ? error.stack : String(error),
|
||||||
|
);
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
error instanceof Error ? error.message : "Failed to request document resend",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2: Submit expert reply for blame case (blameCases collection).
|
||||||
|
* Validates lock ownership and expiry, stores decision, unlocks, moves to WAITING_FOR_SIGNATURES.
|
||||||
|
*/
|
||||||
|
async replyRequestV2(
|
||||||
|
requestId: string,
|
||||||
|
reply: SubmitReplyDto,
|
||||||
|
actorId: string,
|
||||||
|
): Promise<{ requestId: string; status: string }> {
|
||||||
|
try {
|
||||||
|
const request = await this.blameRequestDbService.findById(requestId);
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
throw new NotFoundException("Request not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate no decision exists yet
|
||||||
|
if (request.expert?.decision) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"This request already has an expert decision.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if request is locked
|
||||||
|
if (!request.workflow?.locked) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"You must lock the request before submitting a reply.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lockedByActorId = String(request.workflow?.lockedBy?.actorId || "");
|
||||||
|
const lockedAt = request.workflow?.lockedAt;
|
||||||
|
const isLockedByCurrentActor = lockedByActorId === actorId;
|
||||||
|
|
||||||
|
// Check if lock has expired (15 minutes)
|
||||||
|
let isLockExpired = false;
|
||||||
|
if (lockedAt) {
|
||||||
|
const lockExpiryTime = new Date(lockedAt).getTime() + 15 * 60 * 1000;
|
||||||
|
isLockExpired = Date.now() > lockExpiryTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle different lock scenarios
|
||||||
|
if (!isLockedByCurrentActor) {
|
||||||
|
// Request is locked by another expert
|
||||||
|
if (isLockExpired) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"You must lock the request first before submitting a reply.",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"This request is currently locked by another expert.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current actor is the lock owner
|
||||||
|
if (isLockExpired) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"Your lock time has expired. Please lock the request again before submitting.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// Build expert decision with fields
|
||||||
|
const decisionPayload: any = {
|
||||||
|
guiltyPartyId: new Types.ObjectId(String(reply.guiltyUserId)),
|
||||||
|
description: reply.description,
|
||||||
|
decidedAt: now,
|
||||||
|
decidedByExpertId: new Types.ObjectId(actorId),
|
||||||
|
fields: {
|
||||||
|
accidentWay: {
|
||||||
|
id: reply.fields.accidentWay.id,
|
||||||
|
label: reply.fields.accidentWay.label,
|
||||||
|
},
|
||||||
|
accidentReason: {
|
||||||
|
id: reply.fields.accidentReason.id,
|
||||||
|
label: reply.fields.accidentReason.label,
|
||||||
|
fanavaran: reply.fields.accidentReason.fanavaran,
|
||||||
|
},
|
||||||
|
accidentType: {
|
||||||
|
id: reply.fields.accidentType.id,
|
||||||
|
label: reply.fields.accidentType.label,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePayload = {
|
||||||
|
$set: {
|
||||||
|
"workflow.locked": false,
|
||||||
|
"workflow.currentStep": "WAITING_FOR_SIGNATURES",
|
||||||
|
"workflow.nextStep": null,
|
||||||
|
"expert.decision": decisionPayload,
|
||||||
|
status: CaseStatus.WAITING_FOR_SIGNATURES,
|
||||||
|
},
|
||||||
|
$push: {
|
||||||
|
"workflow.completedSteps": "WAITING_FOR_GUILT_DECISION",
|
||||||
|
},
|
||||||
|
$unset: {
|
||||||
|
"workflow.lockedAt": "",
|
||||||
|
"workflow.lockedBy": "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateResult = await this.blameRequestDbService.findByIdAndUpdate(
|
||||||
|
requestId,
|
||||||
|
updatePayload,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!updateResult) {
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
"Failed to update request with expert reply",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
requestId: String(request._id),
|
||||||
|
status: CaseStatus.WAITING_FOR_SIGNATURES,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
this.logger.error(
|
||||||
|
"replyRequestV2 failed",
|
||||||
|
requestId,
|
||||||
|
error instanceof Error ? error.stack : String(error),
|
||||||
|
);
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
error instanceof Error ? error.message : "Failed to submit expert reply",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async updateDamageExpertStats(
|
private async updateDamageExpertStats(
|
||||||
expertId: string,
|
expertId: string,
|
||||||
requestId: string,
|
requestId: string,
|
||||||
|
|||||||
104
src/expert-blame/expert-blame.v2.controller.ts
Normal file
104
src/expert-blame/expert-blame.v2.controller.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
HttpException,
|
||||||
|
InternalServerErrorException,
|
||||||
|
Param,
|
||||||
|
Put,
|
||||||
|
UseGuards,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { ApiBearerAuth, ApiBody, 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 { ExpertBlameService } from "./expert-blame.service";
|
||||||
|
import { SubmitReplyDto } from "./dto/reply.dto";
|
||||||
|
import { ResendRequestDto } from "./dto/resend.dto";
|
||||||
|
|
||||||
|
@ApiTags("expert-blame-panel (v2)")
|
||||||
|
@Controller("v2/expert-blame")
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
||||||
|
@Roles(RoleEnum.EXPERT)
|
||||||
|
export class ExpertBlameV2Controller {
|
||||||
|
constructor(private readonly expertBlameService: ExpertBlameService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async findAll(@CurrentUser() actor: any) {
|
||||||
|
try {
|
||||||
|
return await this.expertBlameService.findAllV2(actor);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
error instanceof Error ? error.message : "Failed to list blame cases",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(":id")
|
||||||
|
@ApiParam({ name: "id", description: "Blame case request id" })
|
||||||
|
async findOne(@Param("id") id: string, @CurrentUser() actor: any) {
|
||||||
|
try {
|
||||||
|
return await this.expertBlameService.findOneV2(id, actor.sub);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
error instanceof Error ? error.message : "Failed to get blame case details",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put("lock/:id")
|
||||||
|
@ApiParam({ name: "id", description: "Blame case request id" })
|
||||||
|
async lockRequest(@Param("id") id: string, @CurrentUser() actor: any) {
|
||||||
|
try {
|
||||||
|
return await this.expertBlameService.lockRequestV2(id, actor);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
error instanceof Error ? error.message : "Failed to lock blame case",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put("reply/resend/:id")
|
||||||
|
@ApiParam({ name: "id", description: "Blame case request id" })
|
||||||
|
@ApiBody({ type: ResendRequestDto })
|
||||||
|
async resendRequest(
|
||||||
|
@Param("id") id: string,
|
||||||
|
@Body() body: ResendRequestDto,
|
||||||
|
@CurrentUser() actor: any,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
return await this.expertBlameService.resendRequestV2(id, body, actor.sub);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Failed to request document resend",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put("reply/submit/:id")
|
||||||
|
@ApiParam({ name: "id", description: "Blame case request id" })
|
||||||
|
@ApiBody({ type: SubmitReplyDto })
|
||||||
|
async submitReply(
|
||||||
|
@Param("id") id: string,
|
||||||
|
@Body() body: SubmitReplyDto,
|
||||||
|
@CurrentUser() actor: any,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
return await this.expertBlameService.replyRequestV2(id, body, actor.sub);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
error instanceof Error ? error.message : "Failed to submit expert reply",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
77
src/expert-claim/dto/claim-detail-v2.dto.ts
Normal file
77
src/expert-claim/dto/claim-detail-v2.dto.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class ClaimDetailV2ResponseDto {
|
||||||
|
@ApiProperty()
|
||||||
|
claimRequestId: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
publicId: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
claimStatus: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
currentStep: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
nextStep?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Whether claim is locked' })
|
||||||
|
locked: boolean;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
lockedBy?: {
|
||||||
|
actorId: string;
|
||||||
|
actorName: string;
|
||||||
|
lockedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
owner?: {
|
||||||
|
userId: string;
|
||||||
|
fullName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
|
vehicle?: {
|
||||||
|
carName?: string;
|
||||||
|
carModel?: string;
|
||||||
|
carType?: string;
|
||||||
|
plate?: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Blame request ID', example: '507f...' })
|
||||||
|
blameRequestId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Blame request number', example: 'BL12345' })
|
||||||
|
blameRequestNo?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Selected outer damaged parts' })
|
||||||
|
selectedParts?: string[];
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Selected other damaged parts' })
|
||||||
|
otherParts?: string[];
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Required documents status' })
|
||||||
|
requiredDocuments?: Record<string, {
|
||||||
|
uploaded: boolean;
|
||||||
|
fileId?: string;
|
||||||
|
fileName?: string;
|
||||||
|
filePath?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Car angles captured' })
|
||||||
|
carAngles?: Record<string, { captured: boolean; url?: string }>;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Damaged parts captured' })
|
||||||
|
damagedParts?: Record<string, { captured: boolean; url?: string }>;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
createdAt: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
42
src/expert-claim/dto/claim-list-v2.dto.ts
Normal file
42
src/expert-claim/dto/claim-list-v2.dto.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class ClaimListItemV2Dto {
|
||||||
|
@ApiProperty({ description: 'Claim case ID' })
|
||||||
|
claimRequestId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Public ID shared across blame+claim', example: 'A14235' })
|
||||||
|
publicId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Overall case status', example: 'WAITING_FOR_DAMAGE_EXPERT' })
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Workflow step', example: 'USER_SUBMISSION_COMPLETE' })
|
||||||
|
currentStep: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Whether the claim is locked by another expert', example: false })
|
||||||
|
locked: boolean;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Actor who locked this claim' })
|
||||||
|
lockedBy?: {
|
||||||
|
actorId: string;
|
||||||
|
actorName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: 'Vehicle info from snapshot' })
|
||||||
|
vehicle?: {
|
||||||
|
carName?: string;
|
||||||
|
carModel?: string;
|
||||||
|
carType?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Submission date', example: '2026-02-22T10:00:00.000Z' })
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GetClaimListV2ResponseDto {
|
||||||
|
@ApiProperty({ type: [ClaimListItemV2Dto] })
|
||||||
|
list: ClaimListItemV2Dto[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Total count' })
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
70
src/expert-claim/dto/expert-claim-v2.dto.ts
Normal file
70
src/expert-claim/dto/expert-claim-v2.dto.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import {
|
||||||
|
IsString,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsArray,
|
||||||
|
IsBoolean,
|
||||||
|
IsOptional,
|
||||||
|
ValidateNested,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
|
export class CarPartDamageV2Dto {
|
||||||
|
@ApiProperty({ example: 'left' })
|
||||||
|
@IsString()
|
||||||
|
side: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'front_door' })
|
||||||
|
@IsString()
|
||||||
|
part: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PartPricingV2Dto {
|
||||||
|
@ApiProperty({ example: 'part-001' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
partId: string;
|
||||||
|
|
||||||
|
@ApiProperty({ type: CarPartDamageV2Dto })
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => CarPartDamageV2Dto)
|
||||||
|
carPartDamage: CarPartDamageV2Dto;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'Minor' })
|
||||||
|
@IsString()
|
||||||
|
typeOfDamage: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '5000000' })
|
||||||
|
@IsString()
|
||||||
|
price: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '2000000' })
|
||||||
|
@IsString()
|
||||||
|
salary: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: '7000000' })
|
||||||
|
@IsString()
|
||||||
|
totalPayment: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: '500000' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
daghi?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: false })
|
||||||
|
@IsBoolean()
|
||||||
|
factorNeeded: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SubmitExpertReplyV2Dto {
|
||||||
|
@ApiProperty({ example: 'Front door and hood have severe paint damage' })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@ApiProperty({ type: [PartPricingV2Dto] })
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => PartPricingV2Dto)
|
||||||
|
parts: PartPricingV2Dto[];
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import { RequestManagementModule } from "src/request-management/request-manageme
|
|||||||
import { SandHubModule } from "src/sand-hub/sand-hub.module";
|
import { SandHubModule } from "src/sand-hub/sand-hub.module";
|
||||||
import { UsersModule } from "src/users/users.module";
|
import { UsersModule } from "src/users/users.module";
|
||||||
import { ExpertClaimController } from "./expert-claim.controller";
|
import { ExpertClaimController } from "./expert-claim.controller";
|
||||||
|
import { ExpertClaimV2Controller } from "./expert-claim.v2.controller";
|
||||||
import { ExpertClaimService } from "./expert-claim.service";
|
import { ExpertClaimService } from "./expert-claim.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -33,7 +34,7 @@ import { ExpertClaimService } from "./expert-claim.service";
|
|||||||
UsersModule,
|
UsersModule,
|
||||||
ClientModule,
|
ClientModule,
|
||||||
],
|
],
|
||||||
controllers: [ExpertClaimController],
|
controllers: [ExpertClaimController, ExpertClaimV2Controller],
|
||||||
providers: [ExpertClaimService, ClaimFactorsImageDbService],
|
providers: [ExpertClaimService, ClaimFactorsImageDbService],
|
||||||
exports: [ClaimFactorsImageDbService, ExpertClaimService],
|
exports: [ClaimFactorsImageDbService, ExpertClaimService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -29,6 +29,12 @@ import { UserType } from "src/Types&Enums/userType.enum";
|
|||||||
import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service";
|
import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service";
|
||||||
import { ClaimPerIdRs } from "./dto/claim-list-perId-rs.dto";
|
import { ClaimPerIdRs } from "./dto/claim-list-perId-rs.dto";
|
||||||
import { ClaimListDtoRs } from "./dto/claim-list-rs.dto";
|
import { ClaimListDtoRs } from "./dto/claim-list-rs.dto";
|
||||||
|
import { ClaimCaseDbService } from "src/claim-request-management/entites/db-service/claim-case.db.service";
|
||||||
|
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
|
||||||
|
import { ClaimStatus } from "src/Types&Enums/claim-request-management/claimStatus.enum";
|
||||||
|
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
|
||||||
|
import { GetClaimListV2ResponseDto, ClaimListItemV2Dto } from "./dto/claim-list-v2.dto";
|
||||||
|
import { ClaimDetailV2ResponseDto } from "./dto/claim-detail-v2.dto";
|
||||||
import { ClaimSubmitReplyDto, ClaimSubmitResendDto } from "./dto/reply.dto";
|
import { ClaimSubmitReplyDto, ClaimSubmitResendDto } from "./dto/reply.dto";
|
||||||
import { ClaimFactorsImageDbService } from "src/claim-request-management/entites/db-service/factor-image.db.service";
|
import { ClaimFactorsImageDbService } from "src/claim-request-management/entites/db-service/factor-image.db.service";
|
||||||
import { ClaimRequiredDocumentDbService } from "src/claim-request-management/entites/db-service/claim-required-document.db.service";
|
import { ClaimRequiredDocumentDbService } from "src/claim-request-management/entites/db-service/claim-required-document.db.service";
|
||||||
@@ -146,6 +152,7 @@ export class ExpertClaimService {
|
|||||||
private readonly blameVideoDbService: BlameVideoDbService,
|
private readonly blameVideoDbService: BlameVideoDbService,
|
||||||
private readonly claimVideoCaptureDbService: VideoCaptureDbService,
|
private readonly claimVideoCaptureDbService: VideoCaptureDbService,
|
||||||
private readonly httpService: HttpService,
|
private readonly httpService: HttpService,
|
||||||
|
private readonly claimCaseDbService: ClaimCaseDbService,
|
||||||
private readonly sandHubService: SandHubService,
|
private readonly sandHubService: SandHubService,
|
||||||
private readonly damageExpertDbService: DamageExpertDbService,
|
private readonly damageExpertDbService: DamageExpertDbService,
|
||||||
private readonly clientDbService: ClientDbService,
|
private readonly clientDbService: ClientDbService,
|
||||||
@@ -183,11 +190,33 @@ export class ExpertClaimService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getClaimRequestsListForExpert(actor): Promise<ClaimListDtoRs> {
|
async getClaimRequestsListForExpert(actor): Promise<ClaimListDtoRs> {
|
||||||
|
console.log(actor);
|
||||||
|
// const requests = await this.claimRequestManagementDbService.findAllByStatus(
|
||||||
|
// {
|
||||||
|
// claimStatus: {
|
||||||
|
// $in: [
|
||||||
|
// ReqClaimStatus.UnChecked,
|
||||||
|
// ReqClaimStatus.ReviewRequest,
|
||||||
|
// ReqClaimStatus.CheckAgain,
|
||||||
|
// ReqClaimStatus.CloseRequest,
|
||||||
|
// ReqClaimStatus.InPersonVisit,
|
||||||
|
// ReqClaimStatus.CheckedRequest,
|
||||||
|
// ReqClaimStatus.PendingFactorValidation,
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// "damageExpertReply.actorDetail.actorId":actor.sub
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
|
||||||
const requests = await this.claimRequestManagementDbService.findAllByStatus(
|
const requests = await this.claimRequestManagementDbService.findAllByStatus(
|
||||||
|
{
|
||||||
|
$or: [
|
||||||
|
{
|
||||||
|
claimStatus: ReqClaimStatus.UnChecked,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
claimStatus: {
|
claimStatus: {
|
||||||
$in: [
|
$in: [
|
||||||
ReqClaimStatus.UnChecked,
|
|
||||||
ReqClaimStatus.ReviewRequest,
|
ReqClaimStatus.ReviewRequest,
|
||||||
ReqClaimStatus.CheckAgain,
|
ReqClaimStatus.CheckAgain,
|
||||||
ReqClaimStatus.CloseRequest,
|
ReqClaimStatus.CloseRequest,
|
||||||
@@ -196,7 +225,10 @@ export class ExpertClaimService {
|
|||||||
ReqClaimStatus.PendingFactorValidation,
|
ReqClaimStatus.PendingFactorValidation,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
"damageExpertReply.actorDetail.actorId": actor.sub,
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const filteredRequests = [];
|
const filteredRequests = [];
|
||||||
@@ -1526,4 +1558,415 @@ export class ExpertClaimService {
|
|||||||
this.logger.error(error.response?.responseCode, error.response);
|
this.logger.error(error.response?.responseCode, error.response);
|
||||||
throw new HttpException(error.response, error.claimStatus);
|
throw new HttpException(error.response, error.claimStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2: Lock a claim request for expert review.
|
||||||
|
*
|
||||||
|
* Validations:
|
||||||
|
* - Claim must exist
|
||||||
|
* - Status must be WAITING_FOR_DAMAGE_EXPERT
|
||||||
|
* - Must not already be locked by another expert
|
||||||
|
*
|
||||||
|
* On success:
|
||||||
|
* - Sets workflow.locked = true, workflow.lockedBy = actor
|
||||||
|
* - Sets status = EXPERT_REVIEWING
|
||||||
|
* - Sets claimStatus = UNDER_REVIEW
|
||||||
|
* - Sets currentStep = EXPERT_DAMAGE_ASSESSMENT
|
||||||
|
*/
|
||||||
|
async lockClaimRequestV2(claimRequestId: string, actor: any) {
|
||||||
|
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
|
|
||||||
|
if (!claim) {
|
||||||
|
throw new NotFoundException('Claim request not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (claim.status !== ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Claim is not available for locking. Current status: ${claim.status}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
claim.workflow?.locked &&
|
||||||
|
claim.workflow.lockedBy?.actorId?.toString() !== actor.sub
|
||||||
|
) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Claim is already locked by another expert: ${claim.workflow.lockedBy?.actorName}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already locked by this same expert — idempotent
|
||||||
|
if (
|
||||||
|
claim.workflow?.locked &&
|
||||||
|
claim.workflow.lockedBy?.actorId?.toString() === actor.sub
|
||||||
|
) {
|
||||||
|
return { claimRequestId, locked: true, message: 'Already locked by you' };
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||||
|
status: ClaimCaseStatus.EXPERT_REVIEWING,
|
||||||
|
claimStatus: ClaimStatus.UNDER_REVIEW,
|
||||||
|
'workflow.locked': true,
|
||||||
|
'workflow.lockedAt': new Date(),
|
||||||
|
'workflow.lockedBy': {
|
||||||
|
actorId: new Types.ObjectId(actor.sub),
|
||||||
|
actorName: actor.fullName,
|
||||||
|
actorRole: 'damage_expert',
|
||||||
|
},
|
||||||
|
'workflow.currentStep': ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
||||||
|
$push: {
|
||||||
|
history: {
|
||||||
|
event: 'CLAIM_LOCKED',
|
||||||
|
performedBy: actor.sub,
|
||||||
|
performedByName: actor.fullName,
|
||||||
|
performedAt: new Date(),
|
||||||
|
note: `Claim locked by damage expert ${actor.fullName}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
claimRequestId,
|
||||||
|
locked: true,
|
||||||
|
status: ClaimCaseStatus.EXPERT_REVIEWING,
|
||||||
|
claimStatus: ClaimStatus.UNDER_REVIEW,
|
||||||
|
currentStep: ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2: Submit damage expert reply for a claim.
|
||||||
|
*
|
||||||
|
* Validations:
|
||||||
|
* - Claim must exist
|
||||||
|
* - Must be locked by this expert (workflow.lockedBy.actorId === actor.sub)
|
||||||
|
* - Must be in EXPERT_REVIEWING status
|
||||||
|
* - Total payment across all parts must not exceed 30,000,000
|
||||||
|
*
|
||||||
|
* On success:
|
||||||
|
* - Stores reply in evaluation.damageExpertReply
|
||||||
|
* - Unlocks the workflow
|
||||||
|
* - If any part has factorNeeded=true → status = WAITING_FOR_INSURER_APPROVAL, step = EXPERT_COST_EVALUATION
|
||||||
|
* - Otherwise → status = WAITING_FOR_INSURER_APPROVAL, step = INSURER_REVIEW, claimStatus = APPROVED
|
||||||
|
*/
|
||||||
|
async submitExpertReplyV2(
|
||||||
|
claimRequestId: string,
|
||||||
|
reply: import('./dto/expert-claim-v2.dto').SubmitExpertReplyV2Dto,
|
||||||
|
actor: any,
|
||||||
|
) {
|
||||||
|
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
|
|
||||||
|
if (!claim) {
|
||||||
|
throw new NotFoundException('Claim request not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (claim.status !== ClaimCaseStatus.EXPERT_REVIEWING) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Claim is not in a reviewable state. Current status: ${claim.status}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!claim.workflow?.locked) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
'You must lock the claim before submitting a reply',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (claim.workflow.lockedBy?.actorId?.toString() !== actor.sub) {
|
||||||
|
throw new ForbiddenException('This claim is locked by another expert');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Price cap validation
|
||||||
|
const PRICE_CAP = 30_000_000;
|
||||||
|
let totalPrice = 0;
|
||||||
|
for (const part of reply.parts || []) {
|
||||||
|
const parsed = this.parsePersianNumber(String(part.totalPayment ?? '0'));
|
||||||
|
if (!isNaN(parsed)) totalPrice += parsed;
|
||||||
|
}
|
||||||
|
if (totalPrice > PRICE_CAP) {
|
||||||
|
throw new BadRequestException({
|
||||||
|
message: `Total price (${totalPrice.toLocaleString()}) exceeds the cap of ${PRICE_CAP.toLocaleString()}`,
|
||||||
|
error: 'PRICE_CAP_ERROR',
|
||||||
|
totalPrice,
|
||||||
|
priceCap: PRICE_CAP,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const needsFactorUpload = reply.parts?.some((p) => p.factorNeeded === true) ?? false;
|
||||||
|
|
||||||
|
const replyPayload = {
|
||||||
|
description: reply.description,
|
||||||
|
parts: reply.parts,
|
||||||
|
submittedAt: new Date(),
|
||||||
|
actorDetail: {
|
||||||
|
actorId: actor.sub,
|
||||||
|
actorName: actor.fullName,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextStep = needsFactorUpload
|
||||||
|
? ClaimWorkflowStep.EXPERT_COST_EVALUATION
|
||||||
|
: ClaimWorkflowStep.INSURER_REVIEW;
|
||||||
|
|
||||||
|
const nextCaseStatus = ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL;
|
||||||
|
const nextClaimStatus = needsFactorUpload
|
||||||
|
? ClaimStatus.NEEDS_REVISION
|
||||||
|
: ClaimStatus.APPROVED;
|
||||||
|
|
||||||
|
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||||
|
status: nextCaseStatus,
|
||||||
|
claimStatus: nextClaimStatus,
|
||||||
|
'workflow.locked': false,
|
||||||
|
'workflow.currentStep': nextStep,
|
||||||
|
'workflow.nextStep': needsFactorUpload
|
||||||
|
? ClaimWorkflowStep.INSURER_REVIEW
|
||||||
|
: ClaimWorkflowStep.CLAIM_COMPLETED,
|
||||||
|
'evaluation.damageExpertReply': replyPayload,
|
||||||
|
$push: {
|
||||||
|
'workflow.completedSteps': ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
||||||
|
history: {
|
||||||
|
event: 'EXPERT_REPLY_SUBMITTED',
|
||||||
|
performedBy: actor.sub,
|
||||||
|
performedByName: actor.fullName,
|
||||||
|
performedAt: new Date(),
|
||||||
|
note: `Expert reply submitted. factorNeeded=${needsFactorUpload}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
claimRequestId,
|
||||||
|
status: nextCaseStatus,
|
||||||
|
claimStatus: nextClaimStatus,
|
||||||
|
currentStep: nextStep,
|
||||||
|
factorNeeded: needsFactorUpload,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2: Expert marks claim for in-person visit.
|
||||||
|
*
|
||||||
|
* This is used when the expert decides the user must attend in person
|
||||||
|
* before a final assessment can be made.
|
||||||
|
*
|
||||||
|
* Validations:
|
||||||
|
* - Claim must exist
|
||||||
|
* - Must be locked by this expert
|
||||||
|
* - Status must be EXPERT_REVIEWING
|
||||||
|
*
|
||||||
|
* On success:
|
||||||
|
* - Sets status = EXPERT_REVIEWING (remains), claimStatus = NEEDS_REVISION
|
||||||
|
* - Sets evaluation.visitLocation (optional note)
|
||||||
|
* - Records history event IN_PERSON_VISIT_REQUESTED
|
||||||
|
* - Unlocks the workflow so user can act
|
||||||
|
*/
|
||||||
|
async requestInPersonVisitV2(claimRequestId: string, actor: any, note?: string) {
|
||||||
|
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
|
|
||||||
|
if (!claim) {
|
||||||
|
throw new NotFoundException('Claim request not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (claim.status !== ClaimCaseStatus.EXPERT_REVIEWING) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Claim is not in EXPERT_REVIEWING state. Current status: ${claim.status}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!claim.workflow?.locked) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
'You must lock the claim before requesting an in-person visit',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (claim.workflow.lockedBy?.actorId?.toString() !== actor.sub) {
|
||||||
|
throw new ForbiddenException('This claim is locked by another expert');
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||||
|
claimStatus: ClaimStatus.NEEDS_REVISION,
|
||||||
|
'workflow.locked': false,
|
||||||
|
...(note ? { 'evaluation.visitLocation': note } : {}),
|
||||||
|
$push: {
|
||||||
|
history: {
|
||||||
|
event: 'IN_PERSON_VISIT_REQUESTED',
|
||||||
|
performedBy: actor.sub,
|
||||||
|
performedByName: actor.fullName,
|
||||||
|
performedAt: new Date(),
|
||||||
|
note: note || 'Expert requested in-person visit',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
claimRequestId,
|
||||||
|
status: claim.status,
|
||||||
|
claimStatus: ClaimStatus.NEEDS_REVISION,
|
||||||
|
message: 'In-person visit requested. User will be notified.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2: Get claim list for damage expert
|
||||||
|
*
|
||||||
|
* Returns claims that are:
|
||||||
|
* 1. WAITING_FOR_DAMAGE_EXPERT status AND not locked (available for anyone to pick)
|
||||||
|
* 2. WAITING_FOR_DAMAGE_EXPERT status AND not locked (claimStatus PENDING)
|
||||||
|
* 3. Any status locked by THIS expert (their own in-progress work)
|
||||||
|
*/
|
||||||
|
async getClaimListV2(actorId: string): Promise<GetClaimListV2ResponseDto> {
|
||||||
|
const claims = await this.claimCaseDbService.find({
|
||||||
|
$or: [
|
||||||
|
// Available claims: waiting for expert, not locked
|
||||||
|
{
|
||||||
|
status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT,
|
||||||
|
'workflow.locked': { $ne: true },
|
||||||
|
},
|
||||||
|
// This expert's own locked/in-progress claims
|
||||||
|
{
|
||||||
|
'workflow.locked': true,
|
||||||
|
'workflow.lockedBy.actorId': new Types.ObjectId(actorId),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const list = (claims as any[]).map((c) => ({
|
||||||
|
claimRequestId: c._id.toString(),
|
||||||
|
publicId: c.publicId,
|
||||||
|
status: c.status,
|
||||||
|
currentStep: c.workflow?.currentStep || '',
|
||||||
|
locked: c.workflow?.locked || false,
|
||||||
|
lockedBy: c.workflow?.lockedBy
|
||||||
|
? {
|
||||||
|
actorId: c.workflow.lockedBy.actorId?.toString(),
|
||||||
|
actorName: c.workflow.lockedBy.actorName,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
vehicle: c.vehicle
|
||||||
|
? {
|
||||||
|
carName: c.vehicle.carName,
|
||||||
|
carModel: c.vehicle.carModel,
|
||||||
|
carType: c.vehicle.carType,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
createdAt: c.createdAt,
|
||||||
|
})) as ClaimListItemV2Dto[];
|
||||||
|
|
||||||
|
return { list, total: list.length };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2: Get claim detail for damage expert
|
||||||
|
*
|
||||||
|
* Validations:
|
||||||
|
* - Claim must exist
|
||||||
|
* - Claim status must be WAITING_FOR_DAMAGE_EXPERT
|
||||||
|
* - If claim is locked, only the locking expert can access it
|
||||||
|
*/
|
||||||
|
async getClaimDetailV2(
|
||||||
|
claimRequestId: string,
|
||||||
|
actorId: string,
|
||||||
|
): Promise<ClaimDetailV2ResponseDto> {
|
||||||
|
const claim = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
|
|
||||||
|
if (!claim) {
|
||||||
|
throw new NotFoundException('Claim request not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (claim.status !== ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
`This claim is not available for expert review. Current status: ${claim.status}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If locked by someone else, deny access
|
||||||
|
if (
|
||||||
|
claim.workflow?.locked &&
|
||||||
|
claim.workflow.lockedBy?.actorId?.toString() !== actorId
|
||||||
|
) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
`This claim is locked by another expert: ${claim.workflow.lockedBy?.actorName}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasCapture = (data: any, key: string) =>
|
||||||
|
data && (data instanceof Map ? data.get(key) : data[key]);
|
||||||
|
|
||||||
|
// Build requiredDocuments map
|
||||||
|
const requiredDocs = claim.requiredDocuments as any;
|
||||||
|
const requiredDocumentsStatus: Record<string, any> = {};
|
||||||
|
if (requiredDocs) {
|
||||||
|
const keys =
|
||||||
|
requiredDocs instanceof Map
|
||||||
|
? Array.from(requiredDocs.keys())
|
||||||
|
: Object.keys(requiredDocs);
|
||||||
|
for (const k of keys as string[]) {
|
||||||
|
const doc = requiredDocs instanceof Map ? requiredDocs.get(k) : requiredDocs[k];
|
||||||
|
requiredDocumentsStatus[k] = {
|
||||||
|
uploaded: !!doc?.uploaded,
|
||||||
|
fileId: doc?.fileId?.toString(),
|
||||||
|
fileName: doc?.fileName,
|
||||||
|
filePath: doc?.filePath ? buildFileLink(doc.filePath) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build car angles map
|
||||||
|
const carAnglesData = claim.media?.carAngles as any;
|
||||||
|
const carAngles: Record<string, { captured: boolean; url?: string }> = {};
|
||||||
|
for (const k of ['front', 'back', 'left', 'right']) {
|
||||||
|
const cap = hasCapture(carAnglesData, k);
|
||||||
|
carAngles[k] = {
|
||||||
|
captured: !!cap,
|
||||||
|
url: cap?.url || (cap?.path ? buildFileLink(cap.path) : undefined),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build damaged parts map
|
||||||
|
const damagedPartsData = claim.media?.damagedParts as any;
|
||||||
|
const damagedParts: Record<string, { captured: boolean; url?: string }> = {};
|
||||||
|
for (const p of claim.damage?.selectedParts || []) {
|
||||||
|
const cap = hasCapture(damagedPartsData, p);
|
||||||
|
damagedParts[p] = {
|
||||||
|
captured: !!cap,
|
||||||
|
url: cap?.url || (cap?.path ? buildFileLink(cap.path) : undefined),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
claimRequestId: claim._id.toString(),
|
||||||
|
publicId: claim.publicId,
|
||||||
|
status: claim.status,
|
||||||
|
claimStatus: claim.claimStatus || 'PENDING',
|
||||||
|
currentStep: claim.workflow?.currentStep || '',
|
||||||
|
nextStep: claim.workflow?.nextStep,
|
||||||
|
locked: claim.workflow?.locked || false,
|
||||||
|
lockedBy: claim.workflow?.lockedBy
|
||||||
|
? {
|
||||||
|
actorId: claim.workflow.lockedBy.actorId?.toString(),
|
||||||
|
actorName: claim.workflow.lockedBy.actorName,
|
||||||
|
lockedAt: (claim.workflow as any).lockedAt?.toISOString(),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
owner: claim.owner
|
||||||
|
? {
|
||||||
|
userId: claim.owner.userId?.toString(),
|
||||||
|
fullName: claim.owner.fullName,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
vehicle: claim.vehicle,
|
||||||
|
blameRequestId: claim.blameRequestId?.toString(),
|
||||||
|
blameRequestNo: claim.blameRequestNo,
|
||||||
|
selectedParts: claim.damage?.selectedParts,
|
||||||
|
otherParts: claim.damage?.otherParts,
|
||||||
|
requiredDocuments:
|
||||||
|
Object.keys(requiredDocumentsStatus).length > 0
|
||||||
|
? requiredDocumentsStatus
|
||||||
|
: undefined,
|
||||||
|
carAngles,
|
||||||
|
damagedParts,
|
||||||
|
createdAt: (claim as any).createdAt,
|
||||||
|
updatedAt: (claim as any).updatedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
110
src/expert-claim/expert-claim.v2.controller.ts
Normal file
110
src/expert-claim/expert-claim.v2.controller.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { Body, Controller, Get, Param, Patch, Put, UseGuards } from "@nestjs/common";
|
||||||
|
import {
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiBody,
|
||||||
|
ApiOperation,
|
||||||
|
ApiParam,
|
||||||
|
ApiPropertyOptional,
|
||||||
|
ApiTags,
|
||||||
|
} from "@nestjs/swagger";
|
||||||
|
import { IsOptional, IsString } from "class-validator";
|
||||||
|
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 { ExpertClaimService } from "./expert-claim.service";
|
||||||
|
import { SubmitExpertReplyV2Dto } from "./dto/expert-claim-v2.dto";
|
||||||
|
|
||||||
|
class InPersonVisitV2Dto {
|
||||||
|
@ApiPropertyOptional({ example: 'Paint damage requires physical inspection' })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
note?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiTags("expert-claim-panel (v2)")
|
||||||
|
@Controller("v2/expert-claim")
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
||||||
|
@Roles(RoleEnum.DAMAGE_EXPERT)
|
||||||
|
export class ExpertClaimV2Controller {
|
||||||
|
constructor(private readonly expertClaimService: ExpertClaimService) {}
|
||||||
|
|
||||||
|
@Get("requests")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "List available claim requests for damage expert",
|
||||||
|
description:
|
||||||
|
"Returns claims that are WAITING_FOR_DAMAGE_EXPERT and not locked by another expert, plus this expert's own locked/in-progress claims.",
|
||||||
|
})
|
||||||
|
async getClaimListV2(@CurrentUser() actor) {
|
||||||
|
return await this.expertClaimService.getClaimListV2(actor.sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("request/:claimRequestId")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get claim request detail for damage expert",
|
||||||
|
description:
|
||||||
|
"Returns full claim details including captured images, required documents status, and damage selections. Claim must have status WAITING_FOR_DAMAGE_EXPERT. If locked, only the locking expert can access it.",
|
||||||
|
})
|
||||||
|
@ApiParam({ name: "claimRequestId" })
|
||||||
|
async getClaimDetailV2(
|
||||||
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
@CurrentUser() actor,
|
||||||
|
) {
|
||||||
|
return await this.expertClaimService.getClaimDetailV2(
|
||||||
|
claimRequestId,
|
||||||
|
actor.sub,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put("lock/:claimRequestId")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Lock a claim request for review",
|
||||||
|
description:
|
||||||
|
"Claim must have status WAITING_FOR_DAMAGE_EXPERT. Locking sets status to EXPERT_REVIEWING and claimStatus to UNDER_REVIEW. Only one expert can lock a claim at a time.",
|
||||||
|
})
|
||||||
|
@ApiParam({ name: "claimRequestId" })
|
||||||
|
async lockClaimRequestV2(
|
||||||
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
@CurrentUser() actor,
|
||||||
|
) {
|
||||||
|
return await this.expertClaimService.lockClaimRequestV2(claimRequestId, actor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put("reply/submit/:claimRequestId")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Submit expert damage assessment reply",
|
||||||
|
description:
|
||||||
|
"Claim must be locked by this expert (status EXPERT_REVIEWING). Submitting unlocks the claim. If any part has factorNeeded=true the claim moves to EXPERT_COST_EVALUATION; otherwise moves to INSURER_REVIEW. Total payment cap is 30,000,000.",
|
||||||
|
})
|
||||||
|
@ApiParam({ name: "claimRequestId" })
|
||||||
|
@ApiBody({ type: SubmitExpertReplyV2Dto })
|
||||||
|
async submitExpertReplyV2(
|
||||||
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
@Body() body: SubmitExpertReplyV2Dto,
|
||||||
|
@CurrentUser() actor,
|
||||||
|
) {
|
||||||
|
return await this.expertClaimService.submitExpertReplyV2(claimRequestId, body, actor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(":claimRequestId/visit")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Request in-person visit for claim",
|
||||||
|
description:
|
||||||
|
"Expert decides the user must come in person. Claim must be locked by this expert. Sets claimStatus to NEEDS_REVISION and unlocks the claim so the user is informed.",
|
||||||
|
})
|
||||||
|
@ApiParam({ name: "claimRequestId" })
|
||||||
|
@ApiBody({ type: InPersonVisitV2Dto, required: false })
|
||||||
|
async requestInPersonVisitV2(
|
||||||
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
@Body() body: InPersonVisitV2Dto,
|
||||||
|
@CurrentUser() actor,
|
||||||
|
) {
|
||||||
|
return await this.expertClaimService.requestInPersonVisitV2(
|
||||||
|
claimRequestId,
|
||||||
|
actor,
|
||||||
|
body?.note,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/helpers/date-jalali.ts
Normal file
59
src/helpers/date-jalali.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* Converts an ISO date string or Date to Jalali (Persian) date and time.
|
||||||
|
* @param input - ISO date string (e.g. 2026-02-08T13:51:20.747+00:00) or Date
|
||||||
|
* @returns Tuple [dateStr, timeStr] e.g. ['1404/11/24', '13:37']
|
||||||
|
*/
|
||||||
|
export function toJalaliDateAndTime(
|
||||||
|
input: string | Date,
|
||||||
|
): [string, string] {
|
||||||
|
const d = new Date(input);
|
||||||
|
const dateStr = toJalaliDate(d);
|
||||||
|
const timeStr = toJalaliTime(d);
|
||||||
|
return [dateStr, timeStr];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Date to Jalali date string (e.g. 1404/11/24).
|
||||||
|
*/
|
||||||
|
export function toJalaliDate(d: Date): string {
|
||||||
|
const persianDate = d.toLocaleDateString("fa-IR", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
});
|
||||||
|
return persianDigitsToEnglish(persianDate, "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Date to 24h time string (e.g. 13:37).
|
||||||
|
*/
|
||||||
|
export function toJalaliTime(d: Date): string {
|
||||||
|
const hours = d.getHours().toString().padStart(2, "0");
|
||||||
|
const minutes = d.getMinutes().toString().padStart(2, "0");
|
||||||
|
return `${hours}:${minutes}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PERSIAN_TO_ENGLISH: Record<string, string> = {
|
||||||
|
"۰": "0",
|
||||||
|
"۱": "1",
|
||||||
|
"۲": "2",
|
||||||
|
"۳": "3",
|
||||||
|
"۴": "4",
|
||||||
|
"۵": "5",
|
||||||
|
"۶": "6",
|
||||||
|
"۷": "7",
|
||||||
|
"۸": "8",
|
||||||
|
"۹": "9",
|
||||||
|
};
|
||||||
|
|
||||||
|
function persianDigitsToEnglish(str: string, separator: string): string {
|
||||||
|
return str
|
||||||
|
.split(separator)
|
||||||
|
.map((part) =>
|
||||||
|
part
|
||||||
|
.split("")
|
||||||
|
.map((char) => PERSIAN_TO_ENGLISH[char] ?? char)
|
||||||
|
.join(""),
|
||||||
|
)
|
||||||
|
.join("/");
|
||||||
|
}
|
||||||
@@ -30,8 +30,8 @@ async function bootstrap() {
|
|||||||
const config = new DocumentBuilder()
|
const config = new DocumentBuilder()
|
||||||
.setTitle("yara724-backend")
|
.setTitle("yara724-backend")
|
||||||
.setVersion("1.0.0")
|
.setVersion("1.0.0")
|
||||||
.addServer(process.env.BASE_URL + "/api")
|
|
||||||
.addServer(process.env.URL)
|
.addServer(process.env.URL)
|
||||||
|
.addServer(process.env.BASE_URL + "/api")
|
||||||
.addServer("http://192.168.20.249:9001")
|
.addServer("http://192.168.20.249:9001")
|
||||||
.addServer("http://192.168.20.170:9001")
|
.addServer("http://192.168.20.170:9001")
|
||||||
.addServer("http://localhost:9001")
|
.addServer("http://localhost:9001")
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { ApiProperty, ApiSchema } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { IsDateString } from "class-validator";
|
import { IsDateString } from "class-validator";
|
||||||
|
|
||||||
@ApiSchema({
|
|
||||||
name: "Update user profile",
|
|
||||||
description: "Users updates their profile via this API.",
|
|
||||||
})
|
|
||||||
export class UpdateUserProfileDtoRq {
|
export class UpdateUserProfileDtoRq {
|
||||||
// @ApiProperty({})
|
// @ApiProperty({})
|
||||||
// mobile: string;
|
// mobile: string;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
RoadCondition,
|
RoadCondition,
|
||||||
LightCondition,
|
LightCondition,
|
||||||
} from "src/Types&Enums/blame-request-management/accident-conditions.enum";
|
} from "src/Types&Enums/blame-request-management/accident-conditions.enum";
|
||||||
|
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||||
|
|
||||||
export class InitialFormDto {
|
export class InitialFormDto {
|
||||||
@ApiProperty({ required: false, default: false })
|
@ApiProperty({ required: false, default: false })
|
||||||
@@ -135,6 +136,28 @@ export class CreateRequestDto {
|
|||||||
type?: "THIRD_PARTY" | "CAR_BODY";
|
type?: "THIRD_PARTY" | "CAR_BODY";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2 create blame request (new model `BlameRequest`).
|
||||||
|
*/
|
||||||
|
export class CreateBlameRequestDtoV2 {
|
||||||
|
@ApiProperty({ enum: BlameRequestType })
|
||||||
|
type: BlameRequestType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2 step: FIRST_BLAME_CONFESSION
|
||||||
|
*/
|
||||||
|
export class BlameConfessionDtoV2 {
|
||||||
|
@ApiProperty({ required: false, default: false })
|
||||||
|
expertOpinion?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, default: false })
|
||||||
|
imDamaged?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ required: true, default: false })
|
||||||
|
imGuilty?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
// export class DocsOfThisFile {
|
// export class DocsOfThisFile {
|
||||||
// @ApiProperty()
|
// @ApiProperty()
|
||||||
// firstPartyFile?: FirstPartyFileDto;
|
// firstPartyFile?: FirstPartyFileDto;
|
||||||
|
|||||||
46
src/request-management/dto/user-resend.dto.ts
Normal file
46
src/request-management/dto/user-resend.dto.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
export class UserResendResponseDto {
|
||||||
|
@ApiProperty({
|
||||||
|
example: "67fa10c259e15f231a2d1aae",
|
||||||
|
description: "Request ID",
|
||||||
|
})
|
||||||
|
requestId: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: ["drivingLicense", "carCertificate"],
|
||||||
|
description: "List of items the current user needs to upload",
|
||||||
|
})
|
||||||
|
requestedItems: string[];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: "Please resend your driving license and car certificate with better quality",
|
||||||
|
description: "Expert's message explaining why resend is needed",
|
||||||
|
})
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: false,
|
||||||
|
description: "Whether the user has completed uploading all requested items",
|
||||||
|
})
|
||||||
|
completed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserResendUploadResponseDto {
|
||||||
|
@ApiProperty({
|
||||||
|
example: "Documents uploaded successfully",
|
||||||
|
})
|
||||||
|
message: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: ["drivingLicense", "carCertificate"],
|
||||||
|
description: "List of items that were uploaded",
|
||||||
|
})
|
||||||
|
uploadedItems: string[];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: true,
|
||||||
|
description: "Whether all requested items have been uploaded",
|
||||||
|
})
|
||||||
|
allItemsCompleted: boolean;
|
||||||
|
}
|
||||||
27
src/request-management/dto/user-signature.dto.ts
Normal file
27
src/request-management/dto/user-signature.dto.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
export class UserSignatureResponseDto {
|
||||||
|
@ApiProperty({
|
||||||
|
example: "67fa10c259e15f231a2d1aae",
|
||||||
|
description: "Request ID",
|
||||||
|
})
|
||||||
|
requestId: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: true,
|
||||||
|
description: "Whether the user accepted the expert decision",
|
||||||
|
})
|
||||||
|
accepted: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: "COMPLETED",
|
||||||
|
description: "Updated request status",
|
||||||
|
})
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: "Both parties have signed. Case completed.",
|
||||||
|
description: "Result message",
|
||||||
|
})
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { InjectModel } from "@nestjs/mongoose";
|
||||||
|
import { FilterQuery, Model, Types } from "mongoose";
|
||||||
|
import {
|
||||||
|
BlameRequest,
|
||||||
|
BlameRequestDocument,
|
||||||
|
} from "src/request-management/entities/schema/blame-cases.schema";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BlameRequestDbService {
|
||||||
|
constructor(
|
||||||
|
@InjectModel(BlameRequest.name)
|
||||||
|
private readonly blameRequestModel: Model<BlameRequestDocument>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async create(payload: Partial<BlameRequest>): Promise<BlameRequestDocument> {
|
||||||
|
return this.blameRequestModel.create(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(
|
||||||
|
filter: FilterQuery<BlameRequest>,
|
||||||
|
): Promise<BlameRequestDocument | null> {
|
||||||
|
return this.blameRequestModel.findOne(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(id: string | Types.ObjectId): Promise<BlameRequestDocument | null> {
|
||||||
|
return this.blameRequestModel.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Find by id excluding history (e.g. for expert detail view). Returns lean plain object. */
|
||||||
|
async findByIdWithoutHistory(
|
||||||
|
id: string | Types.ObjectId,
|
||||||
|
): Promise<Record<string, unknown> | null> {
|
||||||
|
const doc = await this.blameRequestModel
|
||||||
|
.findById(id)
|
||||||
|
.select("-history")
|
||||||
|
.lean()
|
||||||
|
.exec();
|
||||||
|
return doc as Record<string, unknown> | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByIdAndUpdate(
|
||||||
|
id: string | Types.ObjectId,
|
||||||
|
update: any,
|
||||||
|
): Promise<BlameRequestDocument | null> {
|
||||||
|
return this.blameRequestModel.findByIdAndUpdate(id, update, { new: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Atomic find and update with filter conditions (e.g. for locking). */
|
||||||
|
async findOneAndUpdate(
|
||||||
|
filter: FilterQuery<BlameRequest>,
|
||||||
|
update: any,
|
||||||
|
options?: { new?: boolean },
|
||||||
|
): Promise<BlameRequestDocument | null> {
|
||||||
|
return this.blameRequestModel.findOneAndUpdate(filter, update, {
|
||||||
|
new: options?.new ?? true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async find(
|
||||||
|
filter: FilterQuery<BlameRequest>,
|
||||||
|
options?: { lean?: boolean; select?: string },
|
||||||
|
): Promise<BlameRequestDocument[] | Record<string, unknown>[]> {
|
||||||
|
if (options?.lean) {
|
||||||
|
const query = options?.select
|
||||||
|
? this.blameRequestModel.find(filter).select(options.select).lean()
|
||||||
|
: this.blameRequestModel.find(filter).lean();
|
||||||
|
return query.exec() as Promise<Record<string, unknown>[]>;
|
||||||
|
}
|
||||||
|
const query = options?.select
|
||||||
|
? this.blameRequestModel.find(filter).select(options.select)
|
||||||
|
: this.blameRequestModel.find(filter);
|
||||||
|
return query.exec() as Promise<BlameRequestDocument[]>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
//! NEW
|
||||||
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ReferenceItem {
|
||||||
|
@Prop() id: string;
|
||||||
|
@Prop() label: string;
|
||||||
|
}
|
||||||
|
export const ReferenceItemSchema = SchemaFactory.createForClass(ReferenceItem);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class AccidentReason extends ReferenceItem {
|
||||||
|
@Prop() fanavaran: number;
|
||||||
|
}
|
||||||
|
export const AccidentReasonSchema = SchemaFactory.createForClass(AccidentReason);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class AccidentClassification {
|
||||||
|
|
||||||
|
@Prop({ type: ReferenceItemSchema })
|
||||||
|
accidentWay: ReferenceItem;
|
||||||
|
|
||||||
|
@Prop({ type: ReferenceItemSchema })
|
||||||
|
accidentType: ReferenceItem;
|
||||||
|
|
||||||
|
@Prop({ type: AccidentReasonSchema })
|
||||||
|
accidentReason: AccidentReason;
|
||||||
|
}
|
||||||
|
export const AccidentClassificationSchema =
|
||||||
|
SchemaFactory.createForClass(AccidentClassification);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class Location {
|
||||||
|
@Prop() lat: number;
|
||||||
|
@Prop() lon: number;
|
||||||
|
}
|
||||||
|
export const LocationSchema = SchemaFactory.createForClass(Location);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class AccidentInfo {
|
||||||
|
|
||||||
|
@Prop({ type: Date })
|
||||||
|
date?: Date;
|
||||||
|
|
||||||
|
@Prop()
|
||||||
|
time?: string;
|
||||||
|
|
||||||
|
@Prop({ type: LocationSchema })
|
||||||
|
location?: Location;
|
||||||
|
|
||||||
|
@Prop({ type: AccidentClassificationSchema })
|
||||||
|
classification?: AccidentClassification;
|
||||||
|
}
|
||||||
|
export const AccidentInfoSchema = SchemaFactory.createForClass(AccidentInfo);
|
||||||
69
src/request-management/entities/schema/blame-cases.schema.ts
Normal file
69
src/request-management/entities/schema/blame-cases.schema.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
|
import { HydratedDocument } 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";
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class BlameRequestSnapshot {
|
||||||
|
@Prop({ type: AccidentInfoSchema })
|
||||||
|
accident?: AccidentInfo;
|
||||||
|
|
||||||
|
@Prop({ type: [PartySchema], default: [] })
|
||||||
|
parties?: Party[];
|
||||||
|
}
|
||||||
|
export const BlameRequestSnapshotSchema =
|
||||||
|
SchemaFactory.createForClass(BlameRequestSnapshot);
|
||||||
|
|
||||||
|
|
||||||
|
@Schema({ collection: "blameCases", timestamps: true , id:true })
|
||||||
|
export class BlameRequest {
|
||||||
|
@Prop({ required: true, unique: true, index: true, trim: true })
|
||||||
|
requestNo: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Human-friendly shared id across blame+claim (e.g. A14235).
|
||||||
|
* Generated once at creation time and reused by claim.
|
||||||
|
*/
|
||||||
|
@Prop({ required: true, unique: true, index: true, trim: true })
|
||||||
|
publicId: string;
|
||||||
|
|
||||||
|
@Prop({ required: true, type: String, enum: BlameRequestType })
|
||||||
|
type: BlameRequestType;
|
||||||
|
|
||||||
|
@Prop({ required: true, type: String, enum: CaseStatus, default: CaseStatus.OPEN })
|
||||||
|
status: CaseStatus;
|
||||||
|
|
||||||
|
@Prop({ type: String, enum: BlameStatus, default: BlameStatus.UNKNOWN })
|
||||||
|
blameStatus: BlameStatus;
|
||||||
|
|
||||||
|
@Prop({ type: WorkflowSchema })
|
||||||
|
workflow?: Workflow;
|
||||||
|
|
||||||
|
@Prop({ type: AccidentInfoSchema })
|
||||||
|
accident?: AccidentInfo;
|
||||||
|
|
||||||
|
@Prop({ type: [PartySchema], default: [] })
|
||||||
|
parties: Party[];
|
||||||
|
|
||||||
|
@Prop({ type: ExpertSectionSchema })
|
||||||
|
expert?: ExpertSection;
|
||||||
|
|
||||||
|
@Prop({ type: [HistoryEventSchema], default: [] })
|
||||||
|
history: HistoryEvent[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BlameRequestDocument = HydratedDocument<BlameRequest>;
|
||||||
|
export const BlameRequestSchema = SchemaFactory.createForClass(BlameRequest);
|
||||||
142
src/request-management/entities/schema/expert-section.type.ts
Normal file
142
src/request-management/entities/schema/expert-section.type.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
//! NEW
|
||||||
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
|
import { Types } from "mongoose";
|
||||||
|
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ExpertLink {
|
||||||
|
|
||||||
|
@Prop({ required: true })
|
||||||
|
token: string;
|
||||||
|
|
||||||
|
@Prop({ type: Date })
|
||||||
|
expiresAt: Date;
|
||||||
|
|
||||||
|
@Prop({ default: false })
|
||||||
|
isUsed: boolean;
|
||||||
|
|
||||||
|
@Prop({ default: 0 })
|
||||||
|
openCount: number;
|
||||||
|
|
||||||
|
@Prop()
|
||||||
|
sentTo?: string;
|
||||||
|
|
||||||
|
@Prop()
|
||||||
|
lastOpenedAt?: Date;
|
||||||
|
}
|
||||||
|
export const ExpertLinkSchema = SchemaFactory.createForClass(ExpertLink);
|
||||||
|
|
||||||
|
export enum FilledBy {
|
||||||
|
CUSTOMER = "CUSTOMER",
|
||||||
|
EXPERT = "EXPERT"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ExpertDecision {
|
||||||
|
|
||||||
|
@Prop({ type: Types.ObjectId })
|
||||||
|
guiltyPartyId: Types.ObjectId;
|
||||||
|
|
||||||
|
@Prop()
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@Prop({ type: Date, default: Date.now })
|
||||||
|
decidedAt: Date;
|
||||||
|
|
||||||
|
@Prop({ type: Types.ObjectId })
|
||||||
|
decidedByExpertId?: Types.ObjectId;
|
||||||
|
|
||||||
|
@Prop({ type: Object })
|
||||||
|
fields?: {
|
||||||
|
accidentWay: { id: string; label: string };
|
||||||
|
accidentReason: { id: string; label: string; fanavaran: number };
|
||||||
|
accidentType: { id: string; label: string };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export const ExpertDecisionSchema = SchemaFactory.createForClass(ExpertDecision);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class PartyResendRequest {
|
||||||
|
|
||||||
|
@Prop({ type: Types.ObjectId, required: true })
|
||||||
|
partyId: Types.ObjectId;
|
||||||
|
|
||||||
|
@Prop({ type: [String], default: [] })
|
||||||
|
requestedItems: string[]; // Array of ResendItemType values
|
||||||
|
|
||||||
|
@Prop()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@Prop({ type: Date, default: Date.now })
|
||||||
|
requestedAt: Date;
|
||||||
|
|
||||||
|
@Prop({ type: Boolean, default: false })
|
||||||
|
completed?: boolean;
|
||||||
|
|
||||||
|
@Prop({ type: Date })
|
||||||
|
completedAt?: Date;
|
||||||
|
}
|
||||||
|
export const PartyResendRequestSchema = SchemaFactory.createForClass(PartyResendRequest);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ExpertResend {
|
||||||
|
|
||||||
|
@Prop({ type: [PartyResendRequestSchema], default: [] })
|
||||||
|
parties: PartyResendRequest[];
|
||||||
|
|
||||||
|
@Prop({ type: Date })
|
||||||
|
requestedAt?: Date;
|
||||||
|
|
||||||
|
@Prop({ type: Types.ObjectId })
|
||||||
|
requestedByExpertId?: Types.ObjectId;
|
||||||
|
}
|
||||||
|
export const ExpertResendSchema = SchemaFactory.createForClass(ExpertResend);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class FileRating {
|
||||||
|
|
||||||
|
@Prop({ min: 0, max: 5 })
|
||||||
|
collisionMethodAccuracy: number;
|
||||||
|
|
||||||
|
@Prop({ min: 0, max: 5 })
|
||||||
|
evaluationTimeliness: number;
|
||||||
|
|
||||||
|
@Prop({ min: 0, max: 5 })
|
||||||
|
accidentCauseAccuracy: number;
|
||||||
|
|
||||||
|
@Prop({ min: 0, max: 5 })
|
||||||
|
guiltyVehicleIdentification: number;
|
||||||
|
}
|
||||||
|
export const FileRatingSchema = SchemaFactory.createForClass(FileRating);
|
||||||
|
|
||||||
|
export enum CreationMethod {
|
||||||
|
NORMAL = "NORMAL",
|
||||||
|
LINK = "LINK",
|
||||||
|
IN_PERSON = "IN_PERSON"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ExpertSection {
|
||||||
|
|
||||||
|
@Prop({ type: Types.ObjectId })
|
||||||
|
assignedExpertId?: Types.ObjectId;
|
||||||
|
|
||||||
|
@Prop({ enum: CreationMethod })
|
||||||
|
creationMethod: CreationMethod;
|
||||||
|
|
||||||
|
@Prop({ enum: FilledBy })
|
||||||
|
filledBy?: FilledBy;
|
||||||
|
|
||||||
|
@Prop({ enum: ExpertDecisionSchema })
|
||||||
|
decision?: ExpertDecision;
|
||||||
|
|
||||||
|
@Prop({ type: ExpertResendSchema })
|
||||||
|
resend?: ExpertResend;
|
||||||
|
|
||||||
|
@Prop({ type: FileRatingSchema })
|
||||||
|
rating?: FileRating;
|
||||||
|
|
||||||
|
@Prop({ type: ExpertLinkSchema })
|
||||||
|
link?: ExpertLink;
|
||||||
|
}
|
||||||
|
export const ExpertSectionSchema = SchemaFactory.createForClass(ExpertSection);
|
||||||
32
src/request-management/entities/schema/historyEvent.type.ts
Normal file
32
src/request-management/entities/schema/historyEvent.type.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
//! NEW
|
||||||
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
|
import { Schema as MongooseSchema, Types } from "mongoose";
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class HistoryActor {
|
||||||
|
@Prop({ type: Types.ObjectId })
|
||||||
|
actorId?: Types.ObjectId;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
actorName?: string;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
actorType?: string;
|
||||||
|
}
|
||||||
|
export const HistoryActorSchema = SchemaFactory.createForClass(HistoryActor);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class HistoryEvent {
|
||||||
|
@Prop({ type: String, required: true })
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
@Prop({ type: HistoryActorSchema })
|
||||||
|
actor?: HistoryActor;
|
||||||
|
|
||||||
|
@Prop({ type: Date, default: () => new Date() })
|
||||||
|
timestamp: Date;
|
||||||
|
|
||||||
|
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||||
|
metadata?: any;
|
||||||
|
}
|
||||||
|
export const HistoryEventSchema = SchemaFactory.createForClass(HistoryEvent);
|
||||||
154
src/request-management/entities/schema/partyRole.enum.ts
Normal file
154
src/request-management/entities/schema/partyRole.enum.ts
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
//! NEW
|
||||||
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
|
import { Schema as MongooseSchema, Types } from "mongoose";
|
||||||
|
import { Location, LocationSchema } from "./accidentInformation.type";
|
||||||
|
|
||||||
|
export enum PartyRole {
|
||||||
|
FIRST = "FIRST",
|
||||||
|
SECOND = "SECOND",
|
||||||
|
}
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class Person {
|
||||||
|
@Prop({ type: Types.ObjectId })
|
||||||
|
userId?: Types.ObjectId;
|
||||||
|
|
||||||
|
@Prop() fullName?: string;
|
||||||
|
@Prop() phoneNumber?: string;
|
||||||
|
@Prop({ type: Types.ObjectId }) clientId?: Types.ObjectId;
|
||||||
|
@Prop() birthday?: string;
|
||||||
|
|
||||||
|
// ---- FIRST_INITIAL_FORM (step-manager driven) ----
|
||||||
|
@Prop() nationalCodeOfInsurer?: string;
|
||||||
|
@Prop() nationalCodeOfDriver?: string;
|
||||||
|
@Prop() insurerLicense?: string;
|
||||||
|
@Prop() driverLicense?: string;
|
||||||
|
|
||||||
|
@Prop({ type: Boolean })
|
||||||
|
driverIsInsurer?: boolean;
|
||||||
|
|
||||||
|
@Prop({ type: Boolean })
|
||||||
|
isNewCar?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mirrors existing DTO `userNoCertificate` (true means user has NO certificate).
|
||||||
|
*/
|
||||||
|
@Prop({ type: Boolean })
|
||||||
|
userNoCertificate?: boolean;
|
||||||
|
|
||||||
|
@Prop({ type: Number })
|
||||||
|
insurerBirthday?: number;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
driverBirthday?: string | null;
|
||||||
|
}
|
||||||
|
export const PersonSchema = SchemaFactory.createForClass(Person);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class Vehicle {
|
||||||
|
@Prop() plateId?: string;
|
||||||
|
@Prop() name?: string;
|
||||||
|
@Prop() model?: string;
|
||||||
|
@Prop() type?: string;
|
||||||
|
@Prop() isNew?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full external inquiry payload (Tejarat/SandHub) stored as-is
|
||||||
|
* so we never lose fields that are not mapped yet.
|
||||||
|
*/
|
||||||
|
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||||
|
inquiry?: any;
|
||||||
|
}
|
||||||
|
export const VehicleSchema = SchemaFactory.createForClass(Vehicle);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class Insurance {
|
||||||
|
@Prop() policyNumber?: string;
|
||||||
|
@Prop() company?: string;
|
||||||
|
@Prop() startDate?: string;
|
||||||
|
@Prop() endDate?: string;
|
||||||
|
@Prop() financialCeiling?: string;
|
||||||
|
@Prop({ type: [String] })
|
||||||
|
coverages?: string[];
|
||||||
|
}
|
||||||
|
export const InsuranceSchema = SchemaFactory.createForClass(Insurance);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class PartyStatement {
|
||||||
|
|
||||||
|
@Prop({ default: false })
|
||||||
|
acceptsExpertOpinion?: boolean;
|
||||||
|
|
||||||
|
@Prop({ default: false })
|
||||||
|
claimsDamage?: boolean;
|
||||||
|
|
||||||
|
@Prop({ default: false })
|
||||||
|
admitsGuilt?: boolean;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
export const PartyStatementSchema = SchemaFactory.createForClass(PartyStatement);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class EvidenceBundle {
|
||||||
|
|
||||||
|
@Prop({ type: [String] })
|
||||||
|
images?: string[];
|
||||||
|
|
||||||
|
@Prop({ type: [String] })
|
||||||
|
voices?: string[];
|
||||||
|
|
||||||
|
@Prop() videoId?: string;
|
||||||
|
}
|
||||||
|
export const EvidenceBundleSchema = SchemaFactory.createForClass(EvidenceBundle);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class Signature {
|
||||||
|
@Prop() fileId: string;
|
||||||
|
@Prop() fileName: string;
|
||||||
|
@Prop() fileUrl: string;
|
||||||
|
}
|
||||||
|
export const SignatureSchema = SchemaFactory.createForClass(Signature);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class PartyConfirmation {
|
||||||
|
@Prop() partyRole: PartyRole;
|
||||||
|
@Prop() accepted: boolean;
|
||||||
|
@Prop({ type: SignatureSchema })
|
||||||
|
signature?: Signature;
|
||||||
|
}
|
||||||
|
export const PartyConfirmationSchema =
|
||||||
|
SchemaFactory.createForClass(PartyConfirmation);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class Party {
|
||||||
|
|
||||||
|
@Prop({ enum: PartyRole })
|
||||||
|
role: PartyRole;
|
||||||
|
|
||||||
|
@Prop({ type: PersonSchema })
|
||||||
|
person: Person;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Party-submitted location (step-driven: FIRST_LOCATION / SECOND_LOCATION).
|
||||||
|
*/
|
||||||
|
@Prop({ type: LocationSchema })
|
||||||
|
location?: Location;
|
||||||
|
|
||||||
|
@Prop({ type: VehicleSchema })
|
||||||
|
vehicle?: Vehicle;
|
||||||
|
|
||||||
|
@Prop({ type: InsuranceSchema })
|
||||||
|
insurance?: Insurance;
|
||||||
|
|
||||||
|
@Prop({ type: PartyStatementSchema })
|
||||||
|
statement?: PartyStatement;
|
||||||
|
|
||||||
|
@Prop({ type: EvidenceBundleSchema })
|
||||||
|
evidence?: EvidenceBundle;
|
||||||
|
|
||||||
|
@Prop({ type: PartyConfirmationSchema })
|
||||||
|
confirmation?: PartyConfirmation;
|
||||||
|
}
|
||||||
|
export const PartySchema = SchemaFactory.createForClass(Party);
|
||||||
@@ -368,6 +368,15 @@ export class RequestManagementModel {
|
|||||||
@Prop({ type: String, required: true, index: { unique: true } })
|
@Prop({ type: String, required: true, index: { unique: true } })
|
||||||
requestNumber: string;
|
requestNumber: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Human-friendly shared id across blame+claim (e.g. A14235).
|
||||||
|
* Generated once at creation time and reused by claim.
|
||||||
|
*
|
||||||
|
* NOTE: `sparse` keeps existing documents without this field valid.
|
||||||
|
*/
|
||||||
|
@Prop({ type: String, unique: true, index: true, sparse: true, trim: true })
|
||||||
|
publicId?: string;
|
||||||
|
|
||||||
@Prop()
|
@Prop()
|
||||||
blameStatus: ReqBlameStatus | StepsEnum;
|
blameStatus: ReqBlameStatus | StepsEnum;
|
||||||
|
|
||||||
|
|||||||
40
src/request-management/entities/schema/workflow.type.ts
Normal file
40
src/request-management/entities/schema/workflow.type.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
//! NEW
|
||||||
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
|
import { Types } from "mongoose";
|
||||||
|
import { WorkflowStep } from "src/Types&Enums/blame-request-management/blameWorkflow-steps.enum";
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class ActorLock {
|
||||||
|
@Prop({ type: Types.ObjectId, required: true })
|
||||||
|
actorId: Types.ObjectId;
|
||||||
|
|
||||||
|
@Prop({ type: String, required: true })
|
||||||
|
actorName: string;
|
||||||
|
|
||||||
|
@Prop({ type: String, default: "expert" })
|
||||||
|
actorRole: "expert" | "admin";
|
||||||
|
}
|
||||||
|
export const ActorLockSchema = SchemaFactory.createForClass(ActorLock);
|
||||||
|
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class Workflow {
|
||||||
|
@Prop({ type: String, enum: WorkflowStep })
|
||||||
|
currentStep: WorkflowStep;
|
||||||
|
|
||||||
|
@Prop({ type: String, enum: WorkflowStep })
|
||||||
|
nextStep: WorkflowStep;
|
||||||
|
|
||||||
|
@Prop({ type: [String], enum: WorkflowStep, default: [] })
|
||||||
|
completedSteps?: WorkflowStep[];
|
||||||
|
|
||||||
|
@Prop({ default: false })
|
||||||
|
locked?: boolean;
|
||||||
|
|
||||||
|
|
||||||
|
@Prop({ type: Date })
|
||||||
|
lockedAt?: Date;
|
||||||
|
|
||||||
|
@Prop({ type: ActorLockSchema })
|
||||||
|
lockedBy?: ActorLock;
|
||||||
|
}
|
||||||
|
export const WorkflowSchema = SchemaFactory.createForClass(Workflow);
|
||||||
@@ -6,10 +6,13 @@ import { ClientModule } from "src/client/client.module";
|
|||||||
import { PlatesModule } from "src/plates/plates.module";
|
import { PlatesModule } from "src/plates/plates.module";
|
||||||
import { BlameVideoDbService } from "src/request-management/entities/db-service/blame-video.db.service";
|
import { BlameVideoDbService } from "src/request-management/entities/db-service/blame-video.db.service";
|
||||||
import { RequestManagementDbService } from "src/request-management/entities/db-service/request-management.db.service";
|
import { RequestManagementDbService } from "src/request-management/entities/db-service/request-management.db.service";
|
||||||
|
import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service";
|
||||||
import { SandHubModule } from "src/sand-hub/sand-hub.module";
|
import { SandHubModule } from "src/sand-hub/sand-hub.module";
|
||||||
import { UsersModule } from "src/users/users.module";
|
import { UsersModule } from "src/users/users.module";
|
||||||
import { CronModule } from "src/utils/cron/cron.module";
|
import { CronModule } from "src/utils/cron/cron.module";
|
||||||
import { SmsManagerModule } from "src/utils/sms-manager/sms-manager.module";
|
import { SmsManagerModule } from "src/utils/sms-manager/sms-manager.module";
|
||||||
|
import { PublicIdModule } from "src/utils/public-id/public-id.module";
|
||||||
|
import { WorkflowStepManagementModule } from "src/workflow-step-management/workflow-step-management.module";
|
||||||
import { BlameDocumentDbService } from "./entities/db-service/blame-document.db.service";
|
import { BlameDocumentDbService } from "./entities/db-service/blame-document.db.service";
|
||||||
import { BlameVoiceDbService } from "./entities/db-service/blame.voice.db.service";
|
import { BlameVoiceDbService } from "./entities/db-service/blame.voice.db.service";
|
||||||
import { UserSignDbService } from "./entities/db-service/sign.db.service";
|
import { UserSignDbService } from "./entities/db-service/sign.db.service";
|
||||||
@@ -23,12 +26,14 @@ import {
|
|||||||
BlameVoicesModel,
|
BlameVoicesModel,
|
||||||
} from "./entities/schema/blame-voice.schema";
|
} from "./entities/schema/blame-voice.schema";
|
||||||
import { BlameDocumentModel, BlameDocumentSchema } from "./entities/schema/blame-document.schema";
|
import { BlameDocumentModel, BlameDocumentSchema } from "./entities/schema/blame-document.schema";
|
||||||
|
import { BlameRequest, BlameRequestSchema } from "./entities/schema/blame-cases.schema";
|
||||||
import {
|
import {
|
||||||
RequestManagementModel,
|
RequestManagementModel,
|
||||||
RequestManagementSchema,
|
RequestManagementSchema,
|
||||||
} from "./entities/schema/request-management.schema";
|
} from "./entities/schema/request-management.schema";
|
||||||
import { RequestManagementService } from "./request-management.service";
|
import { RequestManagementService } from "./request-management.service";
|
||||||
import { RequestManagementController } from "./request-management.controller";
|
import { RequestManagementController } from "./request-management.controller";
|
||||||
|
import { RequestManagementV2Controller } from "./request-management.v2.controller";
|
||||||
import { ExpertInitiatedController } from "./expert-initiated.controller";
|
import { ExpertInitiatedController } from "./expert-initiated.controller";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -37,12 +42,15 @@ import { ExpertInitiatedController } from "./expert-initiated.controller";
|
|||||||
ClientModule,
|
ClientModule,
|
||||||
SandHubModule,
|
SandHubModule,
|
||||||
SmsManagerModule,
|
SmsManagerModule,
|
||||||
|
PublicIdModule,
|
||||||
|
WorkflowStepManagementModule,
|
||||||
PlatesModule,
|
PlatesModule,
|
||||||
MulterModule.register({
|
MulterModule.register({
|
||||||
dest: "./files/video",
|
dest: "./files/video",
|
||||||
}),
|
}),
|
||||||
MongooseModule.forFeature([
|
MongooseModule.forFeature([
|
||||||
{ name: RequestManagementModel.name, schema: RequestManagementSchema },
|
{ name: RequestManagementModel.name, schema: RequestManagementSchema },
|
||||||
|
{ name: BlameRequest.name, schema: BlameRequestSchema },
|
||||||
{ name: BlameVideosModel.name, schema: BlameVideoSchema },
|
{ name: BlameVideosModel.name, schema: BlameVideoSchema },
|
||||||
{ name: BlameVoicesModel.name, schema: BlameVoiceSchema },
|
{ name: BlameVoicesModel.name, schema: BlameVoiceSchema },
|
||||||
{ name: UserSignModel.name, schema: UserSignSchema },
|
{ name: UserSignModel.name, schema: UserSignSchema },
|
||||||
@@ -51,7 +59,11 @@ import { ExpertInitiatedController } from "./expert-initiated.controller";
|
|||||||
forwardRef(() => ClaimRequestManagementModule),
|
forwardRef(() => ClaimRequestManagementModule),
|
||||||
CronModule,
|
CronModule,
|
||||||
],
|
],
|
||||||
controllers: [RequestManagementController, ExpertInitiatedController],
|
controllers: [
|
||||||
|
RequestManagementController,
|
||||||
|
RequestManagementV2Controller,
|
||||||
|
ExpertInitiatedController,
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
RequestManagementService,
|
RequestManagementService,
|
||||||
BlameVideoDbService,
|
BlameVideoDbService,
|
||||||
@@ -59,6 +71,7 @@ import { ExpertInitiatedController } from "./expert-initiated.controller";
|
|||||||
UserSignDbService,
|
UserSignDbService,
|
||||||
RequestManagementDbService,
|
RequestManagementDbService,
|
||||||
BlameDocumentDbService,
|
BlameDocumentDbService,
|
||||||
|
BlameRequestDbService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
RequestManagementService,
|
RequestManagementService,
|
||||||
@@ -67,6 +80,7 @@ import { ExpertInitiatedController } from "./expert-initiated.controller";
|
|||||||
BlameVoiceDbService,
|
BlameVoiceDbService,
|
||||||
BlameDocumentDbService,
|
BlameDocumentDbService,
|
||||||
UserSignDbService,
|
UserSignDbService,
|
||||||
|
BlameRequestDbService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class RequestManagementModule {}
|
export class RequestManagementModule {}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
382
src/request-management/request-management.v2.controller.ts
Normal file
382
src/request-management/request-management.v2.controller.ts
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
import { extname } from "node:path";
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
Req,
|
||||||
|
UploadedFile,
|
||||||
|
UploadedFiles,
|
||||||
|
UseGuards,
|
||||||
|
UseInterceptors,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import {
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiBody,
|
||||||
|
ApiConsumes,
|
||||||
|
ApiParam,
|
||||||
|
ApiTags,
|
||||||
|
} from "@nestjs/swagger";
|
||||||
|
import { FileFieldsInterceptor, FileInterceptor } from "@nestjs/platform-express";
|
||||||
|
import { diskStorage } from "multer";
|
||||||
|
import { Request } from "express";
|
||||||
|
import { GlobalGuard } from "src/auth/guards/global.guard";
|
||||||
|
import { RolesGuard } from "src/auth/guards/role.guard";
|
||||||
|
import { Roles } from "src/decorators/roles.decorator";
|
||||||
|
import { CurrentUser } from "src/decorators/user.decorator";
|
||||||
|
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||||
|
import { AddPlateDto } from "src/profile/dto/user/AddPlateDto";
|
||||||
|
import {
|
||||||
|
BlameConfessionDtoV2,
|
||||||
|
CreateBlameRequestDtoV2,
|
||||||
|
DescriptionDto,
|
||||||
|
LocationDto,
|
||||||
|
} from "./dto/create-request-management.dto";
|
||||||
|
import { RequestManagementService } from "./request-management.service";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2 API surface for the redesigned `BlameRequest` model.
|
||||||
|
*
|
||||||
|
* Versioning strategy: prefix the controller route with `v2/` so
|
||||||
|
* all endpoints under this controller are automatically V2.
|
||||||
|
*/
|
||||||
|
@Controller("v2/blame-request-management")
|
||||||
|
@ApiTags("blame-request-management (v2)")
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(GlobalGuard, RolesGuard)
|
||||||
|
@Roles(RoleEnum.USER)
|
||||||
|
export class RequestManagementV2Controller {
|
||||||
|
constructor(
|
||||||
|
private readonly requestManagementService: RequestManagementService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@UseGuards(GlobalGuard)
|
||||||
|
async createRequestV2(
|
||||||
|
@CurrentUser() user,
|
||||||
|
@Body() createRequestDto: CreateBlameRequestDtoV2,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.createRequestV2(
|
||||||
|
user,
|
||||||
|
createRequestDto.type,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("/blame-confession/:requestId")
|
||||||
|
@ApiParam({ name: "requestId" })
|
||||||
|
@ApiBody({ type: BlameConfessionDtoV2 })
|
||||||
|
@UseGuards(GlobalGuard)
|
||||||
|
async blameConfessionV2(
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@Body() body: BlameConfessionDtoV2,
|
||||||
|
@CurrentUser() user,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.blameConfessionV2(requestId, body, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiBody({
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
file: {
|
||||||
|
type: "string",
|
||||||
|
format: "binary",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiConsumes("multipart/form-data")
|
||||||
|
@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);
|
||||||
|
const filename = `${file.originalname}-${unique}${ex}`;
|
||||||
|
callback(null, filename);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
@ApiParam({ name: "requestId" })
|
||||||
|
@Post("upload-video/:requestId")
|
||||||
|
@UseGuards(GlobalGuard)
|
||||||
|
async uploadVideoV2(
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@CurrentUser() user,
|
||||||
|
@UploadedFile() file?: Express.Multer.File,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.uploadFirstPartyVideoV2(
|
||||||
|
requestId,
|
||||||
|
file,
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("/initial-form/:requestId")
|
||||||
|
@ApiParam({ name: "requestId" })
|
||||||
|
@ApiBody({ type: AddPlateDto })
|
||||||
|
@UseGuards(GlobalGuard)
|
||||||
|
async initialFormV2(
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@Body() body: AddPlateDto,
|
||||||
|
@CurrentUser() user,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.initialFormV2(requestId, body, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("/add-detail-location/:requestId")
|
||||||
|
@ApiParam({ name: "requestId" })
|
||||||
|
@ApiBody({ type: LocationDto })
|
||||||
|
@UseGuards(GlobalGuard)
|
||||||
|
async addLocationV2(
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@Body() body: LocationDto,
|
||||||
|
@CurrentUser() user,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.addDetailLocationV2(requestId, body, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiBody({
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
file: {
|
||||||
|
type: "string",
|
||||||
|
format: "binary",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiConsumes("multipart/form-data")
|
||||||
|
@UseInterceptors(
|
||||||
|
FileInterceptor("file", {
|
||||||
|
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];
|
||||||
|
const filename = `${flname}-${unique}${ex}`;
|
||||||
|
callback(null, filename);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
@ApiParam({ name: "requestId" })
|
||||||
|
@Post("upload-voice/:requestId")
|
||||||
|
@UseGuards(GlobalGuard)
|
||||||
|
async uploadVoiceV2(
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@CurrentUser() user,
|
||||||
|
@UploadedFile() voice?: Express.Multer.File,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.uploadVoiceV2(requestId, voice, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("/add-detail-description/:requestId")
|
||||||
|
@ApiParam({ name: "requestId" })
|
||||||
|
@ApiBody({ type: DescriptionDto })
|
||||||
|
@UseGuards(GlobalGuard)
|
||||||
|
async addDescriptionV2(
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@Body() body: DescriptionDto,
|
||||||
|
@CurrentUser() user,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.addDescriptionV2(requestId, body, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("add-second-party/:phoneNumber/:requestId/:frontendRoute")
|
||||||
|
@ApiParam({ name: "phoneNumber" })
|
||||||
|
@ApiParam({ name: "requestId" })
|
||||||
|
@ApiParam({ name: "frontendRoute" })
|
||||||
|
@UseGuards(GlobalGuard)
|
||||||
|
async addSecondPartyV2(
|
||||||
|
@Param("phoneNumber") phoneNumber: string,
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@Param("frontendRoute") frontendRoute: string,
|
||||||
|
@CurrentUser() user,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.addSecondPartyV2(
|
||||||
|
requestId,
|
||||||
|
phoneNumber,
|
||||||
|
frontendRoute,
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2: Get list of documents/items the current user needs to resend
|
||||||
|
*/
|
||||||
|
@Get("resend-requirements/:requestId")
|
||||||
|
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
|
async getResendRequirements(
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@CurrentUser() user: any,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.getResendRequirementsV2(
|
||||||
|
requestId,
|
||||||
|
user.sub,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2: User signs/accepts or rejects expert decision
|
||||||
|
*/
|
||||||
|
@Put("sign/:requestId")
|
||||||
|
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
|
@ApiConsumes("multipart/form-data")
|
||||||
|
@ApiBody({
|
||||||
|
description: "Upload signature file and acceptance decision",
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
required: ["sign", "isAccept"],
|
||||||
|
properties: {
|
||||||
|
sign: {
|
||||||
|
type: "string",
|
||||||
|
format: "binary",
|
||||||
|
description: "Signature image file",
|
||||||
|
},
|
||||||
|
isAccept: {
|
||||||
|
type: "boolean",
|
||||||
|
description: "true to accept expert decision, false to reject",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@UseInterceptors(
|
||||||
|
FileInterceptor("sign", {
|
||||||
|
limits: {
|
||||||
|
fileSize: 10 * 1024 * 1024, // 10MB
|
||||||
|
},
|
||||||
|
storage: diskStorage({
|
||||||
|
destination: "./files/signs",
|
||||||
|
filename: (req, file, callback) => {
|
||||||
|
const unique = Date.now();
|
||||||
|
const ext = extname(file.originalname);
|
||||||
|
const filename = `${file.originalname.split(/[.,\s-]/)[0]}-${unique}${ext}`;
|
||||||
|
callback(null, filename);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
async userSignDecision(
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@Body("isAccept") isAccept: string | boolean,
|
||||||
|
@CurrentUser() user: any,
|
||||||
|
@UploadedFile() sign: Express.Multer.File,
|
||||||
|
) {
|
||||||
|
// Convert string "true"/"false" to boolean
|
||||||
|
const acceptDecision = typeof isAccept === "string"
|
||||||
|
? isAccept === "true"
|
||||||
|
: Boolean(isAccept);
|
||||||
|
|
||||||
|
return this.requestManagementService.userSignDecisionV2(
|
||||||
|
requestId,
|
||||||
|
user.sub,
|
||||||
|
acceptDecision,
|
||||||
|
sign,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2: Upload requested documents/evidence for resend
|
||||||
|
* Field names must match ResendItemType enum values (e.g., drivingLicense, carCertificate, voice, video)
|
||||||
|
*/
|
||||||
|
@Put("resend/:requestId")
|
||||||
|
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
|
@ApiConsumes("multipart/form-data")
|
||||||
|
@ApiBody({
|
||||||
|
description:
|
||||||
|
"Upload files with field names matching requested items (e.g., drivingLicense, carCertificate, voice, video)",
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
drivingLicense: {
|
||||||
|
type: "string",
|
||||||
|
format: "binary",
|
||||||
|
description: "Driving license file (if requested)",
|
||||||
|
},
|
||||||
|
carCertificate: {
|
||||||
|
type: "string",
|
||||||
|
format: "binary",
|
||||||
|
description: "Car certificate file (if requested)",
|
||||||
|
},
|
||||||
|
nationalCertificate: {
|
||||||
|
type: "string",
|
||||||
|
format: "binary",
|
||||||
|
description: "National certificate file (if requested)",
|
||||||
|
},
|
||||||
|
carGreenCard: {
|
||||||
|
type: "string",
|
||||||
|
format: "binary",
|
||||||
|
description: "Car green card file (if requested)",
|
||||||
|
},
|
||||||
|
voice: {
|
||||||
|
type: "string",
|
||||||
|
format: "binary",
|
||||||
|
description: "Voice recording file (if requested)",
|
||||||
|
},
|
||||||
|
video: {
|
||||||
|
type: "string",
|
||||||
|
format: "binary",
|
||||||
|
description: "Video file (if requested)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@UseInterceptors(
|
||||||
|
FileFieldsInterceptor(
|
||||||
|
[
|
||||||
|
{ name: "drivingLicense", maxCount: 1 },
|
||||||
|
{ name: "carCertificate", maxCount: 1 },
|
||||||
|
{ name: "nationalCertificate", maxCount: 1 },
|
||||||
|
{ name: "carGreenCard", maxCount: 1 },
|
||||||
|
{ name: "voice", maxCount: 1 },
|
||||||
|
{ name: "video", maxCount: 1 },
|
||||||
|
],
|
||||||
|
{
|
||||||
|
storage: diskStorage({
|
||||||
|
destination: (req, file, cb) => {
|
||||||
|
cb(null, "./uploads");
|
||||||
|
},
|
||||||
|
filename: (req, file, cb) => {
|
||||||
|
const uniqueSuffix =
|
||||||
|
Date.now() + "-" + Math.round(Math.random() * 1e9);
|
||||||
|
const ext = extname(file.originalname);
|
||||||
|
cb(null, `${file.fieldname}-${uniqueSuffix}${ext}`);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async uploadResendDocuments(
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@CurrentUser() user: any,
|
||||||
|
@UploadedFiles()
|
||||||
|
files: {
|
||||||
|
drivingLicense?: Express.Multer.File[];
|
||||||
|
carCertificate?: Express.Multer.File[];
|
||||||
|
nationalCertificate?: Express.Multer.File[];
|
||||||
|
carGreenCard?: Express.Multer.File[];
|
||||||
|
voice?: Express.Multer.File[];
|
||||||
|
video?: Express.Multer.File[];
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.uploadResendDocumentsV2(
|
||||||
|
requestId,
|
||||||
|
user.sub,
|
||||||
|
files,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -20,6 +20,10 @@ export class SandHubService {
|
|||||||
private loginToken: string | null = null;
|
private loginToken: string | null = null;
|
||||||
private tokenExpiry: Date | null = null;
|
private tokenExpiry: Date | null = null;
|
||||||
|
|
||||||
|
// Tejarat inquiry auth (82.99.202.245:3027)
|
||||||
|
private tejaratAccessToken: string | null = null;
|
||||||
|
private tejaratTokenExpiry: Date | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly httpService: HttpService,
|
private readonly httpService: HttpService,
|
||||||
private readonly sandHubDbService: SandHubDbService,
|
private readonly sandHubDbService: SandHubDbService,
|
||||||
@@ -62,6 +66,128 @@ export class SandHubService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getTejaratAccessToken(): Promise<string> {
|
||||||
|
if (
|
||||||
|
this.tejaratAccessToken &&
|
||||||
|
this.tejaratTokenExpiry &&
|
||||||
|
this.tejaratTokenExpiry > new Date()
|
||||||
|
) {
|
||||||
|
return this.tejaratAccessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl =
|
||||||
|
process.env.TEJARAT_INQUIRY_BASE_URL ?? "http://82.99.202.245:3027";
|
||||||
|
const email = process.env.TEJARAT_INQUIRY_EMAIL;
|
||||||
|
const password = process.env.TEJARAT_INQUIRY_PASSWORD;
|
||||||
|
|
||||||
|
if (!email || !password) {
|
||||||
|
throw new UnauthorizedException(
|
||||||
|
"Tejarat inquiry credentials are not configured (TEJARAT_INQUIRY_EMAIL/TEJARAT_INQUIRY_PASSWORD)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
"Tejarat inquiry token is missing or expired. Fetching a new one...",
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.httpService.axiosRef.post(
|
||||||
|
`${baseUrl}/user/login`,
|
||||||
|
{ email, password },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: "*/*",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
timeout: 30000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const token = response.data?.accessToken;
|
||||||
|
if (!token) {
|
||||||
|
throw new Error("No access token returned from Tejarat login");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tejaratAccessToken = token;
|
||||||
|
// No explicit expiry in response; keep a conservative cache window.
|
||||||
|
this.tejaratTokenExpiry = new Date(Date.now() + 55 * 60 * 1000);
|
||||||
|
|
||||||
|
return this.tejaratAccessToken;
|
||||||
|
} catch (er) {
|
||||||
|
this.logger.error("Failed to login to Tejarat inquiry:", er?.message || er);
|
||||||
|
this.tejaratAccessToken = null;
|
||||||
|
this.tejaratTokenExpiry = null;
|
||||||
|
throw new UnauthorizedException("Tejarat inquiry authentication failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async makeTejaratRequest(url: string, payload: any, maxRetries = 2) {
|
||||||
|
const INITIAL_DELAY = 500;
|
||||||
|
const BACKOFF_FACTOR = 2;
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||||
|
const token = await this.getTejaratAccessToken();
|
||||||
|
try {
|
||||||
|
const response = await axios.post(url, payload, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
if (!response?.data) throw new Error("EMPTY_RESPONSE");
|
||||||
|
return response.data;
|
||||||
|
} catch (err: any) {
|
||||||
|
const status = err?.response?.status;
|
||||||
|
const data = err?.response?.data;
|
||||||
|
|
||||||
|
this.logger.error(
|
||||||
|
`Tejarat request failed (attempt ${attempt + 1}/${maxRetries}) to ${url} with status ${
|
||||||
|
status ?? "NO_STATUS"
|
||||||
|
}`,
|
||||||
|
data ? JSON.stringify(data) : err?.message || err,
|
||||||
|
);
|
||||||
|
|
||||||
|
// If unauthorized, clear token and retry once
|
||||||
|
if (status === 401) {
|
||||||
|
this.tejaratAccessToken = null;
|
||||||
|
this.tejaratTokenExpiry = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const delay = INITIAL_DELAY * Math.pow(BACKOFF_FACTOR, attempt);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
|
if (attempt === maxRetries - 1) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tejarat block inquiry (replaces SandHub call for V2 flows).
|
||||||
|
* Returns both raw + mapped (old-format) response.
|
||||||
|
*/
|
||||||
|
async getTejaratBlockInquiry(userDetail: SandHubDetailDto): Promise<{
|
||||||
|
raw: any;
|
||||||
|
mapped: any;
|
||||||
|
}> {
|
||||||
|
const baseUrl =
|
||||||
|
process.env.TEJARAT_INQUIRY_BASE_URL ?? "http://82.99.202.245:3027";
|
||||||
|
const requestPayload = {
|
||||||
|
leftTwoDigits: String(userDetail.plate.leftDigits),
|
||||||
|
serialLetter: String(userDetail.plate.centerAlphabet),
|
||||||
|
threeDigits: String(userDetail.plate.centerDigits),
|
||||||
|
rightTwoDigits: String(userDetail.plate.ir),
|
||||||
|
nationalCode: userDetail.nationalCodeOfInsurer,
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestUrl = `${baseUrl}/block-inquiry-tejarat`;
|
||||||
|
const raw = await this.makeTejaratRequest(requestUrl, requestPayload);
|
||||||
|
const mapped = this.mapNewApiResponseToOldFormat(raw);
|
||||||
|
return { raw, mapped };
|
||||||
|
}
|
||||||
|
|
||||||
async getSandHubDataFromId(sandHubId) {
|
async getSandHubDataFromId(sandHubId) {
|
||||||
return await this.sandHubDbService.findOneBySandHubId(sandHubId);
|
return await this.sandHubDbService.findOneBySandHubId(sandHubId);
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/utils/public-id/counter.schema.ts
Normal file
16
src/utils/public-id/counter.schema.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
|
|
||||||
|
@Schema({ collection: "counters", versionKey: false })
|
||||||
|
export class CounterModel {
|
||||||
|
/**
|
||||||
|
* Counter name (e.g. "requestPublicId")
|
||||||
|
*/
|
||||||
|
@Prop({ type: String, required: true })
|
||||||
|
_id: string;
|
||||||
|
|
||||||
|
@Prop({ type: Number, required: true, default: 0 })
|
||||||
|
seq: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CounterSchema = SchemaFactory.createForClass(CounterModel);
|
||||||
|
|
||||||
16
src/utils/public-id/public-id.module.ts
Normal file
16
src/utils/public-id/public-id.module.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { MongooseModule } from "@nestjs/mongoose";
|
||||||
|
import { CounterModel, CounterSchema } from "./counter.schema";
|
||||||
|
import { PublicIdService } from "./public-id.service";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
MongooseModule.forFeature([
|
||||||
|
{ name: CounterModel.name, schema: CounterSchema },
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
providers: [PublicIdService],
|
||||||
|
exports: [PublicIdService],
|
||||||
|
})
|
||||||
|
export class PublicIdModule {}
|
||||||
|
|
||||||
55
src/utils/public-id/public-id.service.ts
Normal file
55
src/utils/public-id/public-id.service.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { InjectModel } from "@nestjs/mongoose";
|
||||||
|
import { Model } from "mongoose";
|
||||||
|
import { CounterModel } from "./counter.schema";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PublicIdService {
|
||||||
|
private static readonly COUNTER_NAME = "requestPublicId";
|
||||||
|
private static readonly DIGITS = 5;
|
||||||
|
private static readonly BASE = 10 ** PublicIdService.DIGITS; // 100000
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@InjectModel(CounterModel.name)
|
||||||
|
private readonly counterModel: Model<CounterModel>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a human-friendly id like:
|
||||||
|
* A00001 ... A99999, B00000 ... Z99999, AA00000, AB00000, ...
|
||||||
|
*
|
||||||
|
* - Atomic uniqueness is guaranteed by MongoDB `$inc` on a single counter doc.
|
||||||
|
* - A unique index on `publicId` in target collections provides extra safety.
|
||||||
|
*/
|
||||||
|
async generateRequestPublicId(): Promise<string> {
|
||||||
|
const counter = await this.counterModel.findOneAndUpdate(
|
||||||
|
{ _id: PublicIdService.COUNTER_NAME },
|
||||||
|
{ $inc: { seq: 1 } },
|
||||||
|
{ upsert: true, new: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const seq = counter.seq; // starts at 1
|
||||||
|
const zeroBased = seq - 1;
|
||||||
|
const prefixIndex = Math.floor(zeroBased / PublicIdService.BASE);
|
||||||
|
const digits = zeroBased % PublicIdService.BASE;
|
||||||
|
|
||||||
|
const prefix = this.indexToLetters(prefixIndex);
|
||||||
|
const suffix = String(digits).padStart(PublicIdService.DIGITS, "0");
|
||||||
|
return `${prefix}${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 0 -> A, 25 -> Z, 26 -> AA, 27 -> AB, ...
|
||||||
|
*/
|
||||||
|
private indexToLetters(index: number): string {
|
||||||
|
let n = index;
|
||||||
|
let result = "";
|
||||||
|
do {
|
||||||
|
const rem = n % 26;
|
||||||
|
result = String.fromCharCode(65 + rem) + result;
|
||||||
|
n = Math.floor(n / 26) - 1;
|
||||||
|
} while (n >= 0);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
510
src/workflow-step-management/dto/claim-workflow-step.examples.ts
Normal file
510
src/workflow-step-management/dto/claim-workflow-step.examples.ts
Normal file
@@ -0,0 +1,510 @@
|
|||||||
|
/**
|
||||||
|
* Swagger example bodies for CLAIM workflow step management
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const createClaimWorkflowStepExamples = {
|
||||||
|
claimCreated: {
|
||||||
|
summary: 'Claim Created Step',
|
||||||
|
value: {
|
||||||
|
stepKey: 'CLAIM_CREATED',
|
||||||
|
type: 'CLAIM',
|
||||||
|
stepNumber: 1,
|
||||||
|
isActive: true,
|
||||||
|
stepName_fa: 'ایجاد درخواست خسارت',
|
||||||
|
stepName_en: 'Create Claim Request',
|
||||||
|
description_fa: 'ایجاد درخواست خسارت از پرونده تعیین تقصیر تکمیل شده - فقط طرف آسیبدیده میتواند درخواست دهد',
|
||||||
|
description_en: 'Create claim request from completed blame case - only damaged party can proceed',
|
||||||
|
category: 'CLAIM',
|
||||||
|
requiredPreviousSteps: [],
|
||||||
|
nextPossibleSteps: ['SELECT_OUTER_PARTS'],
|
||||||
|
allowedRoles: ['user'],
|
||||||
|
estimatedDuration: 1,
|
||||||
|
fields: [],
|
||||||
|
metadata: {
|
||||||
|
icon: 'file-plus',
|
||||||
|
color: '#8B5CF6',
|
||||||
|
requiresBlameCase: true,
|
||||||
|
blameStatusRequired: 'COMPLETED',
|
||||||
|
bothPartiesMustAccept: true,
|
||||||
|
userMustBeDamagedParty: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
selectOuterParts: {
|
||||||
|
summary: 'Select Outer Parts Step',
|
||||||
|
value: {
|
||||||
|
stepKey: 'SELECT_OUTER_PARTS',
|
||||||
|
type: 'CLAIM',
|
||||||
|
stepNumber: 2,
|
||||||
|
isActive: true,
|
||||||
|
stepName_fa: 'انتخاب قطعات خارجی آسیبدیده',
|
||||||
|
stepName_en: 'Select Damaged Outer Parts',
|
||||||
|
description_fa: 'انتخاب قطعات خارجی خودرو که در حادثه آسیب دیدهاند (کاپوت، دربها، سپر و...)',
|
||||||
|
description_en: 'Select vehicle outer parts damaged in the accident (hood, doors, bumpers, etc.)',
|
||||||
|
category: 'CLAIM',
|
||||||
|
requiredPreviousSteps: ['CLAIM_CREATED'],
|
||||||
|
nextPossibleSteps: ['SELECT_OTHER_PARTS'],
|
||||||
|
allowedRoles: ['user'],
|
||||||
|
estimatedDuration: 3,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
id: '67fa10c259e15f231a2d1001',
|
||||||
|
name_fa: 'قطعات خارجی',
|
||||||
|
name_en: 'outerParts',
|
||||||
|
label_fa: 'قطعات خارجی آسیبدیده',
|
||||||
|
label_en: 'Damaged Outer Parts',
|
||||||
|
placeholder_fa: 'قطعات خارجی آسیبدیده را انتخاب کنید',
|
||||||
|
placeholder_en: 'Select damaged outer parts',
|
||||||
|
value: [
|
||||||
|
{ label_fa: 'کاپوت', label_en: 'Hood', value: 'hood' },
|
||||||
|
{ label_fa: 'درب جلو راست', label_en: 'Front Right Door', value: 'front_right_door' },
|
||||||
|
{ label_fa: 'درب جلو چپ', label_en: 'Front Left Door', value: 'front_left_door' },
|
||||||
|
{ label_fa: 'درب عقب راست', label_en: 'Rear Right Door', value: 'rear_right_door' },
|
||||||
|
{ label_fa: 'درب عقب چپ', label_en: 'Rear Left Door', value: 'rear_left_door' },
|
||||||
|
{ label_fa: 'سپر جلو', label_en: 'Front Bumper', value: 'front_bumper' },
|
||||||
|
{ label_fa: 'سپر عقب', label_en: 'Rear Bumper', value: 'rear_bumper' },
|
||||||
|
{ label_fa: 'گلگیر جلو راست', label_en: 'Front Right Fender', value: 'front_right_fender' },
|
||||||
|
{ label_fa: 'گلگیر جلو چپ', label_en: 'Front Left Fender', value: 'front_left_fender' },
|
||||||
|
{ label_fa: 'گلگیر عقب راست', label_en: 'Rear Right Fender', value: 'rear_right_fender' },
|
||||||
|
{ label_fa: 'گلگیر عقب چپ', label_en: 'Rear Left Fender', value: 'rear_left_fender' },
|
||||||
|
{ label_fa: 'صندوق عقب', label_en: 'Trunk', value: 'trunk' },
|
||||||
|
{ label_fa: 'سقف', label_en: 'Roof', value: 'roof' }
|
||||||
|
],
|
||||||
|
dataType: 'multiselect',
|
||||||
|
required: true,
|
||||||
|
order: 1,
|
||||||
|
isActive: true,
|
||||||
|
validation: {
|
||||||
|
minItems: 1,
|
||||||
|
maxItems: 13
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
metadata: {
|
||||||
|
icon: 'car',
|
||||||
|
color: '#3B82F6',
|
||||||
|
requiresMultipleSelection: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
selectOtherParts: {
|
||||||
|
summary: 'Select Other Parts & Bank Info Step',
|
||||||
|
value: {
|
||||||
|
stepKey: 'SELECT_OTHER_PARTS',
|
||||||
|
type: 'CLAIM',
|
||||||
|
stepNumber: 3,
|
||||||
|
isActive: true,
|
||||||
|
stepName_fa: 'انتخاب سایر قطعات و اطلاعات بانکی',
|
||||||
|
stepName_en: 'Select Other Parts & Bank Information',
|
||||||
|
description_fa: 'انتخاب سایر قطعات آسیبدیده (غیر از بدنه)، شماره شبا و کد ملی بیمهگذار',
|
||||||
|
description_en: 'Select other damaged parts (non-body), Sheba number and insurer national code',
|
||||||
|
category: 'CLAIM',
|
||||||
|
requiredPreviousSteps: ['SELECT_OUTER_PARTS'],
|
||||||
|
nextPossibleSteps: ['UPLOAD_REQUIRED_DOCUMENTS'],
|
||||||
|
allowedRoles: ['user'],
|
||||||
|
estimatedDuration: 4,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
id: '67fa10c259e15f231a2d1002',
|
||||||
|
name_fa: 'سایر قطعات',
|
||||||
|
name_en: 'otherParts',
|
||||||
|
label_fa: 'سایر قطعات آسیبدیده',
|
||||||
|
label_en: 'Other Damaged Parts',
|
||||||
|
placeholder_fa: 'سایر قطعات آسیبدیده را انتخاب کنید',
|
||||||
|
placeholder_en: 'Select other damaged parts',
|
||||||
|
value: [
|
||||||
|
{ label_fa: 'موتور', label_en: 'Engine', value: 'engine' },
|
||||||
|
{ label_fa: 'سیستم تعلیق', label_en: 'Suspension', value: 'suspension' },
|
||||||
|
{ label_fa: 'سیستم ترمز', label_en: 'Brake System', value: 'brake_system' },
|
||||||
|
{ label_fa: 'سیستم برق', label_en: 'Electrical System', value: 'electrical' },
|
||||||
|
{ label_fa: 'رادیاتور', label_en: 'Radiator', value: 'radiator' },
|
||||||
|
{ label_fa: 'گیربکس', label_en: 'Transmission', value: 'transmission' },
|
||||||
|
{ label_fa: 'اگزوز', label_en: 'Exhaust', value: 'exhaust' },
|
||||||
|
{ label_fa: 'چراغ جلو', label_en: 'Headlight', value: 'headlight' },
|
||||||
|
{ label_fa: 'چراغ عقب', label_en: 'Taillight', value: 'taillight' },
|
||||||
|
{ label_fa: 'آینه', label_en: 'Mirror', value: 'mirror' },
|
||||||
|
{ label_fa: 'شیشه', label_en: 'Glass', value: 'glass' }
|
||||||
|
],
|
||||||
|
dataType: 'multiselect',
|
||||||
|
required: false,
|
||||||
|
order: 1,
|
||||||
|
isActive: true,
|
||||||
|
validation: {
|
||||||
|
minItems: 0,
|
||||||
|
maxItems: 11
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '67fa10c259e15f231a2d1003',
|
||||||
|
name_fa: 'شماره شبا',
|
||||||
|
name_en: 'shebaNumber',
|
||||||
|
label_fa: 'شماره شبا',
|
||||||
|
label_en: 'Sheba Number',
|
||||||
|
placeholder_fa: 'شماره شبا (24 رقم بدون IR)',
|
||||||
|
placeholder_en: 'Sheba number (24 digits without IR)',
|
||||||
|
value: null,
|
||||||
|
dataType: 'string',
|
||||||
|
required: true,
|
||||||
|
order: 2,
|
||||||
|
isActive: true,
|
||||||
|
validation: {
|
||||||
|
pattern: '^[0-9]{24}$',
|
||||||
|
minLength: 24,
|
||||||
|
maxLength: 24
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '67fa10c259e15f231a2d1004',
|
||||||
|
name_fa: 'کد ملی مالک',
|
||||||
|
name_en: 'nationalCodeOfOwner',
|
||||||
|
label_fa: 'کد ملی مالک',
|
||||||
|
label_en: 'National Code of Owner',
|
||||||
|
placeholder_fa: 'کد ملی 10 رقمی مالک',
|
||||||
|
placeholder_en: '10-digit national code of owner',
|
||||||
|
value: null,
|
||||||
|
dataType: 'string',
|
||||||
|
required: true,
|
||||||
|
order: 3,
|
||||||
|
isActive: true,
|
||||||
|
validation: {
|
||||||
|
pattern: '^[0-9]{10}$',
|
||||||
|
minLength: 10,
|
||||||
|
maxLength: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
metadata: {
|
||||||
|
icon: 'list',
|
||||||
|
color: '#10B981',
|
||||||
|
requiresBankInfo: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
uploadRequiredDocuments: {
|
||||||
|
summary: 'Upload Required Documents Step',
|
||||||
|
value: {
|
||||||
|
stepKey: 'UPLOAD_REQUIRED_DOCUMENTS',
|
||||||
|
type: 'CLAIM',
|
||||||
|
stepNumber: 4,
|
||||||
|
isActive: true,
|
||||||
|
stepName_fa: 'آپلود مدارک مورد نیاز',
|
||||||
|
stepName_en: 'Upload Required Documents',
|
||||||
|
description_fa: 'آپلود مدارک مورد نیاز برای بررسی خسارت (لیست داینامیک بر اساس قطعات انتخاب شده)',
|
||||||
|
description_en: 'Upload required documents for claim review (dynamic list based on selected parts)',
|
||||||
|
category: 'CLAIM',
|
||||||
|
requiredPreviousSteps: ['SELECT_OTHER_PARTS'],
|
||||||
|
nextPossibleSteps: ['CAPTURE_PART_DAMAGES'],
|
||||||
|
allowedRoles: ['user'],
|
||||||
|
estimatedDuration: 5,
|
||||||
|
fields: [],
|
||||||
|
metadata: {
|
||||||
|
icon: 'upload',
|
||||||
|
color: '#F59E0B',
|
||||||
|
dynamicFields: true,
|
||||||
|
requiresAPI: '/claim-request-management/car-part-image-required/:claimRequestID',
|
||||||
|
note: 'لیست مدارک به صورت داینامیک از API دریافت میشود'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
capturePartDamages: {
|
||||||
|
summary: 'Capture Part Damages Step',
|
||||||
|
value: {
|
||||||
|
stepKey: 'CAPTURE_PART_DAMAGES',
|
||||||
|
type: 'CLAIM',
|
||||||
|
stepNumber: 5,
|
||||||
|
isActive: true,
|
||||||
|
stepName_fa: 'ثبت تصاویر آسیبهای قطعات',
|
||||||
|
stepName_en: 'Capture Part Damages',
|
||||||
|
description_fa: 'ثبت تصاویر آسیب برای هر قطعه (یک درخواست به ازای هر قطعه)',
|
||||||
|
description_en: 'Capture damage images for each part (one request per part)',
|
||||||
|
category: 'CLAIM',
|
||||||
|
requiredPreviousSteps: ['UPLOAD_REQUIRED_DOCUMENTS'],
|
||||||
|
nextPossibleSteps: ['USER_SUBMISSION_COMPLETE'],
|
||||||
|
allowedRoles: ['user'],
|
||||||
|
estimatedDuration: 10,
|
||||||
|
fields: [],
|
||||||
|
metadata: {
|
||||||
|
icon: 'camera',
|
||||||
|
color: '#EF4444',
|
||||||
|
dynamicFields: true,
|
||||||
|
requiresAPI: '/claim-request-management/capture-car-part-damage/:claimRequestID/:partId',
|
||||||
|
requiresCamera: true,
|
||||||
|
multipleRequests: true,
|
||||||
|
note: 'برای هر قطعه یک درخواست جداگانه ارسال میشود'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
userSubmissionComplete: {
|
||||||
|
summary: 'User Submission Complete',
|
||||||
|
value: {
|
||||||
|
stepKey: 'USER_SUBMISSION_COMPLETE',
|
||||||
|
type: 'CLAIM',
|
||||||
|
stepNumber: 6,
|
||||||
|
isActive: true,
|
||||||
|
stepName_fa: 'تکمیل ثبت اطلاعات کاربر',
|
||||||
|
stepName_en: 'User Submission Complete',
|
||||||
|
description_fa: 'اطلاعات کاربر به طور کامل ثبت شد و در انتظار بررسی کارشناس خسارت',
|
||||||
|
description_en: 'User information completed, waiting for damage expert review',
|
||||||
|
category: 'CLAIM',
|
||||||
|
requiredPreviousSteps: ['CAPTURE_PART_DAMAGES'],
|
||||||
|
nextPossibleSteps: ['EXPERT_DAMAGE_ASSESSMENT'],
|
||||||
|
allowedRoles: ['user', 'damage_expert', 'admin'],
|
||||||
|
estimatedDuration: 0,
|
||||||
|
fields: [],
|
||||||
|
metadata: {
|
||||||
|
icon: 'check-circle',
|
||||||
|
color: '#10B981',
|
||||||
|
readOnly: true,
|
||||||
|
isSystemStep: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
expertDamageAssessment: {
|
||||||
|
summary: 'Expert Damage Assessment',
|
||||||
|
value: {
|
||||||
|
stepKey: 'EXPERT_DAMAGE_ASSESSMENT',
|
||||||
|
type: 'CLAIM',
|
||||||
|
stepNumber: 7,
|
||||||
|
isActive: true,
|
||||||
|
stepName_fa: 'ارزیابی خسارت توسط کارشناس',
|
||||||
|
stepName_en: 'Expert Damage Assessment',
|
||||||
|
description_fa: 'بررسی و ارزیابی خسارت توسط کارشناس خسارت',
|
||||||
|
description_en: 'Damage review and assessment by damage expert',
|
||||||
|
category: 'EXPERT',
|
||||||
|
requiredPreviousSteps: ['USER_SUBMISSION_COMPLETE'],
|
||||||
|
nextPossibleSteps: ['EXPERT_COST_EVALUATION'],
|
||||||
|
allowedRoles: ['damage_expert', 'admin'],
|
||||||
|
estimatedDuration: 30,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
id: '67fa10c259e15f231a2d1005',
|
||||||
|
name_fa: 'وضعیت ارزیابی',
|
||||||
|
name_en: 'assessmentStatus',
|
||||||
|
label_fa: 'وضعیت ارزیابی',
|
||||||
|
label_en: 'Assessment Status',
|
||||||
|
placeholder_fa: 'وضعیت ارزیابی را انتخاب کنید',
|
||||||
|
placeholder_en: 'Select assessment status',
|
||||||
|
value: [
|
||||||
|
{ label_fa: 'تایید شده', label_en: 'Approved', value: 'approved' },
|
||||||
|
{ label_fa: 'نیاز به بازنگری', label_en: 'Needs Revision', value: 'needs_revision' },
|
||||||
|
{ label_fa: 'رد شده', label_en: 'Rejected', value: 'rejected' }
|
||||||
|
],
|
||||||
|
dataType: 'select',
|
||||||
|
required: true,
|
||||||
|
order: 1,
|
||||||
|
isActive: true,
|
||||||
|
validation: {
|
||||||
|
enum: ['approved', 'needs_revision', 'rejected']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '67fa10c259e15f231a2d1006',
|
||||||
|
name_fa: 'توضیحات کارشناس',
|
||||||
|
name_en: 'expertComments',
|
||||||
|
label_fa: 'توضیحات کارشناس',
|
||||||
|
label_en: 'Expert Comments',
|
||||||
|
placeholder_fa: 'توضیحات خود را وارد کنید',
|
||||||
|
placeholder_en: 'Enter your comments',
|
||||||
|
value: null,
|
||||||
|
dataType: 'textarea',
|
||||||
|
required: true,
|
||||||
|
order: 2,
|
||||||
|
isActive: true,
|
||||||
|
validation: {
|
||||||
|
minLength: 10,
|
||||||
|
maxLength: 2000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
metadata: {
|
||||||
|
icon: 'clipboard-check',
|
||||||
|
color: '#8B5CF6',
|
||||||
|
requiresExpertRole: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
expertCostEvaluation: {
|
||||||
|
summary: 'Expert Cost Evaluation',
|
||||||
|
value: {
|
||||||
|
stepKey: 'EXPERT_COST_EVALUATION',
|
||||||
|
type: 'CLAIM',
|
||||||
|
stepNumber: 8,
|
||||||
|
isActive: true,
|
||||||
|
stepName_fa: 'برآورد هزینه توسط کارشناس',
|
||||||
|
stepName_en: 'Expert Cost Evaluation',
|
||||||
|
description_fa: 'محاسبه و برآورد هزینه تعمیرات توسط کارشناس',
|
||||||
|
description_en: 'Repair cost calculation and estimation by expert',
|
||||||
|
category: 'EXPERT',
|
||||||
|
requiredPreviousSteps: ['EXPERT_DAMAGE_ASSESSMENT'],
|
||||||
|
nextPossibleSteps: ['INSURER_REVIEW'],
|
||||||
|
allowedRoles: ['damage_expert', 'admin'],
|
||||||
|
estimatedDuration: 20,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
id: '67fa10c259e15f231a2d1007',
|
||||||
|
name_fa: 'هزینه کل برآورد شده',
|
||||||
|
name_en: 'estimatedTotalCost',
|
||||||
|
label_fa: 'هزینه کل برآورد شده (ریال)',
|
||||||
|
label_en: 'Estimated Total Cost (Rials)',
|
||||||
|
placeholder_fa: 'مبلغ را وارد کنید',
|
||||||
|
placeholder_en: 'Enter amount',
|
||||||
|
value: null,
|
||||||
|
dataType: 'number',
|
||||||
|
required: true,
|
||||||
|
order: 1,
|
||||||
|
isActive: true,
|
||||||
|
validation: {
|
||||||
|
min: 0,
|
||||||
|
max: 999999999999
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '67fa10c259e15f231a2d1008',
|
||||||
|
name_fa: 'جزئیات هزینه',
|
||||||
|
name_en: 'costBreakdown',
|
||||||
|
label_fa: 'جزئیات هزینه',
|
||||||
|
label_en: 'Cost Breakdown',
|
||||||
|
placeholder_fa: 'جزئیات هزینه را وارد کنید',
|
||||||
|
placeholder_en: 'Enter cost breakdown',
|
||||||
|
value: null,
|
||||||
|
dataType: 'textarea',
|
||||||
|
required: true,
|
||||||
|
order: 2,
|
||||||
|
isActive: true,
|
||||||
|
validation: {
|
||||||
|
minLength: 20,
|
||||||
|
maxLength: 5000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
metadata: {
|
||||||
|
icon: 'dollar-sign',
|
||||||
|
color: '#F59E0B',
|
||||||
|
requiresExpertRole: true,
|
||||||
|
requiresCostCalculation: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
insurerReview: {
|
||||||
|
summary: 'Insurer Review',
|
||||||
|
value: {
|
||||||
|
stepKey: 'INSURER_REVIEW',
|
||||||
|
type: 'CLAIM',
|
||||||
|
stepNumber: 9,
|
||||||
|
isActive: true,
|
||||||
|
stepName_fa: 'بررسی بیمهگر',
|
||||||
|
stepName_en: 'Insurer Review',
|
||||||
|
description_fa: 'بررسی نهایی و تایید یا رد خسارت توسط بیمهگر',
|
||||||
|
description_en: 'Final review and approval/rejection by insurer',
|
||||||
|
category: 'INSURER',
|
||||||
|
requiredPreviousSteps: ['EXPERT_COST_EVALUATION'],
|
||||||
|
nextPossibleSteps: ['CLAIM_COMPLETED'],
|
||||||
|
allowedRoles: ['insurer', 'admin'],
|
||||||
|
estimatedDuration: 60,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
id: '67fa10c259e15f231a2d1009',
|
||||||
|
name_fa: 'تصمیم بیمهگر',
|
||||||
|
name_en: 'insurerDecision',
|
||||||
|
label_fa: 'تصمیم بیمهگر',
|
||||||
|
label_en: 'Insurer Decision',
|
||||||
|
placeholder_fa: 'تصمیم خود را انتخاب کنید',
|
||||||
|
placeholder_en: 'Select your decision',
|
||||||
|
value: [
|
||||||
|
{ label_fa: 'تایید', label_en: 'Approved', value: 'approved' },
|
||||||
|
{ label_fa: 'رد', label_en: 'Rejected', value: 'rejected' }
|
||||||
|
],
|
||||||
|
dataType: 'select',
|
||||||
|
required: true,
|
||||||
|
order: 1,
|
||||||
|
isActive: true,
|
||||||
|
validation: {
|
||||||
|
enum: ['approved', 'rejected']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '67fa10c259e15f231a2d100a',
|
||||||
|
name_fa: 'توضیحات بیمهگر',
|
||||||
|
name_en: 'insurerNotes',
|
||||||
|
label_fa: 'توضیحات بیمهگر',
|
||||||
|
label_en: 'Insurer Notes',
|
||||||
|
placeholder_fa: 'توضیحات خود را وارد کنید',
|
||||||
|
placeholder_en: 'Enter your notes',
|
||||||
|
value: null,
|
||||||
|
dataType: 'textarea',
|
||||||
|
required: true,
|
||||||
|
order: 2,
|
||||||
|
isActive: true,
|
||||||
|
validation: {
|
||||||
|
minLength: 10,
|
||||||
|
maxLength: 2000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
metadata: {
|
||||||
|
icon: 'shield-check',
|
||||||
|
color: '#6366F1',
|
||||||
|
requiresInsurerRole: true,
|
||||||
|
isFinalStep: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
claimCompleted: {
|
||||||
|
summary: 'Claim Completed',
|
||||||
|
value: {
|
||||||
|
stepKey: 'CLAIM_COMPLETED',
|
||||||
|
type: 'CLAIM',
|
||||||
|
stepNumber: 10,
|
||||||
|
isActive: true,
|
||||||
|
stepName_fa: 'تکمیل خسارت',
|
||||||
|
stepName_en: 'Claim Completed',
|
||||||
|
description_fa: 'خسارت به طور کامل بررسی و نتیجهگیری شده است',
|
||||||
|
description_en: 'Claim has been fully reviewed and concluded',
|
||||||
|
category: 'FINAL',
|
||||||
|
requiredPreviousSteps: ['INSURER_REVIEW'],
|
||||||
|
nextPossibleSteps: [],
|
||||||
|
allowedRoles: ['user', 'damage_expert', 'insurer', 'admin'],
|
||||||
|
estimatedDuration: 0,
|
||||||
|
fields: [],
|
||||||
|
metadata: {
|
||||||
|
icon: 'check-circle',
|
||||||
|
color: '#10B981',
|
||||||
|
readOnly: true,
|
||||||
|
isSystemStep: true,
|
||||||
|
isFinalStep: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateClaimWorkflowStepExamples = {
|
||||||
|
example1: {
|
||||||
|
summary: 'Update claim step name',
|
||||||
|
value: {
|
||||||
|
stepName_fa: 'انتخاب قطعات خارجی - بروزرسانی شده',
|
||||||
|
stepName_en: 'Select Outer Parts - Updated',
|
||||||
|
category: 'CLAIM',
|
||||||
|
isActive: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
example2: {
|
||||||
|
summary: 'Update claim step metadata',
|
||||||
|
value: {
|
||||||
|
metadata: {
|
||||||
|
icon: 'car',
|
||||||
|
color: '#10B981',
|
||||||
|
requiresMultipleSelection: true,
|
||||||
|
priority: 'high'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
import { IsNotEmpty, IsString, IsNumber, IsBoolean, IsOptional, IsArray, IsEnum, ValidateNested, IsObject } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { ClaimWorkflowStep } from 'src/Types&Enums/claim-request-management/claim-workflow-steps.enum';
|
||||||
|
import { WorkflowStep } from 'src/Types&Enums/blame-request-management/blameWorkflow-steps.enum';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
|
|
||||||
|
// Combined enum for validation
|
||||||
|
const AllWorkflowSteps = { ...WorkflowStep, ...ClaimWorkflowStep };
|
||||||
|
|
||||||
|
export class ClaimStepFieldDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Unique identifier for the field',
|
||||||
|
example: '67fa10c259e15f231a2d1001'
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
id: Types.ObjectId;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Field name in Farsi',
|
||||||
|
example: 'قطعات خارجی'
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
name_fa: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Field name in English',
|
||||||
|
example: 'outerParts'
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
name_en: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Field label in Farsi',
|
||||||
|
example: 'قطعات خارجی آسیبدیده'
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
label_fa?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Field label in English',
|
||||||
|
example: 'Damaged Outer Parts'
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
label_en?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Placeholder text in Farsi',
|
||||||
|
example: 'قطعات خارجی آسیبدیده را انتخاب کنید'
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
placeholder_fa?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Placeholder text in English',
|
||||||
|
example: 'Select damaged outer parts'
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
placeholder_en?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Possible values for select/multiselect fields (null for non-select fields)',
|
||||||
|
example: null,
|
||||||
|
nullable: true,
|
||||||
|
default: null
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
value?: Object[] | null;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Data type of the field',
|
||||||
|
example: 'multiselect',
|
||||||
|
enum: ['string', 'number', 'boolean', 'date', 'select', 'multiselect', 'file', 'textarea', 'email', 'phone', 'object']
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
dataType: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Whether the field is required',
|
||||||
|
example: true,
|
||||||
|
default: false
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
required?: boolean;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Validation rules for the field',
|
||||||
|
example: { minItems: 1, maxItems: 13 }
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
validation?: Object;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Display order of the field in the form',
|
||||||
|
example: 1
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
order?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Whether the field is active',
|
||||||
|
example: true,
|
||||||
|
default: true
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateClaimWorkflowStepDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Unique key identifying the claim workflow step',
|
||||||
|
example: 'CLAIM_CREATED',
|
||||||
|
enum: AllWorkflowSteps
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsEnum(AllWorkflowSteps)
|
||||||
|
stepKey: ClaimWorkflowStep | WorkflowStep;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Workflow type: CLAIM for claim workflow',
|
||||||
|
example: 'CLAIM',
|
||||||
|
default: 'CLAIM'
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Order number of the step in the claim workflow',
|
||||||
|
example: 1
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsNumber()
|
||||||
|
stepNumber: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Whether the step is active',
|
||||||
|
example: true,
|
||||||
|
default: true
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isActive?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Step name in Farsi',
|
||||||
|
example: 'ایجاد درخواست خسارت'
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
stepName_fa: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Step name in English',
|
||||||
|
example: 'Create Claim Request'
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
stepName_en: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Step description in Farsi',
|
||||||
|
example: 'ایجاد درخواست خسارت از پرونده تعیین تقصیر تکمیل شده'
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
description_fa?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Step description in English',
|
||||||
|
example: 'Create claim request from completed blame case'
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
description_en?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Category of the step',
|
||||||
|
example: 'CLAIM',
|
||||||
|
enum: ['CLAIM', 'EXPERT', 'INSURER', 'FINAL']
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
category?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Dynamic fields for this step',
|
||||||
|
type: [ClaimStepFieldDto],
|
||||||
|
example: []
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => ClaimStepFieldDto)
|
||||||
|
fields?: ClaimStepFieldDto[];
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Steps that must be completed before this step',
|
||||||
|
example: [],
|
||||||
|
enum: AllWorkflowSteps,
|
||||||
|
isArray: true
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsEnum(AllWorkflowSteps, { each: true })
|
||||||
|
requiredPreviousSteps?: (ClaimWorkflowStep | WorkflowStep)[];
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Possible next steps after completing this step',
|
||||||
|
example: ['SELECT_OUTER_PARTS'],
|
||||||
|
enum: AllWorkflowSteps,
|
||||||
|
isArray: true
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsEnum(AllWorkflowSteps, { each: true })
|
||||||
|
nextPossibleSteps?: (ClaimWorkflowStep | WorkflowStep)[];
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Additional metadata for the step',
|
||||||
|
example: { icon: 'file-plus', color: '#8B5CF6' }
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Estimated duration in minutes',
|
||||||
|
example: 1
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
estimatedDuration?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Roles allowed to access this step',
|
||||||
|
example: ['user', 'damage_expert', 'admin']
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
allowedRoles?: string[];
|
||||||
|
}
|
||||||
271
src/workflow-step-management/dto/create-workflow-step.dto.ts
Normal file
271
src/workflow-step-management/dto/create-workflow-step.dto.ts
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
import { IsNotEmpty, IsString, IsNumber, IsBoolean, IsOptional, IsArray, IsEnum, ValidateNested, IsObject } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { WorkflowStep } from 'src/Types&Enums/blame-request-management/blameWorkflow-steps.enum';
|
||||||
|
import { ClaimWorkflowStep } from 'src/Types&Enums/claim-request-management/claim-workflow-steps.enum';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
|
|
||||||
|
// Combined enum for validation
|
||||||
|
const AllWorkflowSteps = { ...WorkflowStep, ...ClaimWorkflowStep };
|
||||||
|
|
||||||
|
export class StepFieldDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Unique identifier for the field',
|
||||||
|
example: '507f1f77bcf86cd799439011'
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
id: Types.ObjectId;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Field name in Farsi',
|
||||||
|
example: 'فایل ویدیو'
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
name_fa: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Field name in English',
|
||||||
|
example: 'videoFile'
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
name_en: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Field label in Farsi',
|
||||||
|
example: 'فایل ویدیو'
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
label_fa?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Field label in English',
|
||||||
|
example: 'Video File'
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
label_en?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Placeholder text in Farsi',
|
||||||
|
example: 'فایل ویدیو خود را آپلود کنید'
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
placeholder_fa?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Placeholder text in English',
|
||||||
|
example: 'Upload your video file'
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
placeholder_en?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Possible values for select/multiselect fields (null for non-select fields)',
|
||||||
|
example: null,
|
||||||
|
nullable: true,
|
||||||
|
default: null
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
value?: Object[] | null;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Data type of the field',
|
||||||
|
example: 'file',
|
||||||
|
enum: ['string', 'number', 'boolean', 'date', 'select', 'multiselect', 'file', 'textarea', 'email', 'phone']
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
dataType: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Whether the field is required',
|
||||||
|
example: true,
|
||||||
|
default: false
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
required?: boolean;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Validation rules for the field',
|
||||||
|
example: { maxSize: 52428800, allowedTypes: ['video/mp4', 'video/webm'] }
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
validation?: Object;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Display order of the field in the form',
|
||||||
|
example: 1
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
order?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Whether the field is active',
|
||||||
|
example: true,
|
||||||
|
default: true
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CreateWorkflowStepDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Unique key identifying the workflow step (blame or claim)',
|
||||||
|
example: 'FIRST_VIDEO',
|
||||||
|
enum: AllWorkflowSteps
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsEnum(AllWorkflowSteps)
|
||||||
|
stepKey: WorkflowStep | ClaimWorkflowStep;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Workflow type: THIRD_PARTY (blame) or CLAIM (claim workflow)',
|
||||||
|
example: 'THIRD_PARTY',
|
||||||
|
enum: ['THIRD_PARTY', 'CLAIM']
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
type: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Order number of the step within this workflow type',
|
||||||
|
example: 2
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsNumber()
|
||||||
|
stepNumber: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Whether the step is active',
|
||||||
|
example: true,
|
||||||
|
default: true
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isActive?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Step name in Farsi',
|
||||||
|
example: 'ویدیو طرف اول'
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
stepName_fa: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Step name in English',
|
||||||
|
example: 'First Party Video'
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
stepName_en: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Step description in Farsi',
|
||||||
|
example: 'ضبط ویدیو توسط طرف اول'
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
description_fa?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Step description in English',
|
||||||
|
example: 'Video capture by first party'
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
description_en?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Category of the step within workflow (e.g., INITIAL, FIRST_PARTY, EXPERT)',
|
||||||
|
example: 'FIRST_PARTY',
|
||||||
|
enum: ['INITIAL', 'FIRST_PARTY', 'SECOND_PARTY', 'CLAIM', 'EXPERT', 'INSURER', 'FINAL']
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
category?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Dynamic fields for this step',
|
||||||
|
type: [StepFieldDto],
|
||||||
|
example: [
|
||||||
|
{
|
||||||
|
id: '507f1f77bcf86cd799439011',
|
||||||
|
name_fa: 'فایل ویدیو',
|
||||||
|
name_en: 'videoFile',
|
||||||
|
label_fa: 'فایل ویدیو',
|
||||||
|
label_en: 'Video File',
|
||||||
|
value: null,
|
||||||
|
dataType: 'file',
|
||||||
|
required: true,
|
||||||
|
order: 1,
|
||||||
|
isActive: true,
|
||||||
|
validation: {
|
||||||
|
maxSize: 52428800,
|
||||||
|
allowedTypes: ['video/mp4', 'video/webm']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => StepFieldDto)
|
||||||
|
fields?: StepFieldDto[];
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Steps that must be completed before this step',
|
||||||
|
example: ['CREATED'],
|
||||||
|
enum: AllWorkflowSteps,
|
||||||
|
isArray: true
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsEnum(AllWorkflowSteps, { each: true })
|
||||||
|
requiredPreviousSteps?: (WorkflowStep | ClaimWorkflowStep)[];
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Possible next steps after completing this step',
|
||||||
|
example: ['FIRST_INITIAL_FORM'],
|
||||||
|
enum: AllWorkflowSteps,
|
||||||
|
isArray: true
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsEnum(AllWorkflowSteps, { each: true })
|
||||||
|
nextPossibleSteps?: (WorkflowStep | ClaimWorkflowStep)[];
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Additional metadata for the step',
|
||||||
|
example: { icon: 'video', color: '#3B82F6' }
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
metadata?: Record<string, any>;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Estimated duration in minutes',
|
||||||
|
example: 5
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
estimatedDuration?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: 'Roles allowed to access this step',
|
||||||
|
example: ['user', 'expert', 'admin']
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
allowedRoles?: string[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { PartialType } from '@nestjs/swagger';
|
||||||
|
import { CreateWorkflowStepDto } from './create-workflow-step.dto';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update DTO inherits all fields from Create DTO but makes them all optional
|
||||||
|
* Supports both blame and claim workflow steps
|
||||||
|
*/
|
||||||
|
export class UpdateWorkflowStepDto extends PartialType(CreateWorkflowStepDto) {}
|
||||||
1541
src/workflow-step-management/dto/workflow-step.examples.ts
Normal file
1541
src/workflow-step-management/dto/workflow-step.examples.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,156 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectModel } from '@nestjs/mongoose';
|
||||||
|
import { Model } from 'mongoose';
|
||||||
|
import { WorkflowStepModel, WorkflowStepsDB } from '../schema/workflow-step.schema';
|
||||||
|
import { CreateWorkflowStepDto } from '../../dto/create-workflow-step.dto';
|
||||||
|
import { UpdateWorkflowStepDto } from '../../dto/update-workflow-step.dto';
|
||||||
|
import { WorkflowStep } from 'src/Types&Enums/blame-request-management/blameWorkflow-steps.enum';
|
||||||
|
import { ClaimWorkflowStep } from 'src/Types&Enums/claim-request-management/claim-workflow-steps.enum';
|
||||||
|
|
||||||
|
// Type alias for combined workflow steps
|
||||||
|
type AnyWorkflowStep = WorkflowStep | ClaimWorkflowStep;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class WorkflowStepDbService {
|
||||||
|
constructor(
|
||||||
|
@InjectModel(WorkflowStepsDB)
|
||||||
|
private readonly workflowStepModel: Model<WorkflowStepModel>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new workflow step
|
||||||
|
*/
|
||||||
|
async create(createDto: CreateWorkflowStepDto): Promise<WorkflowStepModel> {
|
||||||
|
const created = new this.workflowStepModel(createDto);
|
||||||
|
return await created.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all active workflow steps
|
||||||
|
*/
|
||||||
|
async findAll(): Promise<WorkflowStepModel[]> {
|
||||||
|
return await this.workflowStepModel
|
||||||
|
.find({ isActive: true })
|
||||||
|
.sort({ stepNumber: 1 })
|
||||||
|
.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all workflow steps including inactive ones
|
||||||
|
*/
|
||||||
|
async findAllIncludingInactive(): Promise<WorkflowStepModel[]> {
|
||||||
|
return await this.workflowStepModel
|
||||||
|
.find()
|
||||||
|
.sort({ stepNumber: 1 })
|
||||||
|
.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a workflow step by ID
|
||||||
|
*/
|
||||||
|
async findById(id: string): Promise<WorkflowStepModel | null> {
|
||||||
|
return await this.workflowStepModel.findById(id).exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a workflow step by step key (supports both blame and claim)
|
||||||
|
*/
|
||||||
|
async findByStepKey(stepKey: AnyWorkflowStep): Promise<WorkflowStepModel | null> {
|
||||||
|
return await this.workflowStepModel.findOne({ stepKey }).exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a workflow step by step number
|
||||||
|
*/
|
||||||
|
async findByStepNumber(stepNumber: number): Promise<WorkflowStepModel | null> {
|
||||||
|
return await this.workflowStepModel.findOne({ stepNumber }).exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find steps by category
|
||||||
|
*/
|
||||||
|
async findByCategory(category: string): Promise<WorkflowStepModel[]> {
|
||||||
|
return await this.workflowStepModel
|
||||||
|
.find({ category, isActive: true })
|
||||||
|
.sort({ stepNumber: 1 })
|
||||||
|
.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a workflow step by ID
|
||||||
|
*/
|
||||||
|
async update(id: string, updateDto: UpdateWorkflowStepDto): Promise<WorkflowStepModel | null> {
|
||||||
|
return await this.workflowStepModel
|
||||||
|
.findByIdAndUpdate(id, updateDto, { new: true })
|
||||||
|
.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a workflow step by step key (supports both blame and claim)
|
||||||
|
*/
|
||||||
|
async updateByStepKey(
|
||||||
|
stepKey: AnyWorkflowStep,
|
||||||
|
updateDto: UpdateWorkflowStepDto,
|
||||||
|
): Promise<WorkflowStepModel | null> {
|
||||||
|
return await this.workflowStepModel
|
||||||
|
.findOneAndUpdate({ stepKey }, updateDto, { new: true })
|
||||||
|
.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Soft delete (deactivate) a workflow step
|
||||||
|
*/
|
||||||
|
async remove(id: string): Promise<WorkflowStepModel | null> {
|
||||||
|
return await this.workflowStepModel
|
||||||
|
.findByIdAndUpdate(id, { isActive: false }, { new: true })
|
||||||
|
.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upsert a workflow step by step key (supports both blame and claim)
|
||||||
|
*/
|
||||||
|
async upsertByStepKey(
|
||||||
|
stepKey: AnyWorkflowStep,
|
||||||
|
data: CreateWorkflowStepDto | UpdateWorkflowStepDto,
|
||||||
|
): Promise<WorkflowStepModel> {
|
||||||
|
return await this.workflowStepModel
|
||||||
|
.findOneAndUpdate({ stepKey }, data, { upsert: true, new: true })
|
||||||
|
.exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bulk upsert workflow steps
|
||||||
|
*/
|
||||||
|
async bulkUpsert(steps: CreateWorkflowStepDto[]): Promise<any> {
|
||||||
|
const bulkOps = steps.map((step) => ({
|
||||||
|
updateOne: {
|
||||||
|
filter: { stepKey: step.stepKey },
|
||||||
|
update: { $set: step as any },
|
||||||
|
upsert: true,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return await this.workflowStepModel.bulkWrite(bulkOps as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the next step number
|
||||||
|
*/
|
||||||
|
async getNextStepNumber(): Promise<number> {
|
||||||
|
const maxStep = await this.workflowStepModel
|
||||||
|
.findOne()
|
||||||
|
.sort({ stepNumber: -1 })
|
||||||
|
.select('stepNumber')
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
return maxStep ? maxStep.stepNumber + 1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a step exists (supports both blame and claim)
|
||||||
|
*/
|
||||||
|
async exists(stepKey: AnyWorkflowStep): Promise<boolean> {
|
||||||
|
const count = await this.workflowStepModel.countDocuments({ stepKey }).exec();
|
||||||
|
return count > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
|
import { WorkflowStep } from 'src/Types&Enums/blame-request-management/blameWorkflow-steps.enum';
|
||||||
|
import { ClaimWorkflowStep } from 'src/Types&Enums/claim-request-management/claim-workflow-steps.enum';
|
||||||
|
|
||||||
|
// Combined enum values for both blame and claim workflows
|
||||||
|
const allWorkflowSteps = [...Object.values(WorkflowStep), ...Object.values(ClaimWorkflowStep)];
|
||||||
|
|
||||||
|
export const WorkflowStepsDB = 'WorkflowSteps';
|
||||||
|
|
||||||
|
export type FieldValueType = Object | string | number | boolean | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Field definition for dynamic form generation
|
||||||
|
* Each step can have multiple fields that represent form inputs
|
||||||
|
*/
|
||||||
|
@Schema({ _id: false })
|
||||||
|
export class StepField {
|
||||||
|
@Prop({ required: true, type: Types.ObjectId })
|
||||||
|
id: Types.ObjectId;
|
||||||
|
|
||||||
|
@Prop({ required: true, type: String })
|
||||||
|
name_fa: string;
|
||||||
|
|
||||||
|
@Prop({ required: true, type: String })
|
||||||
|
name_en: string;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
label_fa?: string;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
label_en?: string;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
placeholder_fa?: string;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
placeholder_en?: string;
|
||||||
|
|
||||||
|
@Prop({ type: [Object], default: null })
|
||||||
|
value: Object[] | null;
|
||||||
|
|
||||||
|
@Prop({ required: true, type: String })
|
||||||
|
dataType: string; // e.g., 'string', 'number', 'boolean', 'date', 'select', 'multiselect', 'file', etc.
|
||||||
|
|
||||||
|
@Prop({ type: Boolean, default: false })
|
||||||
|
required?: boolean;
|
||||||
|
|
||||||
|
@Prop({ type: Object })
|
||||||
|
validation?: Object; // Validation rules (min, max, pattern, etc.)
|
||||||
|
|
||||||
|
@Prop({ type: Number })
|
||||||
|
order?: number; // Field order in the form
|
||||||
|
|
||||||
|
@Prop({ type: Boolean, default: true })
|
||||||
|
isActive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StepFieldSchema = SchemaFactory.createForClass(StepField);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Workflow Step Model
|
||||||
|
* Defines the configuration for each step in the workflow
|
||||||
|
*/
|
||||||
|
@Schema({ versionKey: false, collection: 'workflowSteps', timestamps: true })
|
||||||
|
export class WorkflowStepModel {
|
||||||
|
@Prop({ required: true, type: String, enum: allWorkflowSteps, unique: true })
|
||||||
|
stepKey: WorkflowStep | ClaimWorkflowStep;
|
||||||
|
|
||||||
|
@Prop({ required: true, type: String })
|
||||||
|
type: string; // 'THIRD_PARTY' (blame workflow) or 'CLAIM' (claim workflow)
|
||||||
|
|
||||||
|
@Prop({ required: true, type: Number })
|
||||||
|
stepNumber: number;
|
||||||
|
|
||||||
|
@Prop({ type: Boolean, default: true })
|
||||||
|
isActive: boolean;
|
||||||
|
|
||||||
|
@Prop({ required: true, type: String })
|
||||||
|
stepName_fa: string;
|
||||||
|
|
||||||
|
@Prop({ required: true, type: String })
|
||||||
|
stepName_en: string;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
description_fa?: string;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
description_en?: string;
|
||||||
|
|
||||||
|
@Prop({ type: String })
|
||||||
|
category?: string; // e.g., 'INITIAL', 'FIRST_PARTY', 'SECOND_PARTY', 'EXPERT', 'INSURER'
|
||||||
|
|
||||||
|
@Prop({ type: [StepFieldSchema], default: [] })
|
||||||
|
fields?: StepField[];
|
||||||
|
|
||||||
|
@Prop({ type: [String], enum: allWorkflowSteps, default: [] })
|
||||||
|
requiredPreviousSteps?: (WorkflowStep | ClaimWorkflowStep)[]; // Steps that must be completed before this step
|
||||||
|
|
||||||
|
@Prop({ type: [String], enum: allWorkflowSteps, default: [] })
|
||||||
|
nextPossibleSteps?: (WorkflowStep | ClaimWorkflowStep)[]; // Possible next steps after completing this step
|
||||||
|
|
||||||
|
@Prop({ type: Object })
|
||||||
|
metadata?: Record<string, any>; // Additional metadata for the step
|
||||||
|
|
||||||
|
@Prop({ type: Number })
|
||||||
|
estimatedDuration?: number; // Estimated duration in minutes
|
||||||
|
|
||||||
|
@Prop({ type: [String], default: [] })
|
||||||
|
allowedRoles?: string[]; // Roles allowed to access this step
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WorkflowStepSchema = SchemaFactory.createForClass(WorkflowStepModel);
|
||||||
|
|
||||||
|
// Create indexes for better query performance
|
||||||
|
WorkflowStepSchema.index({ stepKey: 1 }); // Unique via @Prop
|
||||||
|
WorkflowStepSchema.index({ type: 1, isActive: 1 }); // Query by workflow type
|
||||||
|
WorkflowStepSchema.index({ type: 1, stepNumber: 1 }, { unique: true }); // Unique stepNumber within workflow type
|
||||||
|
WorkflowStepSchema.index({ type: 1, category: 1, stepNumber: 1 }); // Sorting within workflow and category
|
||||||
8
src/workflow-step-management/index.ts
Normal file
8
src/workflow-step-management/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export * from './workflow-step-management.module';
|
||||||
|
export * from './workflow-step-management.service';
|
||||||
|
export * from './workflow-step-management.controller';
|
||||||
|
export * from './dto/create-workflow-step.dto';
|
||||||
|
export * from './dto/update-workflow-step.dto';
|
||||||
|
export * from './dto/workflow-step.examples';
|
||||||
|
export * from './entities/schema/workflow-step.schema';
|
||||||
|
export * from './entities/db-service/workflow-step.db.service';
|
||||||
@@ -0,0 +1,353 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Patch,
|
||||||
|
Param,
|
||||||
|
Delete,
|
||||||
|
Query,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiTags,
|
||||||
|
ApiOperation,
|
||||||
|
ApiResponse,
|
||||||
|
ApiBody,
|
||||||
|
ApiParam,
|
||||||
|
ApiQuery,
|
||||||
|
ApiBearerAuth
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
import { WorkflowStepManagementService } from './workflow-step-management.service';
|
||||||
|
import { CreateWorkflowStepDto } from './dto/create-workflow-step.dto';
|
||||||
|
import { UpdateWorkflowStepDto } from './dto/update-workflow-step.dto';
|
||||||
|
import { WorkflowStep } from 'src/Types&Enums/blame-request-management/blameWorkflow-steps.enum';
|
||||||
|
import { ClaimWorkflowStep } from 'src/Types&Enums/claim-request-management/claim-workflow-steps.enum';
|
||||||
|
import {
|
||||||
|
createWorkflowStepExamples,
|
||||||
|
updateWorkflowStepExamples,
|
||||||
|
bulkUpsertWorkflowStepExamples,
|
||||||
|
validateAccessExamples,
|
||||||
|
claimWorkflowStepExamples,
|
||||||
|
} from './dto/workflow-step.examples';
|
||||||
|
|
||||||
|
@ApiTags('Workflow Step Management')
|
||||||
|
@Controller('workflow-step-management')
|
||||||
|
export class WorkflowStepManagementController {
|
||||||
|
constructor(
|
||||||
|
private readonly workflowStepManagementService: WorkflowStepManagementService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new workflow step configuration
|
||||||
|
*/
|
||||||
|
@Post()
|
||||||
|
@HttpCode(HttpStatus.CREATED)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Create a new workflow step',
|
||||||
|
description: 'Creates a new workflow step configuration with fields and validation rules'
|
||||||
|
})
|
||||||
|
@ApiBody({
|
||||||
|
type: CreateWorkflowStepDto,
|
||||||
|
examples: {
|
||||||
|
...createWorkflowStepExamples,
|
||||||
|
...claimWorkflowStepExamples
|
||||||
|
}
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 201, description: 'Workflow step created successfully' })
|
||||||
|
@ApiResponse({ status: 400, description: 'Bad request - Invalid input data' })
|
||||||
|
@ApiResponse({ status: 409, description: 'Conflict - Step already exists' })
|
||||||
|
async create(@Body() createDto: CreateWorkflowStepDto) {
|
||||||
|
return await this.workflowStepManagementService.create(createDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all active workflow steps
|
||||||
|
*/
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Get all workflow steps',
|
||||||
|
description: 'Retrieves all workflow steps, optionally including inactive ones'
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'includeInactive',
|
||||||
|
required: false,
|
||||||
|
type: String,
|
||||||
|
description: 'Set to "true" to include inactive steps',
|
||||||
|
example: 'false'
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: 'List of workflow steps retrieved successfully' })
|
||||||
|
async findAll(@Query('includeInactive') includeInactive?: string) {
|
||||||
|
if (includeInactive === 'true') {
|
||||||
|
return await this.workflowStepManagementService.findAllIncludingInactive();
|
||||||
|
}
|
||||||
|
return await this.workflowStepManagementService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a workflow step by ID
|
||||||
|
*/
|
||||||
|
@Patch(':id')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Update workflow step by ID',
|
||||||
|
description: 'Updates a workflow step using its database ID'
|
||||||
|
})
|
||||||
|
@ApiParam({ name: 'id', description: 'Workflow step ID', example: '507f1f77bcf86cd799439011' })
|
||||||
|
@ApiBody({
|
||||||
|
type: UpdateWorkflowStepDto,
|
||||||
|
examples: updateWorkflowStepExamples
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: 'Workflow step updated successfully' })
|
||||||
|
@ApiResponse({ status: 404, description: 'Workflow step not found' })
|
||||||
|
async update(@Param('id') id: string, @Body() updateDto: UpdateWorkflowStepDto) {
|
||||||
|
return await this.workflowStepManagementService.update(id, updateDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a workflow step by key
|
||||||
|
*/
|
||||||
|
@Patch('step/:stepKey')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Update workflow step by key',
|
||||||
|
description: 'Updates a workflow step using its step key from the enum'
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: 'stepKey',
|
||||||
|
description: 'Workflow step key (blame or claim)',
|
||||||
|
example: 'FIRST_VIDEO'
|
||||||
|
})
|
||||||
|
@ApiBody({
|
||||||
|
type: UpdateWorkflowStepDto,
|
||||||
|
examples: updateWorkflowStepExamples
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: 'Workflow step updated successfully' })
|
||||||
|
@ApiResponse({ status: 404, description: 'Workflow step not found' })
|
||||||
|
async updateByStepKey(
|
||||||
|
@Param('stepKey') stepKey: WorkflowStep | ClaimWorkflowStep,
|
||||||
|
@Body() updateDto: UpdateWorkflowStepDto,
|
||||||
|
) {
|
||||||
|
return await this.workflowStepManagementService.updateByStepKey(stepKey as any, updateDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize default workflow steps
|
||||||
|
*/
|
||||||
|
@Post('initialize')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Initialize default workflow steps',
|
||||||
|
description: 'Creates default workflow step configurations for the system'
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: 'Default steps initialized successfully' })
|
||||||
|
async initialize() {
|
||||||
|
return await this.workflowStepManagementService.initializeDefaultSteps();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific step by stepKey or stepNumber (intelligent lookup)
|
||||||
|
*/
|
||||||
|
@Get('find/:identifier')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Get workflow step by stepKey or stepNumber',
|
||||||
|
description: 'Retrieves a workflow step by either its stepKey (enum) or stepNumber. Automatically detects which one is provided.'
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: 'identifier',
|
||||||
|
description: 'Step key (e.g., "FIRST_VIDEO") or step number (e.g., 3)',
|
||||||
|
example: 'FIRST_VIDEO',
|
||||||
|
examples: {
|
||||||
|
byStepKey: {
|
||||||
|
summary: 'By Step Key',
|
||||||
|
value: 'FIRST_VIDEO'
|
||||||
|
},
|
||||||
|
byStepNumber: {
|
||||||
|
summary: 'By Step Number',
|
||||||
|
value: 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: 'Workflow step retrieved successfully' })
|
||||||
|
@ApiResponse({ status: 404, description: 'Workflow step not found' })
|
||||||
|
async findByStepKeyOrNumber(@Param('identifier') identifier: string) {
|
||||||
|
return await this.workflowStepManagementService.findByStepKeyOrNumber(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific step by ID
|
||||||
|
*/
|
||||||
|
@Get(':id')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Get workflow step by ID',
|
||||||
|
description: 'Retrieves a specific workflow step by its database ID'
|
||||||
|
})
|
||||||
|
@ApiParam({ name: 'id', description: 'Workflow step ID', example: '507f1f77bcf86cd799439011' })
|
||||||
|
@ApiResponse({ status: 200, description: 'Workflow step retrieved successfully' })
|
||||||
|
@ApiResponse({ status: 404, description: 'Workflow step not found' })
|
||||||
|
async findById(@Param('id') id: string) {
|
||||||
|
return await this.workflowStepManagementService.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Soft delete a step
|
||||||
|
*/
|
||||||
|
@Delete(':id')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Delete workflow step',
|
||||||
|
description: 'Soft deletes a workflow step by setting isActive to false'
|
||||||
|
})
|
||||||
|
@ApiParam({ name: 'id', description: 'Workflow step ID', example: '507f1f77bcf86cd799439011' })
|
||||||
|
@ApiResponse({ status: 200, description: 'Workflow step deleted successfully' })
|
||||||
|
@ApiResponse({ status: 404, description: 'Workflow step not found' })
|
||||||
|
async remove(@Param('id') id: string) {
|
||||||
|
return await this.workflowStepManagementService.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// DEPRECATED ENDPOINTS - Will be removed in future versions
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use individual create endpoint instead
|
||||||
|
* Bulk upsert steps (for initial setup or migrations)
|
||||||
|
*/
|
||||||
|
@Post('bulk-upsert')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '[DEPRECATED] Bulk upsert workflow steps',
|
||||||
|
description: 'This endpoint is deprecated and will be removed in a future version. Use the individual create endpoint instead.',
|
||||||
|
deprecated: true
|
||||||
|
})
|
||||||
|
@ApiBody({
|
||||||
|
type: [CreateWorkflowStepDto],
|
||||||
|
examples: bulkUpsertWorkflowStepExamples
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: 'Steps upserted successfully' })
|
||||||
|
async bulkUpsert(@Body() steps: CreateWorkflowStepDto[]) {
|
||||||
|
return await this.workflowStepManagementService.bulkUpsert(steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated This endpoint is no longer needed
|
||||||
|
* Validate step access
|
||||||
|
*/
|
||||||
|
@Post('validate-access')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '[DEPRECATED] Validate step access',
|
||||||
|
description: 'This endpoint is deprecated and will be removed in a future version.',
|
||||||
|
deprecated: true
|
||||||
|
})
|
||||||
|
@ApiBody({
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
stepKey: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Workflow step key (blame or claim)',
|
||||||
|
example: 'FIRST_VIDEO'
|
||||||
|
},
|
||||||
|
completedSteps: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string'
|
||||||
|
},
|
||||||
|
example: ['CREATED']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
examples: validateAccessExamples
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: 'Validation result returned' })
|
||||||
|
async validateAccess(
|
||||||
|
@Body() body: { stepKey: WorkflowStep | ClaimWorkflowStep; completedSteps: (WorkflowStep | ClaimWorkflowStep)[] },
|
||||||
|
) {
|
||||||
|
return await this.workflowStepManagementService.validateStepAccess(
|
||||||
|
body.stepKey as any,
|
||||||
|
body.completedSteps as any,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated This endpoint is no longer needed
|
||||||
|
* Get steps by category
|
||||||
|
*/
|
||||||
|
@Get('category/:category')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '[DEPRECATED] Get steps by category',
|
||||||
|
description: 'This endpoint is deprecated and will be removed in a future version.',
|
||||||
|
deprecated: true
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: 'category',
|
||||||
|
description: 'Step category',
|
||||||
|
example: 'FIRST_PARTY',
|
||||||
|
enum: ['INITIAL', 'FIRST_PARTY', 'SECOND_PARTY', 'RESOLUTION']
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: 'Steps retrieved successfully' })
|
||||||
|
async findByCategory(@Param('category') category: string) {
|
||||||
|
return await this.workflowStepManagementService.findByCategory(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated This endpoint is no longer needed
|
||||||
|
* Get next possible steps from current step
|
||||||
|
*/
|
||||||
|
@Get('next-steps/:stepKey')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '[DEPRECATED] Get next possible steps',
|
||||||
|
description: 'This endpoint is deprecated and will be removed in a future version.',
|
||||||
|
deprecated: true
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: 'stepKey',
|
||||||
|
description: 'Current workflow step key (blame or claim)',
|
||||||
|
example: 'FIRST_VIDEO'
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: 'Next steps retrieved successfully' })
|
||||||
|
async getNextSteps(@Param('stepKey') stepKey: WorkflowStep | ClaimWorkflowStep) {
|
||||||
|
return await this.workflowStepManagementService.getNextSteps(stepKey as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated This endpoint is no longer needed
|
||||||
|
* Get step fields (for form generation)
|
||||||
|
*/
|
||||||
|
@Get('fields/:stepKey')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '[DEPRECATED] Get step fields',
|
||||||
|
description: 'This endpoint is deprecated and will be removed in a future version.',
|
||||||
|
deprecated: true
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: 'stepKey',
|
||||||
|
description: 'Workflow step key (blame or claim)',
|
||||||
|
example: 'FIRST_VIDEO'
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: 'Step fields retrieved successfully' })
|
||||||
|
async getStepFields(@Param('stepKey') stepKey: WorkflowStep | ClaimWorkflowStep) {
|
||||||
|
return await this.workflowStepManagementService.getStepFields(stepKey as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use findById instead
|
||||||
|
* Get a specific step by key
|
||||||
|
*/
|
||||||
|
@Get('step/:stepKey')
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '[DEPRECATED] Get workflow step by key',
|
||||||
|
description: 'This endpoint is deprecated and will be removed in a future version. Use GET /:id instead.',
|
||||||
|
deprecated: true
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: 'stepKey',
|
||||||
|
description: 'Workflow step key (blame or claim)',
|
||||||
|
example: 'FIRST_VIDEO'
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, description: 'Workflow step retrieved successfully' })
|
||||||
|
@ApiResponse({ status: 404, description: 'Workflow step not found' })
|
||||||
|
async findByStepKey(@Param('stepKey') stepKey: WorkflowStep | ClaimWorkflowStep) {
|
||||||
|
return await this.workflowStepManagementService.findByStepKey(stepKey as any);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { MongooseModule } from '@nestjs/mongoose';
|
||||||
|
import { WorkflowStepManagementService } from './workflow-step-management.service';
|
||||||
|
import { WorkflowStepManagementController } from './workflow-step-management.controller';
|
||||||
|
import { WorkflowStepDbService } from './entities/db-service/workflow-step.db.service';
|
||||||
|
import {
|
||||||
|
WorkflowStepsDB,
|
||||||
|
WorkflowStepSchema,
|
||||||
|
} from './entities/schema/workflow-step.schema';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
MongooseModule.forFeature([
|
||||||
|
{ name: WorkflowStepsDB, schema: WorkflowStepSchema },
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
controllers: [WorkflowStepManagementController],
|
||||||
|
providers: [WorkflowStepManagementService, WorkflowStepDbService],
|
||||||
|
exports: [WorkflowStepManagementService, WorkflowStepDbService],
|
||||||
|
})
|
||||||
|
export class WorkflowStepManagementModule {}
|
||||||
1131
src/workflow-step-management/workflow-step-management.service.ts
Normal file
1131
src/workflow-step-management/workflow-step-management.service.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user