forked from Yara724/api
Merge pull request 'YARA-732' (#58) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#58
This commit is contained in:
@@ -28,7 +28,7 @@ import { FieldExpertDbService } from "src/users/entities/db-service/field-expert
|
|||||||
import { RegistrarDbService } from "src/users/entities/db-service/registrar.db.service";
|
import { RegistrarDbService } from "src/users/entities/db-service/registrar.db.service";
|
||||||
import { HashService } from "src/utils/hash/hash.service";
|
import { HashService } from "src/utils/hash/hash.service";
|
||||||
// import { MailService } from "src/utils/mail/mail.service";
|
// import { MailService } from "src/utils/mail/mail.service";
|
||||||
import { OtpService } from "src/utils/otp/otp.service";
|
import { OtpGeneratorService } from "src/sms-orchestration/otp-generator.service";
|
||||||
|
|
||||||
function pick(obj: Record<string, any>, keys: string[]) {
|
function pick(obj: Record<string, any>, keys: string[]) {
|
||||||
const out: Record<string, any> = {};
|
const out: Record<string, any> = {};
|
||||||
@@ -51,7 +51,7 @@ export class ActorAuthService {
|
|||||||
private readonly insurerExpertDbService: InsurerExpertDbService,
|
private readonly insurerExpertDbService: InsurerExpertDbService,
|
||||||
// private readonly mailService: MailService, // Mailer disabled – not used
|
// private readonly mailService: MailService, // Mailer disabled – not used
|
||||||
private readonly clientDbService: ClientDbService,
|
private readonly clientDbService: ClientDbService,
|
||||||
private readonly otpService: OtpService,
|
private readonly otpService: OtpGeneratorService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// TODO convrt to class for dynamic controller
|
// TODO convrt to class for dynamic controller
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import {
|
|||||||
import { JwtService } from "@nestjs/jwt";
|
import { JwtService } from "@nestjs/jwt";
|
||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import { LoginDtoRs } from "src/auth/dto/user/login.dto";
|
import { LoginDtoRs } from "src/auth/dto/user/login.dto";
|
||||||
|
import { OtpGeneratorService } from "src/sms-orchestration/otp-generator.service";
|
||||||
import { UserDbService } from "src/users/entities/db-service/user.db.service";
|
import { UserDbService } from "src/users/entities/db-service/user.db.service";
|
||||||
import { HashService } from "src/utils/hash/hash.service";
|
|
||||||
import { OtpService } from "src/utils/otp/otp.service";
|
|
||||||
import { SmsOrchestrationService } from "src/sms-orchestration/sms-orchestration.service";
|
import { SmsOrchestrationService } from "src/sms-orchestration/sms-orchestration.service";
|
||||||
|
import { HashService } from "src/utils/hash/hash.service";
|
||||||
|
|
||||||
// TODO FIX REGISTER TO USER.SERVICE AND AUTH IN THIS MODULE
|
// TODO FIX REGISTER TO USER.SERVICE AND AUTH IN THIS MODULE
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -24,7 +24,7 @@ export class UserAuthService {
|
|||||||
private readonly jwtService: JwtService,
|
private readonly jwtService: JwtService,
|
||||||
private readonly userDbService: UserDbService,
|
private readonly userDbService: UserDbService,
|
||||||
private readonly hashService: HashService,
|
private readonly hashService: HashService,
|
||||||
private readonly otpCreator: OtpService,
|
private readonly otpCreator: OtpGeneratorService,
|
||||||
private readonly smsOrchestrationService: SmsOrchestrationService,
|
private readonly smsOrchestrationService: SmsOrchestrationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { ClientModule } from "src/client/client.module";
|
|||||||
import { UsersModule } from "src/users/users.module";
|
import { UsersModule } from "src/users/users.module";
|
||||||
import { HashModule } from "src/utils/hash/hash.module";
|
import { HashModule } from "src/utils/hash/hash.module";
|
||||||
// import { MailModule } from "src/utils/mail/mail.module";
|
// import { MailModule } from "src/utils/mail/mail.module";
|
||||||
import { OtpModule } from "src/utils/otp/otp.module";
|
|
||||||
import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.module";
|
import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -20,7 +19,6 @@ import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.
|
|||||||
UsersModule,
|
UsersModule,
|
||||||
ClientModule,
|
ClientModule,
|
||||||
HashModule,
|
HashModule,
|
||||||
OtpModule,
|
|
||||||
PassportModule,
|
PassportModule,
|
||||||
SmsOrchestrationModule,
|
SmsOrchestrationModule,
|
||||||
JwtModule.register({
|
JwtModule.register({
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ import { PublicIdService } from "src/utils/public-id/public-id.service";
|
|||||||
import { ImageRequiredModel } from "./entites/schema/image-required.schema";
|
import { ImageRequiredModel } from "./entites/schema/image-required.schema";
|
||||||
import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service";
|
import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service";
|
||||||
import { ExpertFileActivityDbService } from "src/users/entities/db-service/expert-file-activity.db.service";
|
import { ExpertFileActivityDbService } from "src/users/entities/db-service/expert-file-activity.db.service";
|
||||||
|
import { SandHubService } from "src/sand-hub/sand-hub.service";
|
||||||
import {
|
import {
|
||||||
ExpertFileActivityType,
|
ExpertFileActivityType,
|
||||||
ExpertFileKind,
|
ExpertFileKind,
|
||||||
@@ -76,6 +77,7 @@ import { FactorStatus } from "src/Types&Enums/claim-request-management/factor-st
|
|||||||
import { BranchDbService } from "src/client/entities/db-service/branch.db.service";
|
import { BranchDbService } from "src/client/entities/db-service/branch.db.service";
|
||||||
import { CreationMethod } from "src/request-management/entities/schema/request-management.schema";
|
import { CreationMethod } from "src/request-management/entities/schema/request-management.schema";
|
||||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||||
|
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
|
||||||
import { UserRatingDto } from "./dto/user-rating.dto";
|
import { UserRatingDto } from "./dto/user-rating.dto";
|
||||||
import {
|
import {
|
||||||
canFinalizeExpertResend,
|
canFinalizeExpertResend,
|
||||||
@@ -158,8 +160,61 @@ export class ClaimRequestManagementService {
|
|||||||
private readonly branchDbService: BranchDbService,
|
private readonly branchDbService: BranchDbService,
|
||||||
private readonly claimRequiredDocumentDbService: ClaimRequiredDocumentDbService,
|
private readonly claimRequiredDocumentDbService: ClaimRequiredDocumentDbService,
|
||||||
private readonly publicIdService: PublicIdService,
|
private readonly publicIdService: PublicIdService,
|
||||||
|
private readonly sandHubService: SandHubService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
private parsePlateFromCompactString(
|
||||||
|
plateId: string | undefined,
|
||||||
|
): { leftDigits: number; centerAlphabet: string; centerDigits: number; ir: number } | null {
|
||||||
|
if (!plateId) return null;
|
||||||
|
const parts = String(plateId).split("-");
|
||||||
|
if (parts.length !== 4) return null;
|
||||||
|
const [irRaw, leftRaw, alphaRaw, centerRaw] = parts;
|
||||||
|
const ir = Number(irRaw);
|
||||||
|
const leftDigits = Number(leftRaw);
|
||||||
|
const centerDigits = Number(centerRaw);
|
||||||
|
const centerAlphabet = String(alphaRaw || "").trim();
|
||||||
|
if (
|
||||||
|
!Number.isFinite(ir) ||
|
||||||
|
!Number.isFinite(leftDigits) ||
|
||||||
|
!Number.isFinite(centerDigits) ||
|
||||||
|
!centerAlphabet
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { leftDigits, centerAlphabet, centerDigits, ir };
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveOwnershipPlateForClaim(
|
||||||
|
claimCase: any,
|
||||||
|
blameRequest?: any,
|
||||||
|
): { leftDigits: number; centerAlphabet: string; centerDigits: number; ir: number } | null {
|
||||||
|
const p = claimCase?.vehicle?.plate;
|
||||||
|
if (
|
||||||
|
p &&
|
||||||
|
Number.isFinite(Number(p.leftDigits)) &&
|
||||||
|
Number.isFinite(Number(p.centerDigits)) &&
|
||||||
|
Number.isFinite(Number(p.ir)) &&
|
||||||
|
String(p.centerAlphabet || "").trim()
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
leftDigits: Number(p.leftDigits),
|
||||||
|
centerAlphabet: String(p.centerAlphabet).trim(),
|
||||||
|
centerDigits: Number(p.centerDigits),
|
||||||
|
ir: Number(p.ir),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!blameRequest || !Array.isArray(blameRequest.parties)) return null;
|
||||||
|
const ownerUserId = claimCase?.owner?.userId?.toString?.();
|
||||||
|
const matchedParty =
|
||||||
|
blameRequest.parties.find(
|
||||||
|
(party: any) => String(party?.person?.userId || "") === String(ownerUserId || ""),
|
||||||
|
) || blameRequest.parties.find((party: any) => party?.role === PartyRole.FIRST);
|
||||||
|
const compactPlateId = matchedParty?.vehicle?.plateId;
|
||||||
|
return this.parsePlateFromCompactString(compactPlateId);
|
||||||
|
}
|
||||||
|
|
||||||
private delay(ms: number): Promise<void> {
|
private delay(ms: number): Promise<void> {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
@@ -4008,17 +4063,17 @@ export class ClaimRequestManagementService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// At most two non-top sides allowed
|
// DISABLED: At most two non-top sides allowed
|
||||||
const sideSet = new Set(
|
// const sideSet = new Set(
|
||||||
selectedItems
|
// selectedItems
|
||||||
.map((p) => p.side)
|
// .map((p) => p.side)
|
||||||
.filter((s) => s !== "top"),
|
// .filter((s) => s !== "top"),
|
||||||
);
|
// );
|
||||||
if (sideSet.size > 2) {
|
// if (sideSet.size > 2) {
|
||||||
throw new BadRequestException(
|
// throw new BadRequestException(
|
||||||
`At most two of left/right/front/back can be selected. Selected: ${[...sideSet].join(", ")}`,
|
// `At most two of left/right/front/back can be selected. Selected: ${[...sideSet].join(", ")}`,
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
|
||||||
const selectedPartDocs = selectedItems.map((p) =>
|
const selectedPartDocs = selectedItems.map((p) =>
|
||||||
catalogItemToSelectedPart(p, catalog),
|
catalogItemToSelectedPart(p, catalog),
|
||||||
@@ -4196,6 +4251,22 @@ export class ClaimRequestManagementService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const blameRequest = claimCase.blameRequestId
|
||||||
|
? await this.blameRequestDbService.findById(claimCase.blameRequestId.toString())
|
||||||
|
: null;
|
||||||
|
const ownershipPlate = this.resolveOwnershipPlateForClaim(claimCase, blameRequest);
|
||||||
|
if (!ownershipPlate) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Could not resolve vehicle plate for ownership validation.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// External inquiry checks before moving workflow:
|
||||||
|
// 1) ownership check for nationalCode + plate
|
||||||
|
// await this.sandHubService.getCarOwnershipInfo(ownershipPlate, nationalCode);
|
||||||
|
// 2) sheba check for nationalCode + sheba
|
||||||
|
await this.sandHubService.getShebaValidation(nationalCode, shebaNumber);
|
||||||
|
|
||||||
const updatePayload: any = {
|
const updatePayload: any = {
|
||||||
'damage.otherParts': otherParts.length > 0 ? otherParts : undefined,
|
'damage.otherParts': otherParts.length > 0 ? otherParts : undefined,
|
||||||
'money.sheba': shebaNumber,
|
'money.sheba': shebaNumber,
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { IsString, IsNotEmpty, IsOptional } from "class-validator";
|
import {
|
||||||
|
IsBoolean,
|
||||||
|
IsDateString,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
} from "class-validator";
|
||||||
|
|
||||||
export class CreateBranchDto {
|
export class CreateBranchDto {
|
||||||
@ApiProperty({ example: "شهرک غرب" })
|
@ApiProperty({ example: "شهرک غرب" })
|
||||||
@@ -31,4 +37,18 @@ export class CreateBranchDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
phoneNumber?: string;
|
phoneNumber?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, example: true, default: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isActive?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
required: false,
|
||||||
|
example: "2025-01-01T00:00:00.000Z",
|
||||||
|
description: "Branch activity start datetime (ISO string)",
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
activityStartDate?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,71 @@ export class BranchDbService {
|
|||||||
async findAll(insuranceId: string): Promise<BranchModel[]> {
|
async findAll(insuranceId: string): Promise<BranchModel[]> {
|
||||||
return await this.branchModel.find({
|
return await this.branchModel.find({
|
||||||
clientKey: new Types.ObjectId(insuranceId),
|
clientKey: new Types.ObjectId(insuranceId),
|
||||||
}, { _id: 1, name: 1, code: 1, city: 1, state: 1, address: 1, phoneNumber: 1, createdAtFa: 1, updatedAtFa: 1 });
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAllWithFilters(
|
||||||
|
insuranceId: string,
|
||||||
|
opts?: {
|
||||||
|
search?: string;
|
||||||
|
from?: Date;
|
||||||
|
to?: Date;
|
||||||
|
isActive?: boolean;
|
||||||
|
},
|
||||||
|
): Promise<BranchModel[]> {
|
||||||
|
const filter: any = { clientKey: new Types.ObjectId(insuranceId) };
|
||||||
|
|
||||||
|
if (typeof opts?.isActive === "boolean") {
|
||||||
|
filter.isActive = opts.isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts?.from || opts?.to) {
|
||||||
|
filter.$or = [
|
||||||
|
{
|
||||||
|
createdAt: {
|
||||||
|
...(opts.from ? { $gte: opts.from } : {}),
|
||||||
|
...(opts.to ? { $lte: opts.to } : {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
activityStartDate: {
|
||||||
|
...(opts.from ? { $gte: opts.from } : {}),
|
||||||
|
...(opts.to ? { $lte: opts.to } : {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts?.search?.trim()) {
|
||||||
|
const q = opts.search.trim();
|
||||||
|
const rx = new RegExp(q, "i");
|
||||||
|
filter.$and = [
|
||||||
|
...(filter.$and || []),
|
||||||
|
{
|
||||||
|
$or: [
|
||||||
|
{ name: rx },
|
||||||
|
{ code: rx },
|
||||||
|
{ city: rx },
|
||||||
|
{ state: rx },
|
||||||
|
{ address: rx },
|
||||||
|
{ phoneNumber: rx },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.branchModel.find(filter).lean();
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByIds(ids: Types.ObjectId[]) {
|
||||||
|
return this.branchModel
|
||||||
|
.find({ _id: { $in: ids } })
|
||||||
|
.select({ _id: 1, name: 1, code: 1, city: 1, state: 1, address: 1, phoneNumber: 1, isActive: 1, activityStartDate: 1, createdAt: 1, updatedAt: 1 })
|
||||||
|
.lean();
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByIdAndUpdate(id: string, update: any): Promise<BranchModel | null> {
|
||||||
|
return this.branchModel.findByIdAndUpdate(id, update, { new: true }).lean();
|
||||||
}
|
}
|
||||||
|
|
||||||
async findById(id: string): Promise<BranchModel | null> {
|
async findById(id: string): Promise<BranchModel | null> {
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ export class BranchModel {
|
|||||||
|
|
||||||
@Prop()
|
@Prop()
|
||||||
phoneNumber?: string;
|
phoneNumber?: string;
|
||||||
|
|
||||||
|
@Prop({ type: Boolean, default: true })
|
||||||
|
isActive?: boolean;
|
||||||
|
|
||||||
|
@Prop({ type: Date })
|
||||||
|
activityStartDate?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BranchSchema = SchemaFactory.createForClass(BranchModel);
|
export const BranchSchema = SchemaFactory.createForClass(BranchModel);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
ApiParam,
|
ApiParam,
|
||||||
ApiPropertyOptional,
|
ApiPropertyOptional,
|
||||||
ApiQuery,
|
ApiQuery,
|
||||||
|
ApiResponse,
|
||||||
ApiTags,
|
ApiTags,
|
||||||
} from "@nestjs/swagger";
|
} from "@nestjs/swagger";
|
||||||
import { IsOptional, IsString } from "class-validator";
|
import { IsOptional, IsString } from "class-validator";
|
||||||
@@ -18,6 +19,9 @@ import { ExpertClaimService } from "./expert-claim.service";
|
|||||||
import { ClaimSubmitResendV2Dto, SubmitExpertReplyV2Dto } from "./dto/expert-claim-v2.dto";
|
import { ClaimSubmitResendV2Dto, SubmitExpertReplyV2Dto } from "./dto/expert-claim-v2.dto";
|
||||||
import { UpdateClaimDamagedPartsV2Dto } from "./dto/update-claim-damaged-parts-v2.dto";
|
import { UpdateClaimDamagedPartsV2Dto } from "./dto/update-claim-damaged-parts-v2.dto";
|
||||||
import { FactorValidationV2Dto } from "./dto/factor-validation.dto";
|
import { FactorValidationV2Dto } from "./dto/factor-validation.dto";
|
||||||
|
import { ClaimRequestManagementService } from "src/claim-request-management/claim-request-management.service";
|
||||||
|
import { OuterPartCatalogItemDto } from "src/claim-request-management/dto/select-outer-parts-v2.dto";
|
||||||
|
import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
|
||||||
|
|
||||||
class InPersonVisitV2Dto {
|
class InPersonVisitV2Dto {
|
||||||
@ApiPropertyOptional({ example: 'Paint damage requires physical inspection' })
|
@ApiPropertyOptional({ example: 'Paint damage requires physical inspection' })
|
||||||
@@ -32,7 +36,10 @@ class InPersonVisitV2Dto {
|
|||||||
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
||||||
@Roles(RoleEnum.DAMAGE_EXPERT)
|
@Roles(RoleEnum.DAMAGE_EXPERT)
|
||||||
export class ExpertClaimV2Controller {
|
export class ExpertClaimV2Controller {
|
||||||
constructor(private readonly expertClaimService: ExpertClaimService) { }
|
constructor(
|
||||||
|
private readonly expertClaimService: ExpertClaimService,
|
||||||
|
private readonly claimRequestManagementService: ClaimRequestManagementService,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Get("report/status-counts")
|
@Get("report/status-counts")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
@@ -44,6 +51,26 @@ export class ExpertClaimV2Controller {
|
|||||||
return await this.expertClaimService.getStatusReportBucketsV2(actor);
|
return await this.expertClaimService.getStatusReportBucketsV2(actor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same catalog as `GET v2/claim-request-management/outer-parts-catalog` so factor/resend payloads use identical part ids.
|
||||||
|
*/
|
||||||
|
@Get("outer-parts-catalog")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get outer parts catalog (V2)",
|
||||||
|
description:
|
||||||
|
"Returns outer-damage parts with id/key/side. Optional `carType` filter returns only that type catalog.",
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: "Outer parts catalog",
|
||||||
|
type: [OuterPartCatalogItemDto],
|
||||||
|
})
|
||||||
|
async getOuterPartsCatalog(
|
||||||
|
@Query("carType") carType?: ClaimVehicleTypeV2,
|
||||||
|
): Promise<OuterPartCatalogItemDto[]> {
|
||||||
|
return this.claimRequestManagementService.getOuterPartsCatalogV2(carType);
|
||||||
|
}
|
||||||
|
|
||||||
@Get("requests")
|
@Get("requests")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "List available claim requests for damage expert",
|
summary: "List available claim requests for damage expert",
|
||||||
|
|||||||
@@ -41,12 +41,35 @@ export class ExpertInsurerController {
|
|||||||
constructor(private readonly expertInsurerService: ExpertInsurerService) {}
|
constructor(private readonly expertInsurerService: ExpertInsurerService) {}
|
||||||
|
|
||||||
@Get("branches")
|
@Get("branches")
|
||||||
async getInsuranceBranches(@CurrentUser() insurer) {
|
@ApiQuery({ name: "search", required: false, type: String })
|
||||||
|
@ApiQuery({
|
||||||
|
name: "from",
|
||||||
|
required: false,
|
||||||
|
description: "Optional start datetime (ISO string)",
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: "to",
|
||||||
|
required: false,
|
||||||
|
description: "Optional end datetime (ISO string)",
|
||||||
|
})
|
||||||
|
@ApiQuery({
|
||||||
|
name: "isActive",
|
||||||
|
required: false,
|
||||||
|
description: "Filter active state (true/false)",
|
||||||
|
})
|
||||||
|
async getInsuranceBranches(
|
||||||
|
@CurrentUser() insurer,
|
||||||
|
@Query("search") search?: string,
|
||||||
|
@Query("from") from?: string,
|
||||||
|
@Query("to") to?: string,
|
||||||
|
@Query("isActive") isActive?: string,
|
||||||
|
) {
|
||||||
if (!insurer) {
|
if (!insurer) {
|
||||||
throw new UnauthorizedException("Could not identify the current user.");
|
throw new UnauthorizedException("Could not identify the current user.");
|
||||||
}
|
}
|
||||||
return await this.expertInsurerService.retrieveInsuranceBranches(
|
return await this.expertInsurerService.retrieveInsuranceBranches(
|
||||||
insurer.clientKey,
|
insurer.clientKey,
|
||||||
|
{ search, from, to, isActive },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +89,29 @@ export class ExpertInsurerController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Put("branches/:branchId/status")
|
||||||
|
@ApiParam({ name: "branchId" })
|
||||||
|
@ApiQuery({ name: "isActive", type: Boolean })
|
||||||
|
async setBranchStatus(
|
||||||
|
@CurrentUser() insurer,
|
||||||
|
@Param("branchId") branchId: string,
|
||||||
|
@Query("isActive") isActive: string,
|
||||||
|
) {
|
||||||
|
if (!insurer) {
|
||||||
|
throw new UnauthorizedException("Could not identify the current user.");
|
||||||
|
}
|
||||||
|
const normalized = String(isActive).trim().toLowerCase();
|
||||||
|
if (!["true", "false", "1", "0", "yes", "no"].includes(normalized)) {
|
||||||
|
throw new BadRequestException("isActive must be true/false");
|
||||||
|
}
|
||||||
|
const active = ["true", "1", "yes"].includes(normalized);
|
||||||
|
return this.expertInsurerService.setBranchActive(
|
||||||
|
insurer.clientKey,
|
||||||
|
branchId,
|
||||||
|
active,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Post("experts/blame")
|
@Post("experts/blame")
|
||||||
@ApiBody({ type: CreateBlameExpertByInsurerDto })
|
@ApiBody({ type: CreateBlameExpertByInsurerDto })
|
||||||
async addBlameExpert(
|
async addBlameExpert(
|
||||||
|
|||||||
@@ -1207,12 +1207,66 @@ export class ExpertInsurerService {
|
|||||||
|
|
||||||
const newBranch = await this.branchDbService.create({
|
const newBranch = await this.branchDbService.create({
|
||||||
...branchDto,
|
...branchDto,
|
||||||
|
isActive: branchDto.isActive ?? true,
|
||||||
|
activityStartDate: branchDto.activityStartDate
|
||||||
|
? new Date(branchDto.activityStartDate)
|
||||||
|
: undefined,
|
||||||
clientKey: clientId,
|
clientKey: clientId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return newBranch;
|
return newBranch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private parseOptionalBoolean(value?: string): boolean | undefined {
|
||||||
|
if (value == null || value === "") return undefined;
|
||||||
|
const normalized = String(value).trim().toLowerCase();
|
||||||
|
if (["true", "1", "yes"].includes(normalized)) return true;
|
||||||
|
if (["false", "0", "no"].includes(normalized)) return false;
|
||||||
|
throw new BadRequestException("Invalid boolean value for 'isActive'");
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildHandlingBranchStatusSets() {
|
||||||
|
const blameInHandling = new Set<string>([
|
||||||
|
CaseStatus.OPEN,
|
||||||
|
CaseStatus.WAITING_FOR_SECOND_PARTY,
|
||||||
|
CaseStatus.WAITING_FOR_EXPERT,
|
||||||
|
CaseStatus.WAITING_FOR_DOCUMENT_RESEND,
|
||||||
|
CaseStatus.WAITING_FOR_SIGNATURES,
|
||||||
|
]);
|
||||||
|
const claimInHandling = new Set<string>([
|
||||||
|
ClaimCaseStatus.CREATED,
|
||||||
|
ClaimCaseStatus.SELECTING_OUTER_PARTS,
|
||||||
|
ClaimCaseStatus.SELECTING_OTHER_PARTS,
|
||||||
|
ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS,
|
||||||
|
ClaimCaseStatus.CAPTURING_PART_DAMAGES,
|
||||||
|
ClaimCaseStatus.WAITING_FOR_USER_RESEND,
|
||||||
|
ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT,
|
||||||
|
ClaimCaseStatus.EXPERT_REVIEWING,
|
||||||
|
ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL,
|
||||||
|
ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN,
|
||||||
|
ClaimCaseStatus.INSURER_REVIEW_MIXED_FACTORS_PENDING,
|
||||||
|
ClaimCaseStatus.OWNER_REPAIR_FACTOR_UPLOAD_PENDING,
|
||||||
|
ClaimCaseStatus.EXPERT_VALIDATING_REPAIR_FACTORS,
|
||||||
|
]);
|
||||||
|
return { blameInHandling, claimInHandling };
|
||||||
|
}
|
||||||
|
|
||||||
|
async setBranchActive(
|
||||||
|
clientKey: string,
|
||||||
|
branchId: string,
|
||||||
|
isActive: boolean,
|
||||||
|
) {
|
||||||
|
const clientId = this.getClientId(clientKey);
|
||||||
|
await this.assertBranchBelongsToClient(branchId, clientId);
|
||||||
|
const updated = await this.branchDbService.findByIdAndUpdate(branchId, {
|
||||||
|
$set: { isActive },
|
||||||
|
});
|
||||||
|
if (!updated) {
|
||||||
|
throw new NotFoundException("Branch not found");
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
private async assertBranchBelongsToClient(
|
private async assertBranchBelongsToClient(
|
||||||
branchId: string,
|
branchId: string,
|
||||||
clientId: Types.ObjectId,
|
clientId: Types.ObjectId,
|
||||||
@@ -1445,8 +1499,108 @@ export class ExpertInsurerService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async retrieveInsuranceBranches(insuranceId: string) {
|
async retrieveInsuranceBranches(
|
||||||
return this.branchDbService.findAll(insuranceId);
|
insuranceId: string,
|
||||||
|
opts?: {
|
||||||
|
search?: string;
|
||||||
|
from?: string;
|
||||||
|
to?: string;
|
||||||
|
isActive?: string;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const clientObjectId = this.getClientId(insuranceId);
|
||||||
|
const { fromDate, toDate } = this.parseDateRange(opts?.from, opts?.to);
|
||||||
|
const isActiveFilter = this.parseOptionalBoolean(opts?.isActive);
|
||||||
|
const branches = await this.branchDbService.findAllWithFilters(String(clientObjectId), {
|
||||||
|
search: opts?.search,
|
||||||
|
from: fromDate,
|
||||||
|
to: toDate,
|
||||||
|
isActive: isActiveFilter,
|
||||||
|
});
|
||||||
|
|
||||||
|
const branchIdSet = new Set(branches.map((b: any) => String(b._id)));
|
||||||
|
const ckFilter = this.clientKeyScopeFilter(clientObjectId);
|
||||||
|
const [experts, damageExperts, blameFiles, claimFiles] = await Promise.all([
|
||||||
|
this.expertDbService.findAll(ckFilter as never),
|
||||||
|
this.damageExpertDbService.findAll(ckFilter as never),
|
||||||
|
this.getClientBlameFiles(clientObjectId),
|
||||||
|
this.getClientClaimFiles(clientObjectId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const activeExpertsByBranch = new Map<string, Set<string>>();
|
||||||
|
const expertBranchById = new Map<string, string>();
|
||||||
|
const claimExpertBranchById = new Map<string, string>();
|
||||||
|
for (const e of experts as any[]) {
|
||||||
|
const bid = e?.branchId ? String(e.branchId) : "";
|
||||||
|
if (!bid || !branchIdSet.has(bid)) continue;
|
||||||
|
const eid = String(e?._id || "");
|
||||||
|
if (!eid) continue;
|
||||||
|
expertBranchById.set(eid, bid);
|
||||||
|
if (!activeExpertsByBranch.has(bid)) activeExpertsByBranch.set(bid, new Set());
|
||||||
|
activeExpertsByBranch.get(bid)!.add(eid);
|
||||||
|
}
|
||||||
|
for (const e of damageExperts as any[]) {
|
||||||
|
const bid = e?.branchId ? String(e.branchId) : "";
|
||||||
|
if (!bid || !branchIdSet.has(bid)) continue;
|
||||||
|
const eid = String(e?._id || "");
|
||||||
|
if (!eid) continue;
|
||||||
|
claimExpertBranchById.set(eid, bid);
|
||||||
|
if (!activeExpertsByBranch.has(bid)) activeExpertsByBranch.set(bid, new Set());
|
||||||
|
activeExpertsByBranch.get(bid)!.add(eid);
|
||||||
|
}
|
||||||
|
|
||||||
|
const completedByBranch = new Map<string, Set<string>>();
|
||||||
|
const handlingByBranch = new Map<string, Set<string>>();
|
||||||
|
const { blameInHandling, claimInHandling } = this.buildHandlingBranchStatusSets();
|
||||||
|
const addPublicId = (map: Map<string, Set<string>>, branchId: string, fileKey: string) => {
|
||||||
|
if (!map.has(branchId)) map.set(branchId, new Set());
|
||||||
|
map.get(branchId)!.add(fileKey);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const b of blameFiles as any[]) {
|
||||||
|
const expertId = String(b?.expert?.assignedExpertId || "");
|
||||||
|
if (!expertId) continue;
|
||||||
|
const bid = expertBranchById.get(expertId);
|
||||||
|
if (!bid) continue;
|
||||||
|
const fileKey = String(b?.publicId || b?._id || "");
|
||||||
|
if (!fileKey) continue;
|
||||||
|
const status = String(b?.status || "");
|
||||||
|
if (status === CaseStatus.COMPLETED) addPublicId(completedByBranch, bid, fileKey);
|
||||||
|
if (blameInHandling.has(status)) addPublicId(handlingByBranch, bid, fileKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const c of claimFiles as any[]) {
|
||||||
|
const expertId = String(this.claimDamageExpertActorId(c) || "");
|
||||||
|
if (!expertId) continue;
|
||||||
|
const bid = claimExpertBranchById.get(expertId);
|
||||||
|
if (!bid) continue;
|
||||||
|
const fileKey = String(c?.publicId || c?._id || "");
|
||||||
|
if (!fileKey) continue;
|
||||||
|
const status = String(c?.status || "");
|
||||||
|
if (status === ClaimCaseStatus.COMPLETED) addPublicId(completedByBranch, bid, fileKey);
|
||||||
|
if (claimInHandling.has(status)) addPublicId(handlingByBranch, bid, fileKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = branches.map((branch: any) => {
|
||||||
|
const bid = String(branch._id);
|
||||||
|
return {
|
||||||
|
...branch,
|
||||||
|
totalActiveExperts: activeExpertsByBranch.get(bid)?.size ?? 0,
|
||||||
|
totalFinishedFiles: completedByBranch.get(bid)?.size ?? 0,
|
||||||
|
totalFilesInHandling: handlingByBranch.get(bid)?.size ?? 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: list.length,
|
||||||
|
filters: {
|
||||||
|
search: opts?.search ?? null,
|
||||||
|
from: fromDate ?? null,
|
||||||
|
to: toDate ?? null,
|
||||||
|
isActive: isActiveFilter ?? null,
|
||||||
|
},
|
||||||
|
branches: list,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseDateRange(from?: string, to?: string) {
|
private parseDateRange(from?: string, to?: string) {
|
||||||
|
|||||||
@@ -869,7 +869,68 @@ export class RequestManagementService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: personal inquiry is not part of Tejarat block inquiry flow.
|
// ---- External inquiry 2: personal identity check (insurer/driver nationalCode + birthDate) ----
|
||||||
|
const personalNationalCode =
|
||||||
|
body.nationalCodeOfInsurer || body.nationalCodeOfDriver;
|
||||||
|
const birthDateRaw =
|
||||||
|
body.insurerBirthday ?? body.driverBirthday ?? null;
|
||||||
|
const birthDateDigits = String(birthDateRaw ?? "")
|
||||||
|
.replace(/\D/g, "")
|
||||||
|
.trim();
|
||||||
|
const personalBirthDate = Number(birthDateDigits);
|
||||||
|
if (!personalNationalCode || !Number.isFinite(personalBirthDate) || personalBirthDate <= 0) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Valid nationalCode and birthDate are required for personal inquiry.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
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}`,
|
||||||
|
);
|
||||||
|
throw new HttpException(
|
||||||
|
"Personal identity inquiry failed",
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- External inquiry 3: driving license check (insurerLicense + nationalCode) ----
|
||||||
|
// const licenseNationalCode = body.nationalCodeOfInsurer || body.nationalCodeOfDriver;
|
||||||
|
// const licenseNumber = body.insurerLicense;
|
||||||
|
// if (!licenseNationalCode || !licenseNumber) {
|
||||||
|
// throw new BadRequestException(
|
||||||
|
// "nationalCode and insurerLicense are required for driving license inquiry.",
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// try {
|
||||||
|
// const licenseInquiry = await this.sandHubService.getDrivingLicenseInfo(
|
||||||
|
// licenseNationalCode,
|
||||||
|
// licenseNumber,
|
||||||
|
// );
|
||||||
|
// this.logger.log(
|
||||||
|
// `[SANDHUB] license inquiry success request=${req._id} nationalCode=${licenseNationalCode}: ${JSON.stringify(
|
||||||
|
// licenseInquiry,
|
||||||
|
// )}`,
|
||||||
|
// );
|
||||||
|
// } catch (err: any) {
|
||||||
|
// this.logger.error(
|
||||||
|
// `[SANDHUB] license inquiry failed request=${req._id} nationalCode=${licenseNationalCode}: ${err?.message || err}`,
|
||||||
|
// );
|
||||||
|
// throw new HttpException(
|
||||||
|
// "Driving license inquiry failed",
|
||||||
|
// HttpStatus.BAD_REQUEST,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
// Find client by company code
|
// Find client by company code
|
||||||
const clientName = inquiryMapped?.CompanyName;
|
const clientName = inquiryMapped?.CompanyName;
|
||||||
|
|||||||
@@ -577,12 +577,11 @@ export class SandHubService {
|
|||||||
|
|
||||||
async getShebaValidation(nationalId: string, shebaId: string) {
|
async getShebaValidation(nationalId: string, shebaId: string) {
|
||||||
try {
|
try {
|
||||||
const requestUrl = `${process.env.SANDHUB_BASE_URL}/sheba`;
|
const requestUrl = `${process.env.SANDHUB_BASE_URL}/sheba-tejaratno`;
|
||||||
const requestPayload = {
|
const requestPayload = {
|
||||||
accountOwnerType: "1",
|
AccountOwnerType: "1",
|
||||||
nationalId: nationalId,
|
NationalId: nationalId,
|
||||||
legalId: "",
|
ShebaId: shebaId,
|
||||||
shebaId: shebaId,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.logger.log(`Validating Sheba ID for national code: ${nationalId}`);
|
this.logger.log(`Validating Sheba ID for national code: ${nationalId}`);
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
// import crypto from "crypto"
|
|
||||||
const crypto = require("node:crypto");
|
const crypto = require("node:crypto");
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OtpService {
|
export class OtpGeneratorService {
|
||||||
createDigit(digits: number): string {
|
createDigit(digits: number): string {
|
||||||
const max = Math.pow(10, digits);
|
const max = Math.pow(10, digits);
|
||||||
const randomBytes = crypto.randomBytes(Math.ceil(digits / 2));
|
const randomBytes = crypto.randomBytes(Math.ceil(digits / 2));
|
||||||
const randomNumber = parseInt(randomBytes.toString("hex"), 16) % max;
|
const randomNumber = parseInt(randomBytes.toString("hex"), 16) % max;
|
||||||
return randomNumber.toString().padStart(digits, "0");
|
return randomNumber.toString().padStart(digits, "0");
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
return this.createDigit(5);
|
return this.createDigit(5);
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ import { MongooseModule } from "@nestjs/mongoose";
|
|||||||
import { SmsTextDbService } from "./entities/db-service/sms-text.db.service";
|
import { SmsTextDbService } from "./entities/db-service/sms-text.db.service";
|
||||||
import { SmsText, SmsTextSchema } from "./entities/schema/sms-text.schema";
|
import { SmsText, SmsTextSchema } from "./entities/schema/sms-text.schema";
|
||||||
import { SmsGatewayModule } from "./provider/sms-gateway.module";
|
import { SmsGatewayModule } from "./provider/sms-gateway.module";
|
||||||
|
import { OtpGeneratorService } from "./otp-generator.service";
|
||||||
import { SmsOrchestrationService } from "./sms-orchestration.service";
|
import { SmsOrchestrationService } from "./sms-orchestration.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -10,7 +11,7 @@ import { SmsOrchestrationService } from "./sms-orchestration.service";
|
|||||||
SmsGatewayModule,
|
SmsGatewayModule,
|
||||||
MongooseModule.forFeature([{ name: SmsText.name, schema: SmsTextSchema }]),
|
MongooseModule.forFeature([{ name: SmsText.name, schema: SmsTextSchema }]),
|
||||||
],
|
],
|
||||||
providers: [SmsTextDbService, SmsOrchestrationService],
|
providers: [SmsTextDbService, SmsOrchestrationService, OtpGeneratorService],
|
||||||
exports: [SmsOrchestrationService],
|
exports: [SmsOrchestrationService, OtpGeneratorService],
|
||||||
})
|
})
|
||||||
export class SmsOrchestrationModule {}
|
export class SmsOrchestrationModule {}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { MongooseModule } from "@nestjs/mongoose";
|
|||||||
import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service";
|
import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service";
|
||||||
import { ExpertModel } from "src/users/entities/schema/expert.schema";
|
import { ExpertModel } from "src/users/entities/schema/expert.schema";
|
||||||
import { HashModule } from "src/utils/hash/hash.module";
|
import { HashModule } from "src/utils/hash/hash.module";
|
||||||
import { OtpModule } from "src/utils/otp/otp.module";
|
|
||||||
import { ExpertDbService } from "./entities/db-service/expert.db.service";
|
import { ExpertDbService } from "./entities/db-service/expert.db.service";
|
||||||
import { FieldExpertDbService } from "./entities/db-service/field-expert.db.service";
|
import { FieldExpertDbService } from "./entities/db-service/field-expert.db.service";
|
||||||
import { InsurerExpertDbService } from "./entities/db-service/insurer-expert.db.service";
|
import { InsurerExpertDbService } from "./entities/db-service/insurer-expert.db.service";
|
||||||
@@ -44,7 +43,6 @@ import { UserModel, UserDbSchema } from "./entities/schema/user.schema";
|
|||||||
{ name: RegistrarModel.name, schema: RegistrarDbSchema },
|
{ name: RegistrarModel.name, schema: RegistrarDbSchema },
|
||||||
{ name: ExpertFileActivity.name, schema: ExpertFileActivitySchema },
|
{ name: ExpertFileActivity.name, schema: ExpertFileActivitySchema },
|
||||||
]),
|
]),
|
||||||
OtpModule,
|
|
||||||
HashModule,
|
HashModule,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import { Module } from "@nestjs/common";
|
|
||||||
import { OtpService } from "./otp.service";
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
providers: [OtpService],
|
|
||||||
exports: [OtpService],
|
|
||||||
})
|
|
||||||
export class OtpModule {}
|
|
||||||
Reference in New Issue
Block a user