1
0
forked from Yara724/api

Merge pull request 'Fixed users not being able to view their files' (#132) from s.yahyaee/yara724-api:main into main

Reviewed-on: Yara724/api#132
This commit is contained in:
2026-06-19 09:51:16 +03:30
12 changed files with 745 additions and 222 deletions

View File

@@ -8,6 +8,12 @@ import { JwtService } from "@nestjs/jwt";
import { Request } from "express"; import { Request } from "express";
import { RoleEnum } from "src/Types&Enums/role.enum"; import { RoleEnum } from "src/Types&Enums/role.enum";
const GLOBAL_GUARD_ROLES = new Set<string>([
RoleEnum.USER,
RoleEnum.FIELD_EXPERT,
RoleEnum.REGISTRAR,
]);
@Injectable() @Injectable()
export class GlobalGuard implements CanActivate { export class GlobalGuard implements CanActivate {
constructor(private readonly jwtService: JwtService) {} constructor(private readonly jwtService: JwtService) {}
@@ -17,7 +23,7 @@ export class GlobalGuard implements CanActivate {
const token = this.extractTokenFromHeader(request); const token = this.extractTokenFromHeader(request);
if (!token) { if (!token) {
throw new UnauthorizedException(); throw new UnauthorizedException("Missing Bearer token");
} }
try { try {
@@ -25,17 +31,19 @@ export class GlobalGuard implements CanActivate {
secret: `${process.env.JWT_SECRET}`, secret: `${process.env.JWT_SECRET}`,
}); });
if ( if (!payload?.role || !GLOBAL_GUARD_ROLES.has(String(payload.role))) {
payload.role !== RoleEnum.USER && throw new UnauthorizedException(
payload.role !== RoleEnum.FIELD_EXPERT `Role "${payload?.role ?? "unknown"}" is not allowed on user-panel APIs. Use /user/login for USER, or /actor/login for experts.`,
) { );
throw new UnauthorizedException();
} }
request.user = payload; request.user = payload;
request.identity = request.user; request.identity = request.user;
} catch { } catch (error) {
throw new UnauthorizedException(); if (error instanceof UnauthorizedException) {
throw error;
}
throw new UnauthorizedException("Invalid or expired token");
} }
return true; return true;
} }

View File

@@ -5,16 +5,19 @@ import { Reflector } from "@nestjs/core";
export class RolesGuard implements CanActivate { export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {} constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean { canActivate(context: ExecutionContext): boolean {
// get the roles required
const roles = this.reflector.getAllAndOverride<string[]>("role", [ const roles = this.reflector.getAllAndOverride<string[]>("role", [
context.getHandler(), context.getHandler(),
context.getClass(), context.getClass(),
]); ]);
if (!roles) { if (!roles?.length) {
return false; return false;
} }
const request = context.switchToHttp().getRequest(); const request = context.switchToHttp().getRequest();
const userRoles = request.user?.role?.split(","); const role = request.user?.role;
if (!role) {
return false;
}
const userRoles = String(role).split(",");
return this.validateRoles(roles, userRoles); return this.validateRoles(roles, userRoles);
} }

View File

@@ -62,8 +62,18 @@ import { applyListQueryV2 } from "src/helpers/list-query-v2";
import { partyPersonMatchesUser } from "src/helpers/iran-mobile"; import { partyPersonMatchesUser } from "src/helpers/iran-mobile";
import { import {
buildBlamePartyAccessOrConditions, buildBlamePartyAccessOrConditions,
mongoUserIdEqualityFilter, collectUserIdVariants,
} from "src/helpers/party-access-queries"; } from "src/helpers/party-access-queries";
import {
buildOwnerUserIdAccessFilter,
ownerUserIdMatchesActor,
resolveLinkedUserIdStrings,
} from "src/helpers/user-access-resolver";
import {
blameDamagedPartyMatchesUser,
resolveClaimOwnerParty,
resolveDamagedPartyUserId,
} from "src/helpers/blame-damaged-party";
import { claimCaseInitiatedByFieldExpert } from "src/helpers/tenant-scope"; import { claimCaseInitiatedByFieldExpert } from "src/helpers/tenant-scope";
import { ClaimDetailsV2ResponseDto } from "./dto/claim-details-v2.dto"; import { ClaimDetailsV2ResponseDto } from "./dto/claim-details-v2.dto";
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum"; import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
@@ -2719,14 +2729,12 @@ export class ClaimRequestManagementService {
currentUserId, currentUserId,
actor?.role, actor?.role,
); );
if ( await this.assertEffectiveUserIsDamagedPartyOnClaim(
!claim.owner?.userId || claim,
claim.owner.userId.toString() !== effectiveUserId effectiveUserId,
) { actor as { username?: string },
throw new ForbiddenException( "You do not have access to this claim file",
"You do not have access to this claim file", );
);
}
const replyKey = claim.evaluation?.damageExpertReplyFinal const replyKey = claim.evaluation?.damageExpertReplyFinal
? "damageExpertReplyFinal" ? "damageExpertReplyFinal"
@@ -4580,6 +4588,19 @@ export class ClaimRequestManagementService {
const claimNo = await this.generateUniqueClaimNumber(); const claimNo = await this.generateUniqueClaimNumber();
// 8. Create claim case // 8. Create claim case
const ownerParty = resolveClaimOwnerParty(blameRequest);
if (!ownerParty?.person?.userId) {
throw new BadRequestException(
"Could not resolve claim owner from blame parties",
);
}
const damagedUserId = (() => {
const oid = String(ownerParty.person.userId);
const dp = (blameRequest.parties || []).find(
(p: any) => p.person?.userId && String(p.person.userId) !== oid,
);
return dp?.person?.userId ? String(dp.person.userId) : null;
})();
const newClaim = await this.claimCaseDbService.create({ const newClaim = await this.claimCaseDbService.create({
requestNo: claimNo, requestNo: claimNo,
publicId: blameRequest.publicId, publicId: blameRequest.publicId,
@@ -4594,32 +4615,19 @@ export class ClaimRequestManagementService {
completedSteps: [ClaimWorkflowStep.CLAIM_CREATED], completedSteps: [ClaimWorkflowStep.CLAIM_CREATED],
locked: false, locked: false,
}, },
owner: { ...(damagedUserId
userId: new Types.ObjectId(currentUserId), ? { damagedPartyUserId: new Types.ObjectId(damagedUserId) }
userRole: currentUserParty.role as any, : {}),
fullName: currentUserParty.person?.fullName, owner: (() => {
userClientKey: currentUserParty.person?.clientId const cid = ownerParty.person.clientId;
? String(currentUserParty.person.clientId) return {
: undefined, userId: new Types.ObjectId(String(ownerParty.person.userId)),
// clientId must be the GUILTY party's clientId — their insurer pays the claim userRole: ownerParty.role as any,
...(() => { fullName: currentUserParty.person?.fullName,
if (isCarBody) { userClientKey: cid ? String(cid) : undefined,
// CAR_BODY has no guilty party — use the first party's own clientId ...(cid ? { clientId: new Types.ObjectId(String(cid)) } : {}),
const firstParty = parties.find((p) => p.role === "FIRST"); };
const cid = firstParty?.person?.clientId; })(),
return cid ? { clientId: new Types.ObjectId(String(cid)) } : {};
}
const guiltyPartyId = String(
blameRequest?.expert?.decision?.guiltyPartyId ?? "",
);
const guiltyParty = parties.find(
(p) => String(p.person?.userId) === guiltyPartyId,
);
const cid = guiltyParty?.person?.clientId;
return cid ? { clientId: new Types.ObjectId(String(cid)) } : {};
})(),
},
history: [ history: [
{ {
type: "CLAIM_CREATED", type: "CLAIM_CREATED",
@@ -4725,9 +4733,12 @@ 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 damagedParty = parties.find( const ownerParty = resolveClaimOwnerParty(blameRequest);
(p) => p.person?.userId && String(p.person.userId) === damagedUserId, 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,
@@ -4743,12 +4754,15 @@ export class ClaimRequestManagementService {
completedSteps: [ClaimWorkflowStep.CLAIM_CREATED], completedSteps: [ClaimWorkflowStep.CLAIM_CREATED],
locked: false, locked: false,
}, },
damagedPartyUserId: new Types.ObjectId(damagedUserId),
owner: { owner: {
userId: new Types.ObjectId(damagedUserId), userId: new Types.ObjectId(String(ownerParty.person.userId)),
userRole: damagedParty?.role as any, userRole: ownerParty.role as any,
...(damagedParty?.person?.clientId ...(ownerParty.person.clientId
? { ? {
clientId: new Types.ObjectId(damagedParty.person.clientId as any), clientId: new Types.ObjectId(
String(ownerParty.person.clientId),
),
} }
: {}), : {}),
}, },
@@ -4782,32 +4796,128 @@ export class ClaimRequestManagementService {
}; };
} }
private async assertActorCanViewClaimV2( /**
* True when the actor appears as a non-owner (damaged) party on the claim's
* snapshot.parties. This is the fastest, blame-free fallback: the snapshot is
* written at claim-creation time and always holds the correct user IDs.
*
* Logic:
* - Any party whose person.userId is in linkedUserIds and is NOT the claim
* owner (guilty party) is considered the damaged party.
* - If owner.userId is unknown we accept any matching party (CAR_BODY flows).
*/
private claimSnapshotPartyMatchesAsDamagedParty(
claim: any, claim: any,
currentUserId: string, linkedUserIdStrings: string[],
actor?: { sub: string; role?: string }, ): boolean {
const parties: any[] = claim?.snapshot?.parties ?? [];
if (!parties.length) return false;
const ownerIdStr = claim?.owner?.userId
? String(claim.owner.userId)
: null;
for (const party of parties) {
const partyUserId = party?.person?.userId;
if (partyUserId == null) continue;
const partyStr = String(partyUserId);
// Must be this actor
if (!linkedUserIdStrings.some((id) => id === partyStr)) continue;
// Must NOT be the guilty-owner (unless owner is unknown — CAR_BODY)
if (ownerIdStr && partyStr === ownerIdStr) continue;
return true;
}
return false;
}
private async assertEffectiveUserIsDamagedPartyOnClaim(
claimCase: any,
effectiveUserId: string,
actor?: { username?: string },
message = "Only the damaged party can perform this action on the claim",
): Promise<void> { ): Promise<void> {
const ownerId = claim.owner?.userId?.toString(); const linkedUserIds = await resolveLinkedUserIdStrings(
if (ownerId && ownerId === currentUserId) { this.userDbService,
{ sub: effectiveUserId, username: actor?.username },
);
const actorCtx = { sub: effectiveUserId, username: actor?.username };
// 0. damagedPartyUserId stored directly on the claim (fastest, most reliable)
if (ownerUserIdMatchesActor(claimCase?.damagedPartyUserId, linkedUserIds)) {
return; return;
} }
if ( // 1. Owner (= guilty party for THIRD_PARTY) — kept for backward compat
(!actor?.role || actor.role === RoleEnum.USER) && // on CAR_BODY where owner IS the damaged party.
claim.blameRequestId if (ownerUserIdMatchesActor(claimCase?.owner?.userId, linkedUserIds)) {
) { return;
}
// 2. Blame-based resolution (primary path for THIRD_PARTY)
if (claimCase?.blameRequestId) {
const blame = await this.blameRequestDbService.findById( const blame = await this.blameRequestDbService.findById(
claim.blameRequestId.toString(), claimCase.blameRequestId.toString(),
); );
const isParty = if (
Array.isArray(blame?.parties) && blame &&
blame.parties.some((p: any) => blameDamagedPartyMatchesUser(blame, actorCtx, linkedUserIds)
partyPersonMatchesUser(p?.person, { ) {
sub: currentUserId, return;
username: (actor as { username?: string })?.username, }
}), }
// 3. Snapshot-party fallback: claim.snapshot.parties is written at creation
// and is reliable even when blame lookup fails or userId mapping differs.
if (this.claimSnapshotPartyMatchesAsDamagedParty(claimCase, linkedUserIds)) {
return;
}
throw new ForbiddenException(message);
}
private async assertActorCanViewClaimV2(
claim: any,
currentUserId: string,
actor?: { sub: string; role?: string; username?: string },
): Promise<void> {
const linkedUserIds = await resolveLinkedUserIdStrings(
this.userDbService,
{ sub: currentUserId, username: actor?.username },
);
if (ownerUserIdMatchesActor(claim.owner?.userId, linkedUserIds)) {
return;
}
// damagedPartyUserId is set at claim-creation time — the most direct check.
if (ownerUserIdMatchesActor(claim.damagedPartyUserId, linkedUserIds)) {
return;
}
if (!actor?.role || actor.role === RoleEnum.USER) {
const userCtx = { sub: currentUserId, username: actor?.username };
// Any party on the linked blame can VIEW the claim.
// (Mutation guards use assertEffectiveUserIsDamagedPartyOnClaim which is
// stricter and only allows the damaged party to act.)
if (claim.blameRequestId) {
const blame = await this.blameRequestDbService.findById(
claim.blameRequestId.toString(),
); );
if (isParty) { if (blame) {
const parties: any[] = (blame as any).parties ?? [];
const isParty = parties.some((p: any) =>
partyPersonMatchesUser(p?.person, userCtx, linkedUserIds),
);
if (isParty) return;
}
}
// Snapshot-party fallback (reliable for claims with snapshot written).
if (this.claimSnapshotPartyMatchesAsDamagedParty(claim, linkedUserIds)) {
return; return;
} }
} }
@@ -4878,9 +4988,10 @@ export class ClaimRequestManagementService {
return currentUserId; return currentUserId;
} }
} }
return claimCase.owner?.userId return (
? String(claimCase.owner.userId) resolveDamagedPartyUserId(blameRequest) ??
: currentUserId; (claimCase.owner?.userId ? String(claimCase.owner.userId) : currentUserId)
);
} }
async createClaimFromBlameForRegistrarV1( async createClaimFromBlameForRegistrarV1(
@@ -4936,9 +5047,12 @@ 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 damagedParty = parties.find( const ownerParty = resolveClaimOwnerParty(blameRequest);
(p) => p.person?.userId && String(p.person.userId) === damagedUserId, 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,
@@ -4953,12 +5067,15 @@ export class ClaimRequestManagementService {
completedSteps: [ClaimWorkflowStep.CLAIM_CREATED], completedSteps: [ClaimWorkflowStep.CLAIM_CREATED],
locked: false, locked: false,
}, },
damagedPartyUserId: new Types.ObjectId(damagedUserId),
owner: { owner: {
userId: new Types.ObjectId(damagedUserId), userId: new Types.ObjectId(String(ownerParty.person.userId)),
userRole: damagedParty?.role as any, userRole: ownerParty.role as any,
...(damagedParty?.person?.clientId ...(ownerParty.person.clientId
? { ? {
clientId: new Types.ObjectId(damagedParty.person.clientId as any), clientId: new Types.ObjectId(
String(ownerParty.person.clientId),
),
} }
: {}), : {}),
}, },
@@ -5025,15 +5142,12 @@ export class ClaimRequestManagementService {
currentUserId, currentUserId,
actor?.role, actor?.role,
); );
// 2. Validate user is the claim owner (or field expert acting for IN_PERSON claim) await this.assertEffectiveUserIsDamagedPartyOnClaim(
if ( claimCase,
!claimCase.owner?.userId || effectiveUserId,
claimCase.owner.userId.toString() !== effectiveUserId actor as { username?: string },
) { "Only the claim owner (damaged party) can select damaged parts",
throw new ForbiddenException( );
"Only the claim owner (damaged party) can select damaged parts",
);
}
// 3. Validate workflow step is correct (should be at SELECT_OUTER_PARTS) // 3. Validate workflow step is correct (should be at SELECT_OUTER_PARTS)
if ( if (
@@ -5245,15 +5359,13 @@ export class ClaimRequestManagementService {
currentUserId, currentUserId,
actor?.role, actor?.role,
); );
// 2. Validate user is the claim owner (or field expert for IN_PERSON claim) // 2. Validate user is the damaged party (or field expert for IN_PERSON claim)
if ( await this.assertEffectiveUserIsDamagedPartyOnClaim(
!claimCase.owner?.userId || claimCase,
claimCase.owner.userId.toString() !== effectiveUserId effectiveUserId,
) { actor as { username?: string },
throw new ForbiddenException( "Only the claim owner (damaged party) can submit other parts and bank information",
"Only the claim owner (damaged party) can submit other parts and bank information", );
);
}
// 3. Validate workflow step is correct (should be at SELECT_OTHER_PARTS) // 3. Validate workflow step is correct (should be at SELECT_OTHER_PARTS)
if ( if (
@@ -5468,14 +5580,12 @@ export class ClaimRequestManagementService {
currentUserId, currentUserId,
actor?.role, actor?.role,
); );
if ( await this.assertEffectiveUserIsDamagedPartyOnClaim(
!claimCase.owner?.userId || claimCase,
claimCase.owner.userId.toString() !== effectiveUserId effectiveUserId,
) { actor as { username?: string },
throw new ForbiddenException( "Only the claim owner can view capture requirements",
"Only the claim owner can view capture requirements", );
);
}
// Build car-type aware outer-parts lookup (complete source: static catalog). // Build car-type aware outer-parts lookup (complete source: static catalog).
// Uses the raw catalog shape (`{id, key, titleFa, side, ...}`) because the // Uses the raw catalog shape (`{id, key, titleFa, side, ...}`) because the
@@ -5764,14 +5874,12 @@ export class ClaimRequestManagementService {
currentUserId, currentUserId,
actor?.role, actor?.role,
); );
if ( await this.assertEffectiveUserIsDamagedPartyOnClaim(
!claimCase.owner?.userId || claimCase,
claimCase.owner.userId.toString() !== effectiveUserId effectiveUserId,
) { actor as { username?: string },
throw new ForbiddenException( "Only the claim owner can complete this step",
"Only the claim owner can complete this step", );
);
}
if ( if (
claimCase.workflow?.currentStep !== ClaimWorkflowStep.USER_EXPERT_RESEND claimCase.workflow?.currentStep !== ClaimWorkflowStep.USER_EXPERT_RESEND
) { ) {
@@ -5840,14 +5948,12 @@ export class ClaimRequestManagementService {
currentUserId, currentUserId,
actor?.role, actor?.role,
); );
if ( await this.assertEffectiveUserIsDamagedPartyOnClaim(
!claimCase.owner?.userId || claimCase,
claimCase.owner.userId.toString() !== effectiveUserId effectiveUserId,
) { actor as { username?: string },
throw new ForbiddenException( "Only the claim owner can upload documents",
"Only the claim owner can upload documents", );
);
}
const step = claimCase.workflow?.currentStep; const step = claimCase.workflow?.currentStep;
const isResendUpload = step === ClaimWorkflowStep.USER_EXPERT_RESEND; const isResendUpload = step === ClaimWorkflowStep.USER_EXPERT_RESEND;
@@ -6186,12 +6292,12 @@ export class ClaimRequestManagementService {
currentUserId, currentUserId,
actor?.role, actor?.role,
); );
if ( await this.assertEffectiveUserIsDamagedPartyOnClaim(
!claimCase.owner?.userId || claimCase,
claimCase.owner.userId.toString() !== effectiveUserId effectiveUserId,
) { actor as { username?: string },
throw new ForbiddenException("Only the claim owner can capture parts"); "Only the claim owner can capture parts",
} );
const capStep = claimCase.workflow?.currentStep; const capStep = claimCase.workflow?.currentStep;
const isResendCapture = capStep === ClaimWorkflowStep.USER_EXPERT_RESEND; const isResendCapture = capStep === ClaimWorkflowStep.USER_EXPERT_RESEND;
@@ -6483,14 +6589,12 @@ export class ClaimRequestManagementService {
currentUserId, currentUserId,
actor?.role, actor?.role,
); );
if ( await this.assertEffectiveUserIsDamagedPartyOnClaim(
!claimCase.owner?.userId || claimCase,
claimCase.owner.userId.toString() !== effectiveUserId effectiveUserId,
) { actor as { username?: string },
throw new ForbiddenException( "Only the claim owner can upload car capture video",
"Only the claim owner can upload car capture video", );
);
}
if (!fileDetail) { if (!fileDetail) {
throw new BadRequestException("Video file is required."); throw new BadRequestException("Video file is required.");
@@ -6606,14 +6710,12 @@ export class ClaimRequestManagementService {
currentUserId, currentUserId,
actor?.role, actor?.role,
); );
if ( await this.assertEffectiveUserIsDamagedPartyOnClaim(
!claimCase.owner?.userId || claimCase,
claimCase.owner.userId.toString() !== effectiveUserId effectiveUserId,
) { actor as { username?: string },
throw new ForbiddenException( "Only the claim owner can submit an objection",
"Only the claim owner can submit an objection", );
);
}
if (claimCase.evaluation?.objection?.submittedAt) { if (claimCase.evaluation?.objection?.submittedAt) {
throw new ConflictException( throw new ConflictException(
@@ -6827,14 +6929,12 @@ export class ClaimRequestManagementService {
currentUserId, currentUserId,
actor?.role, actor?.role,
); );
if ( await this.assertEffectiveUserIsDamagedPartyOnClaim(
!claimCase.owner?.userId || claimCase,
claimCase.owner.userId.toString() !== effectiveUserId effectiveUserId,
) { actor as { username?: string },
throw new ForbiddenException( "Only the claim owner can submit a satisfaction rating.",
"Only the claim owner can submit a satisfaction rating.", );
);
}
if (claimCase.status !== ClaimCaseStatus.COMPLETED) { if (claimCase.status !== ClaimCaseStatus.COMPLETED) {
throw new BadRequestException( throw new BadRequestException(
@@ -6932,14 +7032,12 @@ export class ClaimRequestManagementService {
currentUserId, currentUserId,
actor?.role, actor?.role,
); );
if ( await this.assertEffectiveUserIsDamagedPartyOnClaim(
!claimCase.owner?.userId || claimCase,
claimCase.owner.userId.toString() !== effectiveUserId effectiveUserId,
) { actor as { username?: string },
throw new ForbiddenException( "Only the claim owner (damaged party) can sign at this step.",
"Only the claim owner (damaged party) can sign at this step.", );
);
}
if (!claimCaseStatusAllowsOwnerInsurerSignStep(claimCase.status)) { if (!claimCaseStatusAllowsOwnerInsurerSignStep(claimCase.status)) {
throw new BadRequestException( throw new BadRequestException(
@@ -7243,38 +7341,102 @@ export class ClaimRequestManagementService {
{ lean: true }, { lean: true },
); );
} else { } else {
const partyBlameOr = buildBlamePartyAccessOrConditions({ const linkedUserIds = await resolveLinkedUserIdStrings(
this.userDbService,
{
sub: currentUserId,
username: (actor as { username?: string })?.username,
},
);
const actorCtx = {
sub: currentUserId, sub: currentUserId,
username: (actor as { username?: string })?.username, username: (actor as { username?: string })?.username,
}); };
const partyBlameIds = await this.blameRequestDbService const partyBlameOr = buildBlamePartyAccessOrConditions(
.find( actorCtx,
partyBlameOr.length === 1 linkedUserIds,
? (partyBlameOr[0] as any)
: ({ $or: partyBlameOr } as any),
{ select: "_id", lean: true },
)
.then((docs) => docs.map((d) => (d as any)._id));
const ownerOr = mongoUserIdEqualityFilter(
"owner.userId",
currentUserId,
); );
const claimOr: Record<string, unknown>[] = [];
if (ownerOr) claimOr.push(ownerOr);
if (partyBlameIds.length) {
claimOr.push({ blameRequestId: { $in: partyBlameIds } });
}
claims = const idVariants = collectUserIdVariants(...linkedUserIds);
claimOr.length === 0
? [] const ownerOr = buildOwnerUserIdAccessFilter(idVariants);
: await this.claimCaseDbService.find(
claimOr.length === 1 // damagedPartyUserId filter — direct match without blame lookup
? (claimOr[0] as any) const damagedOr =
: ({ $or: claimOr } as any), idVariants.length === 1
? { damagedPartyUserId: idVariants[0] }
: idVariants.length > 1
? { damagedPartyUserId: { $in: idVariants } }
: null;
// Build snapshot-party query: claim.snapshot.parties.person.userId
// is the most direct and reliable path — it's copied from the blame
// at claim-creation time and always has the correct damaged-party userId.
const snapshotPartyFilter =
idVariants.length === 1
? { "snapshot.parties.person.userId": idVariants[0] }
: idVariants.length > 1
? { "snapshot.parties.person.userId": { $in: idVariants } }
: null;
const [ownerClaims, damagedDirectClaims, snapshotClaims, candidateBlames] =
await Promise.all([
ownerOr
? this.claimCaseDbService.find(ownerOr as any, { lean: true })
: Promise.resolve([]),
damagedOr
? this.claimCaseDbService.find(damagedOr as any, { lean: true })
: Promise.resolve([]),
snapshotPartyFilter
? this.claimCaseDbService.find(snapshotPartyFilter as any, {
lean: true,
})
: Promise.resolve([]),
partyBlameOr.length
? this.blameRequestDbService.find(
partyBlameOr.length === 1
? (partyBlameOr[0] as any)
: ({ $or: partyBlameOr } as any),
{
lean: true,
select: "type parties expert.decision blameStatus",
},
)
: Promise.resolve([]),
]);
// Any blame where the user is a party (not just damaged) surfaces
// the linked claim. Mutation guards protect against acting as the
// wrong party; viewing is intentionally open to all parties.
const partyBlameIds = (candidateBlames as any[]).map((b) => b._id);
const damagedClaims =
partyBlameIds.length > 0
? await this.claimCaseDbService.find(
{ blameRequestId: { $in: partyBlameIds } },
{ lean: true }, { lean: true },
); )
: [];
// Merge: owner claims + blame-resolved damaged claims + snapshot-party claims
// snapshotClaims may include guilty-owner claims too — filter them out here.
const snapshotDamagedClaims = (snapshotClaims as any[]).filter((c) => {
if (!c?.owner?.userId) return true; // unknown owner: allow
const ownerStr = String(c.owner.userId);
// Exclude if the user IS the owner (already in ownerClaims)
return !linkedUserIds.some((id) => id === ownerStr);
});
const byId = new Map<string, any>();
for (const c of [
...(ownerClaims as any[]),
...(damagedDirectClaims as any[]),
...(damagedClaims as any[]),
...snapshotDamagedClaims,
]) {
byId.set(String(c._id), c);
}
claims = [...byId.values()];
} }
const list = (claims as any[]).map((c) => ({ const list = (claims as any[]).map((c) => ({
claimRequestId: c._id.toString(), claimRequestId: c._id.toString(),

View File

@@ -116,6 +116,14 @@ export class ClaimCase {
@Prop({ type: Types.ObjectId, index: true }) @Prop({ type: Types.ObjectId, index: true })
initiatedByFieldExpertId?: Types.ObjectId; initiatedByFieldExpertId?: Types.ObjectId;
/**
* The damaged party's userId, resolved from the blame at claim-creation time.
* Stored here so view/list access does not require an extra blame lookup.
* For CAR_BODY this is the FIRST party; for THIRD_PARTY it is the non-guilty party.
*/
@Prop({ type: Types.ObjectId, index: true })
damagedPartyUserId?: Types.ObjectId;
@Prop({ type: ClaimWorkflowSchema, default: () => ({}) }) @Prop({ type: ClaimWorkflowSchema, default: () => ({}) })
workflow?: ClaimWorkflow; workflow?: ClaimWorkflow;

View File

@@ -0,0 +1,58 @@
import { Types } from "mongoose";
import { partyPersonMatchesUser } from "./iran-mobile";
import {
blameDamagedPartyMatchesUser,
resolveDamagedPartyPerson,
resolveGuiltyPartyUserId,
} from "./blame-damaged-party";
describe("blame damaged party helpers", () => {
const guiltyId = new Types.ObjectId();
const damagedId = new Types.ObjectId();
const thirdPartyBlame = {
type: "THIRD_PARTY",
expert: { decision: { guiltyPartyId: guiltyId } },
parties: [
{ role: "FIRST", person: { userId: guiltyId, phoneNumber: "09111111111" } },
{ role: "SECOND", person: { userId: damagedId, phoneNumber: "09912356917" } },
],
};
it("resolves guilty party id from expert decision", () => {
expect(resolveGuiltyPartyUserId(thirdPartyBlame)).toBe(String(guiltyId));
});
it("resolves damaged party person as non-guilty", () => {
expect(resolveDamagedPartyPerson(thirdPartyBlame)).toEqual(
thirdPartyBlame.parties[1].person,
);
});
it("matches damaged party by userId even when claim.owner is guilty", () => {
expect(
blameDamagedPartyMatchesUser(
thirdPartyBlame,
{ sub: String(damagedId), username: "09912356917" },
[],
),
).toBe(true);
expect(
blameDamagedPartyMatchesUser(
thirdPartyBlame,
{ sub: String(guiltyId), username: "09111111111" },
[],
),
).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);
});
});

View File

@@ -0,0 +1,121 @@
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
import { partyPersonMatchesUser } from "./iran-mobile";
/** Resolve guilty party user id from blame case (expert decision → statement → AGREED first party). */
export function resolveGuiltyPartyUserId(blame: {
parties?: Array<{
role?: string;
person?: { userId?: unknown };
statement?: { admitsGuilt?: boolean };
}>;
expert?: { decision?: { guiltyPartyId?: unknown } };
blameStatus?: string;
}): string | null {
const parties = blame?.parties ?? [];
const decisionId = blame?.expert?.decision?.guiltyPartyId;
if (decisionId != null && String(decisionId)) {
return String(decisionId);
}
const admitsGuilt = parties.find((p) => p.statement?.admitsGuilt === true);
if (admitsGuilt?.person?.userId != null) {
return String(admitsGuilt.person.userId);
}
if (blame?.blameStatus === "AGREED") {
const first = parties.find((p) => p.role === "FIRST");
if (first?.person?.userId != null) {
return String(first.person.userId);
}
}
return null;
}
/** Person row for the damaged party on a blame case (claim beneficiary / user-panel actor). */
export function resolveDamagedPartyPerson(blame: {
type?: string;
parties?: Array<{
role?: string;
person?: { userId?: unknown; phoneNumber?: string; clientId?: unknown };
statement?: { admitsGuilt?: boolean; claimsDamage?: boolean };
}>;
expert?: { decision?: { guiltyPartyId?: unknown } };
blameStatus?: string;
}): { userId?: unknown; phoneNumber?: string; clientId?: unknown } | null {
const parties = blame?.parties ?? [];
if (!parties.length) return null;
const isCarBody =
blame?.type === BlameRequestType.CAR_BODY || blame?.type === "CAR_BODY";
if (isCarBody) {
const first = parties.find((p) => p.role === "FIRST");
return first?.person ?? null;
}
const guiltyId = resolveGuiltyPartyUserId(blame);
if (!guiltyId) {
const claimsDamage = parties.find((p) => p.statement?.claimsDamage === true);
return claimsDamage?.person ?? null;
}
const damagedParty = parties.find(
(p) =>
p.person?.userId != null && String(p.person.userId) !== String(guiltyId),
);
if (damagedParty?.person) return damagedParty.person;
const nonGuilty = parties.find(
(p) => p.person && String(p.person.userId ?? "") !== String(guiltyId),
);
return nonGuilty?.person ?? null;
}
export function blameDamagedPartyMatchesUser(
blame: Parameters<typeof resolveDamagedPartyPerson>[0],
user: { sub?: string; username?: string },
linkedUserIdStrings: string[] = [],
): boolean {
const person = resolveDamagedPartyPerson(blame);
if (!person) return false;
return partyPersonMatchesUser(person, user, linkedUserIdStrings);
}
/** Blame party that owns the claim row (`owner.userId` = guilty for THIRD_PARTY, first party for CAR_BODY). */
export function resolveClaimOwnerParty(blame: {
type?: string;
parties?: Array<{
role?: string;
person?: { userId?: unknown; phoneNumber?: string; clientId?: unknown };
statement?: { admitsGuilt?: boolean };
}>;
expert?: { decision?: { guiltyPartyId?: unknown } };
blameStatus?: string;
}): {
role?: string;
person?: { userId?: unknown; phoneNumber?: string; clientId?: unknown };
} | null {
const parties = blame?.parties ?? [];
const isCarBody =
blame?.type === BlameRequestType.CAR_BODY || blame?.type === "CAR_BODY";
if (isCarBody) {
return parties.find((p) => p.role === "FIRST") ?? null;
}
const guiltyId = resolveGuiltyPartyUserId(blame);
if (!guiltyId) return null;
return (
parties.find(
(p) => p.person?.userId != null && String(p.person.userId) === guiltyId,
) ?? null
);
}
export function resolveDamagedPartyUserId(
blame: Parameters<typeof resolveDamagedPartyPerson>[0],
): string | null {
const person = resolveDamagedPartyPerson(blame);
return person?.userId != null ? String(person.userId) : null;
}

View File

@@ -52,15 +52,19 @@ export function partyPersonMatchesUser(
| null | null
| undefined, | undefined,
user: { sub?: string; username?: string } | null | undefined, user: { sub?: string; username?: string } | null | undefined,
linkedUserIdStrings: string[] = [],
): boolean { ): boolean {
if (!person || !user) return false; if (!person || !user) return false;
if (
person.userId != null && const userIds = new Set<string>();
user.sub && if (user.sub) userIds.add(String(user.sub));
String(person.userId) === String(user.sub) for (const id of linkedUserIdStrings) {
) { if (id) userIds.add(String(id));
}
if (person.userId != null && userIds.has(String(person.userId))) {
return true; return true;
} }
if (!person.phoneNumber || !user.username) return false; if (!person.phoneNumber || !user.username) return false;
const stored = const stored =
normalizeIranMobile(person.phoneNumber) ?? person.phoneNumber; normalizeIranMobile(person.phoneNumber) ?? person.phoneNumber;

View File

@@ -37,6 +37,22 @@ describe("party access queries", () => {
}, },
}); });
}); });
it("includes linked duplicate-account user ids", () => {
const linked = new Types.ObjectId();
const or = buildBlamePartyAccessOrConditions(
{ sub: String(userId), username: "09123456789" },
[String(linked)],
);
expect(or[0]).toEqual({
$or: expect.arrayContaining([
{ "parties.person.userId": String(userId) },
{ "parties.person.userId": userId },
{ "parties.person.userId": String(linked) },
{ "parties.person.userId": linked },
]),
});
});
}); });
describe("partyPersonMatchesUser", () => { describe("partyPersonMatchesUser", () => {
@@ -57,5 +73,16 @@ describe("party access queries", () => {
), ),
).toBe(true); ).toBe(true);
}); });
it("matches linked duplicate-account user ids", () => {
const linked = new Types.ObjectId();
expect(
partyPersonMatchesUser(
{ userId: linked, phoneNumber: "09111111111" },
{ sub: String(userId), username: "09999999999" },
[String(linked)],
),
).toBe(true);
});
}); });
}); });

View File

@@ -2,6 +2,36 @@ import { Types } from "mongoose";
import { RoleEnum } from "src/Types&Enums/role.enum"; import { RoleEnum } from "src/Types&Enums/role.enum";
import { iranMobileLookupVariants } from "./iran-mobile"; import { iranMobileLookupVariants } from "./iran-mobile";
/** ObjectId + string variants for one or more user ids (legacy duplicate accounts). */
export function collectUserIdVariants(
...ids: (string | Types.ObjectId | undefined | null)[]
): unknown[] {
const seen = new Set<string>();
const out: unknown[] = [];
for (const id of ids) {
if (id == null || id === "") continue;
const s = String(id);
if (seen.has(s)) continue;
seen.add(s);
out.push(s);
if (Types.ObjectId.isValid(s)) {
out.push(new Types.ObjectId(s));
}
}
return out;
}
export function buildUserIdFieldOrFilter(
fieldPath: string,
idVariants: unknown[],
): Record<string, unknown> | null {
if (!idVariants.length) return null;
if (idVariants.length === 1) {
return { [fieldPath]: idVariants[0] };
}
return { $or: idVariants.map((id) => ({ [fieldPath]: id })) };
}
/** /**
* Mongo filter matching a user id field stored as ObjectId or legacy string. * Mongo filter matching a user id field stored as ObjectId or legacy string.
*/ */
@@ -9,26 +39,23 @@ export function mongoUserIdEqualityFilter(
fieldPath: string, fieldPath: string,
userSub: string, userSub: string,
): Record<string, unknown> | null { ): Record<string, unknown> | null {
if (!userSub) return null; return buildUserIdFieldOrFilter(fieldPath, collectUserIdVariants(userSub));
const variants: unknown[] = [userSub];
if (Types.ObjectId.isValid(userSub)) {
variants.push(new Types.ObjectId(userSub));
}
if (variants.length === 1) {
return { [fieldPath]: variants[0] };
}
return { $or: variants.map((v) => ({ [fieldPath]: v })) };
} }
/** `$or` branches: user is a party on a blame case (by userId and/or phone). */ /** `$or` branches: user is a party on a blame case (by userId and/or phone). */
export function buildBlamePartyAccessOrConditions(user: { export function buildBlamePartyAccessOrConditions(
sub?: string; user: {
username?: string; sub?: string;
}): Record<string, unknown>[] { username?: string;
},
linkedUserIdStrings: string[] = [],
): Record<string, unknown>[] {
const or: Record<string, unknown>[] = []; const or: Record<string, unknown>[] = [];
const userIdCond = user?.sub const idVariants = collectUserIdVariants(user.sub, ...linkedUserIdStrings);
? mongoUserIdEqualityFilter("parties.person.userId", user.sub) const userIdCond = buildUserIdFieldOrFilter(
: null; "parties.person.userId",
idVariants,
);
if (userIdCond) or.push(userIdCond); if (userIdCond) or.push(userIdCond);
const phoneVariants = user?.username const phoneVariants = user?.username

View File

@@ -0,0 +1,52 @@
import { Types } from "mongoose";
import { buildUserLookupByPhone } from "./iran-mobile";
import { UserDbService } from "src/users/entities/db-service/user.db.service";
import { collectUserIdVariants } from "./party-access-queries";
/** All Mongo user ids that may represent the same person (JWT sub + phone aliases). */
export async function resolveLinkedUserIdStrings(
userDbService: UserDbService,
actor: { sub?: string; username?: string },
): Promise<string[]> {
const ids = new Set<string>();
if (actor?.sub) ids.add(String(actor.sub));
if (actor?.username?.trim()) {
const rows = await userDbService.findAllByPhone(actor.username);
for (const row of rows) {
const id = (row as { _id?: unknown })._id;
if (id != null) ids.add(String(id));
}
}
return [...ids];
}
export async function resolveLinkedUserIdVariants(
userDbService: UserDbService,
actor: { sub?: string; username?: string },
): Promise<unknown[]> {
const strings = await resolveLinkedUserIdStrings(userDbService, actor);
return collectUserIdVariants(...strings);
}
export function ownerUserIdMatchesActor(
ownerUserId: unknown,
linkedUserIdStrings: string[],
): boolean {
if (ownerUserId == null || linkedUserIdStrings.length === 0) return false;
const owner = String(ownerUserId);
return linkedUserIdStrings.some((id) => id === owner);
}
export function buildOwnerUserIdAccessFilter(
linkedUserIdVariants: unknown[],
): Record<string, unknown> | null {
if (!linkedUserIdVariants.length) return null;
if (linkedUserIdVariants.length === 1) {
return { "owner.userId": linkedUserIdVariants[0] };
}
return {
$or: linkedUserIdVariants.map((id) => ({ "owner.userId": id })),
};
}

View File

@@ -97,7 +97,11 @@ import {
normalizeIranMobile, normalizeIranMobile,
partyPersonMatchesUser, partyPersonMatchesUser,
} from "src/helpers/iran-mobile"; } from "src/helpers/iran-mobile";
import { buildBlamePartyAccessOrConditions } from "src/helpers/party-access-queries"; import {
buildBlamePartyAccessOrConditions,
collectUserIdVariants,
} from "src/helpers/party-access-queries";
import { resolveLinkedUserIdStrings } from "src/helpers/user-access-resolver";
@Injectable() @Injectable()
export class RequestManagementService { export class RequestManagementService {
@@ -4562,9 +4566,9 @@ export class RequestManagementService {
phone: string, phone: string,
otp: string, otp: string,
): Promise<Types.ObjectId> => { ): Promise<Types.ObjectId> => {
const user = await this.userDbService.findOne({ const user = await this.userDbService.findOne(
$or: [{ username: phone }, { mobile: phone }], buildUserLookupByPhone(phone),
}); );
if (!user) if (!user)
throw new BadRequestException(`User not found for phone: ${phone}`); throw new BadRequestException(`User not found for phone: ${phone}`);
const u = user as any; const u = user as any;
@@ -4783,9 +4787,9 @@ export class RequestManagementService {
// Verify the OTP and resolve the user id. // Verify the OTP and resolve the user id.
const now = Date.now(); const now = Date.now();
const user = await this.userDbService.findOne({ const user = await this.userDbService.findOne(
$or: [{ username: phone }, { mobile: phone }], buildUserLookupByPhone(phone),
}); );
if (!user) if (!user)
throw new BadRequestException(`User not found for phone: ${phone}`); throw new BadRequestException(`User not found for phone: ${phone}`);
const u = user as any; const u = user as any;
@@ -5029,7 +5033,14 @@ export class RequestManagementService {
query: ListQueryV2Dto = {}, query: ListQueryV2Dto = {},
): Promise<GetUserBlameListV2ResponseDto> { ): Promise<GetUserBlameListV2ResponseDto> {
try { try {
const orConditions = buildBlamePartyAccessOrConditions(user); const linkedUserIds =
user?.role === RoleEnum.USER || !user?.role
? await resolveLinkedUserIdStrings(this.userDbService, user)
: [];
const orConditions = buildBlamePartyAccessOrConditions(
user,
linkedUserIds,
);
if (user?.role === RoleEnum.FIELD_EXPERT && user?.sub) { if (user?.role === RoleEnum.FIELD_EXPERT && user?.sub) {
orConditions.push({ orConditions.push({
@@ -5043,6 +5054,34 @@ export class RequestManagementService {
}); });
} }
// Snapshot-party fallback: find blames via claims where this user appears
// in claim.snapshot.parties (the most reliable path for IN_PERSON flows).
const idVariants = collectUserIdVariants(
...(linkedUserIds.length > 0 ? linkedUserIds : [user?.sub].filter(Boolean)),
);
if (idVariants.length > 0) {
const snapshotFilter =
idVariants.length === 1
? { "snapshot.parties.person.userId": idVariants[0] }
: { "snapshot.parties.person.userId": { $in: idVariants } };
const linkedClaims = (await this.claimCaseDbService.find(
snapshotFilter as any,
{ lean: true, select: "blameRequestId owner.userId" },
)) as any[];
for (const c of linkedClaims) {
if (!c.blameRequestId) continue;
// Exclude if this user IS the owner (guilty party) — they already show
// up via the claim-owner route; we only want the damaged party here.
const ownerStr = c.owner?.userId ? String(c.owner.userId) : null;
const isOwner = ownerStr
? idVariants.some((id) => String(id) === ownerStr)
: false;
if (isOwner) continue;
orConditions.push({ _id: c.blameRequestId });
}
}
if (orConditions.length === 0) { if (orConditions.length === 0) {
return { list: [], total: 0 }; return { list: [], total: 0 };
} }
@@ -5067,7 +5106,7 @@ export class RequestManagementService {
String(req.initiatedByRegistrarId) === String(user.sub)); String(req.initiatedByRegistrarId) === String(user.sub));
const party = req.parties?.find((p: any) => const party = req.parties?.find((p: any) =>
partyPersonMatchesUser(p?.person, user), partyPersonMatchesUser(p?.person, user, linkedUserIds),
); );
const obj = req.toObject(); const obj = req.toObject();
@@ -5332,6 +5371,10 @@ export class RequestManagementService {
if (!req) { if (!req) {
throw new NotFoundException("Request not found"); throw new NotFoundException("Request not found");
} }
const linkedUserIds =
user?.role === RoleEnum.USER || !user?.role
? await resolveLinkedUserIdStrings(this.userDbService, user)
: [];
const isFieldExpertOwner = const isFieldExpertOwner =
req.expertInitiated && req.expertInitiated &&
req.initiatedByFieldExpertId && req.initiatedByFieldExpertId &&
@@ -5342,7 +5385,9 @@ export class RequestManagementService {
String(req.initiatedByRegistrarId) === String(user?.sub); String(req.initiatedByRegistrarId) === String(user?.sub);
const isParty = const isParty =
Array.isArray(req.parties) && Array.isArray(req.parties) &&
req.parties.some((p: any) => partyPersonMatchesUser(p?.person, user)); req.parties.some((p: any) =>
partyPersonMatchesUser(p?.person, user, linkedUserIds),
);
if (!isFieldExpertOwner && !isRegistrarOwner && !isParty) { if (!isFieldExpertOwner && !isRegistrarOwner && !isParty) {
throw new ForbiddenException("You do not have access to this request"); throw new ForbiddenException("You do not have access to this request");
} }
@@ -5392,7 +5437,7 @@ export class RequestManagementService {
isFieldExpertOwner || isRegistrarOwner isFieldExpertOwner || isRegistrarOwner
? allParties ? allParties
: allParties.filter((p: any) => : allParties.filter((p: any) =>
partyPersonMatchesUser(p?.person, user), partyPersonMatchesUser(p?.person, user, linkedUserIds),
); );
const parties = await Promise.all( const parties = await Promise.all(
visibleParties.map((p: any) => this.sanitizePartyForBlameUserView(p)), visibleParties.map((p: any) => this.sanitizePartyForBlameUserView(p)),
@@ -5586,7 +5631,8 @@ export class RequestManagementService {
role: PartyRole.FIRST, role: PartyRole.FIRST,
person: { person: {
userId: firstPartyUserId, userId: firstPartyUserId,
phoneNumber: formData.firstPartyPhoneNumber, phoneNumber: normalizeIranMobile(formData.firstPartyPhoneNumber) ??
formData.firstPartyPhoneNumber,
nationalCodeOfInsurer: firstPartyPlate.nationalCodeOfInsurer, nationalCodeOfInsurer: firstPartyPlate.nationalCodeOfInsurer,
nationalCodeOfDriver: firstPartyPlate.nationalCodeOfDriver, nationalCodeOfDriver: firstPartyPlate.nationalCodeOfDriver,
insurerLicense: firstPartyPlate.insurerLicense, insurerLicense: firstPartyPlate.insurerLicense,
@@ -5771,7 +5817,7 @@ export class RequestManagementService {
role, role,
person: { person: {
userId, userId,
phoneNumber, phoneNumber: normalizeIranMobile(phoneNumber) ?? phoneNumber,
nationalCodeOfInsurer: plateDto.nationalCodeOfInsurer, nationalCodeOfInsurer: plateDto.nationalCodeOfInsurer,
nationalCodeOfDriver: plateDto.nationalCodeOfDriver, nationalCodeOfDriver: plateDto.nationalCodeOfDriver,
insurerLicense: plateDto.insurerLicense, insurerLicense: plateDto.insurerLicense,

View File

@@ -1,6 +1,7 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose"; import { InjectModel } from "@nestjs/mongoose";
import { FilterQuery, Model, UpdateQuery } from "mongoose"; import { FilterQuery, Model, UpdateQuery } from "mongoose";
import { buildUserLookupByPhone } from "src/helpers/iran-mobile";
import { UserDocument, UserModel } from "../schema/user.schema"; import { UserDocument, UserModel } from "../schema/user.schema";
@Injectable() @Injectable()
@@ -17,6 +18,12 @@ export class UserDbService {
return await this.userModel.findOne(user); return await this.userModel.findOne(user);
} }
async findAllByPhone(phone: string): Promise<UserModel[]> {
return (await this.userModel
.find(buildUserLookupByPhone(phone))
.lean()) as UserModel[];
}
async findPlate(plate: number) { async findPlate(plate: number) {
return await this.userModel.find({ "plates.leftDigits": { $in: plate } }); return await this.userModel.find({ "plates.leftDigits": { $in: plate } });
} }