From 5a89a0ff160b7bade1e59f7691a23af3e59b11e9 Mon Sep 17 00:00:00 2001 From: SepehrYahyaee <7heycallmegray@gmail.com> Date: Sat, 20 Jun 2026 11:53:18 +0330 Subject: [PATCH] Fixed mismatched userId on field expert for claim --- .../claim-request-management.service.ts | 90 +++++-------------- src/common/auth/auth.controller.ts | 21 ----- src/common/auth/auth.module.ts | 32 ------- src/common/auth/auth.service.ts | 31 ------- src/common/auth/constants/hash.constants.ts | 10 +++ src/common/auth/dtos/index.ts | 2 - src/common/auth/dtos/user-login.dto.ts | 10 --- src/common/auth/dtos/user-register.dto.ts | 7 -- src/common/auth/providers/hash.provider.ts | 68 -------------- src/common/auth/providers/index.ts | 1 - src/common/auth/types/index.ts | 1 + src/common/auth/types/payload.types.ts | 1 - src/core/config/config.schema.ts | 4 + src/expert-claim/expert-claim.service.ts | 7 +- src/expert-initiated/dtos/login.dto.ts | 32 ------- .../expert-initiated.controller.ts | 16 ---- .../expert-initiated.module.ts | 25 ------ .../expert-initiated.service.ts | 32 ------- .../repositories/field-expert.repository.ts | 22 ----- .../schemas/field-expert.schema.ts | 27 ------ .../user/repositories/user.repository.ts | 21 ++++- src/features/user/schemas/otp.schema.ts | 7 +- src/features/user/user.module.ts | 11 ++- src/helpers/blame-damaged-party.spec.ts | 34 +++++-- src/helpers/blame-damaged-party.ts | 42 +++++++++ .../request-management.service.ts | 4 + 26 files changed, 144 insertions(+), 414 deletions(-) delete mode 100644 src/common/auth/auth.controller.ts delete mode 100644 src/common/auth/auth.module.ts delete mode 100644 src/common/auth/auth.service.ts delete mode 100644 src/common/auth/dtos/index.ts delete mode 100644 src/common/auth/dtos/user-login.dto.ts delete mode 100644 src/common/auth/dtos/user-register.dto.ts delete mode 100644 src/common/auth/providers/hash.provider.ts delete mode 100644 src/common/auth/providers/index.ts delete mode 100644 src/expert-initiated/dtos/login.dto.ts delete mode 100644 src/expert-initiated/expert-initiated.controller.ts delete mode 100644 src/expert-initiated/expert-initiated.module.ts delete mode 100644 src/expert-initiated/expert-initiated.service.ts delete mode 100644 src/expert-initiated/repositories/field-expert.repository.ts delete mode 100644 src/expert-initiated/schemas/field-expert.schema.ts diff --git a/src/claim-request-management/claim-request-management.service.ts b/src/claim-request-management/claim-request-management.service.ts index 994a9b8..8699764 100644 --- a/src/claim-request-management/claim-request-management.service.ts +++ b/src/claim-request-management/claim-request-management.service.ts @@ -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, } : {}), }, diff --git a/src/common/auth/auth.controller.ts b/src/common/auth/auth.controller.ts deleted file mode 100644 index 31fa0ff..0000000 --- a/src/common/auth/auth.controller.ts +++ /dev/null @@ -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); - } -} diff --git a/src/common/auth/auth.module.ts b/src/common/auth/auth.module.ts deleted file mode 100644 index b4bf1b9..0000000 --- a/src/common/auth/auth.module.ts +++ /dev/null @@ -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("JWT_SECRET"), - signOptions: { - expiresIn: configService.get("JWT_EXPIRY"), - }, - }), - }), - ], - controllers: [AuthController], - providers: [ - { provide: APP_GUARD, useClass: AuthGuard }, - { provide: APP_GUARD, useClass: RolesGuard }, - AuthService, - HashService, - ], -}) -export class AuthModule {} diff --git a/src/common/auth/auth.service.ts b/src/common/auth/auth.service.ts deleted file mode 100644 index 3b8d0c5..0000000 --- a/src/common/auth/auth.service.ts +++ /dev/null @@ -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 - } -} diff --git a/src/common/auth/constants/hash.constants.ts b/src/common/auth/constants/hash.constants.ts index a01f4a2..a96d14c 100644 --- a/src/common/auth/constants/hash.constants.ts +++ b/src/common/auth/constants/hash.constants.ts @@ -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, +}; diff --git a/src/common/auth/dtos/index.ts b/src/common/auth/dtos/index.ts deleted file mode 100644 index 56b9e5a..0000000 --- a/src/common/auth/dtos/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { UserRegisterDto } from "./user-register.dto"; -export { UserLoginDto } from "./user-login.dto"; diff --git a/src/common/auth/dtos/user-login.dto.ts b/src/common/auth/dtos/user-login.dto.ts deleted file mode 100644 index 810a9c1..0000000 --- a/src/common/auth/dtos/user-login.dto.ts +++ /dev/null @@ -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; -} diff --git a/src/common/auth/dtos/user-register.dto.ts b/src/common/auth/dtos/user-register.dto.ts deleted file mode 100644 index 5ee465e..0000000 --- a/src/common/auth/dtos/user-register.dto.ts +++ /dev/null @@ -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; -} diff --git a/src/common/auth/providers/hash.provider.ts b/src/common/auth/providers/hash.provider.ts deleted file mode 100644 index b77f88d..0000000 --- a/src/common/auth/providers/hash.provider.ts +++ /dev/null @@ -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("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 { - 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); - } -} diff --git a/src/common/auth/providers/index.ts b/src/common/auth/providers/index.ts deleted file mode 100644 index fd681a4..0000000 --- a/src/common/auth/providers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { HashService } from "./hash.provider"; diff --git a/src/common/auth/types/index.ts b/src/common/auth/types/index.ts index 5af126d..598ca59 100644 --- a/src/common/auth/types/index.ts +++ b/src/common/auth/types/index.ts @@ -1 +1,2 @@ export { JwtPayload } from "./payload.types"; +export { PasswordHash } from "./password.types"; diff --git a/src/common/auth/types/payload.types.ts b/src/common/auth/types/payload.types.ts index a939f9b..b7418e1 100644 --- a/src/common/auth/types/payload.types.ts +++ b/src/common/auth/types/payload.types.ts @@ -1,7 +1,6 @@ export interface JwtPayload { sub: string; role: string; - clientId?: string; iat?: number; exp?: number; } diff --git a/src/core/config/config.schema.ts b/src/core/config/config.schema.ts index e0c6804..ee6c1bc 100644 --- a/src/core/config/config.schema.ts +++ b/src/core/config/config.schema.ts @@ -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() diff --git a/src/expert-claim/expert-claim.service.ts b/src/expert-claim/expert-claim.service.ts index 21e3972..e849ac4 100644 --- a/src/expert-claim/expert-claim.service.ts +++ b/src/expert-claim/expert-claim.service.ts @@ -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 { - 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(), diff --git a/src/expert-initiated/dtos/login.dto.ts b/src/expert-initiated/dtos/login.dto.ts deleted file mode 100644 index 68b650b..0000000 --- a/src/expert-initiated/dtos/login.dto.ts +++ /dev/null @@ -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; -} diff --git a/src/expert-initiated/expert-initiated.controller.ts b/src/expert-initiated/expert-initiated.controller.ts deleted file mode 100644 index 8f2a1e8..0000000 --- a/src/expert-initiated/expert-initiated.controller.ts +++ /dev/null @@ -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); - } -} diff --git a/src/expert-initiated/expert-initiated.module.ts b/src/expert-initiated/expert-initiated.module.ts deleted file mode 100644 index 2050ef1..0000000 --- a/src/expert-initiated/expert-initiated.module.ts +++ /dev/null @@ -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 {} diff --git a/src/expert-initiated/expert-initiated.service.ts b/src/expert-initiated/expert-initiated.service.ts deleted file mode 100644 index 2a88ce7..0000000 --- a/src/expert-initiated/expert-initiated.service.ts +++ /dev/null @@ -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"); - } -} diff --git a/src/expert-initiated/repositories/field-expert.repository.ts b/src/expert-initiated/repositories/field-expert.repository.ts deleted file mode 100644 index 2e4784d..0000000 --- a/src/expert-initiated/repositories/field-expert.repository.ts +++ /dev/null @@ -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, - ) {} - - async retrieveById(id: string): Promise { - return await this.fieldExpertModel.findById(id).exec(); - } - - async retrieveByEmail(email: string): Promise { - return await this.fieldExpertModel.findOne({ - email, - }); - } -} diff --git a/src/expert-initiated/schemas/field-expert.schema.ts b/src/expert-initiated/schemas/field-expert.schema.ts deleted file mode 100644 index cde1c7d..0000000 --- a/src/expert-initiated/schemas/field-expert.schema.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose"; -import { HydratedDocument, Types } from "mongoose"; - -export type FieldExpertDocument = HydratedDocument; - -@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); diff --git a/src/features/user/repositories/user.repository.ts b/src/features/user/repositories/user.repository.ts index e557dd6..06c303e 100644 --- a/src/features/user/repositories/user.repository.ts +++ b/src/features/user/repositories/user.repository.ts @@ -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, ) {} - async addUser() {} + async create(cellphoneNumber: string): Promise { + return this.model.create({ cellphoneNumber }); + } async retrieveByCellPhoneNumber( cellphoneNumber: string, @@ -19,4 +21,19 @@ export class UserRepository { cellphoneNumber, }); } + + async saveOtp(cellphoneNumber: string, otp: Otp): Promise { + await this.model.updateOne({ cellphoneNumber }, { $set: { otp } }); + } + + async clearOtp(cellphoneNumber: string): Promise { + await this.model.updateOne({ cellphoneNumber }, { $unset: { otp: "" } }); + } + + async incrementOtpAttempts(cellphoneNumber: string): Promise { + await this.model.updateOne( + { cellphoneNumber }, + { $inc: { "otp.attempts": 1 } }, + ); + } } diff --git a/src/features/user/schemas/otp.schema.ts b/src/features/user/schemas/otp.schema.ts index 49a009c..700a231 100644 --- a/src/features/user/schemas/otp.schema.ts +++ b/src/features/user/schemas/otp.schema.ts @@ -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); diff --git a/src/features/user/user.module.ts b/src/features/user/user.module.ts index 12bbc94..8bc27e2 100644 --- a/src/features/user/user.module.ts +++ b/src/features/user/user.module.ts @@ -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], diff --git a/src/helpers/blame-damaged-party.spec.ts b/src/helpers/blame-damaged-party.spec.ts index 9502b0e..4d523bf 100644 --- a/src/helpers/blame-damaged-party.spec.ts +++ b/src/helpers/blame-damaged-party.spec.ts @@ -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), + }); }); }); diff --git a/src/helpers/blame-damaged-party.ts b/src/helpers/blame-damaged-party.ts index e1666b8..d762983 100644 --- a/src/helpers/blame-damaged-party.ts +++ b/src/helpers/blame-damaged-party.ts @@ -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[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[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 } : {}), + }; +} diff --git a/src/request-management/request-management.service.ts b/src/request-management/request-management.service.ts index c6f18e0..6f41905 100644 --- a/src/request-management/request-management.service.ts +++ b/src/request-management/request-management.service.ts @@ -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,