forked from Yara724/api
Merge pull request 'Fixed mismatched userId on field expert for claim' (#137) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#137
This commit is contained in:
@@ -71,6 +71,7 @@ import {
|
|||||||
} from "src/helpers/user-access-resolver";
|
} from "src/helpers/user-access-resolver";
|
||||||
import {
|
import {
|
||||||
blameDamagedPartyMatchesUser,
|
blameDamagedPartyMatchesUser,
|
||||||
|
resolveClaimOwnerFieldsFromBlame,
|
||||||
resolveClaimOwnerParty,
|
resolveClaimOwnerParty,
|
||||||
resolveDamagedPartyUserId,
|
resolveDamagedPartyUserId,
|
||||||
} from "src/helpers/blame-damaged-party";
|
} from "src/helpers/blame-damaged-party";
|
||||||
@@ -4672,7 +4673,7 @@ export class ClaimRequestManagementService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* V2: Field expert creates a claim from an expert-initiated IN_PERSON completed blame.
|
* V2: Field expert creates a claim from an expert-initiated IN_PERSON completed blame.
|
||||||
* The claim owner is set to the damaged party (first for CAR_BODY, non-guilty for THIRD_PARTY).
|
* Owner: damaged party `userId` (SMS / user actions), guilty party `clientId` (insurer queue).
|
||||||
*/
|
*/
|
||||||
async createClaimFromBlameForExpertV2(
|
async createClaimFromBlameForExpertV2(
|
||||||
blameRequestId: string,
|
blameRequestId: string,
|
||||||
@@ -4702,29 +4703,11 @@ export class ClaimRequestManagementService {
|
|||||||
"Only the field expert who created this blame file can create the claim.",
|
"Only the field expert who created this blame file can create the claim.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const parties = blameRequest.parties || [];
|
const ownerFields = resolveClaimOwnerFieldsFromBlame(blameRequest);
|
||||||
const isCarBody = blameRequest.type === BlameRequestType.CAR_BODY;
|
if (!ownerFields) {
|
||||||
let damagedUserId: string;
|
throw new BadRequestException(
|
||||||
if (isCarBody) {
|
"Could not resolve claim owner (damaged party) from blame",
|
||||||
if (parties.length < 1)
|
|
||||||
throw new BadRequestException("Blame request has no party");
|
|
||||||
const first = parties.find((p) => p.role === "FIRST");
|
|
||||||
if (!first?.person?.userId)
|
|
||||||
throw new BadRequestException("First party has no userId");
|
|
||||||
damagedUserId = String(first.person.userId);
|
|
||||||
} else {
|
|
||||||
const guiltyPartyId = blameRequest.expert?.decision?.guiltyPartyId
|
|
||||||
? String(blameRequest.expert.decision.guiltyPartyId)
|
|
||||||
: null;
|
|
||||||
if (!guiltyPartyId)
|
|
||||||
throw new BadRequestException("Blame request has no guilty party set");
|
|
||||||
const damagedParty = parties.find(
|
|
||||||
(p) => p.person?.userId && String(p.person.userId) !== guiltyPartyId,
|
|
||||||
);
|
);
|
||||||
if (!damagedParty?.person?.userId) {
|
|
||||||
throw new BadRequestException("Could not determine damaged party");
|
|
||||||
}
|
|
||||||
damagedUserId = String(damagedParty.person.userId);
|
|
||||||
}
|
}
|
||||||
const existingClaim = await this.claimCaseDbService.findOne({
|
const existingClaim = await this.claimCaseDbService.findOne({
|
||||||
blameRequestId: new Types.ObjectId(blameRequestId),
|
blameRequestId: new Types.ObjectId(blameRequestId),
|
||||||
@@ -4733,12 +4716,6 @@ export class ClaimRequestManagementService {
|
|||||||
throw new ConflictException("A claim for this blame case already exists");
|
throw new ConflictException("A claim for this blame case already exists");
|
||||||
}
|
}
|
||||||
const claimNo = await this.generateUniqueClaimNumber();
|
const claimNo = await this.generateUniqueClaimNumber();
|
||||||
const ownerParty = resolveClaimOwnerParty(blameRequest);
|
|
||||||
if (!ownerParty?.person?.userId) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
"Could not resolve claim owner (guilty party) from blame",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const newClaim = await this.claimCaseDbService.create({
|
const newClaim = await this.claimCaseDbService.create({
|
||||||
requestNo: claimNo,
|
requestNo: claimNo,
|
||||||
publicId: blameRequest.publicId,
|
publicId: blameRequest.publicId,
|
||||||
@@ -4754,15 +4731,14 @@ export class ClaimRequestManagementService {
|
|||||||
completedSteps: [ClaimWorkflowStep.CLAIM_CREATED],
|
completedSteps: [ClaimWorkflowStep.CLAIM_CREATED],
|
||||||
locked: false,
|
locked: false,
|
||||||
},
|
},
|
||||||
damagedPartyUserId: new Types.ObjectId(damagedUserId),
|
damagedPartyUserId: new Types.ObjectId(ownerFields.userId),
|
||||||
owner: {
|
owner: {
|
||||||
userId: new Types.ObjectId(String(ownerParty.person.userId)),
|
userId: new Types.ObjectId(ownerFields.userId),
|
||||||
userRole: ownerParty.role as any,
|
userRole: ownerFields.userRole as any,
|
||||||
...(ownerParty.person.clientId
|
...(ownerFields.clientId
|
||||||
? {
|
? {
|
||||||
clientId: new Types.ObjectId(
|
clientId: new Types.ObjectId(ownerFields.clientId),
|
||||||
String(ownerParty.person.clientId),
|
userClientKey: ownerFields.clientId,
|
||||||
),
|
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
@@ -5020,26 +4996,11 @@ export class ClaimRequestManagementService {
|
|||||||
"Only the registrar who created this blame file can create the claim.",
|
"Only the registrar who created this blame file can create the claim.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const parties = blameRequest.parties || [];
|
const ownerFields = resolveClaimOwnerFieldsFromBlame(blameRequest);
|
||||||
const isCarBody = blameRequest.type === BlameRequestType.CAR_BODY;
|
if (!ownerFields) {
|
||||||
let damagedUserId: string;
|
throw new BadRequestException(
|
||||||
if (isCarBody) {
|
"Could not resolve claim owner (damaged party) from blame",
|
||||||
const first = parties.find((p) => p.role === "FIRST");
|
|
||||||
if (!first?.person?.userId)
|
|
||||||
throw new BadRequestException("First party has no userId");
|
|
||||||
damagedUserId = String(first.person.userId);
|
|
||||||
} else {
|
|
||||||
const guiltyPartyId = blameRequest.expert?.decision?.guiltyPartyId
|
|
||||||
? String(blameRequest.expert.decision.guiltyPartyId)
|
|
||||||
: null;
|
|
||||||
if (!guiltyPartyId)
|
|
||||||
throw new BadRequestException("Blame request has no guilty party set");
|
|
||||||
const damagedParty = parties.find(
|
|
||||||
(p) => p.person?.userId && String(p.person.userId) !== guiltyPartyId,
|
|
||||||
);
|
);
|
||||||
if (!damagedParty?.person?.userId)
|
|
||||||
throw new BadRequestException("Could not determine damaged party");
|
|
||||||
damagedUserId = String(damagedParty.person.userId);
|
|
||||||
}
|
}
|
||||||
const existingClaim = await this.claimCaseDbService.findOne({
|
const existingClaim = await this.claimCaseDbService.findOne({
|
||||||
blameRequestId: new Types.ObjectId(blameRequestId),
|
blameRequestId: new Types.ObjectId(blameRequestId),
|
||||||
@@ -5047,12 +5008,6 @@ export class ClaimRequestManagementService {
|
|||||||
if (existingClaim)
|
if (existingClaim)
|
||||||
throw new ConflictException("A claim for this blame case already exists");
|
throw new ConflictException("A claim for this blame case already exists");
|
||||||
const claimNo = await this.generateUniqueClaimNumber();
|
const claimNo = await this.generateUniqueClaimNumber();
|
||||||
const ownerParty = resolveClaimOwnerParty(blameRequest);
|
|
||||||
if (!ownerParty?.person?.userId) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
"Could not resolve claim owner (guilty party) from blame",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const newClaim = await this.claimCaseDbService.create({
|
const newClaim = await this.claimCaseDbService.create({
|
||||||
requestNo: claimNo,
|
requestNo: claimNo,
|
||||||
publicId: blameRequest.publicId,
|
publicId: blameRequest.publicId,
|
||||||
@@ -5067,15 +5022,14 @@ export class ClaimRequestManagementService {
|
|||||||
completedSteps: [ClaimWorkflowStep.CLAIM_CREATED],
|
completedSteps: [ClaimWorkflowStep.CLAIM_CREATED],
|
||||||
locked: false,
|
locked: false,
|
||||||
},
|
},
|
||||||
damagedPartyUserId: new Types.ObjectId(damagedUserId),
|
damagedPartyUserId: new Types.ObjectId(ownerFields.userId),
|
||||||
owner: {
|
owner: {
|
||||||
userId: new Types.ObjectId(String(ownerParty.person.userId)),
|
userId: new Types.ObjectId(ownerFields.userId),
|
||||||
userRole: ownerParty.role as any,
|
userRole: ownerFields.userRole as any,
|
||||||
...(ownerParty.person.clientId
|
...(ownerFields.clientId
|
||||||
? {
|
? {
|
||||||
clientId: new Types.ObjectId(
|
clientId: new Types.ObjectId(ownerFields.clientId),
|
||||||
String(ownerParty.person.clientId),
|
userClientKey: ownerFields.clientId,
|
||||||
),
|
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import { Body, Controller, Post } from "@nestjs/common";
|
|
||||||
import { Public } from "./decorators";
|
|
||||||
import { UserLoginDto, UserRegisterDto } from "./dtos";
|
|
||||||
import { AuthService } from "./auth.service";
|
|
||||||
|
|
||||||
@Controller("auth")
|
|
||||||
export class AuthController {
|
|
||||||
constructor(private readonly authService: AuthService) {}
|
|
||||||
|
|
||||||
@Public()
|
|
||||||
@Post("user/register")
|
|
||||||
async register(@Body() userRegisterDto: UserRegisterDto) {
|
|
||||||
return await this.authService.registerUser(userRegisterDto);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Public()
|
|
||||||
@Post("user/login")
|
|
||||||
async login(@Body() userLoginDto: UserLoginDto) {
|
|
||||||
return await this.authService.loginUser(userLoginDto);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Module } from "@nestjs/common";
|
|
||||||
import { ConfigService } from "@nestjs/config";
|
|
||||||
import { APP_GUARD } from "@nestjs/core";
|
|
||||||
import { JwtModule } from "@nestjs/jwt";
|
|
||||||
import { StringValue } from "ms";
|
|
||||||
import { AuthGuard, RolesGuard } from "./guards";
|
|
||||||
import { AuthController } from "./auth.controller";
|
|
||||||
import { AuthService } from "./auth.service";
|
|
||||||
import { HashService } from "./providers";
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
JwtModule.registerAsync({
|
|
||||||
global: true,
|
|
||||||
inject: [ConfigService],
|
|
||||||
useFactory: (configService: ConfigService) => ({
|
|
||||||
secret: configService.get<string>("JWT_SECRET"),
|
|
||||||
signOptions: {
|
|
||||||
expiresIn: configService.get<StringValue>("JWT_EXPIRY"),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
controllers: [AuthController],
|
|
||||||
providers: [
|
|
||||||
{ provide: APP_GUARD, useClass: AuthGuard },
|
|
||||||
{ provide: APP_GUARD, useClass: RolesGuard },
|
|
||||||
AuthService,
|
|
||||||
HashService,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class AuthModule {}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
|
||||||
import { UserRepository } from "src/features/user/repositories";
|
|
||||||
import { UserLoginDto, UserRegisterDto } from "./dtos";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AuthService {
|
|
||||||
constructor(private readonly userRepo: UserRepository) {}
|
|
||||||
|
|
||||||
async registerUser(userRegisterDto: UserRegisterDto) {
|
|
||||||
const user = await this.userRepo.retrieveByCellPhoneNumber(
|
|
||||||
userRegisterDto.cellphoneNumber,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
// Create user + Send OTP and update database
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send OTP to the cellphone number of user and update database
|
|
||||||
// TODO: Create a mock otp for once we got no otp senders or development phase
|
|
||||||
}
|
|
||||||
|
|
||||||
async loginUser(userLoginDto: UserLoginDto) {
|
|
||||||
const user = await this.userRepo.retrieveByCellPhoneNumber(
|
|
||||||
userLoginDto.cellphoneNumber,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!user) throw new BadRequestException("User does not exist");
|
|
||||||
|
|
||||||
// Generate token (access + refresh) and save inside the database, return them to the user
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,18 @@
|
|||||||
import { ScryptOptions } from "node:crypto";
|
import { ScryptOptions } from "node:crypto";
|
||||||
|
|
||||||
|
export const CURRENT_ALGORITHM = "scrypt";
|
||||||
|
export const CURRENT_VERSION = 1; // 0 = legacy salt:hash ; 1 = scrypt with different salt and hash and differnet format of salt:hash!
|
||||||
|
|
||||||
export const KEY_LENGTH = 64;
|
export const KEY_LENGTH = 64;
|
||||||
export const SCRYPT_PARAMS: ScryptOptions = {
|
export const SCRYPT_PARAMS: ScryptOptions = {
|
||||||
N: 19456, // CPU/memory cost
|
N: 19456, // CPU/memory cost
|
||||||
r: 8, // block size
|
r: 8, // block size
|
||||||
p: 1, // parallelization
|
p: 1, // parallelization
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const KEY_LENGTH_FOR_OTP = 32;
|
||||||
|
export const SCRYPT_PARAMS_FOR_OTP: ScryptOptions = {
|
||||||
|
N: 1024,
|
||||||
|
r: 8,
|
||||||
|
p: 1,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
export { UserRegisterDto } from "./user-register.dto";
|
|
||||||
export { UserLoginDto } from "./user-login.dto";
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { IsMobilePhone, IsNumberString, IsString } from "class-validator";
|
|
||||||
|
|
||||||
export class UserLoginDto {
|
|
||||||
@IsString({ message: "Cellphone number must be string" })
|
|
||||||
@IsMobilePhone("fa-IR")
|
|
||||||
cellphoneNumber: string;
|
|
||||||
|
|
||||||
@IsNumberString()
|
|
||||||
otp: string;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { IsMobilePhone, IsNumberString, IsString } from "class-validator";
|
|
||||||
|
|
||||||
export class UserRegisterDto {
|
|
||||||
@IsString({ message: "Cellphone number must be string" })
|
|
||||||
@IsMobilePhone("fa-IR")
|
|
||||||
cellphoneNumber: string;
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import {
|
|
||||||
BinaryLike,
|
|
||||||
randomBytes,
|
|
||||||
scrypt,
|
|
||||||
ScryptOptions,
|
|
||||||
timingSafeEqual,
|
|
||||||
} from "node:crypto";
|
|
||||||
import { promisify } from "node:util";
|
|
||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
import { ConfigService } from "@nestjs/config";
|
|
||||||
import { KEY_LENGTH, SCRYPT_PARAMS } from "../constants/hash.constants";
|
|
||||||
|
|
||||||
const scryptAsync = promisify<
|
|
||||||
BinaryLike,
|
|
||||||
BinaryLike,
|
|
||||||
number,
|
|
||||||
ScryptOptions,
|
|
||||||
Buffer
|
|
||||||
>(scrypt);
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class HashService {
|
|
||||||
private readonly pepper: string;
|
|
||||||
|
|
||||||
constructor(private readonly configService: ConfigService) {
|
|
||||||
this.pepper = this.configService.get<string>("HASH_PEPPER");
|
|
||||||
}
|
|
||||||
|
|
||||||
private applyPepper(input: string): string {
|
|
||||||
return this.pepper ? `${input}${this.pepper}` : input;
|
|
||||||
}
|
|
||||||
|
|
||||||
private generateSalt(bytes: number = 16): string {
|
|
||||||
return randomBytes(bytes).toString("hex");
|
|
||||||
}
|
|
||||||
|
|
||||||
private async scrypt(password: string): Promise<[string, string]> {
|
|
||||||
const salt = this.generateSalt();
|
|
||||||
const pepperedInput = this.applyPepper(password);
|
|
||||||
const hashedPassword = await scryptAsync(
|
|
||||||
pepperedInput,
|
|
||||||
salt,
|
|
||||||
KEY_LENGTH,
|
|
||||||
SCRYPT_PARAMS,
|
|
||||||
);
|
|
||||||
|
|
||||||
return [salt, hashedPassword.toString("hex")];
|
|
||||||
}
|
|
||||||
|
|
||||||
private async verifyScrypt(
|
|
||||||
password: string,
|
|
||||||
salt: string,
|
|
||||||
hashedPassword: string,
|
|
||||||
): Promise<boolean> {
|
|
||||||
const pepperedInput = this.applyPepper(password);
|
|
||||||
const derived = await scryptAsync(
|
|
||||||
pepperedInput,
|
|
||||||
salt,
|
|
||||||
KEY_LENGTH,
|
|
||||||
SCRYPT_PARAMS,
|
|
||||||
);
|
|
||||||
const storedBuffer = Buffer.from(hashedPassword, "hex");
|
|
||||||
|
|
||||||
if (derived.length !== storedBuffer.length) return false;
|
|
||||||
|
|
||||||
return timingSafeEqual(derived, storedBuffer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { HashService } from "./hash.provider";
|
|
||||||
@@ -1 +1,2 @@
|
|||||||
export { JwtPayload } from "./payload.types";
|
export { JwtPayload } from "./payload.types";
|
||||||
|
export { PasswordHash } from "./password.types";
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
export interface JwtPayload {
|
export interface JwtPayload {
|
||||||
sub: string;
|
sub: string;
|
||||||
role: string;
|
role: string;
|
||||||
clientId?: string;
|
|
||||||
iat?: number;
|
iat?: number;
|
||||||
exp?: number;
|
exp?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import {
|
|||||||
IsBooleanString,
|
IsBooleanString,
|
||||||
IsEnum,
|
IsEnum,
|
||||||
IsNumber,
|
IsNumber,
|
||||||
|
IsNumberString,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsPositive,
|
IsPositive,
|
||||||
IsString,
|
IsString,
|
||||||
IsUrl,
|
IsUrl,
|
||||||
|
Length,
|
||||||
Matches,
|
Matches,
|
||||||
Max,
|
Max,
|
||||||
Min,
|
Min,
|
||||||
@@ -80,9 +82,11 @@ export class EnvironmentVariables {
|
|||||||
JWT_EXPIRY: StringValue;
|
JWT_EXPIRY: StringValue;
|
||||||
|
|
||||||
// --------------------------------------------------------- //
|
// --------------------------------------------------------- //
|
||||||
|
|
||||||
@IsString({ message: "HASH_PEPPER must be string" })
|
@IsString({ message: "HASH_PEPPER must be string" })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
HASH_PEPPER?: string;
|
HASH_PEPPER?: string;
|
||||||
|
|
||||||
// --------------------------------------------------------- //
|
// --------------------------------------------------------- //
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|||||||
@@ -440,12 +440,13 @@ export class ExpertClaimService {
|
|||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Owner mobile: linked blame party phone when available, else `users.mobile`. */
|
/** Damaged-party mobile for claim SMS: prefer `damagedPartyUserId`, else `owner.userId`. */
|
||||||
private async resolveClaimOwnerPhone(
|
private async resolveClaimOwnerPhone(
|
||||||
claim: any,
|
claim: any,
|
||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
if (!claim?.owner?.userId) return undefined;
|
const notifyUserId = claim?.damagedPartyUserId ?? claim?.owner?.userId;
|
||||||
const ownerUserId = String(claim.owner.userId);
|
if (!notifyUserId) return undefined;
|
||||||
|
const ownerUserId = String(notifyUserId);
|
||||||
if (claim.blameRequestId) {
|
if (claim.blameRequestId) {
|
||||||
const blame = await this.blameRequestDbService.findById(
|
const blame = await this.blameRequestDbService.findById(
|
||||||
claim.blameRequestId.toString(),
|
claim.blameRequestId.toString(),
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
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 {}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
import { InjectModel } from "@nestjs/mongoose";
|
import { InjectModel } from "@nestjs/mongoose";
|
||||||
import { User } from "../schemas";
|
|
||||||
import { Model } from "mongoose";
|
import { Model } from "mongoose";
|
||||||
|
import { Otp, User } from "../schemas";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserRepository {
|
export class UserRepository {
|
||||||
@@ -10,7 +10,9 @@ export class UserRepository {
|
|||||||
private readonly model: Model<User>,
|
private readonly model: Model<User>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async addUser() {}
|
async create(cellphoneNumber: string): Promise<User> {
|
||||||
|
return this.model.create({ cellphoneNumber });
|
||||||
|
}
|
||||||
|
|
||||||
async retrieveByCellPhoneNumber(
|
async retrieveByCellPhoneNumber(
|
||||||
cellphoneNumber: string,
|
cellphoneNumber: string,
|
||||||
@@ -19,4 +21,19 @@ export class UserRepository {
|
|||||||
cellphoneNumber,
|
cellphoneNumber,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async saveOtp(cellphoneNumber: string, otp: Otp): Promise<void> {
|
||||||
|
await this.model.updateOne({ cellphoneNumber }, { $set: { otp } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearOtp(cellphoneNumber: string): Promise<void> {
|
||||||
|
await this.model.updateOne({ cellphoneNumber }, { $unset: { otp: "" } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async incrementOtpAttempts(cellphoneNumber: string): Promise<void> {
|
||||||
|
await this.model.updateOne(
|
||||||
|
{ cellphoneNumber },
|
||||||
|
{ $inc: { "otp.attempts": 1 } },
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,17 +8,14 @@ export class Otp {
|
|||||||
@Prop()
|
@Prop()
|
||||||
hash: string;
|
hash: string;
|
||||||
|
|
||||||
@Prop()
|
|
||||||
salt: string;
|
|
||||||
|
|
||||||
@Prop()
|
@Prop()
|
||||||
expiresAt: Date;
|
expiresAt: Date;
|
||||||
|
|
||||||
@Prop({ default: 0 })
|
@Prop({ default: 0 })
|
||||||
attempts: number;
|
attempts?: number;
|
||||||
|
|
||||||
@Prop({ default: Date.now })
|
@Prop({ default: Date.now })
|
||||||
generatedAt: Date;
|
generatedAt?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OtpSchema = SchemaFactory.createForClass(Otp);
|
export const OtpSchema = SchemaFactory.createForClass(Otp);
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
import { MongooseModule } from "@nestjs/mongoose";
|
||||||
import { UserRepository } from "./repositories";
|
import { UserRepository } from "./repositories";
|
||||||
|
import { User, UserSchema } from "./schemas";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [
|
||||||
|
MongooseModule.forFeature([
|
||||||
|
{
|
||||||
|
name: User.name,
|
||||||
|
schema: UserSchema,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [UserRepository],
|
providers: [UserRepository],
|
||||||
exports: [UserRepository],
|
exports: [UserRepository],
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Types } from "mongoose";
|
|||||||
import { partyPersonMatchesUser } from "./iran-mobile";
|
import { partyPersonMatchesUser } from "./iran-mobile";
|
||||||
import {
|
import {
|
||||||
blameDamagedPartyMatchesUser,
|
blameDamagedPartyMatchesUser,
|
||||||
|
resolveClaimOwnerFieldsFromBlame,
|
||||||
resolveDamagedPartyPerson,
|
resolveDamagedPartyPerson,
|
||||||
resolveGuiltyPartyUserId,
|
resolveGuiltyPartyUserId,
|
||||||
} from "./blame-damaged-party";
|
} from "./blame-damaged-party";
|
||||||
@@ -46,13 +47,30 @@ describe("blame damaged party helpers", () => {
|
|||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("matches damaged party by phone when userId differs (duplicate accounts)", () => {
|
it("resolveClaimOwnerFieldsFromBlame uses damaged userId and guilty clientId", () => {
|
||||||
expect(
|
const guiltyClientId = new Types.ObjectId();
|
||||||
partyPersonMatchesUser(
|
const blame = {
|
||||||
{ userId: damagedId, phoneNumber: "9123456789" },
|
type: "THIRD_PARTY",
|
||||||
{ sub: "other-user-id", username: "09123456789" },
|
expert: { decision: { guiltyPartyId: guiltyId } },
|
||||||
[],
|
parties: [
|
||||||
),
|
{
|
||||||
).toBe(true);
|
role: "FIRST",
|
||||||
|
person: {
|
||||||
|
userId: guiltyId,
|
||||||
|
phoneNumber: "09111111111",
|
||||||
|
clientId: guiltyClientId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "SECOND",
|
||||||
|
person: { userId: damagedId, phoneNumber: "09912356917" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
expect(resolveClaimOwnerFieldsFromBlame(blame)).toEqual({
|
||||||
|
userId: String(damagedId),
|
||||||
|
userRole: "SECOND",
|
||||||
|
clientId: String(guiltyClientId),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -119,3 +119,45 @@ export function resolveDamagedPartyUserId(
|
|||||||
const person = resolveDamagedPartyPerson(blame);
|
const person = resolveDamagedPartyPerson(blame);
|
||||||
return person?.userId != null ? String(person.userId) : null;
|
return person?.userId != null ? String(person.userId) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Guilty party row on the blame case (payer's insurer is on `person.clientId`). */
|
||||||
|
export function resolveGuiltyPartyPerson(
|
||||||
|
blame: Parameters<typeof resolveClaimOwnerParty>[0],
|
||||||
|
): { userId?: unknown; phoneNumber?: string; clientId?: unknown } | null {
|
||||||
|
const party = resolveClaimOwnerParty(blame);
|
||||||
|
return party?.person ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claim `owner` fields for initiator-filled IN_PERSON claims (field expert / registrar).
|
||||||
|
* - `userId` = damaged party (SMS, user sign-off, claim beneficiary)
|
||||||
|
* - `clientId` = guilty party's insurer (damage-expert tenant queue)
|
||||||
|
*/
|
||||||
|
export function resolveClaimOwnerFieldsFromBlame(
|
||||||
|
blame: Parameters<typeof resolveDamagedPartyPerson>[0],
|
||||||
|
): {
|
||||||
|
userId: string;
|
||||||
|
userRole?: string;
|
||||||
|
clientId?: string;
|
||||||
|
} | null {
|
||||||
|
const damagedPerson = resolveDamagedPartyPerson(blame);
|
||||||
|
if (damagedPerson?.userId == null) return null;
|
||||||
|
|
||||||
|
const parties = blame?.parties ?? [];
|
||||||
|
const damagedUserId = String(damagedPerson.userId);
|
||||||
|
const damagedPartyRow = parties.find(
|
||||||
|
(p) =>
|
||||||
|
p.person?.userId != null &&
|
||||||
|
String(p.person.userId) === damagedUserId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const guiltyPerson = resolveGuiltyPartyPerson(blame);
|
||||||
|
const payingClientId =
|
||||||
|
guiltyPerson?.clientId != null ? String(guiltyPerson.clientId) : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: damagedUserId,
|
||||||
|
userRole: damagedPartyRow?.role,
|
||||||
|
...(payingClientId ? { clientId: payingClientId } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -5633,6 +5633,7 @@ export class RequestManagementService {
|
|||||||
userId: firstPartyUserId,
|
userId: firstPartyUserId,
|
||||||
phoneNumber: normalizeIranMobile(formData.firstPartyPhoneNumber) ??
|
phoneNumber: normalizeIranMobile(formData.firstPartyPhoneNumber) ??
|
||||||
formData.firstPartyPhoneNumber,
|
formData.firstPartyPhoneNumber,
|
||||||
|
clientId: (client as any)?._id ?? (client as any)?._doc?._id,
|
||||||
nationalCodeOfInsurer: firstPartyPlate.nationalCodeOfInsurer,
|
nationalCodeOfInsurer: firstPartyPlate.nationalCodeOfInsurer,
|
||||||
nationalCodeOfDriver: firstPartyPlate.nationalCodeOfDriver,
|
nationalCodeOfDriver: firstPartyPlate.nationalCodeOfDriver,
|
||||||
insurerLicense: firstPartyPlate.insurerLicense,
|
insurerLicense: firstPartyPlate.insurerLicense,
|
||||||
@@ -5813,11 +5814,14 @@ export class RequestManagementService {
|
|||||||
`Client not found for company: ${clientName}`,
|
`Client not found for company: ${clientName}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const resolvedClientId =
|
||||||
|
(client as any)?._id ?? (client as any)?._doc?._id;
|
||||||
return {
|
return {
|
||||||
role,
|
role,
|
||||||
person: {
|
person: {
|
||||||
userId,
|
userId,
|
||||||
phoneNumber: normalizeIranMobile(phoneNumber) ?? phoneNumber,
|
phoneNumber: normalizeIranMobile(phoneNumber) ?? phoneNumber,
|
||||||
|
...(resolvedClientId ? { clientId: resolvedClientId } : {}),
|
||||||
nationalCodeOfInsurer: plateDto.nationalCodeOfInsurer,
|
nationalCodeOfInsurer: plateDto.nationalCodeOfInsurer,
|
||||||
nationalCodeOfDriver: plateDto.nationalCodeOfDriver,
|
nationalCodeOfDriver: plateDto.nationalCodeOfDriver,
|
||||||
insurerLicense: plateDto.insurerLicense,
|
insurerLicense: plateDto.insurerLicense,
|
||||||
|
|||||||
Reference in New Issue
Block a user