From b9e73732258459a613c3dde1946366f3ec3dbf5f Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Mon, 1 Jun 2026 09:38:56 +0330 Subject: [PATCH 1/7] Reactivated inquiry of birth date --- .../request-management.service.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/request-management/request-management.service.ts b/src/request-management/request-management.service.ts index d3bc5d3..a598ce7 100644 --- a/src/request-management/request-management.service.ts +++ b/src/request-management/request-management.service.ts @@ -1016,15 +1016,15 @@ export class RequestManagementService { } try { - // const personalInquiry = await this.sandHubService.getPersonalInquiry( - // personalNationalCode, - // personalBirthDate, - // ); - // this.logger.log( - // `[SANDHUB] personal inquiry success request=${req._id} nationalCode=${personalNationalCode}: ${JSON.stringify( - // personalInquiry, - // )}`, - // ); + const personalInquiry = await this.sandHubService.getPersonalInquiry( + personalNationalCode, + personalBirthDate, + ); + this.logger.log( + `[SANDHUB] personal inquiry success request=${req._id} nationalCode=${personalNationalCode}: ${JSON.stringify( + personalInquiry, + )}`, + ); } catch (err: any) { this.logger.error( `[SANDHUB] personal inquiry failed request=${req._id} nationalCode=${personalNationalCode}: ${err?.message || err}`, From cec349b7c2edeb1495740467569685af4fa64f7d Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Mon, 1 Jun 2026 11:38:30 +0330 Subject: [PATCH 2/7] Centralized damaged parts alongside all their required info --- .../claim-request-management.service.ts | 5 +- .../dto/claim-damaged-part.enricher.ts | 138 ++++++++++++++++++ .../dto/claim-damaged-part.types.ts | 40 +++++ src/expert-claim/expert-claim.service.ts | 80 +++++----- 4 files changed, 215 insertions(+), 48 deletions(-) create mode 100644 src/expert-claim/dto/claim-damaged-part.enricher.ts create mode 100644 src/expert-claim/dto/claim-damaged-part.types.ts diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index a024567..766c2cf 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -3985,7 +3985,10 @@ export class ClaimRequestManagementService { ...(currentUserParty.person?.clientId ? { clientId: new Types.ObjectId( - currentUserParty.person.clientId as any, + String(blameRequest?.expert?.decision?.guiltyPartyId) === + String(blameRequest?.parties[0]?.person?.userId) + ? blameRequest?.parties[0]?.person?.clientId + : blameRequest?.parties[1]?.person?.clientId, ), } : {}), diff --git a/src/expert-claim/dto/claim-damaged-part.enricher.ts b/src/expert-claim/dto/claim-damaged-part.enricher.ts new file mode 100644 index 0000000..99d8741 --- /dev/null +++ b/src/expert-claim/dto/claim-damaged-part.enricher.ts @@ -0,0 +1,138 @@ +// claim-damaged-part.enricher.ts + +import { + DamagedPartCaptureStatus, + EnrichedDamagedPart, +} from "./claim-damaged-part.types"; + +export function buildEnrichedDamagedParts(params: { + selectedParts: any[]; + damagedPartsData: any; + evaluationBlock: any; + expertAddedParts?: any[]; + getDamagedPartCaptureBlob: (data: any, key: string, parts: any[]) => any; + hasDamagedPartCapture: (data: any, key: string, parts: any[]) => boolean; + catalogLikeKeyFromPart: (part: any) => string; + buildFileLink: (path: string) => string; +}): EnrichedDamagedPart[] { + const { + selectedParts, + damagedPartsData, + evaluationBlock, + expertAddedParts = [], + getDamagedPartCaptureBlob, + hasDamagedPartCapture, + catalogLikeKeyFromPart, + buildFileLink, + } = params; + + const priceDropLines: any[] = evaluationBlock?.priceDrop?.partLines ?? []; + const objectionParts: any[] = + evaluationBlock?.objection?.objectionParts ?? []; + const newObjectionParts: any[] = evaluationBlock?.objection?.newParts ?? []; + + // Resend block — lives at evaluation.damageExpertResend + const damageExpertResend = evaluationBlock?.damageExpertResend; + const resendCarParts: any[] = damageExpertResend?.resendCarParts ?? []; + const resendFulfilledAt: Date | undefined = damageExpertResend?.fulfilledAt; + + function isPartInResend(sp: any): boolean { + return resendCarParts.some( + (r: any) => + r.id === sp.id || r.name === sp.name || r.catalogKey === sp.catalogKey, + ); + } + + function resolveOrigin(sp: any): EnrichedDamagedPart["origin"] { + if ( + newObjectionParts.some((p: any) => p.id === sp.id || p.name === sp.name) + ) { + return "added_by_objection"; + } + if ( + expertAddedParts.some((p: any) => p.id === sp.id || p.name === sp.name) + ) { + return "added_by_expert"; + } + return "user_selected"; + } + + function resolveCaptureStatus( + sp: any, + captured: boolean, + ): DamagedPartCaptureStatus { + const inResend = isPartInResend(sp); + if (!captured) { + return inResend ? "resend_requested" : "pending"; + } + // captured=true + was in a resend request that is now fulfilled → "resent" + return inResend && resendFulfilledAt ? "resent" : "uploaded"; + } + + return selectedParts.map((sp, index) => { + const ck = sp.catalogKey ?? catalogLikeKeyFromPart(sp); + + const cap = getDamagedPartCaptureBlob( + damagedPartsData, + ck, + selectedParts, + ) as { url?: string; path?: string } | undefined; + const captured = hasDamagedPartCapture(damagedPartsData, ck, selectedParts); + const url = cap?.url || (cap?.path ? buildFileLink(cap.path) : undefined); + + const matchingPriceDrop = priceDropLines.find( + (line: any) => + line.partId === sp.id || + line.priceDropPartKey?.toLowerCase() === sp.name?.toLowerCase(), + ); + + const isObjected = objectionParts.some( + (p: any) => p.id === sp.id || p.name === sp.name, + ); + const isNewlyAdded = newObjectionParts.some( + (p: any) => p.id === sp.id || p.name === sp.name, + ); + const isNewByExpert = expertAddedParts.some( + (p: any) => p.id === sp.id || p.name === sp.name, + ); + const inResend = isPartInResend(sp); + + return { + index, + partId: sp.id ?? null, + id: sp.id ?? null, + name: sp.name, + side: sp.side, + label_fa: sp.label_fa, + ...(sp.catalogKey ? { catalogKey: sp.catalogKey } : {}), + + origin: resolveOrigin(sp), + captureStatus: resolveCaptureStatus(sp, captured), + captured, + url, + + objection: { + isObjected, + isNewlyAdded, + ...(isNewlyAdded ? { addedBy: isNewByExpert ? "expert" : "user" } : {}), + }, + + resend: { + wasRequested: inResend, + isFulfilled: inResend && !!resendFulfilledAt, + ...(inResend && damageExpertResend?.requestedAt + ? { requestedAt: damageExpertResend.requestedAt } + : {}), + ...(inResend && resendFulfilledAt + ? { fulfilledAt: resendFulfilledAt } + : {}), + }, + + pricing: { + hasPriceDrop: !!matchingPriceDrop, + severity: matchingPriceDrop?.severity ?? null, + coefficient: matchingPriceDrop?.coefficient ?? null, + }, + }; + }); +} diff --git a/src/expert-claim/dto/claim-damaged-part.types.ts b/src/expert-claim/dto/claim-damaged-part.types.ts new file mode 100644 index 0000000..99cccd6 --- /dev/null +++ b/src/expert-claim/dto/claim-damaged-part.types.ts @@ -0,0 +1,40 @@ +// claim-damaged-part.types.ts — update the type +export type DamagedPartCaptureStatus = + | "pending" // never uploaded + | "uploaded" // uploaded, no issues + | "resend_requested" // expert asked to redo this part's photo + | "resent"; // owner re-uploaded after resend request + +export interface EnrichedDamagedPart { + index: number; + partId: number | null; + id: number | null; + name: string; + side: string; + label_fa: string; + catalogKey?: string; + + origin: "user_selected" | "added_by_objection" | "added_by_expert"; + captureStatus: DamagedPartCaptureStatus; + captured: boolean; + url?: string; + + objection: { + isObjected: boolean; + isNewlyAdded: boolean; + addedBy?: "user" | "expert"; + }; + + resend: { + wasRequested: boolean; // expert included this part in a resend request + isFulfilled: boolean; // owner completed the resend + requestedAt?: Date; + fulfilledAt?: Date; + }; + + pricing: { + hasPriceDrop: boolean; + severity: string | null; + coefficient: number | null; + }; +} diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index 6d040ef..d3aed24 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -132,6 +132,7 @@ import { import { CLAIM_V2_TOTAL_PAYMENT_CAP_TOMAN } from "src/constants/repair-amount-limits"; import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto"; import { applyListQueryV2 } from "src/helpers/list-query-v2"; +import { buildEnrichedDamagedParts } from "./dto/claim-damaged-part.enricher"; const CLAIM_V2_TOTAL_PAYMENT_CAP = CLAIM_V2_TOTAL_PAYMENT_CAP_TOMAN; @@ -3867,56 +3868,19 @@ export class ExpertClaimService { const objectionParts = evaluationBlock?.objection?.objectionParts || []; const newObjectionParts = evaluationBlock?.objection?.newParts || []; - const damagedParts = selectedNormExpert.map((sp, index) => { - const ck = sp.catalogKey ?? catalogLikeKeyFromPart(sp); - const cap = getDamagedPartCaptureBlob( - damagedPartsDataExpert, - ck, - selectedNormExpert, - ) as { url?: string; path?: string } | undefined; + // Inside getClaimDetailV2, replace the damagedParts block: - // 1. Cross-reference Price Drop items via partId or name match - const matchingPriceDrop = priceDropLines.find( - (line: any) => - line.partId === sp.id || - line.priceDropPartKey?.toLowerCase() === sp.name?.toLowerCase(), - ); - - // 2. Evaluate if this specific part is flag-linked within an active user objection - const isObjected = objectionParts.some( - (objPart: any) => objPart.id === sp.id || objPart.name === sp.name, - ); - const isNewAddedPart = newObjectionParts.some( - (newPart: any) => newPart.id === sp.id || newPart.name === sp.name, - ); - - return { - index, - partId: catalogPartIdFromSelectedPart(sp), - id: sp.id, - name: sp.name, - side: sp.side, - label_fa: sp.label_fa, - captured: hasDamagedPartCapture( - damagedPartsDataExpert, - ck, - selectedNormExpert, - ), - url: cap?.url || (cap?.path ? buildFileLink(cap.path) : undefined), - - // --- Added Consolidated Production Enrichments --- - severity: matchingPriceDrop ? matchingPriceDrop.severity : null, - coefficient: matchingPriceDrop ? matchingPriceDrop.coefficient : null, - objectionStatus: { - hasObjection: isObjected, - isNewlyAddedByObjection: isNewAddedPart, - // You can map extra parameters here if the front-end requires reason texts - }, - }; + const damagedParts = buildEnrichedDamagedParts({ + selectedParts: selectedNormExpert, + damagedPartsData: damagedPartsDataExpert, + evaluationBlock: (claim as any).evaluation, // damageExpertResend lives here already + expertAddedParts: (claim.damage as any)?.expertAddedParts ?? [], + getDamagedPartCaptureBlob, + hasDamagedPartCapture, + catalogLikeKeyFromPart, + buildFileLink, }); - // --- Combine both branches of the conflict, preserving necessary logic from both --- - // 1. Vehicle payload logic from "upstream" let vehiclePayload = claim.vehicle as any; const hasClaimVehicle = @@ -4377,9 +4341,29 @@ export class ExpertClaimService { ? [...(claim as any).damage.selectedParts] : []; + // Diff to find parts the expert is adding that weren't in the original selection + const previousPartIds = new Set( + previousNorm.map((p) => p.id ?? `${p.name}::${p.side}`), + ); + const expertAddedNow = nextNorm.filter( + (p) => !previousPartIds.has(p.id ?? `${p.name}::${p.side}`), + ); + + // Merge with parts the expert added in any previous edits on this claim (idempotent) + const existingExpertAdded: any[] = + (claim.damage as any)?.expertAddedParts ?? []; + const existingExpertAddedIds = new Set( + existingExpertAdded.map((p: any) => p.id ?? `${p.name}::${p.side}`), + ); + const expertAddedToAppend = expertAddedNow.filter( + (p) => !existingExpertAddedIds.has(p.id ?? `${p.name}::${p.side}`), + ); + const mergedExpertAdded = [...existingExpertAdded, ...expertAddedToAppend]; + const $set: Record = { "damage.selectedParts": nextNorm, "media.damagedParts": nextMedia, + "damage.expertAddedParts": mergedExpertAdded, }; await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, { @@ -4396,6 +4380,7 @@ export class ExpertClaimService { metadata: { previousSelectedParts: previous, selectedParts: nextNorm, + expertAddedParts: mergedExpertAdded, ...(damagedPartsEditSnapshot && { expertProfileSnapshot: damagedPartsEditSnapshot, }), @@ -4408,6 +4393,7 @@ export class ExpertClaimService { claimRequestId: claim._id.toString(), selectedParts: nextNorm, previousSelectedParts: previous, + expertAddedParts: mergedExpertAdded, message: "Damaged parts updated successfully.", }; } From 4a189ba4efaeb2ff43f4457685a745348f1b6d0d Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Mon, 1 Jun 2026 11:49:25 +0330 Subject: [PATCH 3/7] YARA-972 --- .../workflow-step-management.module.ts | 12 +- .../workflow-step-management.service.ts | 1229 +++++++++-------- 2 files changed, 661 insertions(+), 580 deletions(-) diff --git a/src/workflow-step-management/workflow-step-management.module.ts b/src/workflow-step-management/workflow-step-management.module.ts index 77f3aea..e52b4c3 100644 --- a/src/workflow-step-management/workflow-step-management.module.ts +++ b/src/workflow-step-management/workflow-step-management.module.ts @@ -1,12 +1,12 @@ -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 { 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'; +} from "./entities/schema/workflow-step.schema"; @Module({ imports: [ diff --git a/src/workflow-step-management/workflow-step-management.service.ts b/src/workflow-step-management/workflow-step-management.service.ts index f30f3f9..77e5251 100644 --- a/src/workflow-step-management/workflow-step-management.service.ts +++ b/src/workflow-step-management/workflow-step-management.service.ts @@ -1,19 +1,40 @@ -import { Injectable, NotFoundException, BadRequestException, ConflictException } from '@nestjs/common'; -import { WorkflowStepDbService } from './entities/db-service/workflow-step.db.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 { WorkflowStepModel } from './entities/schema/workflow-step.schema'; +import { + Injectable, + NotFoundException, + BadRequestException, + ConflictException, + Logger, +} from "@nestjs/common"; +import { WorkflowStepDbService } from "./entities/db-service/workflow-step.db.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 { WorkflowStepModel } from "./entities/schema/workflow-step.schema"; // Type alias for combined workflow steps type AnyWorkflowStep = WorkflowStep | ClaimWorkflowStep; @Injectable() export class WorkflowStepManagementService { - constructor( - private readonly workflowStepDbService: WorkflowStepDbService, - ) {} + private readonly logger = new Logger(WorkflowStepManagementService.name); + + constructor(private readonly workflowStepDbService: WorkflowStepDbService) {} + + async onModuleInit() { + try { + this.logger.log("Seeding workflow steps..."); + const result = await this.initializeDefaultSteps(); + this.logger.log( + `Workflow steps seeded — upserted: ${result?.upsertedCount ?? 0}, modified: ${result?.modifiedCount ?? 0}, matched: ${result?.matchedCount ?? 0}`, + ); + } catch (err) { + this.logger.error( + "Failed to seed workflow steps", + err instanceof Error ? err.stack : String(err), + ); + } + } /** * Create a new workflow step @@ -22,7 +43,9 @@ export class WorkflowStepManagementService { // Check if step already exists const exists = await this.workflowStepDbService.exists(createDto.stepKey); if (exists) { - throw new ConflictException(`Workflow step with key ${createDto.stepKey} already exists`); + throw new ConflictException( + `Workflow step with key ${createDto.stepKey} already exists`, + ); } return await this.workflowStepDbService.create(createDto); @@ -59,7 +82,9 @@ export class WorkflowStepManagementService { async findByStepKey(stepKey: AnyWorkflowStep): Promise { const step = await this.workflowStepDbService.findByStepKey(stepKey as any); if (!step) { - throw new NotFoundException(`Workflow step with key ${stepKey} not found`); + throw new NotFoundException( + `Workflow step with key ${stepKey} not found`, + ); } return step; } @@ -67,24 +92,32 @@ export class WorkflowStepManagementService { /** * Find a workflow step by stepKey or stepNumber (intelligent lookup) */ - async findByStepKeyOrNumber(identifier: string | number): Promise { + async findByStepKeyOrNumber( + identifier: string | number, + ): Promise { // If identifier is a number or can be parsed as a number, search by stepNumber - const stepNumber = typeof identifier === 'number' ? identifier : Number(identifier); - + const stepNumber = + typeof identifier === "number" ? identifier : Number(identifier); + if (!isNaN(stepNumber)) { // Search by step number - const step = await this.workflowStepDbService.findByStepNumber(stepNumber); + const step = + await this.workflowStepDbService.findByStepNumber(stepNumber); if (step) { return step; } } - + // Otherwise, search by stepKey (supports both blame and claim) - const step = await this.workflowStepDbService.findByStepKey(identifier as any); + const step = await this.workflowStepDbService.findByStepKey( + identifier as any, + ); if (!step) { - throw new NotFoundException(`Workflow step with identifier ${identifier} not found`); + throw new NotFoundException( + `Workflow step with identifier ${identifier} not found`, + ); } - + return step; } @@ -98,7 +131,10 @@ export class WorkflowStepManagementService { /** * Update a workflow step by ID */ - async update(id: string, updateDto: UpdateWorkflowStepDto): Promise { + async update( + id: string, + updateDto: UpdateWorkflowStepDto, + ): Promise { const updated = await this.workflowStepDbService.update(id, updateDto); if (!updated) { throw new NotFoundException(`Workflow step with ID ${id} not found`); @@ -109,10 +145,18 @@ export class WorkflowStepManagementService { /** * Update a workflow step by step key (supports both blame and claim) */ - async updateByStepKey(stepKey: AnyWorkflowStep, updateDto: UpdateWorkflowStepDto): Promise { - const updated = await this.workflowStepDbService.updateByStepKey(stepKey as any, updateDto); + async updateByStepKey( + stepKey: AnyWorkflowStep, + updateDto: UpdateWorkflowStepDto, + ): Promise { + const updated = await this.workflowStepDbService.updateByStepKey( + stepKey as any, + updateDto, + ); if (!updated) { - throw new NotFoundException(`Workflow step with key ${stepKey} not found`); + throw new NotFoundException( + `Workflow step with key ${stepKey} not found`, + ); } return updated; } @@ -133,14 +177,19 @@ export class WorkflowStepManagementService { */ async getNextSteps(stepKey: AnyWorkflowStep): Promise { const currentStep = await this.findByStepKey(stepKey); - - if (!currentStep.nextPossibleSteps || currentStep.nextPossibleSteps.length === 0) { + + if ( + !currentStep.nextPossibleSteps || + currentStep.nextPossibleSteps.length === 0 + ) { return []; } const nextSteps: WorkflowStepModel[] = []; for (const nextStepKey of currentStep.nextPossibleSteps) { - const step = await this.workflowStepDbService.findByStepKey(nextStepKey as any); + const step = await this.workflowStepDbService.findByStepKey( + nextStepKey as any, + ); if (step && step.isActive) { nextSteps.push(step); } @@ -158,7 +207,10 @@ export class WorkflowStepManagementService { ): Promise<{ canAccess: boolean; missingSteps: AnyWorkflowStep[] }> { const step = await this.findByStepKey(stepKey); - if (!step.requiredPreviousSteps || step.requiredPreviousSteps.length === 0) { + if ( + !step.requiredPreviousSteps || + step.requiredPreviousSteps.length === 0 + ) { return { canAccess: true, missingSteps: [] }; } @@ -177,7 +229,7 @@ export class WorkflowStepManagementService { */ async getStepFields(stepKey: AnyWorkflowStep): Promise { const step = await this.findByStepKey(stepKey); - + return { stepKey: step.stepKey, stepName_fa: step.stepName_fa, @@ -202,963 +254,992 @@ export class WorkflowStepManagementService { const defaultSteps: CreateWorkflowStepDto[] = [ { stepKey: WorkflowStep.CREATED, - type: 'THIRD_PARTY', + type: "THIRD_PARTY", stepNumber: 1, isActive: true, - stepName_fa: 'ایجاد نوع درخواست', - stepName_en: 'request_type', - description_fa: 'انتخاب نوع درخواست توسط کاربر (شخص ثالث یا بدنه)', - description_en: 'Select request type by user (THIRD_PARTY or CAR_BODY)', - category: 'INITIAL', + stepName_fa: "ایجاد نوع درخواست", + stepName_en: "request_type", + description_fa: "انتخاب نوع درخواست توسط کاربر (شخص ثالث یا بدنه)", + description_en: "Select request type by user (THIRD_PARTY or CAR_BODY)", + category: "INITIAL", requiredPreviousSteps: [], nextPossibleSteps: [WorkflowStep.FIRST_BLAME_CONFESSION], - allowedRoles: ['user'], + allowedRoles: ["user"], estimatedDuration: 1, fields: [ { - id: '507f1f77bcf86cd799439011' as any, - name_fa: 'نوع درخواست', - name_en: 'requestType', - label_fa: 'نوع درخواست', - label_en: 'Request Type', - placeholder_fa: 'نوع درخواست خود را انتخاب کنید', - placeholder_en: 'Select your request type', + id: "507f1f77bcf86cd799439011" as any, + name_fa: "نوع درخواست", + name_en: "requestType", + label_fa: "نوع درخواست", + label_en: "Request Type", + placeholder_fa: "نوع درخواست خود را انتخاب کنید", + placeholder_en: "Select your request type", value: [ - { label_fa: 'شخص ثالث', label_en: 'Third Party', value: 'THIRD_PARTY' }, - { label_fa: 'بدنه', label_en: 'Car Body', value: 'CAR_BODY' } + { + label_fa: "شخص ثالث", + label_en: "Third Party", + value: "THIRD_PARTY", + }, + { label_fa: "بدنه", label_en: "Car Body", value: "CAR_BODY" }, ], - dataType: 'select', + dataType: "select", required: true, order: 1, isActive: true, validation: { - enum: ['THIRD_PARTY', 'CAR_BODY'] - } - } + enum: ["THIRD_PARTY", "CAR_BODY"], + }, + }, ], metadata: { - icon: 'clipboard-list', - color: '#8B5CF6' - } + icon: "clipboard-list", + color: "#8B5CF6", + }, }, { stepKey: WorkflowStep.FIRST_BLAME_CONFESSION, - type: 'THIRD_PARTY', + type: "THIRD_PARTY", stepNumber: 2, isActive: true, - stepName_fa: 'اعتراف طرف اول', - stepName_en: 'First Party Blame Confession', - description_fa: 'طرف اول وضعیت تقصیر و اعتراف خود را مشخص می‌کند', - description_en: 'First party declares blame status and confession', - category: 'FIRST_PARTY', + stepName_fa: "اعتراف طرف اول", + stepName_en: "First Party Blame Confession", + description_fa: "طرف اول وضعیت تقصیر و اعتراف خود را مشخص می‌کند", + description_en: "First party declares blame status and confession", + category: "FIRST_PARTY", requiredPreviousSteps: [WorkflowStep.CREATED], nextPossibleSteps: [WorkflowStep.FIRST_VIDEO], - allowedRoles: ['user'], + allowedRoles: ["user"], estimatedDuration: 2, fields: [ { - id: '507f1f77bcf86cd799439012' as any, - name_fa: 'وضعیت تقصیر', - name_en: 'blameStatus', - label_fa: 'وضعیت تقصیر', - label_en: 'Blame Status', - placeholder_fa: 'وضعیت تقصیر را انتخاب کنید', - placeholder_en: 'Select blame status', + id: "507f1f77bcf86cd799439012" as any, + name_fa: "وضعیت تقصیر", + name_en: "blameStatus", + label_fa: "وضعیت تقصیر", + label_en: "Blame Status", + placeholder_fa: "وضعیت تقصیر را انتخاب کنید", + placeholder_en: "Select blame status", value: [ - { label_fa: 'موافق', label_en: 'Agreed', value: 'AGREED' }, - { label_fa: 'مخالف', label_en: 'Disagreement', value: 'DISAGREEMENT' } + { label_fa: "موافق", label_en: "Agreed", value: "AGREED" }, + { + label_fa: "مخالف", + label_en: "Disagreement", + value: "DISAGREEMENT", + }, ], - dataType: 'select', + dataType: "select", required: true, order: 1, isActive: true, validation: { - enum: ['AGREED', 'DISAGREEMENT'] - } + enum: ["AGREED", "DISAGREEMENT"], + }, }, { - id: '507f1f77bcf86cd799439013' as any, - name_fa: 'اعتراف', - name_en: 'confess', - label_fa: 'اعتراف', - label_en: 'Confession', - placeholder_fa: 'نوع اعتراف خود را انتخاب کنید', - placeholder_en: 'Select your confession type', + id: "507f1f77bcf86cd799439013" as any, + name_fa: "اعتراف", + name_en: "confess", + label_fa: "اعتراف", + label_en: "Confession", + placeholder_fa: "نوع اعتراف خود را انتخاب کنید", + placeholder_en: "Select your confession type", value: [ - { label_fa: 'مقصر', label_en: 'Guilty', value: 'guilty' }, - { label_fa: 'آسیب دیده', label_en: 'Damaged', value: 'damaged' } + { label_fa: "مقصر", label_en: "Guilty", value: "guilty" }, + { label_fa: "آسیب دیده", label_en: "Damaged", value: "damaged" }, ], - dataType: 'select', + dataType: "select", required: true, order: 2, isActive: true, validation: { - enum: ['guilty', 'damaged'] - } - } + enum: ["guilty", "damaged"], + }, + }, ], metadata: { - icon: 'balance-scale', - color: '#F59E0B', - requiresAuth: true - } + icon: "balance-scale", + color: "#F59E0B", + requiresAuth: true, + }, }, { stepKey: WorkflowStep.FIRST_VIDEO, - type: 'THIRD_PARTY', + type: "THIRD_PARTY", stepNumber: 3, isActive: true, - stepName_fa: 'ضبط ویدیو طرف اول', - stepName_en: 'First Party Video Capture', - description_fa: 'ضبط ویدیو مستقیم از دوربین توسط طرف اول (گالری مجاز نیست)', - description_en: 'Live video capture from camera by first party (gallery not allowed)', - category: 'FIRST_PARTY', + stepName_fa: "ضبط ویدیو طرف اول", + stepName_en: "First Party Video Capture", + description_fa: + "ضبط ویدیو مستقیم از دوربین توسط طرف اول (گالری مجاز نیست)", + description_en: + "Live video capture from camera by first party (gallery not allowed)", + category: "FIRST_PARTY", requiredPreviousSteps: [WorkflowStep.FIRST_BLAME_CONFESSION], nextPossibleSteps: [WorkflowStep.FIRST_INITIAL_FORM], - allowedRoles: ['user'], + allowedRoles: ["user"], estimatedDuration: 3, fields: [ { - id: '507f1f77bcf86cd799439014' as any, - name_fa: 'فایل ویدیو', - name_en: 'file', - label_fa: 'ضبط ویدیو', - label_en: 'Video Recording', - placeholder_fa: 'ویدیو خود را از دوربین ضبط کنید', - placeholder_en: 'Record your video from camera', + id: "507f1f77bcf86cd799439014" as any, + name_fa: "فایل ویدیو", + name_en: "file", + label_fa: "ضبط ویدیو", + label_en: "Video Recording", + placeholder_fa: "ویدیو خود را از دوربین ضبط کنید", + placeholder_en: "Record your video from camera", value: null, - dataType: 'video-capture', + dataType: "video-capture", required: true, order: 1, isActive: true, validation: { maxSize: 52428800, maxDuration: 180, - allowedTypes: ['video/mp4', 'video/webm'], - captureMode: 'camera-only', - allowGallery: false - } - } + allowedTypes: ["video/mp4", "video/webm"], + captureMode: "camera-only", + allowGallery: false, + }, + }, ], metadata: { - icon: 'video-camera', - color: '#EF4444', - captureType: 'live-camera', + icon: "video-camera", + color: "#EF4444", + captureType: "live-camera", allowGallerySelection: false, - requiresCamera: true - } + requiresCamera: true, + }, }, { stepKey: WorkflowStep.FIRST_INITIAL_FORM, - type: 'THIRD_PARTY', + type: "THIRD_PARTY", stepNumber: 4, isActive: true, - stepName_fa: 'فرم اصلی اطلاعات راننده و بیمه‌گذار', - stepName_en: 'Main Form - Driver & Insurer Information', - description_fa: 'وارد کردن اطلاعات کامل راننده، بیمه‌گذار و خودرو', - description_en: 'Enter complete driver, insurer and vehicle information', - category: 'FIRST_PARTY', + stepName_fa: "فرم اصلی اطلاعات راننده و بیمه‌گذار", + stepName_en: "Main Form - Driver & Insurer Information", + description_fa: "وارد کردن اطلاعات کامل راننده، بیمه‌گذار و خودرو", + description_en: + "Enter complete driver, insurer and vehicle information", + category: "FIRST_PARTY", requiredPreviousSteps: [WorkflowStep.FIRST_VIDEO], nextPossibleSteps: [WorkflowStep.FIRST_LOCATION], - allowedRoles: ['user'], + allowedRoles: ["user"], estimatedDuration: 5, fields: [ { - id: '507f1f77bcf86cd799439015' as any, - name_fa: 'کد ملی مالک', - name_en: 'nationalCodeOfOwner', - label_fa: 'کد ملی مالک', - label_en: 'National Code of Owner', - placeholder_fa: 'کد ملی ۱۰ رقمی مالک را وارد کنید', - placeholder_en: 'Enter 10-digit national code of insurer', + id: "507f1f77bcf86cd799439015" as any, + name_fa: "کد ملی مالک", + name_en: "nationalCodeOfOwner", + label_fa: "کد ملی مالک", + label_en: "National Code of Owner", + placeholder_fa: "کد ملی ۱۰ رقمی مالک را وارد کنید", + placeholder_en: "Enter 10-digit national code of insurer", value: null, - dataType: 'string', + dataType: "string", required: true, order: 1, isActive: true, validation: { - pattern: '^[0-9]{10}$', + pattern: "^[0-9]{10}$", minLength: 10, - maxLength: 10 - } + maxLength: 10, + }, }, { - id: '507f1f77bcf86cd799439016' as any, - name_fa: 'کد ملی راننده', - name_en: 'nationalCodeOfDriver', - label_fa: 'کد ملی راننده', - label_en: 'National Code of Driver', - placeholder_fa: 'کد ملی ۱۰ رقمی راننده را وارد کنید', - placeholder_en: 'Enter 10-digit national code of driver', + id: "507f1f77bcf86cd799439016" as any, + name_fa: "کد ملی راننده", + name_en: "nationalCodeOfDriver", + label_fa: "کد ملی راننده", + label_en: "National Code of Driver", + placeholder_fa: "کد ملی ۱۰ رقمی راننده را وارد کنید", + placeholder_en: "Enter 10-digit national code of driver", value: null, - dataType: 'string', + dataType: "string", required: true, order: 2, isActive: true, validation: { - pattern: '^[0-9]{10}$', + pattern: "^[0-9]{10}$", minLength: 10, - maxLength: 10 - } + maxLength: 10, + }, }, { - id: '507f1f77bcf86cd799439017' as any, - name_fa: 'شماره گواهینامه بیمه‌گذار', - name_en: 'insurerLicense', - label_fa: 'شماره گواهینامه بیمه‌گذار', - label_en: 'Insurer License Number', - placeholder_fa: 'شماره گواهینامه بیمه‌گذار را وارد کنید', - placeholder_en: 'Enter insurer license number', + id: "507f1f77bcf86cd799439017" as any, + name_fa: "شماره گواهینامه بیمه‌گذار", + name_en: "insurerLicense", + label_fa: "شماره گواهینامه بیمه‌گذار", + label_en: "Insurer License Number", + placeholder_fa: "شماره گواهینامه بیمه‌گذار را وارد کنید", + placeholder_en: "Enter insurer license number", value: null, - dataType: 'string', + dataType: "string", required: true, order: 3, isActive: true, validation: { minLength: 5, - maxLength: 20 - } + maxLength: 20, + }, }, { - id: '507f1f77bcf86cd799439018' as any, - name_fa: 'شماره گواهینامه راننده', - name_en: 'driverLicense', - label_fa: 'شماره گواهینامه راننده', - label_en: 'Driver License Number', - placeholder_fa: 'شماره گواهینامه راننده را وارد کنید', - placeholder_en: 'Enter driver license number', + id: "507f1f77bcf86cd799439018" as any, + name_fa: "شماره گواهینامه راننده", + name_en: "driverLicense", + label_fa: "شماره گواهینامه راننده", + label_en: "Driver License Number", + placeholder_fa: "شماره گواهینامه راننده را وارد کنید", + placeholder_en: "Enter driver license number", value: null, - dataType: 'string', + dataType: "string", required: true, order: 4, isActive: true, validation: { minLength: 5, - maxLength: 20 - } + maxLength: 20, + }, }, { - id: '507f1f77bcf86cd799439019' as any, - name_fa: 'پلاک خودرو', - name_en: 'plate', - label_fa: 'پلاک خودرو', - label_en: 'Vehicle Plate', - placeholder_fa: 'اطلاعات پلاک خودرو را وارد کنید', - placeholder_en: 'Enter vehicle plate information', + id: "507f1f77bcf86cd799439019" as any, + name_fa: "پلاک خودرو", + name_en: "plate", + label_fa: "پلاک خودرو", + label_en: "Vehicle Plate", + placeholder_fa: "اطلاعات پلاک خودرو را وارد کنید", + placeholder_en: "Enter vehicle plate information", value: null, - dataType: 'object', + dataType: "object", required: true, order: 5, isActive: true, validation: { - type: 'object', + type: "object", properties: { - leftDigits: { type: 'number', min: 10, max: 99 }, - centerAlphabet: { type: 'string', pattern: '^[آ-ی]{1}$' }, - centerDigits: { type: 'number', min: 100, max: 999 }, - ir: { type: 'number', min: 10, max: 99 } - } - } + leftDigits: { type: "number", min: 10, max: 99 }, + centerAlphabet: { type: "string", pattern: "^[آ-ی]{1}$" }, + centerDigits: { type: "number", min: 100, max: 999 }, + ir: { type: "number", min: 10, max: 99 }, + }, + }, }, { - id: '507f1f77bcf86cd79943901a' as any, - name_fa: 'راننده همان بیمه‌گذار است', - name_en: 'driverIsInsurer', - label_fa: 'راننده همان بیمه‌گذار است', - label_en: 'Driver is Insurer', - placeholder_fa: '', - placeholder_en: '', + id: "507f1f77bcf86cd79943901a" as any, + name_fa: "راننده همان بیمه‌گذار است", + name_en: "driverIsInsurer", + label_fa: "راننده همان بیمه‌گذار است", + label_en: "Driver is Insurer", + placeholder_fa: "", + placeholder_en: "", value: null, - dataType: 'boolean', + dataType: "boolean", required: true, order: 6, isActive: true, validation: { - type: 'boolean' - } + type: "boolean", + }, }, { - id: '507f1f77bcf86cd79943901b' as any, - name_fa: 'خودرو صفر است', - name_en: 'isNewCar', - label_fa: 'خودرو صفر است', - label_en: 'Is New Car', - placeholder_fa: '', - placeholder_en: '', + id: "507f1f77bcf86cd79943901b" as any, + name_fa: "خودرو صفر است", + name_en: "isNewCar", + label_fa: "خودرو صفر است", + label_en: "Is New Car", + placeholder_fa: "", + placeholder_en: "", value: null, - dataType: 'boolean', + dataType: "boolean", required: true, order: 7, isActive: true, validation: { - type: 'boolean' - } + type: "boolean", + }, }, { - id: '507f1f77bcf86cd79943901c' as any, - name_fa: 'بدون گواهی', - name_en: 'userNoCertificate', - label_fa: 'کاربر گواهی ندارد', - label_en: 'User Has No Certificate', - placeholder_fa: '', - placeholder_en: '', + id: "507f1f77bcf86cd79943901c" as any, + name_fa: "بدون گواهی", + name_en: "userNoCertificate", + label_fa: "کاربر گواهی ندارد", + label_en: "User Has No Certificate", + placeholder_fa: "", + placeholder_en: "", value: null, - dataType: 'boolean', + dataType: "boolean", required: true, order: 8, isActive: true, validation: { - type: 'boolean' - } + type: "boolean", + }, }, { - id: '507f1f77bcf86cd79943901d' as any, - name_fa: 'تاریخ تولد بیمه‌گذار', - name_en: 'insurerBirthday', - label_fa: 'تاریخ تولد بیمه‌گذار', - label_en: 'Insurer Birthday', - placeholder_fa: 'تاریخ تولد بیمه‌گذار (timestamp)', - placeholder_en: 'Insurer birthday (timestamp)', + id: "507f1f77bcf86cd79943901d" as any, + name_fa: "تاریخ تولد بیمه‌گذار", + name_en: "insurerBirthday", + label_fa: "تاریخ تولد بیمه‌گذار", + label_en: "Insurer Birthday", + placeholder_fa: "تاریخ تولد بیمه‌گذار (timestamp)", + placeholder_en: "Insurer birthday (timestamp)", value: null, - dataType: 'number', + dataType: "number", required: true, order: 9, isActive: true, validation: { - type: 'number', - min: 0 - } + type: "number", + min: 0, + }, }, { - id: '507f1f77bcf86cd79943901e' as any, - name_fa: 'تاریخ تولد راننده', - name_en: 'driverBirthday', - label_fa: 'تاریخ تولد راننده', - label_en: 'Driver Birthday', - placeholder_fa: 'تاریخ تولد راننده را وارد کنید', - placeholder_en: 'Enter driver birthday', + id: "507f1f77bcf86cd79943901e" as any, + name_fa: "تاریخ تولد راننده", + name_en: "driverBirthday", + label_fa: "تاریخ تولد راننده", + label_en: "Driver Birthday", + placeholder_fa: "تاریخ تولد راننده را وارد کنید", + placeholder_en: "Enter driver birthday", value: null, - dataType: 'string', + dataType: "string", required: true, order: 10, isActive: true, validation: { - format: 'date' - } - } + format: "date", + }, + }, ], metadata: { - icon: 'clipboard-list', - color: '#8B5CF6', - formType: 'detailed', - hasComplexValidation: true - } + icon: "clipboard-list", + color: "#8B5CF6", + formType: "detailed", + hasComplexValidation: true, + }, }, { stepKey: WorkflowStep.FIRST_LOCATION, - type: 'THIRD_PARTY', + type: "THIRD_PARTY", stepNumber: 5, isActive: true, - stepName_fa: 'موقعیت مکانی حادثه', - stepName_en: 'Accident Location', - description_fa: 'ثبت موقعیت جغرافیایی محل وقوع حادثه', - description_en: 'Record geographical location of accident scene', - category: 'FIRST_PARTY', + stepName_fa: "موقعیت مکانی حادثه", + stepName_en: "Accident Location", + description_fa: "ثبت موقعیت جغرافیایی محل وقوع حادثه", + description_en: "Record geographical location of accident scene", + category: "FIRST_PARTY", requiredPreviousSteps: [WorkflowStep.FIRST_INITIAL_FORM], nextPossibleSteps: [WorkflowStep.FIRST_DESCRIPTION], - allowedRoles: ['user'], + allowedRoles: ["user"], estimatedDuration: 2, fields: [ { - id: '507f1f77bcf86cd79943901f' as any, - name_fa: 'عرض جغرافیایی', - name_en: 'lat', - label_fa: 'عرض جغرافیایی', - label_en: 'Latitude', - placeholder_fa: 'عرض جغرافیایی را وارد کنید', - placeholder_en: 'Enter latitude', + id: "507f1f77bcf86cd79943901f" as any, + name_fa: "عرض جغرافیایی", + name_en: "lat", + label_fa: "عرض جغرافیایی", + label_en: "Latitude", + placeholder_fa: "عرض جغرافیایی را وارد کنید", + placeholder_en: "Enter latitude", value: null, - dataType: 'number', + dataType: "number", required: true, order: 1, isActive: true, validation: { - type: 'number', + type: "number", min: -90, - max: 90 - } + max: 90, + }, }, { - id: '507f1f77bcf86cd799439020' as any, - name_fa: 'طول جغرافیایی', - name_en: 'lon', - label_fa: 'طول جغرافیایی', - label_en: 'Longitude', - placeholder_fa: 'طول جغرافیایی را وارد کنید', - placeholder_en: 'Enter longitude', + id: "507f1f77bcf86cd799439020" as any, + name_fa: "طول جغرافیایی", + name_en: "lon", + label_fa: "طول جغرافیایی", + label_en: "Longitude", + placeholder_fa: "طول جغرافیایی را وارد کنید", + placeholder_en: "Enter longitude", value: null, - dataType: 'number', + dataType: "number", required: true, order: 2, isActive: true, validation: { - type: 'number', + type: "number", min: -180, - max: 180 - } - } + max: 180, + }, + }, ], metadata: { - icon: 'map-pin', - color: '#10B981', + icon: "map-pin", + color: "#10B981", requiresGPS: true, - autoDetectLocation: true - } + autoDetectLocation: true, + }, }, { stepKey: WorkflowStep.FIRST_VOICE, - type: 'THIRD_PARTY', + type: "THIRD_PARTY", stepNumber: 6, isActive: true, - stepName_fa: 'ضبط صدای طرف اول', - stepName_en: 'First Party Voice Recording', - description_fa: 'ضبط توضیحات صوتی توسط طرف اول', - description_en: 'Voice recording of explanations by first party', - category: 'FIRST_PARTY', + stepName_fa: "ضبط صدای طرف اول", + stepName_en: "First Party Voice Recording", + description_fa: "ضبط توضیحات صوتی توسط طرف اول", + description_en: "Voice recording of explanations by first party", + category: "FIRST_PARTY", requiredPreviousSteps: [WorkflowStep.FIRST_LOCATION], nextPossibleSteps: [WorkflowStep.FIRST_DESCRIPTION], - allowedRoles: ['user'], + allowedRoles: ["user"], estimatedDuration: 3, fields: [ { - id: '507f1f77bcf86cd799439021' as any, - name_fa: 'فایل صوتی', - name_en: 'file', - label_fa: 'ضبط صدا', - label_en: 'Voice Recording', - placeholder_fa: 'توضیحات صوتی خود را ضبط کنید', - placeholder_en: 'Record your voice explanation', + id: "507f1f77bcf86cd799439021" as any, + name_fa: "فایل صوتی", + name_en: "file", + label_fa: "ضبط صدا", + label_en: "Voice Recording", + placeholder_fa: "توضیحات صوتی خود را ضبط کنید", + placeholder_en: "Record your voice explanation", value: null, - dataType: 'audio-file', + dataType: "audio-file", required: true, order: 1, isActive: true, validation: { maxSize: 10485760, maxDuration: 300, - allowedTypes: ['audio/mp3', 'audio/mpeg', 'audio/wav', 'audio/m4a', 'audio/webm'] - } - } + allowedTypes: [ + "audio/mp3", + "audio/mpeg", + "audio/wav", + "audio/m4a", + "audio/webm", + ], + }, + }, ], metadata: { - icon: 'microphone', - color: '#F59E0B', - recordingType: 'voice', - requiresMicrophone: true - } + icon: "microphone", + color: "#F59E0B", + recordingType: "voice", + requiresMicrophone: true, + }, }, { stepKey: WorkflowStep.FIRST_DESCRIPTION, - type: 'THIRD_PARTY', + type: "THIRD_PARTY", stepNumber: 7, isActive: true, - stepName_fa: 'توضیحات طرف اول', - stepName_en: 'First Party Description', - description_fa: 'ثبت توضیحات و جزئیات حادثه توسط طرف اول', - description_en: 'Record accident description and details by first party', - category: 'FIRST_PARTY', + stepName_fa: "توضیحات طرف اول", + stepName_en: "First Party Description", + description_fa: "ثبت توضیحات و جزئیات حادثه توسط طرف اول", + description_en: + "Record accident description and details by first party", + category: "FIRST_PARTY", requiredPreviousSteps: [WorkflowStep.FIRST_VOICE], nextPossibleSteps: [WorkflowStep.FIRST_INVITE_SECOND], - allowedRoles: ['user'], + allowedRoles: ["user"], estimatedDuration: 4, fields: [ { - id: '507f1f77bcf86cd799439022' as any, - name_fa: 'توضیحات', - name_en: 'desc', - label_fa: 'توضیحات حادثه', - label_en: 'Accident Description', - placeholder_fa: 'توضیحات کامل حادثه را وارد کنید', - placeholder_en: 'Enter complete accident description', + id: "507f1f77bcf86cd799439022" as any, + name_fa: "توضیحات", + name_en: "desc", + label_fa: "توضیحات حادثه", + label_en: "Accident Description", + placeholder_fa: "توضیحات کامل حادثه را وارد کنید", + placeholder_en: "Enter complete accident description", value: null, - dataType: 'textarea', + dataType: "textarea", required: true, order: 1, isActive: true, validation: { minLength: 10, - maxLength: 2000 - } + maxLength: 2000, + }, }, { - id: '507f1f77bcf86cd799439023' as any, - name_fa: 'تاریخ حادثه', - name_en: 'accidentDate', - label_fa: 'تاریخ حادثه', - label_en: 'Accident Date', - placeholder_fa: 'تاریخ وقوع حادثه را وارد کنید', - placeholder_en: 'Enter accident date', + id: "507f1f77bcf86cd799439023" as any, + name_fa: "تاریخ حادثه", + name_en: "accidentDate", + label_fa: "تاریخ حادثه", + label_en: "Accident Date", + placeholder_fa: "تاریخ وقوع حادثه را وارد کنید", + placeholder_en: "Enter accident date", value: null, - dataType: 'date', + dataType: "date", required: true, order: 2, isActive: true, validation: { - format: 'YYYY-MM-DD' - } + format: "YYYY-MM-DD", + }, }, { - id: '507f1f77bcf86cd799439024' as any, - name_fa: 'زمان حادثه', - name_en: 'accidentTime', - label_fa: 'زمان حادثه', - label_en: 'Accident Time', - placeholder_fa: 'زمان وقوع حادثه را وارد کنید', - placeholder_en: 'Enter accident time', + id: "507f1f77bcf86cd799439024" as any, + name_fa: "زمان حادثه", + name_en: "accidentTime", + label_fa: "زمان حادثه", + label_en: "Accident Time", + placeholder_fa: "زمان وقوع حادثه را وارد کنید", + placeholder_en: "Enter accident time", value: null, - dataType: 'time', + dataType: "time", required: true, order: 3, isActive: true, validation: { - format: 'HH:mm' - } - } + format: "HH:mm", + }, + }, ], metadata: { - icon: 'file-text', - color: '#6366F1' - } + icon: "file-text", + color: "#6366F1", + }, }, { stepKey: WorkflowStep.FIRST_INVITE_SECOND, - type: 'THIRD_PARTY', + type: "THIRD_PARTY", stepNumber: 8, isActive: true, - stepName_fa: 'دعوت طرف دوم', - stepName_en: 'Invite Second Party', - description_fa: 'ارسال دعوتنامه به طرف دوم برای ثبت اطلاعات', - description_en: 'Send invitation to second party to register information', - category: 'FIRST_PARTY', + stepName_fa: "دعوت طرف دوم", + stepName_en: "Invite Second Party", + description_fa: "ارسال دعوتنامه به طرف دوم برای ثبت اطلاعات", + description_en: + "Send invitation to second party to register information", + category: "FIRST_PARTY", requiredPreviousSteps: [WorkflowStep.FIRST_DESCRIPTION], nextPossibleSteps: [WorkflowStep.SECOND_INITIAL_FORM], - allowedRoles: ['user'], + allowedRoles: ["user"], estimatedDuration: 1, fields: [], metadata: { - icon: 'user-plus', - color: '#8B5CF6', + icon: "user-plus", + color: "#8B5CF6", requiresPhoneNumber: true, - actionType: 'invitation' - } + actionType: "invitation", + }, }, { stepKey: WorkflowStep.SECOND_INITIAL_FORM, - type: 'THIRD_PARTY', + type: "THIRD_PARTY", stepNumber: 9, isActive: true, - stepName_fa: 'فرم اصلی اطلاعات طرف دوم', - stepName_en: 'Second Party Main Form', - description_fa: 'وارد کردن اطلاعات کامل راننده، بیمه‌گذار و خودرو طرف دوم', - description_en: 'Enter complete driver, insurer and vehicle information for second party', - category: 'SECOND_PARTY', + stepName_fa: "فرم اصلی اطلاعات طرف دوم", + stepName_en: "Second Party Main Form", + description_fa: + "وارد کردن اطلاعات کامل راننده، بیمه‌گذار و خودرو طرف دوم", + description_en: + "Enter complete driver, insurer and vehicle information for second party", + category: "SECOND_PARTY", requiredPreviousSteps: [WorkflowStep.FIRST_INVITE_SECOND], nextPossibleSteps: [WorkflowStep.SECOND_LOCATION], - allowedRoles: ['user'], + allowedRoles: ["user"], estimatedDuration: 5, fields: [ { - id: '507f1f77bcf86cd799439025' as any, - name_fa: 'کد ملی مالک', - name_en: 'nationalCodeOfOwner', - label_fa: 'کد ملی مالک', - label_en: 'National Code of Owner', - placeholder_fa: 'کد ملی ۱۰ رقمی مالک را وارد کنید', - placeholder_en: 'Enter 10-digit national code of insurer', + id: "507f1f77bcf86cd799439025" as any, + name_fa: "کد ملی مالک", + name_en: "nationalCodeOfOwner", + label_fa: "کد ملی مالک", + label_en: "National Code of Owner", + placeholder_fa: "کد ملی ۱۰ رقمی مالک را وارد کنید", + placeholder_en: "Enter 10-digit national code of insurer", value: null, - dataType: 'string', + dataType: "string", required: true, order: 1, isActive: true, validation: { - pattern: '^[0-9]{10}$', + pattern: "^[0-9]{10}$", minLength: 10, - maxLength: 10 - } + maxLength: 10, + }, }, { - id: '507f1f77bcf86cd799439026' as any, - name_fa: 'کد ملی راننده', - name_en: 'nationalCodeOfDriver', - label_fa: 'کد ملی راننده', - label_en: 'National Code of Driver', - placeholder_fa: 'کد ملی ۱۰ رقمی راننده را وارد کنید', - placeholder_en: 'Enter 10-digit national code of driver', + id: "507f1f77bcf86cd799439026" as any, + name_fa: "کد ملی راننده", + name_en: "nationalCodeOfDriver", + label_fa: "کد ملی راننده", + label_en: "National Code of Driver", + placeholder_fa: "کد ملی ۱۰ رقمی راننده را وارد کنید", + placeholder_en: "Enter 10-digit national code of driver", value: null, - dataType: 'string', + dataType: "string", required: true, order: 2, isActive: true, validation: { - pattern: '^[0-9]{10}$', + pattern: "^[0-9]{10}$", minLength: 10, - maxLength: 10 - } + maxLength: 10, + }, }, { - id: '507f1f77bcf86cd799439027' as any, - name_fa: 'شماره گواهینامه بیمه‌گذار', - name_en: 'insurerLicense', - label_fa: 'شماره گواهینامه بیمه‌گذار', - label_en: 'Insurer License Number', - placeholder_fa: 'شماره گواهینامه بیمه‌گذار را وارد کنید', - placeholder_en: 'Enter insurer license number', + id: "507f1f77bcf86cd799439027" as any, + name_fa: "شماره گواهینامه بیمه‌گذار", + name_en: "insurerLicense", + label_fa: "شماره گواهینامه بیمه‌گذار", + label_en: "Insurer License Number", + placeholder_fa: "شماره گواهینامه بیمه‌گذار را وارد کنید", + placeholder_en: "Enter insurer license number", value: null, - dataType: 'string', + dataType: "string", required: true, order: 3, isActive: true, validation: { minLength: 5, - maxLength: 20 - } + maxLength: 20, + }, }, { - id: '507f1f77bcf86cd799439028' as any, - name_fa: 'شماره گواهینامه راننده', - name_en: 'driverLicense', - label_fa: 'شماره گواهینامه راننده', - label_en: 'Driver License Number', - placeholder_fa: 'شماره گواهینامه راننده را وارد کنید', - placeholder_en: 'Enter driver license number', + id: "507f1f77bcf86cd799439028" as any, + name_fa: "شماره گواهینامه راننده", + name_en: "driverLicense", + label_fa: "شماره گواهینامه راننده", + label_en: "Driver License Number", + placeholder_fa: "شماره گواهینامه راننده را وارد کنید", + placeholder_en: "Enter driver license number", value: null, - dataType: 'string', + dataType: "string", required: true, order: 4, isActive: true, validation: { minLength: 5, - maxLength: 20 - } + maxLength: 20, + }, }, { - id: '507f1f77bcf86cd799439029' as any, - name_fa: 'پلاک خودرو', - name_en: 'plate', - label_fa: 'پلاک خودرو', - label_en: 'Vehicle Plate', - placeholder_fa: 'اطلاعات پلاک خودرو را وارد کنید', - placeholder_en: 'Enter vehicle plate information', + id: "507f1f77bcf86cd799439029" as any, + name_fa: "پلاک خودرو", + name_en: "plate", + label_fa: "پلاک خودرو", + label_en: "Vehicle Plate", + placeholder_fa: "اطلاعات پلاک خودرو را وارد کنید", + placeholder_en: "Enter vehicle plate information", value: null, - dataType: 'object', + dataType: "object", required: true, order: 5, isActive: true, validation: { - type: 'object', + type: "object", properties: { - leftDigits: { type: 'number', min: 10, max: 99 }, - centerAlphabet: { type: 'string', pattern: '^[آ-ی]{1}$' }, - centerDigits: { type: 'number', min: 100, max: 999 }, - ir: { type: 'number', min: 10, max: 99 } - } - } + leftDigits: { type: "number", min: 10, max: 99 }, + centerAlphabet: { type: "string", pattern: "^[آ-ی]{1}$" }, + centerDigits: { type: "number", min: 100, max: 999 }, + ir: { type: "number", min: 10, max: 99 }, + }, + }, }, { - id: '507f1f77bcf86cd79943902a' as any, - name_fa: 'راننده همان بیمه‌گذار است', - name_en: 'driverIsInsurer', - label_fa: 'راننده همان بیمه‌گذار است', - label_en: 'Driver is Insurer', + id: "507f1f77bcf86cd79943902a" as any, + name_fa: "راننده همان بیمه‌گذار است", + name_en: "driverIsInsurer", + label_fa: "راننده همان بیمه‌گذار است", + label_en: "Driver is Insurer", value: null, - dataType: 'boolean', + dataType: "boolean", required: true, order: 6, - isActive: true + isActive: true, }, { - id: '507f1f77bcf86cd79943902b' as any, - name_fa: 'خودرو صفر است', - name_en: 'isNewCar', - label_fa: 'خودرو صفر است', - label_en: 'Is New Car', + id: "507f1f77bcf86cd79943902b" as any, + name_fa: "خودرو صفر است", + name_en: "isNewCar", + label_fa: "خودرو صفر است", + label_en: "Is New Car", value: null, - dataType: 'boolean', + dataType: "boolean", required: true, order: 7, - isActive: true + isActive: true, }, { - id: '507f1f77bcf86cd79943902c' as any, - name_fa: 'بدون گواهی', - name_en: 'userNoCertificate', - label_fa: 'کاربر گواهی ندارد', - label_en: 'User Has No Certificate', + id: "507f1f77bcf86cd79943902c" as any, + name_fa: "بدون گواهی", + name_en: "userNoCertificate", + label_fa: "کاربر گواهی ندارد", + label_en: "User Has No Certificate", value: null, - dataType: 'boolean', + dataType: "boolean", required: true, order: 8, - isActive: true + isActive: true, }, { - id: '507f1f77bcf86cd79943902d' as any, - name_fa: 'تاریخ تولد بیمه‌گذار', - name_en: 'insurerBirthday', - label_fa: 'تاریخ تولد بیمه‌گذار', - label_en: 'Insurer Birthday', - placeholder_fa: 'تاریخ تولد بیمه‌گذار (timestamp)', - placeholder_en: 'Insurer birthday (timestamp)', + id: "507f1f77bcf86cd79943902d" as any, + name_fa: "تاریخ تولد بیمه‌گذار", + name_en: "insurerBirthday", + label_fa: "تاریخ تولد بیمه‌گذار", + label_en: "Insurer Birthday", + placeholder_fa: "تاریخ تولد بیمه‌گذار (timestamp)", + placeholder_en: "Insurer birthday (timestamp)", value: null, - dataType: 'number', + dataType: "number", required: true, order: 9, isActive: true, validation: { - type: 'number', - min: 0 - } + type: "number", + min: 0, + }, }, { - id: '507f1f77bcf86cd79943902e' as any, - name_fa: 'تاریخ تولد راننده', - name_en: 'driverBirthday', - label_fa: 'تاریخ تولد راننده', - label_en: 'Driver Birthday', - placeholder_fa: 'تاریخ تولد راننده را وارد کنید', - placeholder_en: 'Enter driver birthday', + id: "507f1f77bcf86cd79943902e" as any, + name_fa: "تاریخ تولد راننده", + name_en: "driverBirthday", + label_fa: "تاریخ تولد راننده", + label_en: "Driver Birthday", + placeholder_fa: "تاریخ تولد راننده را وارد کنید", + placeholder_en: "Enter driver birthday", value: null, - dataType: 'string', + dataType: "string", required: true, order: 10, isActive: true, validation: { - format: 'date' - } - } + format: "date", + }, + }, ], metadata: { - icon: 'clipboard-list', - color: '#8B5CF6', - formType: 'detailed', + icon: "clipboard-list", + color: "#8B5CF6", + formType: "detailed", hasComplexValidation: true, - party: 'second' - } + party: "second", + }, }, { stepKey: WorkflowStep.SECOND_LOCATION, - type: 'THIRD_PARTY', + type: "THIRD_PARTY", stepNumber: 10, isActive: true, - stepName_fa: 'موقعیت مکانی طرف دوم', - stepName_en: 'Second Party Location', - description_fa: 'ثبت موقعیت جغرافیایی طرف دوم', - description_en: 'Record second party geographical location', - category: 'SECOND_PARTY', + stepName_fa: "موقعیت مکانی طرف دوم", + stepName_en: "Second Party Location", + description_fa: "ثبت موقعیت جغرافیایی طرف دوم", + description_en: "Record second party geographical location", + category: "SECOND_PARTY", requiredPreviousSteps: [WorkflowStep.SECOND_INITIAL_FORM], nextPossibleSteps: [WorkflowStep.SECOND_VOICE], - allowedRoles: ['user'], + allowedRoles: ["user"], estimatedDuration: 2, fields: [ { - id: '507f1f77bcf86cd79943902f' as any, - name_fa: 'عرض جغرافیایی', - name_en: 'lat', - label_fa: 'عرض جغرافیایی', - label_en: 'Latitude', - placeholder_fa: 'عرض جغرافیایی را وارد کنید', - placeholder_en: 'Enter latitude', + id: "507f1f77bcf86cd79943902f" as any, + name_fa: "عرض جغرافیایی", + name_en: "lat", + label_fa: "عرض جغرافیایی", + label_en: "Latitude", + placeholder_fa: "عرض جغرافیایی را وارد کنید", + placeholder_en: "Enter latitude", value: null, - dataType: 'number', + dataType: "number", required: true, order: 1, isActive: true, validation: { - type: 'number', + type: "number", min: -90, - max: 90 - } + max: 90, + }, }, { - id: '507f1f77bcf86cd799439030' as any, - name_fa: 'طول جغرافیایی', - name_en: 'lon', - label_fa: 'طول جغرافیایی', - label_en: 'Longitude', - placeholder_fa: 'طول جغرافیایی را وارد کنید', - placeholder_en: 'Enter longitude', + id: "507f1f77bcf86cd799439030" as any, + name_fa: "طول جغرافیایی", + name_en: "lon", + label_fa: "طول جغرافیایی", + label_en: "Longitude", + placeholder_fa: "طول جغرافیایی را وارد کنید", + placeholder_en: "Enter longitude", value: null, - dataType: 'number', + dataType: "number", required: true, order: 2, isActive: true, validation: { - type: 'number', + type: "number", min: -180, - max: 180 - } - } + max: 180, + }, + }, ], metadata: { - icon: 'map-pin', - color: '#10B981', + icon: "map-pin", + color: "#10B981", requiresGPS: true, autoDetectLocation: true, - party: 'second' - } + party: "second", + }, }, { stepKey: WorkflowStep.SECOND_VOICE, - type: 'THIRD_PARTY', + type: "THIRD_PARTY", stepNumber: 11, isActive: true, - stepName_fa: 'ضبط صدای طرف دوم', - stepName_en: 'Second Party Voice Recording', - description_fa: 'ضبط توضیحات صوتی توسط طرف دوم', - description_en: 'Voice recording of explanations by second party', - category: 'SECOND_PARTY', + stepName_fa: "ضبط صدای طرف دوم", + stepName_en: "Second Party Voice Recording", + description_fa: "ضبط توضیحات صوتی توسط طرف دوم", + description_en: "Voice recording of explanations by second party", + category: "SECOND_PARTY", requiredPreviousSteps: [WorkflowStep.SECOND_LOCATION], nextPossibleSteps: [WorkflowStep.SECOND_DESCRIPTION], - allowedRoles: ['user'], + allowedRoles: ["user"], estimatedDuration: 3, fields: [ { - id: '507f1f77bcf86cd799439031' as any, - name_fa: 'فایل صوتی', - name_en: 'file', - label_fa: 'ضبط صدا', - label_en: 'Voice Recording', - placeholder_fa: 'توضیحات صوتی خود را ضبط کنید', - placeholder_en: 'Record your voice explanation', + id: "507f1f77bcf86cd799439031" as any, + name_fa: "فایل صوتی", + name_en: "file", + label_fa: "ضبط صدا", + label_en: "Voice Recording", + placeholder_fa: "توضیحات صوتی خود را ضبط کنید", + placeholder_en: "Record your voice explanation", value: null, - dataType: 'audio-file', + dataType: "audio-file", required: true, order: 1, isActive: true, validation: { maxSize: 10485760, maxDuration: 300, - allowedTypes: ['audio/mp3', 'audio/mpeg', 'audio/wav', 'audio/m4a', 'audio/webm'] - } - } + allowedTypes: [ + "audio/mp3", + "audio/mpeg", + "audio/wav", + "audio/m4a", + "audio/webm", + ], + }, + }, ], metadata: { - icon: 'microphone', - color: '#F59E0B', - recordingType: 'voice', + icon: "microphone", + color: "#F59E0B", + recordingType: "voice", requiresMicrophone: true, - party: 'second' - } + party: "second", + }, }, { stepKey: WorkflowStep.SECOND_DESCRIPTION, - type: 'THIRD_PARTY', + type: "THIRD_PARTY", stepNumber: 12, isActive: true, - stepName_fa: 'توضیحات طرف دوم', - stepName_en: 'Second Party Description', - description_fa: 'ثبت توضیحات و جزئیات حادثه توسط طرف دوم', - description_en: 'Record accident description and details by second party', - category: 'SECOND_PARTY', + stepName_fa: "توضیحات طرف دوم", + stepName_en: "Second Party Description", + description_fa: "ثبت توضیحات و جزئیات حادثه توسط طرف دوم", + description_en: + "Record accident description and details by second party", + category: "SECOND_PARTY", requiredPreviousSteps: [WorkflowStep.SECOND_VOICE], nextPossibleSteps: [WorkflowStep.WAITING_FOR_GUILT_DECISION], - allowedRoles: ['user'], + allowedRoles: ["user"], estimatedDuration: 4, fields: [ { - id: '507f1f77bcf86cd799439032' as any, - name_fa: 'توضیحات', - name_en: 'desc', - label_fa: 'توضیحات حادثه', - label_en: 'Accident Description', - placeholder_fa: 'توضیحات کامل حادثه را وارد کنید', - placeholder_en: 'Enter complete accident description', + id: "507f1f77bcf86cd799439032" as any, + name_fa: "توضیحات", + name_en: "desc", + label_fa: "توضیحات حادثه", + label_en: "Accident Description", + placeholder_fa: "توضیحات کامل حادثه را وارد کنید", + placeholder_en: "Enter complete accident description", value: null, - dataType: 'textarea', + dataType: "textarea", required: true, order: 1, isActive: true, validation: { minLength: 10, - maxLength: 2000 - } + maxLength: 2000, + }, }, { - id: '507f1f77bcf86cd799439033' as any, - name_fa: 'تاریخ حادثه', - name_en: 'accidentDate', - label_fa: 'تاریخ حادثه', - label_en: 'Accident Date', - placeholder_fa: 'تاریخ وقوع حادثه را وارد کنید', - placeholder_en: 'Enter accident date', + id: "507f1f77bcf86cd799439033" as any, + name_fa: "تاریخ حادثه", + name_en: "accidentDate", + label_fa: "تاریخ حادثه", + label_en: "Accident Date", + placeholder_fa: "تاریخ وقوع حادثه را وارد کنید", + placeholder_en: "Enter accident date", value: null, - dataType: 'date', + dataType: "date", required: true, order: 2, isActive: true, validation: { - format: 'YYYY-MM-DD' - } + format: "YYYY-MM-DD", + }, }, { - id: '507f1f77bcf86cd799439034' as any, - name_fa: 'زمان حادثه', - name_en: 'accidentTime', - label_fa: 'زمان حادثه', - label_en: 'Accident Time', - placeholder_fa: 'زمان وقوع حادثه را وارد کنید', - placeholder_en: 'Enter accident time', + id: "507f1f77bcf86cd799439034" as any, + name_fa: "زمان حادثه", + name_en: "accidentTime", + label_fa: "زمان حادثه", + label_en: "Accident Time", + placeholder_fa: "زمان وقوع حادثه را وارد کنید", + placeholder_en: "Enter accident time", value: null, - dataType: 'time', + dataType: "time", required: true, order: 3, isActive: true, validation: { - format: 'HH:mm' - } - } + format: "HH:mm", + }, + }, ], metadata: { - icon: 'file-text', - color: '#6366F1', - party: 'second' - } + icon: "file-text", + color: "#6366F1", + party: "second", + }, }, // CAR_BODY workflow: accident type form (replaces confession for CAR_BODY) { stepKey: WorkflowStep.CAR_BODY_ACCIDENT_TYPE, - type: 'CAR_BODY', + type: "CAR_BODY", stepNumber: 2, isActive: true, - stepName_fa: 'نوع حادثه بدنه', - stepName_en: 'Car Body Accident Type', - description_fa: 'انتخاب اینکه حادثه با خودرو بوده یا با شیء', - description_en: 'Select whether accident was with another car or with an object', - category: 'FIRST_PARTY', + stepName_fa: "نوع حادثه بدنه", + stepName_en: "Car Body Accident Type", + description_fa: "انتخاب اینکه حادثه با خودرو بوده یا با شیء", + description_en: + "Select whether accident was with another car or with an object", + category: "FIRST_PARTY", requiredPreviousSteps: [WorkflowStep.CREATED], nextPossibleSteps: [WorkflowStep.FIRST_VIDEO], - allowedRoles: ['user'], + allowedRoles: ["user"], estimatedDuration: 1, fields: [ { - id: '507f1f77bcf86cd7994390cb' as any, - name_fa: 'نوع حادثه', - name_en: 'accidentType', - label_fa: 'نوع حادثه', - label_en: 'Accident Type', - placeholder_fa: 'حادثه با خودرو یا شیء؟', - placeholder_en: 'Accident with car or object?', + id: "507f1f77bcf86cd7994390cb" as any, + name_fa: "نوع حادثه", + name_en: "accidentType", + label_fa: "نوع حادثه", + label_en: "Accident Type", + placeholder_fa: "حادثه با خودرو یا شیء؟", + placeholder_en: "Accident with car or object?", value: [ - { label_fa: 'با خودرو', label_en: 'With Car', value: 'car' }, - { label_fa: 'با شیء', label_en: 'With Object', value: 'object' } + { label_fa: "با خودرو", label_en: "With Car", value: "car" }, + { label_fa: "با شیء", label_en: "With Object", value: "object" }, ], - dataType: 'select', + dataType: "select", required: true, order: 1, isActive: true, - validation: { enum: ['car', 'object'] } - } + validation: { enum: ["car", "object"] }, + }, ], - metadata: { icon: 'car', color: '#10B981' } + metadata: { icon: "car", color: "#10B981" }, }, // Add more default steps as needed... ]; From 06af79fa475fe48e359822110615205b924d1fbc Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Mon, 1 Jun 2026 12:46:19 +0330 Subject: [PATCH 4/7] Fixed sheba --- .../claim-request-management.service.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index 766c2cf..60931a5 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -4654,11 +4654,8 @@ export class ClaimRequestManagementService { const updatePayload: any = { "damage.otherParts": otherParts.length > 0 ? otherParts : undefined, - - ...(step !== ClaimWorkflowStep.USER_EXPERT_RESEND && { - "money.sheba": "", - "money.nationalCodeOfInsurer": nationalCode, - }), + "money.sheba": shebaNumber, + "money.nationalCodeOfInsurer": nationalCode, status: ClaimWorkflowStep.CAPTURE_PART_DAMAGES, "workflow.currentStep": ClaimWorkflowStep.CAPTURE_PART_DAMAGES, From fde6464739a94c37b88aa9874724e01cc7dc20b4 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Mon, 1 Jun 2026 13:06:09 +0330 Subject: [PATCH 5/7] YARA-951 --- src/expert-blame/expert-blame.service.ts | 184 +++++++++++------------ 1 file changed, 90 insertions(+), 94 deletions(-) diff --git a/src/expert-blame/expert-blame.service.ts b/src/expert-blame/expert-blame.service.ts index 1a3331a..1339b4e 100644 --- a/src/expert-blame/expert-blame.service.ts +++ b/src/expert-blame/expert-blame.service.ts @@ -303,66 +303,58 @@ export class ExpertBlameService { requireActorClientKey(actor); const expertId = actor.sub; - // Fetch all DISAGREEMENT cases const allCases = await this.blameRequestDbService.find( - { - blameStatus: BlameStatus.DISAGREEMENT, - }, + { blameStatus: BlameStatus.DISAGREEMENT }, { lean: true }, ); - // Filter to show only: - // 1. Same insurance tenant (party clientId or expert-initiated by this actor) - // 2. Fresh requests (WAITING_FOR_EXPERT and no decision) - // 3. Requests decided by current expert - // 4. Expert-initiated: only the initiating field expert sees them const visibleCases = (allCases as Record[]).filter( (doc) => { - if (!blameCaseAccessibleToExpert(doc, actor)) { - return false; - } - - const expertInitiated = doc.expertInitiated === true; - const initiatedByFieldExpertId = doc.initiatedByFieldExpertId; - - if (expertInitiated && initiatedByFieldExpertId) { - if (String(initiatedByFieldExpertId) !== expertId) { - return false; // Only the initiating field expert can see this file - } - return true; // Initiating expert can see their expert-initiated file - } + // Always filter by tenant first + if (!blameCaseAccessibleToExpert(doc, actor)) return false; const status = doc.status as string; - const decision = doc.expert as any; - const decidedByExpertId = decision?.decision?.decidedByExpertId; - const hasDecision = !!decision?.decision; + const decision = (doc.expert as any)?.decision; + const decidedByExpertId = decision?.decidedByExpertId + ? String(decision.decidedByExpertId) + : null; + const lockedById = String( + (doc.workflow as any)?.lockedBy?.actorId ?? "", + ); + const lockEnforced = + (doc.workflow as any)?.locked && + this.isBlameV2WorkflowLockCurrentlyEnforced(doc as any); - // Fresh request (no decision yet) - if (status === CaseStatus.WAITING_FOR_EXPERT && !hasDecision) { - return true; + // Expert-initiated files: only the initiating expert sees them + if (doc.expertInitiated === true && doc.initiatedByFieldExpertId) { + return String(doc.initiatedByFieldExpertId) === expertId; } - // 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 ( + // Bucket 1: Available — waiting, no decision, no active lock by someone else + const isAvailable = status === CaseStatus.WAITING_FOR_EXPERT && - lockedBy && - String(lockedBy) === expertId - ) { - return true; - } + !decidedByExpertId && + (!lockEnforced || lockedById === expertId); - return false; + // Bucket 2: Mine — decided by me + const isDecidedByMe = + !!decidedByExpertId && decidedByExpertId === expertId; + + // Bucket 3: Mine — currently locked/assigned to me (in progress) + const isLockedByMe = lockEnforced && lockedById === expertId; + + // Bucket 4: Mine — persistently assigned to me for review + const assignedForReviewById = String( + (doc.workflow as any)?.assignedForReviewBy?.actorId ?? "", + ); + const isAssignedToMe = + !!assignedForReviewById && assignedForReviewById === expertId; + + return isAvailable || isDecidedByMe || isLockedByMe || isAssignedToMe; }, ); + // Reconcile stale locks in-memory (same as before) const staleIds = new Set(); for (const doc of visibleCases) { const w = doc.workflow as Record | undefined; @@ -854,75 +846,79 @@ export class ExpertBlameService { actor: any, ): Promise> { try { - // requireActorClientKey(actor); const actorId = actor.sub; await this.expireBlameCaseWorkflowLockV2IfStale(requestId); + const doc = await this.blameRequestDbService.findByIdWithoutHistory(requestId); if (!doc) { throw new NotFoundException("Request not found"); } - // assertBlameCaseForExpertTenant(doc, actor); - const type = doc.type as string; - if (type === BlameRequestType.CAR_BODY) { + + // Tenant check + assertBlameCaseForExpertTenant(doc, actor); + + if (doc.type === BlameRequestType.CAR_BODY) { throw new ForbiddenException( "CAR_BODY type requests are automatically handled and do not require expert review.", ); } - // Access control - const expertInitiated = doc.expertInitiated === true; - const initiatedByFieldExpertId = doc.initiatedByFieldExpertId; + // Expert-initiated: only the initiating expert if ( - expertInitiated && - initiatedByFieldExpertId && - String(initiatedByFieldExpertId) !== actorId + doc.expertInitiated === true && + doc.initiatedByFieldExpertId && + String(doc.initiatedByFieldExpertId) !== actorId ) { throw new ForbiddenException( "Only the field expert who created this file can view and review it.", ); } - if (this.isBlameV2WorkflowLockCurrentlyEnforced(doc)) { - const w = doc.workflow as - | { lockedBy?: { actorId?: unknown } } - | undefined; - const lockerId = String(w?.lockedBy?.actorId ?? ""); - if (lockerId && lockerId !== actorId) { + const decision = (doc.expert as any)?.decision; + const decidedByExpertId = decision?.decidedByExpertId + ? String(decision.decidedByExpertId) + : null; + const lockedById = String((doc.workflow as any)?.lockedBy?.actorId ?? ""); + const lockEnforced = + (doc.workflow as any)?.locked && + this.isBlameV2WorkflowLockCurrentlyEnforced(doc); + const assignedForReviewById = String( + (doc.workflow as any)?.assignedForReviewBy?.actorId ?? "", + ); + + // Access gates — must satisfy at least one bucket + const isAvailable = + doc.status === CaseStatus.WAITING_FOR_EXPERT && !decidedByExpertId; + const isDecidedByMe = decidedByExpertId === actorId; + const isLockedByMe = lockEnforced && lockedById === actorId; + const isAssignedToMe = + !!assignedForReviewById && assignedForReviewById === actorId; + + if (!isAvailable && !isDecidedByMe && !isLockedByMe && !isAssignedToMe) { + // Give a specific reason if possible + if (lockEnforced && lockedById && lockedById !== actorId) { throw new ForbiddenException( "This request is locked by another expert.", ); } - } - - const decision = (doc.expert as any)?.decision; - const decidedByExpertId = decision?.decidedByExpertId; - if (decidedByExpertId && String(decidedByExpertId) !== actorId) { + if (decidedByExpertId && decidedByExpertId !== actorId) { + throw new ForbiddenException( + "You do not have permission to view this request. It has been handled by another expert.", + ); + } throw new ForbiddenException( - "You do not have permission to view this request. It has been handled by another expert.", + "You do not have permission to view this request.", ); } + // Build evidence URLs const parties = Array.isArray(doc.parties) ? doc.parties : []; - - const typedParties = parties as Array<{ - vehicle?: { - inquiry?: { - mapped?: any; - }; - }; - evidence?: { - videoId?: string | number; - voices?: (string | number)[]; - videoUrl?: string; - voiceUrls?: string[]; - }; - }>; - - // Evidence (videos + voices) - for (const party of typedParties) { + for (const party of parties as Array<{ + evidence?: Record; + }>) { if (!party.evidence) continue; - const evidence = party.evidence as Record; + const evidence = party.evidence; if (evidence.videoId) { const videoDoc = await this.blameVideoDbService.findById( @@ -931,7 +927,7 @@ export class ExpertBlameService { if (videoDoc?.path) evidence.videoUrl = buildFileLink(videoDoc.path); } - if (evidence.voices && Array.isArray(evidence.voices)) { + if (Array.isArray(evidence.voices)) { const voiceUrls: string[] = []; for (const voiceId of evidence.voices) { const voiceDoc = await this.blameVoiceDbService.findById( @@ -950,31 +946,31 @@ export class ExpertBlameService { const updatedAt = doc.updatedAt ? new Date(doc.updatedAt as string | number) : new Date(); - const [createdDate, createdTime] = toJalaliDateAndTime(createdAt); const [updatedDate, updatedTime] = toJalaliDateAndTime(updatedAt); - doc.createdAtFormatted = `${createdDate} ${createdTime}`; doc.updatedAtFormatted = `${updatedDate} ${updatedTime}`; - // Omit heavy SandHub inquiry blob from expert response (keep other vehicle fields) - for (const party of typedParties) { - const veh = party?.vehicle as Record | undefined; - if (veh && Object.prototype.hasOwnProperty.call(veh, "inquiry")) { - delete veh.inquiry; + // Strip heavy SandHub inquiry blob + for (const party of parties as Array<{ + vehicle?: Record; + }>) { + if ( + party.vehicle && + Object.prototype.hasOwnProperty.call(party.vehicle, "inquiry") + ) { + delete party.vehicle.inquiry; } } 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 From 6261af8a29b88f9085941be7ad51a41148b619c0 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Mon, 1 Jun 2026 13:21:32 +0330 Subject: [PATCH 6/7] Fixed lock --- src/expert-blame/expert-blame.service.ts | 1 + src/expert-claim/expert-claim.service.ts | 118 ++++++++++++++---- .../expert-workflow-review-assignee.ts | 37 +++--- 3 files changed, 117 insertions(+), 39 deletions(-) diff --git a/src/expert-blame/expert-blame.service.ts b/src/expert-blame/expert-blame.service.ts index 1339b4e..0a78afc 100644 --- a/src/expert-blame/expert-blame.service.ts +++ b/src/expert-blame/expert-blame.service.ts @@ -556,6 +556,7 @@ export class ExpertBlameService { const assigneeToPersist = blameWorkflowAssigneeToPersistOnLockExpiry( request.workflow, + request, ); const expireSet: Record = { "workflow.locked": false }; if (assigneeToPersist) { diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index d3aed24..aa14f85 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -3294,22 +3294,26 @@ export class ExpertClaimService { const claims = await this.claimCaseDbService.find({ $or: [ - // Available claims: waiting for expert, not locked + // Available: waiting, not locked { status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT, "workflow.locked": { $ne: true }, + // No persistent assignee (or expired without decision) + $or: [ + { "workflow.assignedForReviewBy": { $exists: false } }, + { "workflow.assignedForReviewBy": null }, + ], }, - // Expert reviewing but lock cleared (e.g. TTL expiry) — still list until status is reconciled - { - status: ClaimCaseStatus.EXPERT_REVIEWING, - "workflow.locked": { $ne: true }, - }, - // This expert's own locked/in-progress claims + // This expert's locked/in-progress claims { "workflow.locked": true, "workflow.lockedBy.actorId": new Types.ObjectId(actorId), }, - // User uploaded all factors; expert must approve/reject (unlocked queue) + // This expert's persistently assigned claims (decided or resend/objection follow-up) + { + "workflow.assignedForReviewBy.actorId": new Types.ObjectId(actorId), + }, + // Factor validation queue (open to all tenant experts — no lock needed) { $or: [ { @@ -3324,6 +3328,16 @@ export class ExpertClaimService { }, ], }, + // WAITING_FOR_USER_RESEND assigned to this expert + { + status: ClaimCaseStatus.WAITING_FOR_USER_RESEND, + "workflow.assignedForReviewBy.actorId": new Types.ObjectId(actorId), + }, + // EXPERT_REVIEWING with stale lock (reconciliation will fix status, show in meantime) + { + status: ClaimCaseStatus.EXPERT_REVIEWING, + "workflow.locked": { $ne: true }, + }, ], }); @@ -3331,10 +3345,12 @@ export class ExpertClaimService { claimCaseTouchesClient(c, clientKey), ); + // Reconcile stale locks — if no decision was made, also clear assignedForReviewBy const staleLockToReconcile = filtered.filter( (c) => c.workflow?.locked && !this.isClaimV2WorkflowLockCurrentlyEnforced(c), ); + const touchedIds = ( await Promise.all( staleLockToReconcile.map(async (c) => { @@ -3357,9 +3373,39 @@ export class ExpertClaimService { } } + // Post-filter: remove files that belong to another expert after reconciliation + const finalFiltered = filtered.filter((c) => { + const assignedId = String(c.workflow?.assignedForReviewBy?.actorId ?? ""); + const lockEnforced = + c.workflow?.locked && this.isClaimV2WorkflowLockCurrentlyEnforced(c); + const lockedById = String(c.workflow?.lockedBy?.actorId ?? ""); + const isFactorValidation = claimIsAwaitingExpertFactorValidationV2(c); + + // Factor validation is open to all tenant experts + if (isFactorValidation) return true; + + // Available: no assignee, not locked by someone else + const isAvailable = + c.status === ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT && + !assignedId && + (!lockEnforced || lockedById === actorId); + + // Mine: assigned to me (covers decided, resend, objection follow-up) + const isAssignedToMe = !!assignedId && assignedId === actorId; + + // Mine: currently locked by me + const isLockedByMe = lockEnforced && lockedById === actorId; + + // Mine: resend pending and I'm the assigned expert + const isResendMine = + c.status === ClaimCaseStatus.WAITING_FOR_USER_RESEND && isAssignedToMe; + + return isAvailable || isAssignedToMe || isLockedByMe || isResendMine; + }); + const blameIds = [ ...new Set( - filtered + finalFiltered .map((c) => c.blameRequestId?.toString()) .filter((id): id is string => !!id), ), @@ -3375,7 +3421,7 @@ export class ExpertClaimService { blames.map((b) => [String(b._id), b]), ); - const list = filtered.map((c) => { + const list = finalFiltered.map((c) => { const awaitingFactorValidation = claimIsAwaitingExpertFactorValidationV2(c); const v = this.vehicleForExpertFromClaimAndBlameMap(c, blameById); @@ -3390,6 +3436,7 @@ export class ExpertClaimService { c.status === ClaimCaseStatus.EXPERT_REVIEWING ? ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT : c.status; + return { claimRequestId: c._id.toString(), publicId: c.publicId, @@ -3406,11 +3453,7 @@ export class ExpertClaimService { } : undefined, vehicle: v - ? { - carName: v.carName, - carModel: v.carModel, - carType: v.carType, - } + ? { carName: v.carName, carModel: v.carModel, carType: v.carType } : undefined, ...fileCtx, createdAt: c.createdAt, @@ -3727,6 +3770,7 @@ export class ExpertClaimService { const assigneeToPersist = claimWorkflowAssigneeToPersistOnLockExpiry( claim.workflow, + claim, ); const expireLockSet: Record = { "workflow.locked": false }; if (assigneeToPersist) { @@ -3795,21 +3839,53 @@ export class ExpertClaimService { claim.status === ClaimCaseStatus.EXPERT_REVIEWING; const isFactorValidationPending = claimIsAwaitingExpertFactorValidationV2(claim); + const isResendPending = + claim.status === ClaimCaseStatus.WAITING_FOR_USER_RESEND; - if (!isDamageExpertPhase && !isFactorValidationPending) { + if ( + !isDamageExpertPhase && + !isFactorValidationPending && + !isResendPending + ) { throw new ForbiddenException( `This claim is not available for expert review. Current status: ${claim.status}`, ); } - const lockBlocksOthers = this.isClaimV2WorkflowLockCurrentlyEnforced(claim); - if (lockBlocksOthers) { - const lockerId = claim.workflow?.lockedBy?.actorId?.toString(); - if (lockerId && lockerId !== actorId) { + // Ownership access control — same 4-bucket logic as list + const assignedForReviewById = String( + (claim.workflow as any)?.assignedForReviewBy?.actorId ?? "", + ); + const lockEnforced = this.isClaimV2WorkflowLockCurrentlyEnforced(claim); + const lockedById = String(claim.workflow?.lockedBy?.actorId ?? ""); + + const isAvailable = + isDamageExpertPhase && !assignedForReviewById && !lockEnforced; + const isAssignedToMe = + !!assignedForReviewById && assignedForReviewById === actorId; + const isLockedByMe = lockEnforced && lockedById === actorId; + // Factor validation is open to all tenant experts (no individual ownership) + const isFactorValidationOpen = isFactorValidationPending; + + if ( + !isAvailable && + !isAssignedToMe && + !isLockedByMe && + !isFactorValidationOpen + ) { + if (lockEnforced && lockedById && lockedById !== actorId) { throw new ForbiddenException( - `This claim is locked by another expert: ${claim.workflow.lockedBy?.actorName}`, + `This claim is locked by another expert: ${claim.workflow?.lockedBy?.actorName}`, ); } + if (assignedForReviewById && assignedForReviewById !== actorId) { + throw new ForbiddenException( + "This claim has been assigned to another expert.", + ); + } + throw new ForbiddenException( + "You do not have permission to view this claim.", + ); } const hasCapture = (data: any, key: string) => diff --git a/src/helpers/expert-workflow-review-assignee.ts b/src/helpers/expert-workflow-review-assignee.ts index 81219b5..b2afc9f 100644 --- a/src/helpers/expert-workflow-review-assignee.ts +++ b/src/helpers/expert-workflow-review-assignee.ts @@ -25,9 +25,8 @@ export function resolvePersistentReviewAssigneeId( if (fromField != null && String(fromField).length > 0) { return String(fromField); } - const fromHistory = history?.find( - (h) => h.type === assignedHistoryType, - )?.actor?.actorId; + const fromHistory = history?.find((h) => h.type === assignedHistoryType) + ?.actor?.actorId; if (fromHistory != null && String(fromHistory).length > 0) { return String(fromHistory); } @@ -43,8 +42,7 @@ export function buildExpertAssignConflictForOtherReviewer( message: string; lockedBy: { actorId: string; actorName?: string; lockedAt?: string }; } { - const assignee = - workflow?.assignedForReviewBy ?? workflow?.lockedBy; + const assignee = workflow?.assignedForReviewBy ?? workflow?.lockedBy; const lockedAt = workflow?.lockedAt; return { success: false, @@ -53,25 +51,28 @@ export function buildExpertAssignConflictForOtherReviewer( lockedBy: { actorId: assigneeId, actorName: assignee?.actorName, - lockedAt: lockedAt - ? new Date(lockedAt as Date).toISOString() - : undefined, + lockedAt: lockedAt ? new Date(lockedAt as Date).toISOString() : undefined, }, }; } -/** Copy lock holder into `assignedForReviewBy` when persisting a TTL unlock. */ export function blameWorkflowAssigneeToPersistOnLockExpiry( - workflow: WorkflowAssigneeRef | undefined, -): WorkflowAssigneeRef["assignedForReviewBy"] | undefined { - if (workflow?.assignedForReviewBy) { - return undefined; - } - return workflow?.lockedBy; + workflow: any, + request: any, +) { + const hasDecision = !!request?.expert?.decision; + return hasDecision ? (workflow?.assignedForReviewBy ?? null) : null; } export function claimWorkflowAssigneeToPersistOnLockExpiry( - workflow: WorkflowAssigneeRef | undefined, -): WorkflowAssigneeRef["assignedForReviewBy"] | undefined { - return blameWorkflowAssigneeToPersistOnLockExpiry(workflow); + workflow: any, + claim: any, +): typeof workflow.assignedForReviewBy | null { + // If a decision or resend was already submitted, preserve ownership + const hasDecision = + !!claim?.evaluation?.damageExpertReply || + !!claim?.evaluation?.damageExpertReplyFinal || + !!claim?.evaluation?.damageExpertResend; + + return hasDecision ? (workflow?.assignedForReviewBy ?? null) : null; } From 5fab0a00b6238fa981cfd589365693585584ad49 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Mon, 1 Jun 2026 14:11:23 +0330 Subject: [PATCH 7/7] Fixed inquiry of birthdate --- src/helpers/date-jalali.ts | 42 +++++++++++++++----------------- src/sand-hub/sand-hub.service.ts | 1 + 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/helpers/date-jalali.ts b/src/helpers/date-jalali.ts index cb1eeb9..a618255 100644 --- a/src/helpers/date-jalali.ts +++ b/src/helpers/date-jalali.ts @@ -96,14 +96,13 @@ function jalaliPartsToGregorian( jMonth: number, jDay: number, ): string | null { - // Jalali to Julian Day Number, then to Gregorian - // Algorithm: https://www.fourmilab.ch/documents/calendar/ const jy = jYear - 979; const jm = jMonth - 1; const jd = jDay - 1; + // Jalali day number — correct leap cycle is every 33 years with 8 leaps let jDayNo = - 365 * jy + Math.floor(jy / 4) - Math.floor(jy / 100) + Math.floor(jy / 400); + 365 * jy + Math.floor(jy / 33) * 8 + Math.floor(((jy % 33) + 3) / 4); const jalaliMonthDays = [31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29]; for (let i = 0; i < jm; i++) { @@ -111,29 +110,29 @@ function jalaliPartsToGregorian( } jDayNo += jd; - // Convert to Gregorian day number (offset from 1970-01-01 in Julian days) - const gDayNo = jDayNo + 79; + // Offset to Gregorian day number anchored at 1600 + let gDayNo = jDayNo + 79; - let gy = 1979 + 400 * Math.floor(gDayNo / 146097); - let days = gDayNo % 146097; + let gy = 1600 + 400 * Math.floor(gDayNo / 146097); + gDayNo %= 146097; let leap = true; - if (days >= 36525) { - days--; - gy += 100 * Math.floor(days / 36524); - days %= 36524; - if (days >= 365) days++; + if (gDayNo >= 36525) { + gDayNo--; + gy += 100 * Math.floor(gDayNo / 36524); + gDayNo %= 36524; + if (gDayNo >= 365) gDayNo++; else leap = false; } - gy += 4 * Math.floor(days / 1461); - days %= 1461; + gy += 4 * Math.floor(gDayNo / 1461); + gDayNo %= 1461; - if (days >= 366) { + if (gDayNo >= 366) { leap = false; - days--; - gy += Math.floor(days / 365); - days %= 365; + gDayNo--; + gy += Math.floor(gDayNo / 365); + gDayNo %= 365; } const gregorianMonthDays = [ @@ -153,19 +152,18 @@ function jalaliPartsToGregorian( let gm = 0; for (let i = 0; i < 12; i++) { - if (days < gregorianMonthDays[i]) { + if (gDayNo < gregorianMonthDays[i]) { gm = i + 1; break; } - days -= gregorianMonthDays[i]; + gDayNo -= gregorianMonthDays[i]; } - const gd = days + 1; + const gd = gDayNo + 1; const mm = String(gm).padStart(2, "0"); const dd = String(gd).padStart(2, "0"); const result = `${gy}-${mm}-${dd}`; - // Sanity check const check = new Date(result); if (isNaN(check.getTime())) return null; diff --git a/src/sand-hub/sand-hub.service.ts b/src/sand-hub/sand-hub.service.ts index 801cb07..b156e41 100644 --- a/src/sand-hub/sand-hub.service.ts +++ b/src/sand-hub/sand-hub.service.ts @@ -564,6 +564,7 @@ export class SandHubService { const requestUrl = `${process.env.SANHUB_BASE_URL}/personal-inquiry/tejarat-no`; const gregorianBirthdate = jalaliToGregorianDate(birthDate); + console.log(gregorianBirthdate); if (!gregorianBirthdate) { throw new BadRequestException( `Invalid birth date for personal inquiry: ${birthDate}. Expected a Jalali date (e.g. 13770624 or "1377-06-24").`,