forked from Yara724/api
Merge pull request 'YARA-784, YARA-789, YARA-791' (#17) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#17
This commit is contained in:
@@ -63,7 +63,9 @@ import { PublicIdService } from "src/utils/public-id/public-id.service";
|
||||
import { ImageRequiredModel } from "./entites/schema/image-required.schema";
|
||||
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 { BranchDbService } from "src/client/entities/db-service/branch.db.service";import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { BranchDbService } from "src/client/entities/db-service/branch.db.service";
|
||||
import { CreationMethod } from "src/request-management/entities/schema/request-management.schema";
|
||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { UserRatingDto } from "./dto/user-rating.dto";
|
||||
|
||||
@Injectable()
|
||||
@@ -336,6 +338,166 @@ export class ClaimRequestManagementService {
|
||||
return trueItems;
|
||||
}
|
||||
|
||||
/** One section of CarDamagePartDto: either a single object or an array of objects (legacy). */
|
||||
private carPartDamageSectionToRows(section: unknown): Record<string, unknown>[] {
|
||||
if (section == null) return [];
|
||||
if (Array.isArray(section)) {
|
||||
return section.filter(
|
||||
(row): row is Record<string, unknown> =>
|
||||
!!row && typeof row === "object",
|
||||
);
|
||||
}
|
||||
if (typeof section === "object") {
|
||||
return [section as Record<string, unknown>];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private carPartDamageSectionHasTrue(section: unknown): boolean {
|
||||
for (const row of this.carPartDamageSectionToRows(section)) {
|
||||
for (const val of Object.values(row)) {
|
||||
if (val === true) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* At most two of left, right, front, back may contain any damaged part (true).
|
||||
* `top` and claim `otherParts` are not subject to this rule.
|
||||
*/
|
||||
private assertCarPartDamageAtMostTwoOfFourSides(dto: CarDamagePartDto): void {
|
||||
const d = dto as unknown as Record<string, unknown>;
|
||||
const quadrants = [
|
||||
{ key: "left", section: d.left },
|
||||
{ key: "right", section: d.right },
|
||||
{ key: "front", section: d.front },
|
||||
{ key: "back", section: d.back },
|
||||
];
|
||||
const used = quadrants.filter((q) =>
|
||||
this.carPartDamageSectionHasTrue(q.section),
|
||||
);
|
||||
if (used.length > 2) {
|
||||
throw new BadRequestException(
|
||||
`At most two of the four sides (left, right, front, back) may include damaged parts. ` +
|
||||
`Sides with selections: ${used.map((u) => u.key).join(", ")}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps legacy expert CarDamagePartDto (nested booleans) to v2 outer part slugs (ClaimCase.damage.selectedParts).
|
||||
*/
|
||||
private carDamagePartDtoToOuterPartSlugs(dto: CarDamagePartDto): string[] {
|
||||
const out = new Set<string>();
|
||||
const add = (slug: string | undefined) => {
|
||||
if (slug) out.add(slug);
|
||||
};
|
||||
const walkMainOnSide = (
|
||||
side: "left" | "right",
|
||||
items: unknown,
|
||||
map: Record<string, string>,
|
||||
) => {
|
||||
for (const row of this.carPartDamageSectionToRows(items)) {
|
||||
for (const [key, val] of Object.entries(row)) {
|
||||
if (val !== true) continue;
|
||||
const slug = map[`${side}_${key}`] ?? map[key];
|
||||
add(slug);
|
||||
}
|
||||
}
|
||||
};
|
||||
// MainParts on left / right (side-specific door / fender)
|
||||
walkMainOnSide("left", (dto as any).left, {
|
||||
left_frontDoor: "front_left_door",
|
||||
left_backDoor: "rear_left_door",
|
||||
left_frontFender: "front_left_fender",
|
||||
left_backFender: "rear_left_fender",
|
||||
frontDoor: "front_left_door",
|
||||
backDoor: "rear_left_door",
|
||||
frontFender: "front_left_fender",
|
||||
backFender: "rear_left_fender",
|
||||
});
|
||||
walkMainOnSide("right", (dto as any).right, {
|
||||
right_frontDoor: "front_right_door",
|
||||
right_backDoor: "rear_right_door",
|
||||
right_frontFender: "front_right_fender",
|
||||
right_backFender: "rear_right_fender",
|
||||
frontDoor: "front_right_door",
|
||||
backDoor: "rear_right_door",
|
||||
frontFender: "front_right_fender",
|
||||
backFender: "rear_right_fender",
|
||||
});
|
||||
const walkSimple = (items: unknown, keyToSlug: Record<string, string>) => {
|
||||
for (const row of this.carPartDamageSectionToRows(items)) {
|
||||
for (const [key, val] of Object.entries(row)) {
|
||||
if (val === true) add(keyToSlug[key]);
|
||||
}
|
||||
}
|
||||
};
|
||||
walkSimple((dto as any).front, {
|
||||
frontBumper: "front_bumper",
|
||||
carHood: "hood",
|
||||
});
|
||||
walkSimple((dto as any).back, {
|
||||
backBumper: "rear_bumper",
|
||||
carTrunk: "trunk",
|
||||
});
|
||||
walkSimple((dto as any).top, {
|
||||
roof: "roof",
|
||||
});
|
||||
return Array.from(out);
|
||||
}
|
||||
|
||||
/** Normalize expert DTO otherParts (strings or legacy { label: true }[]) to string[]. */
|
||||
private normalizeExpertOtherPartsInput(raw: any[] | undefined): string[] {
|
||||
if (!raw || !Array.isArray(raw)) return [];
|
||||
const out: string[] = [];
|
||||
for (const item of raw) {
|
||||
if (typeof item === "string" && item.trim()) {
|
||||
out.push(item.trim());
|
||||
continue;
|
||||
}
|
||||
if (item && typeof item === "object") {
|
||||
for (const [k, v] of Object.entries(item)) {
|
||||
if (v === true && k) out.push(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private async assertFieldExpertInPersonBlameForClaimCase(
|
||||
claimCase: any,
|
||||
expert: any,
|
||||
): Promise<void> {
|
||||
const blameRequestId = claimCase?.blameRequestId?.toString?.();
|
||||
if (!blameRequestId) {
|
||||
throw new BadRequestException("Claim case has no linked blame request.");
|
||||
}
|
||||
const blameRequest = await this.blameRequestDbService.findById(blameRequestId);
|
||||
if (!blameRequest) {
|
||||
throw new NotFoundException("Blame request not found for this claim.");
|
||||
}
|
||||
if (
|
||||
!blameRequest.expertInitiated ||
|
||||
blameRequest.creationMethod !== CreationMethod.IN_PERSON
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
"This endpoint is only for expert-initiated IN_PERSON claim files.",
|
||||
);
|
||||
}
|
||||
if (!blameRequest.initiatedByFieldExpertId) {
|
||||
throw new BadRequestException(
|
||||
"Blame request is missing initiating field expert.",
|
||||
);
|
||||
}
|
||||
if (String(blameRequest.initiatedByFieldExpertId) !== String(expert.sub)) {
|
||||
throw new ForbiddenException(
|
||||
"Only the initiating field expert can complete claim data for this file.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async setCarPartDamageAndImage(cl, trueItems) {
|
||||
cl.carPartDamage = trueItems;
|
||||
const jsonTrueItems = JSON.parse(JSON.stringify(trueItems));
|
||||
@@ -389,7 +551,9 @@ export class ClaimRequestManagementService {
|
||||
|
||||
/**
|
||||
* Field expert completes claim-needed data for expert-initiated IN_PERSON files.
|
||||
* Only the initiating expert can call this. Accepts car part damage and/or sheba, nationalCodeOfInsurer, otherParts.
|
||||
* Only the initiating field expert can call this. Accepts car part damage and/or sheba, nationalCodeOfInsurer, otherParts.
|
||||
*
|
||||
* V2: Resolves `ClaimCase` in `claimCases` collection (workflow). Legacy v1 claim documents are still supported.
|
||||
*/
|
||||
async expertCompleteClaimData(
|
||||
claimRequestId: string,
|
||||
@@ -401,6 +565,16 @@ export class ClaimRequestManagementService {
|
||||
otherParts?: any[];
|
||||
},
|
||||
): Promise<{ success: boolean; claimRequestId: string }> {
|
||||
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||
if (claimCase) {
|
||||
return this.expertCompleteClaimDataForClaimCaseV2(
|
||||
claimRequestId,
|
||||
expert,
|
||||
dto,
|
||||
claimCase,
|
||||
);
|
||||
}
|
||||
|
||||
const claim = await this.claimDbService.findOne(claimRequestId);
|
||||
if (!claim) {
|
||||
throw new NotFoundException("Claim file not found");
|
||||
@@ -422,6 +596,7 @@ export class ClaimRequestManagementService {
|
||||
if (claim.carPartDamage && (claim.carPartDamage as any[]).length > 0) {
|
||||
throw new BadRequestException("Car part damage is already set.");
|
||||
}
|
||||
this.assertCarPartDamageAtMostTwoOfFourSides(dto.carPartDamage);
|
||||
const trueItems = this.findTrueItems(dto.carPartDamage);
|
||||
const claimDoc = (await this.claimDbService.findOne(
|
||||
claimRequestId,
|
||||
@@ -450,6 +625,178 @@ export class ClaimRequestManagementService {
|
||||
return { success: true, claimRequestId };
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 ClaimCase: outer parts + optional bank/other parts (expert-initiated IN_PERSON only).
|
||||
*/
|
||||
private async expertCompleteClaimDataForClaimCaseV2(
|
||||
claimRequestId: string,
|
||||
expert: any,
|
||||
dto: {
|
||||
carPartDamage?: CarDamagePartDto;
|
||||
sheba?: string;
|
||||
nationalCodeOfInsurer?: string;
|
||||
otherParts?: any[];
|
||||
},
|
||||
claimCase: any,
|
||||
): Promise<{ success: boolean; claimRequestId: string }> {
|
||||
await this.assertFieldExpertInPersonBlameForClaimCase(claimCase, expert);
|
||||
|
||||
let working = claimCase;
|
||||
|
||||
if (dto.carPartDamage) {
|
||||
if (working.workflow?.currentStep !== ClaimWorkflowStep.SELECT_OUTER_PARTS) {
|
||||
throw new BadRequestException(
|
||||
`Cannot set outer parts: workflow step must be ${ClaimWorkflowStep.SELECT_OUTER_PARTS}. Current: ${working.workflow?.currentStep}`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
working.damage?.selectedParts &&
|
||||
working.damage.selectedParts.length > 0
|
||||
) {
|
||||
throw new BadRequestException("Car part damage is already set.");
|
||||
}
|
||||
this.assertCarPartDamageAtMostTwoOfFourSides(dto.carPartDamage);
|
||||
const selectedParts = this.carDamagePartDtoToOuterPartSlugs(dto.carPartDamage);
|
||||
if (!selectedParts.length) {
|
||||
throw new BadRequestException(
|
||||
"No outer damaged parts could be derived from carPartDamage. Check part selections.",
|
||||
);
|
||||
}
|
||||
|
||||
const updated = 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(expert.sub),
|
||||
actorName:
|
||||
`${expert.firstName || ""} ${expert.lastName || ""}`.trim() ||
|
||||
"field_expert",
|
||||
actorType: "field_expert",
|
||||
},
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
stepKey: ClaimWorkflowStep.SELECT_OUTER_PARTS,
|
||||
selectedParts,
|
||||
description: "Field expert submitted outer damage parts",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
if (!updated) {
|
||||
throw new InternalServerErrorException("Failed to update claim case");
|
||||
}
|
||||
working = updated;
|
||||
}
|
||||
|
||||
const hasBankOrOther =
|
||||
dto.sheba != null ||
|
||||
dto.nationalCodeOfInsurer != null ||
|
||||
dto.otherParts != null;
|
||||
if (hasBankOrOther) {
|
||||
const fresh = await this.claimCaseDbService.findById(claimRequestId);
|
||||
if (!fresh) throw new NotFoundException("Claim case not found");
|
||||
if (fresh.workflow?.currentStep !== ClaimWorkflowStep.SELECT_OTHER_PARTS) {
|
||||
throw new BadRequestException(
|
||||
`Bank/other parts can only be set at ${ClaimWorkflowStep.SELECT_OTHER_PARTS}. Current: ${fresh.workflow?.currentStep}`,
|
||||
);
|
||||
}
|
||||
|
||||
const mergedSheba =
|
||||
dto.sheba != null ? dto.sheba : fresh.money?.sheba;
|
||||
const mergedNational =
|
||||
dto.nationalCodeOfInsurer != null
|
||||
? dto.nationalCodeOfInsurer
|
||||
: fresh.money?.nationalCodeOfInsurer;
|
||||
|
||||
if (fresh.money?.sheba && dto.sheba != null && dto.sheba !== fresh.money.sheba) {
|
||||
throw new ConflictException("Sheba is already set for this claim.");
|
||||
}
|
||||
if (
|
||||
fresh.money?.nationalCodeOfInsurer &&
|
||||
dto.nationalCodeOfInsurer != null &&
|
||||
dto.nationalCodeOfInsurer !== fresh.money.nationalCodeOfInsurer
|
||||
) {
|
||||
throw new ConflictException(
|
||||
"National code of insurer is already set for this claim.",
|
||||
);
|
||||
}
|
||||
|
||||
const otherPartsArr = this.normalizeExpertOtherPartsInput(dto.otherParts);
|
||||
const setPatch: Record<string, unknown> = {};
|
||||
if (dto.sheba != null) setPatch["money.sheba"] = dto.sheba;
|
||||
if (dto.nationalCodeOfInsurer != null) {
|
||||
setPatch["money.nationalCodeOfInsurer"] = dto.nationalCodeOfInsurer;
|
||||
}
|
||||
if (dto.otherParts != null) {
|
||||
setPatch["damage.otherParts"] =
|
||||
otherPartsArr.length > 0 ? otherPartsArr : [];
|
||||
}
|
||||
|
||||
const bankComplete =
|
||||
mergedSheba &&
|
||||
mergedNational &&
|
||||
/^[0-9]{24}$/.test(String(mergedSheba).replace(/\s/g, "")) &&
|
||||
/^[0-9]{10}$/.test(String(mergedNational).replace(/\s/g, ""));
|
||||
|
||||
if (bankComplete) {
|
||||
const shebaNorm = String(mergedSheba).replace(/\s/g, "");
|
||||
const nationalNorm = String(mergedNational).replace(/\s/g, "");
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
"money.sheba": shebaNorm,
|
||||
"money.nationalCodeOfInsurer": nationalNorm,
|
||||
...(dto.otherParts != null
|
||||
? {
|
||||
"damage.otherParts":
|
||||
otherPartsArr.length > 0 ? otherPartsArr : [],
|
||||
}
|
||||
: {}),
|
||||
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(expert.sub),
|
||||
actorName:
|
||||
`${expert.firstName || ""} ${expert.lastName || ""}`.trim() ||
|
||||
"field_expert",
|
||||
actorType: "field_expert",
|
||||
},
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
stepKey: ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
||||
description:
|
||||
"Field expert submitted bank info and optional other parts",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
if (Object.keys(setPatch).length === 0) {
|
||||
throw new BadRequestException(
|
||||
"Provide sheba (24 digits) and nationalCodeOfInsurer (10 digits) together to finish this step, or supply partial fields incrementally.",
|
||||
);
|
||||
}
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
$set: setPatch,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true, claimRequestId };
|
||||
}
|
||||
|
||||
async selectCarOtherPartDamage(
|
||||
requestId: string,
|
||||
body: any, // Use your DTO for better type safety
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { AddPlateDto } from "src/profile/dto/user/AddPlateDto";
|
||||
import {
|
||||
CarBodyFormDto,
|
||||
DescriptionDto,
|
||||
InitialFormDto,
|
||||
} from "./create-request-management.dto";
|
||||
|
||||
/**
|
||||
* V2 (IN_PERSON): single expert description/location for the scene.
|
||||
*/
|
||||
export class ExpertCompleteCarBodyFormV2Dto {
|
||||
@ApiProperty({
|
||||
description: "First party phone number",
|
||||
example: "09123456789",
|
||||
})
|
||||
firstPartyPhoneNumber: string;
|
||||
|
||||
@ApiProperty({ type: InitialFormDto })
|
||||
firstPartyInitialForm: InitialFormDto;
|
||||
|
||||
@ApiProperty({ type: CarBodyFormDto })
|
||||
carBodyForm: CarBodyFormDto;
|
||||
|
||||
@ApiProperty({ type: AddPlateDto })
|
||||
firstPartyPlate: AddPlateDto;
|
||||
|
||||
@ApiProperty({ type: DescriptionDto })
|
||||
expertDescription: DescriptionDto;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { LocationDto } from "./create-request-management.dto";
|
||||
|
||||
/**
|
||||
* V2 (IN_PERSON): one scene location provided by expert.
|
||||
*/
|
||||
export class ExpertCompleteLocationV2Dto {
|
||||
@ApiProperty({ type: LocationDto })
|
||||
location: LocationDto;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { AddPlateDto } from "src/profile/dto/user/AddPlateDto";
|
||||
import { DescriptionDto, InitialFormDto } from "./create-request-management.dto";
|
||||
|
||||
/**
|
||||
* V2 (IN_PERSON): second party inputs without separate description/location.
|
||||
*/
|
||||
export class ExpertSecondPartyInfoV2Dto {
|
||||
@ApiProperty({
|
||||
description: "Second party phone number",
|
||||
example: "09123456789",
|
||||
})
|
||||
phoneNumber: string;
|
||||
|
||||
@ApiProperty({ type: InitialFormDto })
|
||||
initialForm: InitialFormDto;
|
||||
|
||||
@ApiProperty({ type: AddPlateDto })
|
||||
plate: AddPlateDto;
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 (IN_PERSON): single expert description shared for the whole scene.
|
||||
*/
|
||||
export class ExpertCompleteThirdPartyFormV2Dto {
|
||||
@ApiProperty({
|
||||
description: "First party phone number",
|
||||
example: "09123456789",
|
||||
})
|
||||
firstPartyPhoneNumber: string;
|
||||
|
||||
@ApiProperty({ type: InitialFormDto })
|
||||
firstPartyInitialForm: InitialFormDto;
|
||||
|
||||
@ApiProperty({ type: AddPlateDto })
|
||||
firstPartyPlate: AddPlateDto;
|
||||
|
||||
@ApiProperty({ type: ExpertSecondPartyInfoV2Dto })
|
||||
secondParty: ExpertSecondPartyInfoV2Dto;
|
||||
|
||||
@ApiProperty({ type: DescriptionDto })
|
||||
expertDescription: DescriptionDto;
|
||||
|
||||
@ApiProperty({
|
||||
description:
|
||||
"Phone number of the guilty party. Must match either first or second party phone number.",
|
||||
example: "09123456789",
|
||||
})
|
||||
guiltyPartyPhoneNumber: string;
|
||||
}
|
||||
@@ -27,14 +27,14 @@ import { Roles } from "src/decorators/roles.decorator";
|
||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { RequestManagementService } from "./request-management.service";
|
||||
import { CreateExpertInitiatedFileDto } from "./dto/expert-initiated.dto";
|
||||
import { ExpertCompleteThirdPartyFormDto } from "./dto/expert-complete-third-party-form.dto";
|
||||
import { ExpertCompleteCarBodyFormDto } from "./dto/expert-complete-car-body-form.dto";
|
||||
import { ExpertCompleteThirdPartyFormV2Dto } from "./dto/expert-complete-third-party-form.v2.dto";
|
||||
import { ExpertCompleteCarBodyFormV2Dto } from "./dto/expert-complete-car-body-form.v2.dto";
|
||||
import { ExpertAccidentFieldsDto } from "./dto/expert-accident-fields.dto";
|
||||
import { ExpertCompleteClaimDataDto } from "./dto/expert-complete-claim-data.dto";
|
||||
import { ExpertUploadPartySignatureDto } from "./dto/expert-upload-party-signature.dto";
|
||||
import { VerifyPartyOtpsDto } from "./dto/verify-party-otps.dto";
|
||||
import { SendPartyOtpsDto } from "./dto/send-party-otps.dto";
|
||||
import { ExpertCompleteLocationsDto } from "./dto/expert-complete-locations.dto";
|
||||
import { ExpertCompleteLocationV2Dto } from "./dto/expert-complete-location.v2.dto";
|
||||
import { ClaimRequestManagementService } from "src/claim-request-management/claim-request-management.service";
|
||||
import { PartyRole } from "./entities/schema/partyRole.enum";
|
||||
|
||||
@@ -225,11 +225,11 @@ export class ExpertInitiatedV2Controller {
|
||||
@ApiOperation({
|
||||
summary: "[V2] Submit all blame data (IN_PERSON)",
|
||||
description:
|
||||
"For IN_PERSON files only. Send THIRD_PARTY or CAR_BODY form EXCEPT locations. After this, call add-locations to submit lat/lon and move workflow to WAITING_FOR_SIGNATURES.",
|
||||
"For IN_PERSON files only. Send THIRD_PARTY or CAR_BODY form EXCEPT location. Use one shared expertDescription (not per-party descriptions). After this, call add-locations once to submit scene lat/lon and move workflow to WAITING_FOR_SIGNATURES.",
|
||||
})
|
||||
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||
@ApiBody({
|
||||
description: "Choose THIRD_PARTY or CAR_BODY example according to the file type. All nested fields are listed so you can fill or test without guessing property names.",
|
||||
description: "Choose THIRD_PARTY or CAR_BODY example according to the file type. IN_PERSON v2 uses one shared expertDescription for the scene.",
|
||||
examples: {
|
||||
THIRD_PARTY: {
|
||||
summary: "THIRD_PARTY",
|
||||
@@ -253,7 +253,7 @@ export class ExpertInitiatedV2Controller {
|
||||
insurerBirthday: 13770624,
|
||||
driverBirthday: "1370-01-01",
|
||||
},
|
||||
firstPartyDescription: { desc: "توضیح حادثه طرف اول" },
|
||||
expertDescription: { desc: "توضیح حادثه کارشناس (مشترک برای صحنه)" },
|
||||
secondParty: {
|
||||
phoneNumber: "09187654321",
|
||||
initialForm: {
|
||||
@@ -273,7 +273,6 @@ export class ExpertInitiatedV2Controller {
|
||||
insurerBirthday: 13700720,
|
||||
driverBirthday: "1370-01-01",
|
||||
},
|
||||
description: { desc: "توضیح حادثه طرف دوم" },
|
||||
},
|
||||
guiltyPartyPhoneNumber: "09123456789",
|
||||
},
|
||||
@@ -302,7 +301,7 @@ export class ExpertInitiatedV2Controller {
|
||||
insurerBirthday: 1370,
|
||||
driverBirthday: "1370-01-01",
|
||||
},
|
||||
firstPartyDescription: {
|
||||
expertDescription: {
|
||||
desc: "توضیح حادثه",
|
||||
accidentDate: "2025-01-15",
|
||||
accidentTime: "14:30",
|
||||
@@ -319,7 +318,7 @@ export class ExpertInitiatedV2Controller {
|
||||
async completeBlameDataV2(
|
||||
@CurrentUser() expert: any,
|
||||
@Param("requestId") requestId: string,
|
||||
@Body() formData: any,
|
||||
@Body() formData: ExpertCompleteThirdPartyFormV2Dto | ExpertCompleteCarBodyFormV2Dto,
|
||||
) {
|
||||
return this.requestManagementService.expertCompleteBlameDataV2(
|
||||
expert,
|
||||
@@ -330,25 +329,18 @@ export class ExpertInitiatedV2Controller {
|
||||
|
||||
@Post("add-locations/:requestId")
|
||||
@ApiOperation({
|
||||
summary: "[V2] Submit location(s) for expert-initiated IN_PERSON blame",
|
||||
summary: "[V2] Submit one scene location for expert-initiated IN_PERSON blame",
|
||||
description:
|
||||
"Submit first party (and second party for THIRD_PARTY) location after complete-blame-data. This transitions workflow to WAITING_FOR_SIGNATURES.",
|
||||
"Submit one scene location after complete-blame-data. This transitions workflow to WAITING_FOR_SIGNATURES.",
|
||||
})
|
||||
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||
@ApiBody({
|
||||
type: ExpertCompleteLocationsDto,
|
||||
type: ExpertCompleteLocationV2Dto,
|
||||
examples: {
|
||||
THIRD_PARTY: {
|
||||
summary: "THIRD_PARTY locations",
|
||||
scene: {
|
||||
summary: "One scene location (all IN_PERSON types)",
|
||||
value: {
|
||||
firstPartyLocation: { lat: 35.6892, lon: 51.389 },
|
||||
secondPartyLocation: { lat: 35.7001, lon: 51.4102 },
|
||||
},
|
||||
},
|
||||
CAR_BODY: {
|
||||
summary: "CAR_BODY location",
|
||||
value: {
|
||||
firstPartyLocation: { lat: 35.6892, lon: 51.389 },
|
||||
location: { lat: 35.6892, lon: 51.389 },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -357,12 +349,12 @@ export class ExpertInitiatedV2Controller {
|
||||
async addLocationsV2(
|
||||
@CurrentUser() expert: any,
|
||||
@Param("requestId") requestId: string,
|
||||
@Body() dto: ExpertCompleteLocationsDto,
|
||||
@Body() dto: ExpertCompleteLocationV2Dto,
|
||||
) {
|
||||
return this.requestManagementService.expertAddLocationsForBlameV2(
|
||||
expert,
|
||||
requestId,
|
||||
dto,
|
||||
dto as any,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3872,10 +3872,19 @@ export class RequestManagementService {
|
||||
|
||||
async getAllBlameRequestsV2(user: any): Promise<any> {
|
||||
try {
|
||||
const userIdFilter =
|
||||
user?.sub && Types.ObjectId.isValid(user.sub)
|
||||
? { "parties.person.userId": new Types.ObjectId(user.sub) }
|
||||
: null;
|
||||
const phoneFilter = user?.username
|
||||
? { "parties.person.phoneNumber": user.username }
|
||||
: null;
|
||||
|
||||
const filters = [userIdFilter, phoneFilter].filter(Boolean);
|
||||
if (filters.length === 0) return [];
|
||||
|
||||
const requests = await this.blameRequestDbService.find(
|
||||
{
|
||||
"parties.person.userId": new Types.ObjectId(user.sub),
|
||||
},
|
||||
filters.length === 1 ? (filters[0] as any) : ({ $or: filters } as any),
|
||||
{
|
||||
select: "requestNo type status blameStatus createdAt updatedAt parties",
|
||||
}
|
||||
@@ -3883,7 +3892,9 @@ export class RequestManagementService {
|
||||
|
||||
const enriched = requests.map((req: any) => {
|
||||
const party = req.parties.find(
|
||||
(p: any) => String(p.person.userId) === String(user.sub)
|
||||
(p: any) =>
|
||||
(p?.person?.userId && String(p.person.userId) === String(user.sub)) ||
|
||||
(p?.person?.phoneNumber && p.person.phoneNumber === user?.username),
|
||||
);
|
||||
|
||||
const obj = req.toObject();
|
||||
@@ -4039,6 +4050,9 @@ export class RequestManagementService {
|
||||
if (!formData.carBodyForm) {
|
||||
throw new BadRequestException("carBodyForm is required.");
|
||||
}
|
||||
if (!formData?.expertDescription?.desc) {
|
||||
throw new BadRequestException("expertDescription.desc is required.");
|
||||
}
|
||||
|
||||
const firstPartyUserId = await this.getOrCreateUserByPhoneNumber(
|
||||
formData.firstPartyPhoneNumber,
|
||||
@@ -4110,12 +4124,12 @@ export class RequestManagementService {
|
||||
},
|
||||
},
|
||||
statement: {
|
||||
description: formData.firstPartyDescription?.desc,
|
||||
accidentDate: formData.firstPartyDescription?.accidentDate,
|
||||
accidentTime: formData.firstPartyDescription?.accidentTime,
|
||||
weatherCondition: formData.firstPartyDescription?.weatherCondition,
|
||||
roadCondition: formData.firstPartyDescription?.roadCondition,
|
||||
lightCondition: formData.firstPartyDescription?.lightCondition,
|
||||
description: formData.expertDescription?.desc,
|
||||
accidentDate: formData.expertDescription?.accidentDate,
|
||||
accidentTime: formData.expertDescription?.accidentTime,
|
||||
weatherCondition: formData.expertDescription?.weatherCondition,
|
||||
roadCondition: formData.expertDescription?.roadCondition,
|
||||
lightCondition: formData.expertDescription?.lightCondition,
|
||||
},
|
||||
location: req.parties?.[0]?.location,
|
||||
carBodyFirstForm: {
|
||||
@@ -4184,6 +4198,9 @@ export class RequestManagementService {
|
||||
if (!formData.secondParty || !formData.guiltyPartyPhoneNumber) {
|
||||
throw new BadRequestException("secondParty and guiltyPartyPhoneNumber are required.");
|
||||
}
|
||||
if (!formData?.expertDescription?.desc) {
|
||||
throw new BadRequestException("expertDescription.desc is required.");
|
||||
}
|
||||
|
||||
const firstPartyUserId = await this.getOrCreateUserByPhoneNumber(
|
||||
formData.firstPartyPhoneNumber,
|
||||
@@ -4254,7 +4271,7 @@ export class RequestManagementService {
|
||||
firstPartyUserId,
|
||||
formData.firstPartyInitialForm,
|
||||
formData.firstPartyPlate,
|
||||
formData.firstPartyDescription?.desc || "",
|
||||
formData.expertDescription?.desc || "",
|
||||
PartyRole.FIRST,
|
||||
);
|
||||
const secondParty = await buildPartyFromForm(
|
||||
@@ -4262,7 +4279,7 @@ export class RequestManagementService {
|
||||
secondPartyUserId,
|
||||
formData.secondParty.initialForm,
|
||||
formData.secondParty.plate,
|
||||
formData.secondParty.description?.desc || "",
|
||||
formData.expertDescription?.desc || "",
|
||||
PartyRole.SECOND,
|
||||
);
|
||||
|
||||
@@ -4320,7 +4337,7 @@ export class RequestManagementService {
|
||||
async expertAddLocationsForBlameV2(
|
||||
expert: any,
|
||||
requestId: string,
|
||||
dto: { firstPartyLocation: LocationDto; secondPartyLocation?: LocationDto },
|
||||
dto: { location: LocationDto },
|
||||
): Promise<any> {
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Request not found");
|
||||
@@ -4336,21 +4353,10 @@ export class RequestManagementService {
|
||||
|
||||
const firstIdx = this.getPartyIndex(req, PartyRole.FIRST);
|
||||
if (firstIdx === -1) throw new BadRequestException("First party not found");
|
||||
if (!dto?.firstPartyLocation) {
|
||||
throw new BadRequestException("firstPartyLocation is required");
|
||||
}
|
||||
req.parties[firstIdx].location = dto.firstPartyLocation as any;
|
||||
|
||||
if (req.type === BlameRequestType.THIRD_PARTY) {
|
||||
const secondIdx = this.getPartyIndex(req, PartyRole.SECOND);
|
||||
if (secondIdx === -1) throw new BadRequestException("Second party not found");
|
||||
if (!dto?.secondPartyLocation) {
|
||||
throw new BadRequestException(
|
||||
"secondPartyLocation is required for THIRD_PARTY",
|
||||
);
|
||||
}
|
||||
req.parties[secondIdx].location = dto.secondPartyLocation as any;
|
||||
if (!dto?.location) {
|
||||
throw new BadRequestException("location is required");
|
||||
}
|
||||
req.parties[firstIdx].location = dto.location as any;
|
||||
|
||||
const completed = Array.isArray(req.workflow?.completedSteps)
|
||||
? req.workflow.completedSteps
|
||||
@@ -4358,12 +4364,6 @@ export class RequestManagementService {
|
||||
if (!completed.includes(WorkflowStep.FIRST_LOCATION as any)) {
|
||||
completed.push(WorkflowStep.FIRST_LOCATION as any);
|
||||
}
|
||||
if (
|
||||
req.type === BlameRequestType.THIRD_PARTY &&
|
||||
!completed.includes(WorkflowStep.SECOND_LOCATION as any)
|
||||
) {
|
||||
completed.push(WorkflowStep.SECOND_LOCATION as any);
|
||||
}
|
||||
|
||||
req.workflow = {
|
||||
...(req.workflow || {}),
|
||||
@@ -4383,8 +4383,7 @@ export class RequestManagementService {
|
||||
actorType: expert?.role === RoleEnum.REGISTRAR ? "registrar" : "field_expert",
|
||||
},
|
||||
metadata: {
|
||||
hasFirstLocation: true,
|
||||
hasSecondLocation: req.type === BlameRequestType.THIRD_PARTY,
|
||||
hasLocation: true,
|
||||
},
|
||||
} as any);
|
||||
|
||||
@@ -4467,6 +4466,12 @@ export class RequestManagementService {
|
||||
const firstPartyIndex = this.getPartyIndex(req, PartyRole.FIRST);
|
||||
if (firstPartyIndex === -1) throw new BadRequestException("First party not found");
|
||||
const firstParty = req.parties[firstPartyIndex];
|
||||
if (
|
||||
Array.isArray(firstParty?.evidence?.voices) &&
|
||||
firstParty.evidence.voices.length > 0
|
||||
) {
|
||||
throw new ConflictException("Voice already uploaded for this file");
|
||||
}
|
||||
|
||||
const voiceDocument = await this.blameVoiceDbService.create({
|
||||
fileName: voiceFile.filename,
|
||||
|
||||
Reference in New Issue
Block a user