forked from Yara724/api
Merge pull request 'Fixed unified damagedParts + Simplified Captcha' (#110) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#110
This commit is contained in:
@@ -13,9 +13,9 @@ export class CaptchaService {
|
|||||||
generate(): GeneratedCaptcha {
|
generate(): GeneratedCaptcha {
|
||||||
const captcha = svgCaptcha.create({
|
const captcha = svgCaptcha.create({
|
||||||
size: 5,
|
size: 5,
|
||||||
ignoreChars: "0oO1ilI",
|
ignoreChars: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||||
noise: 3,
|
noise: 1,
|
||||||
color: true,
|
color: false,
|
||||||
background: "#f8fafc",
|
background: "#f8fafc",
|
||||||
width: 160,
|
width: 160,
|
||||||
height: 56,
|
height: 56,
|
||||||
|
|||||||
@@ -27,26 +27,48 @@ export function buildEnrichedDamagedParts(params: {
|
|||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
const priceDropLines: any[] = evaluationBlock?.priceDrop?.partLines ?? [];
|
const priceDropLines: any[] = evaluationBlock?.priceDrop?.partLines ?? [];
|
||||||
|
|
||||||
|
// objectionParts stores { partId, carPartDamage, side } — not { id, name }
|
||||||
const objectionParts: any[] =
|
const objectionParts: any[] =
|
||||||
evaluationBlock?.objection?.objectionParts ?? [];
|
evaluationBlock?.objection?.objectionParts ?? [];
|
||||||
|
|
||||||
|
// newParts stores { partId, partName, side } — not { id, name }
|
||||||
const newObjectionParts: any[] = evaluationBlock?.objection?.newParts ?? [];
|
const newObjectionParts: any[] = evaluationBlock?.objection?.newParts ?? [];
|
||||||
|
|
||||||
// Resend block — lives at evaluation.damageExpertResend
|
|
||||||
const damageExpertResend = evaluationBlock?.damageExpertResend;
|
const damageExpertResend = evaluationBlock?.damageExpertResend;
|
||||||
const resendCarParts: any[] = damageExpertResend?.resendCarParts ?? [];
|
const resendCarParts: any[] = damageExpertResend?.resendCarParts ?? [];
|
||||||
const resendFulfilledAt: Date | undefined = damageExpertResend?.fulfilledAt;
|
const resendFulfilledAt: Date | undefined = damageExpertResend?.fulfilledAt;
|
||||||
|
|
||||||
|
// Helpers that match against the ACTUAL stored field names
|
||||||
|
function partMatchesObjection(sp: any, objPart: any): boolean {
|
||||||
|
if (sp.id != null && objPart.partId != null && sp.id === objPart.partId)
|
||||||
|
return true;
|
||||||
|
const objName = objPart.carPartDamage ?? objPart.name ?? objPart.partName;
|
||||||
|
if (objName && (sp.name === objName || sp.label_fa === objName))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function partMatchesNewObjection(sp: any, newPart: any): boolean {
|
||||||
|
if (sp.id != null && newPart.partId != null && sp.id === newPart.partId)
|
||||||
|
return true;
|
||||||
|
const newName = newPart.partName ?? newPart.name;
|
||||||
|
if (newName && (sp.name === newName || sp.label_fa === newName))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function isPartInResend(sp: any): boolean {
|
function isPartInResend(sp: any): boolean {
|
||||||
return resendCarParts.some(
|
return resendCarParts.some(
|
||||||
(r: any) =>
|
(r: any) =>
|
||||||
r.id === sp.id || r.name === sp.name || r.catalogKey === sp.catalogKey,
|
(sp.id != null && r.id === sp.id) ||
|
||||||
|
r.catalogKey === sp.catalogKey ||
|
||||||
|
r.name === sp.name,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveOrigin(sp: any): EnrichedDamagedPart["origin"] {
|
function resolveOrigin(sp: any): EnrichedDamagedPart["origin"] {
|
||||||
if (
|
if (newObjectionParts.some((p) => partMatchesNewObjection(sp, p))) {
|
||||||
newObjectionParts.some((p: any) => p.id === sp.id || p.name === sp.name)
|
|
||||||
) {
|
|
||||||
return "added_by_objection";
|
return "added_by_objection";
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -65,7 +87,6 @@ export function buildEnrichedDamagedParts(params: {
|
|||||||
if (!captured) {
|
if (!captured) {
|
||||||
return inResend ? "resend_requested" : "pending";
|
return inResend ? "resend_requested" : "pending";
|
||||||
}
|
}
|
||||||
// captured=true + was in a resend request that is now fulfilled → "resent"
|
|
||||||
return inResend && resendFulfilledAt ? "resent" : "uploaded";
|
return inResend && resendFulfilledAt ? "resent" : "uploaded";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,8 +98,30 @@ export function buildEnrichedDamagedParts(params: {
|
|||||||
ck,
|
ck,
|
||||||
selectedParts,
|
selectedParts,
|
||||||
) as { url?: string; path?: string } | undefined;
|
) as { url?: string; path?: string } | undefined;
|
||||||
const captured = hasDamagedPartCapture(damagedPartsData, ck, selectedParts);
|
|
||||||
const url = cap?.url || (cap?.path ? buildFileLink(cap.path) : undefined);
|
// Fallback: if id is null but media.damagedParts has a match by name
|
||||||
|
const captured =
|
||||||
|
hasDamagedPartCapture(damagedPartsData, ck, selectedParts) ||
|
||||||
|
(sp.id == null && Array.isArray(damagedPartsData)
|
||||||
|
? (damagedPartsData as any[]).some(
|
||||||
|
(d) => d.name === sp.name && d.side === sp.side && d.path,
|
||||||
|
)
|
||||||
|
: false);
|
||||||
|
|
||||||
|
const capUrl =
|
||||||
|
cap?.url ||
|
||||||
|
(cap?.path ? buildFileLink(cap.path) : undefined) ||
|
||||||
|
(sp.id == null && Array.isArray(damagedPartsData)
|
||||||
|
? (() => {
|
||||||
|
const match = (damagedPartsData as any[]).find(
|
||||||
|
(d) => d.name === sp.name && d.side === sp.side && d.path,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
match?.url ||
|
||||||
|
(match?.path ? buildFileLink(match.path) : undefined)
|
||||||
|
);
|
||||||
|
})()
|
||||||
|
: undefined);
|
||||||
|
|
||||||
const matchingPriceDrop = priceDropLines.find(
|
const matchingPriceDrop = priceDropLines.find(
|
||||||
(line: any) =>
|
(line: any) =>
|
||||||
@@ -86,11 +129,9 @@ export function buildEnrichedDamagedParts(params: {
|
|||||||
line.priceDropPartKey?.toLowerCase() === sp.name?.toLowerCase(),
|
line.priceDropPartKey?.toLowerCase() === sp.name?.toLowerCase(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const isObjected = objectionParts.some(
|
const isObjected = objectionParts.some((p) => partMatchesObjection(sp, p));
|
||||||
(p: any) => p.id === sp.id || p.name === sp.name,
|
const isNewlyAdded = newObjectionParts.some((p) =>
|
||||||
);
|
partMatchesNewObjection(sp, p),
|
||||||
const isNewlyAdded = newObjectionParts.some(
|
|
||||||
(p: any) => p.id === sp.id || p.name === sp.name,
|
|
||||||
);
|
);
|
||||||
const isNewByExpert = expertAddedParts.some(
|
const isNewByExpert = expertAddedParts.some(
|
||||||
(p: any) => p.id === sp.id || p.name === sp.name,
|
(p: any) => p.id === sp.id || p.name === sp.name,
|
||||||
@@ -109,7 +150,7 @@ export function buildEnrichedDamagedParts(params: {
|
|||||||
origin: resolveOrigin(sp),
|
origin: resolveOrigin(sp),
|
||||||
captureStatus: resolveCaptureStatus(sp, captured),
|
captureStatus: resolveCaptureStatus(sp, captured),
|
||||||
captured,
|
captured,
|
||||||
url,
|
url: capUrl,
|
||||||
|
|
||||||
objection: {
|
objection: {
|
||||||
isObjected,
|
isObjected,
|
||||||
|
|||||||
@@ -4122,7 +4122,7 @@ export class ExpertClaimService {
|
|||||||
blameRequestId: claim.blameRequestId?.toString(),
|
blameRequestId: claim.blameRequestId?.toString(),
|
||||||
blameRequestNo: claim.blameRequestNo,
|
blameRequestNo: claim.blameRequestNo,
|
||||||
money: moneyPayload,
|
money: moneyPayload,
|
||||||
selectedParts: selectedNormExpert,
|
// selectedParts: selectedNormExpert,
|
||||||
otherParts: claim.damage?.otherParts,
|
otherParts: claim.damage?.otherParts,
|
||||||
requiredDocuments:
|
requiredDocuments:
|
||||||
Object.keys(requiredDocumentsStatus).length > 0
|
Object.keys(requiredDocumentsStatus).length > 0
|
||||||
@@ -4134,7 +4134,7 @@ export class ExpertClaimService {
|
|||||||
evaluation: enrichedEvaluation as
|
evaluation: enrichedEvaluation as
|
||||||
| ClaimDetailV2ResponseDto["evaluation"]
|
| ClaimDetailV2ResponseDto["evaluation"]
|
||||||
| undefined,
|
| undefined,
|
||||||
objection,
|
// objection,
|
||||||
videoCapture,
|
videoCapture,
|
||||||
blameCase: blameCaseForApi,
|
blameCase: blameCaseForApi,
|
||||||
blameExpertDecision,
|
blameExpertDecision,
|
||||||
@@ -4242,9 +4242,10 @@ export class ExpertClaimService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Manual path — change totalPriceDrop → total to match calculated path
|
||||||
const manualPayload = {
|
const manualPayload = {
|
||||||
manualOverride: true,
|
manualOverride: true,
|
||||||
totalPriceDrop: body.manualPriceDrop,
|
total: body.manualPriceDrop, // ← was totalPriceDrop
|
||||||
partLines: [],
|
partLines: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -4269,6 +4270,18 @@ export class ExpertClaimService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculated path — existing logic unchanged
|
// Calculated path — existing logic unchanged
|
||||||
|
// At the top of the calculated path, after the manualPriceDrop early return:
|
||||||
|
const existingPriceDrop = (claim as any).evaluation?.priceDrop;
|
||||||
|
if (
|
||||||
|
existingPriceDrop?.manualOverride === true &&
|
||||||
|
!body.partSeverities?.length
|
||||||
|
) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"This claim has a manual price-drop correction. " +
|
||||||
|
"To recalculate, provide partSeverities explicitly — this will replace the manual correction.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const carType = claim.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
|
const carType = claim.vehicle?.carType as ClaimVehicleTypeV2 | undefined;
|
||||||
const selectedNorm = normalizeDamageSelectedParts(
|
const selectedNorm = normalizeDamageSelectedParts(
|
||||||
claim.damage?.selectedParts,
|
claim.damage?.selectedParts,
|
||||||
|
|||||||
32
src/expert-initiated/dtos/login.dto.ts
Normal file
32
src/expert-initiated/dtos/login.dto.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsEmail, IsNotEmpty, IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class FieldExpertLoginDto {
|
||||||
|
@ApiProperty()
|
||||||
|
@IsEmail({ allow_underscores: true })
|
||||||
|
@IsString({
|
||||||
|
message: "email is not string",
|
||||||
|
context: {
|
||||||
|
errorCode: 4000,
|
||||||
|
note: "The validated email type must be string",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
readonly email: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsNotEmpty({
|
||||||
|
message: "password is empty",
|
||||||
|
context: {
|
||||||
|
errorCode: 4001,
|
||||||
|
note: "The validated password must not be empty",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@IsString({
|
||||||
|
message: "password is not string",
|
||||||
|
context: {
|
||||||
|
errorCode: 4002,
|
||||||
|
note: "The validated password type must be string",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
readonly password: string;
|
||||||
|
}
|
||||||
16
src/expert-initiated/expert-initiated.controller.ts
Normal file
16
src/expert-initiated/expert-initiated.controller.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Body, Controller, Post } from "@nestjs/common";
|
||||||
|
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
|
||||||
|
import { FieldExpertLoginDto } from "./dtos/login.dto";
|
||||||
|
import { FieldExpertService } from "./expert-initiated.service";
|
||||||
|
|
||||||
|
@Controller("expert-initiated")
|
||||||
|
@ApiTags("Expert Initiated Flow (Field Expert)")
|
||||||
|
@ApiBearerAuth()
|
||||||
|
export class FieldExpertController {
|
||||||
|
constructor(private readonly fieldExpertService: FieldExpertService) {}
|
||||||
|
|
||||||
|
@Post("login")
|
||||||
|
async login(@Body() fieldExpertLoginDto: FieldExpertLoginDto) {
|
||||||
|
return await this.fieldExpertService.login(fieldExpertLoginDto);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/expert-initiated/expert-initiated.module.ts
Normal file
25
src/expert-initiated/expert-initiated.module.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { MongooseModule } from "@nestjs/mongoose";
|
||||||
|
import { FieldExpert, FieldExpertSchema } from "./schemas/field-expert.schema";
|
||||||
|
import { FieldExpertController } from "./expert-initiated.controller";
|
||||||
|
import { FieldExpertService } from "./expert-initiated.service";
|
||||||
|
import { FieldExpertRepository } from "./repositories/field-expert.repository";
|
||||||
|
import { AuthModule } from "src/auth/auth.module";
|
||||||
|
import { JwtModule } from "@nestjs/jwt";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
MongooseModule.forFeature([
|
||||||
|
{
|
||||||
|
name: FieldExpert.name,
|
||||||
|
schema: FieldExpertSchema,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
AuthModule,
|
||||||
|
JwtModule,
|
||||||
|
],
|
||||||
|
controllers: [FieldExpertController],
|
||||||
|
providers: [FieldExpertRepository, FieldExpertService],
|
||||||
|
exports: [],
|
||||||
|
})
|
||||||
|
export class ExpertInitiatedModule {}
|
||||||
32
src/expert-initiated/expert-initiated.service.ts
Normal file
32
src/expert-initiated/expert-initiated.service.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { FieldExpertRepository } from "./repositories/field-expert.repository";
|
||||||
|
import { FieldExpertLoginDto } from "./dtos/login.dto";
|
||||||
|
import { UserAuthService } from "src/auth/auth-services/user.auth.service";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FieldExpertService {
|
||||||
|
constructor(
|
||||||
|
private readonly fieldExpertRepository: FieldExpertRepository,
|
||||||
|
private readonly authService: UserAuthService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async login(fieldExpertLoginDto: FieldExpertLoginDto) {
|
||||||
|
const user = await this.fieldExpertRepository.retrieveByEmail(
|
||||||
|
fieldExpertLoginDto.email,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!user) throw new NotFoundException("User not found");
|
||||||
|
|
||||||
|
const validate = await this.authService.validateUser(
|
||||||
|
user.email,
|
||||||
|
user.password,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!validate)
|
||||||
|
throw new BadRequestException("Username/Password does not match");
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/expert-initiated/repositories/field-expert.repository.ts
Normal file
22
src/expert-initiated/repositories/field-expert.repository.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { InjectModel } from "@nestjs/mongoose";
|
||||||
|
import { Model } from "mongoose";
|
||||||
|
import { FieldExpert } from "../schemas/field-expert.schema";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FieldExpertRepository {
|
||||||
|
constructor(
|
||||||
|
@InjectModel(FieldExpert.name)
|
||||||
|
private readonly fieldExpertModel: Model<FieldExpert>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async retrieveById(id: string): Promise<FieldExpert> {
|
||||||
|
return await this.fieldExpertModel.findById(id).exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
async retrieveByEmail(email: string): Promise<FieldExpert> {
|
||||||
|
return await this.fieldExpertModel.findOne({
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/expert-initiated/schemas/field-expert.schema.ts
Normal file
27
src/expert-initiated/schemas/field-expert.schema.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
|
import { HydratedDocument, Types } from "mongoose";
|
||||||
|
|
||||||
|
export type FieldExpertDocument = HydratedDocument<FieldExpert>;
|
||||||
|
|
||||||
|
@Schema({
|
||||||
|
id: true,
|
||||||
|
timestamps: true,
|
||||||
|
})
|
||||||
|
export class FieldExpert {
|
||||||
|
@Prop({ unique: true })
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@Prop({ required: true })
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
@Prop({ required: true })
|
||||||
|
firstName: string;
|
||||||
|
|
||||||
|
@Prop({ required: true })
|
||||||
|
lastName: string;
|
||||||
|
|
||||||
|
@Prop({ required: true })
|
||||||
|
cellphone: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FieldExpertSchema = SchemaFactory.createForClass(FieldExpert);
|
||||||
Reference in New Issue
Block a user