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";
|
||||
import {
|
||||
blameDamagedPartyMatchesUser,
|
||||
resolveClaimOwnerFieldsFromBlame,
|
||||
resolveClaimOwnerParty,
|
||||
resolveDamagedPartyUserId,
|
||||
} 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.
|
||||
* 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(
|
||||
blameRequestId: string,
|
||||
@@ -4702,29 +4703,11 @@ export class ClaimRequestManagementService {
|
||||
"Only the field expert who created this blame file can create the claim.",
|
||||
);
|
||||
}
|
||||
const parties = blameRequest.parties || [];
|
||||
const isCarBody = blameRequest.type === BlameRequestType.CAR_BODY;
|
||||
let damagedUserId: string;
|
||||
if (isCarBody) {
|
||||
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,
|
||||
const ownerFields = resolveClaimOwnerFieldsFromBlame(blameRequest);
|
||||
if (!ownerFields) {
|
||||
throw new BadRequestException(
|
||||
"Could not resolve claim owner (damaged party) from blame",
|
||||
);
|
||||
if (!damagedParty?.person?.userId) {
|
||||
throw new BadRequestException("Could not determine damaged party");
|
||||
}
|
||||
damagedUserId = String(damagedParty.person.userId);
|
||||
}
|
||||
const existingClaim = await this.claimCaseDbService.findOne({
|
||||
blameRequestId: new Types.ObjectId(blameRequestId),
|
||||
@@ -4733,12 +4716,6 @@ export class ClaimRequestManagementService {
|
||||
throw new ConflictException("A claim for this blame case already exists");
|
||||
}
|
||||
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({
|
||||
requestNo: claimNo,
|
||||
publicId: blameRequest.publicId,
|
||||
@@ -4754,15 +4731,14 @@ export class ClaimRequestManagementService {
|
||||
completedSteps: [ClaimWorkflowStep.CLAIM_CREATED],
|
||||
locked: false,
|
||||
},
|
||||
damagedPartyUserId: new Types.ObjectId(damagedUserId),
|
||||
damagedPartyUserId: new Types.ObjectId(ownerFields.userId),
|
||||
owner: {
|
||||
userId: new Types.ObjectId(String(ownerParty.person.userId)),
|
||||
userRole: ownerParty.role as any,
|
||||
...(ownerParty.person.clientId
|
||||
userId: new Types.ObjectId(ownerFields.userId),
|
||||
userRole: ownerFields.userRole as any,
|
||||
...(ownerFields.clientId
|
||||
? {
|
||||
clientId: new Types.ObjectId(
|
||||
String(ownerParty.person.clientId),
|
||||
),
|
||||
clientId: new Types.ObjectId(ownerFields.clientId),
|
||||
userClientKey: ownerFields.clientId,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
@@ -5020,26 +4996,11 @@ export class ClaimRequestManagementService {
|
||||
"Only the registrar who created this blame file can create the claim.",
|
||||
);
|
||||
}
|
||||
const parties = blameRequest.parties || [];
|
||||
const isCarBody = blameRequest.type === BlameRequestType.CAR_BODY;
|
||||
let damagedUserId: string;
|
||||
if (isCarBody) {
|
||||
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,
|
||||
const ownerFields = resolveClaimOwnerFieldsFromBlame(blameRequest);
|
||||
if (!ownerFields) {
|
||||
throw new BadRequestException(
|
||||
"Could not resolve claim owner (damaged party) from blame",
|
||||
);
|
||||
if (!damagedParty?.person?.userId)
|
||||
throw new BadRequestException("Could not determine damaged party");
|
||||
damagedUserId = String(damagedParty.person.userId);
|
||||
}
|
||||
const existingClaim = await this.claimCaseDbService.findOne({
|
||||
blameRequestId: new Types.ObjectId(blameRequestId),
|
||||
@@ -5047,12 +5008,6 @@ export class ClaimRequestManagementService {
|
||||
if (existingClaim)
|
||||
throw new ConflictException("A claim for this blame case already exists");
|
||||
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({
|
||||
requestNo: claimNo,
|
||||
publicId: blameRequest.publicId,
|
||||
@@ -5067,15 +5022,14 @@ export class ClaimRequestManagementService {
|
||||
completedSteps: [ClaimWorkflowStep.CLAIM_CREATED],
|
||||
locked: false,
|
||||
},
|
||||
damagedPartyUserId: new Types.ObjectId(damagedUserId),
|
||||
damagedPartyUserId: new Types.ObjectId(ownerFields.userId),
|
||||
owner: {
|
||||
userId: new Types.ObjectId(String(ownerParty.person.userId)),
|
||||
userRole: ownerParty.role as any,
|
||||
...(ownerParty.person.clientId
|
||||
userId: new Types.ObjectId(ownerFields.userId),
|
||||
userRole: ownerFields.userRole as any,
|
||||
...(ownerFields.clientId
|
||||
? {
|
||||
clientId: new Types.ObjectId(
|
||||
String(ownerParty.person.clientId),
|
||||
),
|
||||
clientId: new Types.ObjectId(ownerFields.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";
|
||||
|
||||
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 SCRYPT_PARAMS: ScryptOptions = {
|
||||
N: 19456, // CPU/memory cost
|
||||
r: 8, // block size
|
||||
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 { PasswordHash } from "./password.types";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export interface JwtPayload {
|
||||
sub: string;
|
||||
role: string;
|
||||
clientId?: string;
|
||||
iat?: number;
|
||||
exp?: number;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ import {
|
||||
IsBooleanString,
|
||||
IsEnum,
|
||||
IsNumber,
|
||||
IsNumberString,
|
||||
IsOptional,
|
||||
IsPositive,
|
||||
IsString,
|
||||
IsUrl,
|
||||
Length,
|
||||
Matches,
|
||||
Max,
|
||||
Min,
|
||||
@@ -80,9 +82,11 @@ export class EnvironmentVariables {
|
||||
JWT_EXPIRY: StringValue;
|
||||
|
||||
// --------------------------------------------------------- //
|
||||
|
||||
@IsString({ message: "HASH_PEPPER must be string" })
|
||||
@IsOptional()
|
||||
HASH_PEPPER?: string;
|
||||
|
||||
// --------------------------------------------------------- //
|
||||
|
||||
@IsOptional()
|
||||
|
||||
@@ -440,12 +440,13 @@ export class ExpertClaimService {
|
||||
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(
|
||||
claim: any,
|
||||
): Promise<string | undefined> {
|
||||
if (!claim?.owner?.userId) return undefined;
|
||||
const ownerUserId = String(claim.owner.userId);
|
||||
const notifyUserId = claim?.damagedPartyUserId ?? claim?.owner?.userId;
|
||||
if (!notifyUserId) return undefined;
|
||||
const ownerUserId = String(notifyUserId);
|
||||
if (claim.blameRequestId) {
|
||||
const blame = await this.blameRequestDbService.findById(
|
||||
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 { InjectModel } from "@nestjs/mongoose";
|
||||
import { User } from "../schemas";
|
||||
import { Model } from "mongoose";
|
||||
import { Otp, User } from "../schemas";
|
||||
|
||||
@Injectable()
|
||||
export class UserRepository {
|
||||
@@ -10,7 +10,9 @@ export class UserRepository {
|
||||
private readonly model: Model<User>,
|
||||
) {}
|
||||
|
||||
async addUser() {}
|
||||
async create(cellphoneNumber: string): Promise<User> {
|
||||
return this.model.create({ cellphoneNumber });
|
||||
}
|
||||
|
||||
async retrieveByCellPhoneNumber(
|
||||
cellphoneNumber: string,
|
||||
@@ -19,4 +21,19 @@ export class UserRepository {
|
||||
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()
|
||||
hash: string;
|
||||
|
||||
@Prop()
|
||||
salt: string;
|
||||
|
||||
@Prop()
|
||||
expiresAt: Date;
|
||||
|
||||
@Prop({ default: 0 })
|
||||
attempts: number;
|
||||
attempts?: number;
|
||||
|
||||
@Prop({ default: Date.now })
|
||||
generatedAt: Date;
|
||||
generatedAt?: Date;
|
||||
}
|
||||
|
||||
export const OtpSchema = SchemaFactory.createForClass(Otp);
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { MongooseModule } from "@nestjs/mongoose";
|
||||
import { UserRepository } from "./repositories";
|
||||
import { User, UserSchema } from "./schemas";
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
imports: [
|
||||
MongooseModule.forFeature([
|
||||
{
|
||||
name: User.name,
|
||||
schema: UserSchema,
|
||||
},
|
||||
]),
|
||||
],
|
||||
controllers: [],
|
||||
providers: [UserRepository],
|
||||
exports: [UserRepository],
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Types } from "mongoose";
|
||||
import { partyPersonMatchesUser } from "./iran-mobile";
|
||||
import {
|
||||
blameDamagedPartyMatchesUser,
|
||||
resolveClaimOwnerFieldsFromBlame,
|
||||
resolveDamagedPartyPerson,
|
||||
resolveGuiltyPartyUserId,
|
||||
} from "./blame-damaged-party";
|
||||
@@ -46,13 +47,30 @@ describe("blame damaged party helpers", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("matches damaged party by phone when userId differs (duplicate accounts)", () => {
|
||||
expect(
|
||||
partyPersonMatchesUser(
|
||||
{ userId: damagedId, phoneNumber: "9123456789" },
|
||||
{ sub: "other-user-id", username: "09123456789" },
|
||||
[],
|
||||
),
|
||||
).toBe(true);
|
||||
it("resolveClaimOwnerFieldsFromBlame uses damaged userId and guilty clientId", () => {
|
||||
const guiltyClientId = new Types.ObjectId();
|
||||
const blame = {
|
||||
type: "THIRD_PARTY",
|
||||
expert: { decision: { guiltyPartyId: guiltyId } },
|
||||
parties: [
|
||||
{
|
||||
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);
|
||||
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,
|
||||
phoneNumber: normalizeIranMobile(formData.firstPartyPhoneNumber) ??
|
||||
formData.firstPartyPhoneNumber,
|
||||
clientId: (client as any)?._id ?? (client as any)?._doc?._id,
|
||||
nationalCodeOfInsurer: firstPartyPlate.nationalCodeOfInsurer,
|
||||
nationalCodeOfDriver: firstPartyPlate.nationalCodeOfDriver,
|
||||
insurerLicense: firstPartyPlate.insurerLicense,
|
||||
@@ -5813,11 +5814,14 @@ export class RequestManagementService {
|
||||
`Client not found for company: ${clientName}`,
|
||||
);
|
||||
}
|
||||
const resolvedClientId =
|
||||
(client as any)?._id ?? (client as any)?._doc?._id;
|
||||
return {
|
||||
role,
|
||||
person: {
|
||||
userId,
|
||||
phoneNumber: normalizeIranMobile(phoneNumber) ?? phoneNumber,
|
||||
...(resolvedClientId ? { clientId: resolvedClientId } : {}),
|
||||
nationalCodeOfInsurer: plateDto.nationalCodeOfInsurer,
|
||||
nationalCodeOfDriver: plateDto.nationalCodeOfDriver,
|
||||
insurerLicense: plateDto.insurerLicense,
|
||||
|
||||
Reference in New Issue
Block a user