Merge pull request 'locations dynamically in fanavaran' (#284) from s.hajizadeh/yara724api:main into main

Reviewed-on: Yara724/api#284
This commit is contained in:
2026-09-02 15:32:25 +03:30
10 changed files with 307 additions and 4 deletions

View File

@@ -102,6 +102,7 @@ import { ImageRequiredModel } from "./entites/schema/image-required.schema";
import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service"; import { DamageExpertDbService } from "src/users/entities/db-service/damage-expert.db.service";
import { FileMakerDbService } from "src/users/entities/db-service/file-maker.db.service"; import { FileMakerDbService } from "src/users/entities/db-service/file-maker.db.service";
import { FieldExpertDbService } from "src/users/entities/db-service/field-expert.db.service"; import { FieldExpertDbService } from "src/users/entities/db-service/field-expert.db.service";
import { FanavaranLocationService } from "src/fanavaran/fanavaran-location.service";
import { ExpertFileActivityDbService } from "src/users/entities/db-service/expert-file-activity.db.service"; import { ExpertFileActivityDbService } from "src/users/entities/db-service/expert-file-activity.db.service";
import { SandHubService } from "src/sand-hub/sand-hub.service"; import { SandHubService } from "src/sand-hub/sand-hub.service";
import { import {
@@ -393,6 +394,7 @@ export class ClaimRequestManagementService {
private readonly fileMakerDbService: FileMakerDbService, private readonly fileMakerDbService: FileMakerDbService,
private readonly fieldExpertDbService: FieldExpertDbService, private readonly fieldExpertDbService: FieldExpertDbService,
private readonly plateNormalizer: PlateNormalizerService, private readonly plateNormalizer: PlateNormalizerService,
private readonly fanavaranLocationService: FanavaranLocationService,
) {} ) {}
private requiredDocumentKeysV2(isCarBody: boolean): string[] { private requiredDocumentKeysV2(isCarBody: boolean): string[] {
@@ -4365,12 +4367,63 @@ export class ClaimRequestManagementService {
private async getFanavaranAuthHeaders( private async getFanavaranAuthHeaders(
clientKey: FanavaranClientKey, clientKey: FanavaranClientKey,
auditSession?: FanavaranAuditSession, auditSession?: FanavaranAuditSession,
claimCaseId?: string,
) { ) {
const caseId = claimCaseId ?? auditSession?.claimCaseId;
const locationOverride = caseId
? await this.resolveFanavaranLocationForClaim(caseId, clientKey)
: undefined;
return this.fanavaranAuthService.getRequestHeaders(clientKey, { return this.fanavaranAuthService.getRequestHeaders(clientKey, {
auditSession, auditSession,
locationOverride,
}); });
} }
/**
* Parsian + V4/V5 FileMaker flows: Location from file-maker.locations[].id,
* else file-reviewer.locations[].id, else tenant auth.location.
*/
private async resolveFanavaranLocationForClaim(
claimCaseId: string,
clientKey: FanavaranClientKey,
): Promise<string> {
const defaultLocation = getFanavaranClientProfile(clientKey).auth.location;
if (clientKey !== "parsian") {
return defaultLocation;
}
try {
const claimCase = await this.claimCaseDbService.findById(claimCaseId);
if (!claimCase?.blameRequestId) {
return defaultLocation;
}
const blame = await this.blameRequestDbService.findById(
String(claimCase.blameRequestId),
);
if (!(blame as any)?.isMadeByFileMaker) {
return defaultLocation;
}
return this.fanavaranLocationService.resolveBusinessLocation({
clientKey,
isMadeByFileMaker: true,
fileMakerId: (blame as any)?.initiatedByFieldExpertId
? String((blame as any).initiatedByFieldExpertId)
: null,
fileReviewerId: (blame as any)?.assignedFileReviewerId
? String((blame as any).assignedFileReviewerId)
: null,
});
} catch (error) {
this.logger.warn(
`[Fanavaran Location] Failed to resolve for claimCaseId=${claimCaseId}; using tenant default`,
error,
);
return defaultLocation;
}
}
private async getPolicyIdFromNationalCode( private async getPolicyIdFromNationalCode(
nationalCodeOfInsurer: string, nationalCodeOfInsurer: string,
config: { config: {
@@ -4417,6 +4470,7 @@ export class ClaimRequestManagementService {
const headers = await this.getFanavaranAuthHeaders( const headers = await this.getFanavaranAuthHeaders(
clientKey, clientKey,
auditSession, auditSession,
options?.claimCaseId,
); );
const requestHeaders = { const requestHeaders = {
...headers, ...headers,
@@ -5220,6 +5274,8 @@ export class ClaimRequestManagementService {
content, content,
[{ path: candidate.path, fileName: candidate.fileName }], [{ path: candidate.path, fileName: candidate.fileName }],
clientKey, clientKey,
undefined,
claimCaseId,
); );
this.logger.log( this.logger.log(
`${logPrefix} [${i + 1}/${pending.length}] SUCCESS: status=${response.status} body=${JSON.stringify(response.data)}`, `${logPrefix} [${i + 1}/${pending.length}] SUCCESS: status=${response.status} body=${JSON.stringify(response.data)}`,
@@ -6276,9 +6332,14 @@ export class ClaimRequestManagementService {
payload: Record<string, unknown>, payload: Record<string, unknown>,
clientKey: FanavaranClientKey, clientKey: FanavaranClientKey,
auditSession?: FanavaranAuditSession, auditSession?: FanavaranAuditSession,
claimCaseId?: string,
) { ) {
this.fanavaranAuthService.assertNotInBackoff(clientKey); this.fanavaranAuthService.assertNotInBackoff(clientKey);
const headers = await this.getFanavaranAuthHeaders(clientKey, auditSession); const headers = await this.getFanavaranAuthHeaders(
clientKey,
auditSession,
claimCaseId,
);
try { try {
const response = await firstValueFrom( const response = await firstValueFrom(
@@ -6303,9 +6364,14 @@ export class ClaimRequestManagementService {
files: Array<{ path: string; fileName: string }>, files: Array<{ path: string; fileName: string }>,
clientKey: FanavaranClientKey, clientKey: FanavaranClientKey,
auditSession?: FanavaranAuditSession, auditSession?: FanavaranAuditSession,
claimCaseId?: string,
) { ) {
this.fanavaranAuthService.assertNotInBackoff(clientKey); this.fanavaranAuthService.assertNotInBackoff(clientKey);
const headers = await this.getFanavaranAuthHeaders(clientKey, auditSession); const headers = await this.getFanavaranAuthHeaders(
clientKey,
auditSession,
claimCaseId,
);
const form = new FormData(); const form = new FormData();
form.append("Param", JSON.stringify(content), { form.append("Param", JSON.stringify(content), {
@@ -7242,6 +7308,7 @@ export class ClaimRequestManagementService {
const headers = await this.getFanavaranAuthHeaders( const headers = await this.getFanavaranAuthHeaders(
clientKey, clientKey,
auditSession, auditSession,
claimCaseId,
); );
const requestHeaders = { const requestHeaders = {

View File

@@ -17,6 +17,7 @@ import { RequestManagementModule } from "src/request-management/request-manageme
import { SandHubModule } from "src/sand-hub/sand-hub.module"; import { SandHubModule } from "src/sand-hub/sand-hub.module";
import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.module"; import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.module";
import { UsersModule } from "src/users/users.module"; import { UsersModule } from "src/users/users.module";
import { FanavaranLookupModule } from "src/fanavaran/fanavaran-lookup.module";
import { ExpertClaimController } from "./expert-claim.controller"; import { ExpertClaimController } from "./expert-claim.controller";
import { ExpertClaimV2Controller } from "./expert-claim.v2.controller"; import { ExpertClaimV2Controller } from "./expert-claim.v2.controller";
import { ExpertClaimService } from "./expert-claim.service"; import { ExpertClaimService } from "./expert-claim.service";
@@ -41,6 +42,7 @@ import { ExpertClaimService } from "./expert-claim.service";
SmsOrchestrationModule, SmsOrchestrationModule,
UsersModule, UsersModule,
ClientModule, ClientModule,
FanavaranLookupModule,
], ],
controllers: [ExpertClaimController, ExpertClaimV2Controller], controllers: [ExpertClaimController, ExpertClaimV2Controller],
providers: [ExpertClaimService, ClaimFactorsImageDbService], providers: [ExpertClaimService, ClaimFactorsImageDbService],

View File

@@ -25,6 +25,7 @@ import {
FanavaranAutoSubmitResult, FanavaranAutoSubmitResult,
FanavaranExpertiseSubmitResult, FanavaranExpertiseSubmitResult,
} from "src/claim-request-management/claim-request-management.service"; } from "src/claim-request-management/claim-request-management.service";
import { FanavaranLocationService } from "src/fanavaran/fanavaran-location.service";
import { ClaimSignDbService } from "src/claim-request-management/entites/db-service/claim-sign.db.service"; import { ClaimSignDbService } from "src/claim-request-management/entites/db-service/claim-sign.db.service";
import { DamageImageDbService } from "src/claim-request-management/entites/db-service/damage-image.db.service"; import { DamageImageDbService } from "src/claim-request-management/entites/db-service/damage-image.db.service";
import { VideoCaptureDbService } from "src/claim-request-management/entites/db-service/video-capture.db.service"; import { VideoCaptureDbService } from "src/claim-request-management/entites/db-service/video-capture.db.service";
@@ -291,6 +292,7 @@ export class ExpertClaimService {
private readonly expertFileActivityDbService: ExpertFileActivityDbService, private readonly expertFileActivityDbService: ExpertFileActivityDbService,
private readonly claimSignDbService: ClaimSignDbService, private readonly claimSignDbService: ClaimSignDbService,
private readonly claimRequestManagementService: ClaimRequestManagementService, private readonly claimRequestManagementService: ClaimRequestManagementService,
private readonly fanavaranLocationService: FanavaranLocationService,
) {} ) {}
private appendFanavaranAutoSubmitToMessage( private appendFanavaranAutoSubmitToMessage(
@@ -2590,6 +2592,12 @@ export class ExpertClaimService {
// Atomically claim it — findOneAndUpdate with null/missing guard // Atomically claim it — findOneAndUpdate with null/missing guard
const reviewerOid = new Types.ObjectId(actor.sub); const reviewerOid = new Types.ObjectId(actor.sub);
await this.fanavaranLocationService.assertMakerReviewerLocationCompatible({
fileMakerId: (blame as any)?.initiatedByFieldExpertId
? String((blame as any).initiatedByFieldExpertId)
: null,
fileReviewerId: actor.sub,
});
const updated = await this.blameRequestDbService.findOneAndUpdate( const updated = await this.blameRequestDbService.findOneAndUpdate(
{ {
_id: (blame as any)._id, _id: (blame as any)._id,

View File

@@ -1,8 +1,30 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsEmail, IsEnum, IsMongoId, IsNotEmpty, IsOptional, IsString } from "class-validator"; import { Type } from "class-transformer";
import {
IsArray,
IsEmail,
IsEnum,
IsMongoId,
IsNotEmpty,
IsOptional,
IsString,
ValidateNested,
} from "class-validator";
import { RoleEnum } from "src/Types&Enums/role.enum"; import { RoleEnum } from "src/Types&Enums/role.enum";
import { UserType } from "src/Types&Enums/userType.enum"; import { UserType } from "src/Types&Enums/userType.enum";
export class ExpertLocationDto {
@ApiProperty({ example: "210050", description: "Fanavaran Location / OpBUId" })
@IsString()
@IsNotEmpty()
id: string;
@ApiProperty({ example: "شعبه غرب" })
@IsString()
@IsNotEmpty()
name: string;
}
export class CreateInsurerExpertDto { export class CreateInsurerExpertDto {
@ApiProperty({ example: "Ali" }) @ApiProperty({ example: "Ali" })
@IsString() @IsString()
@@ -88,6 +110,17 @@ export class CreateFileMakerByInsurerDto extends CreateInsurerExpertDto {
default: RoleEnum.FILE_MAKER, default: RoleEnum.FILE_MAKER,
}) })
role?: RoleEnum.FILE_MAKER; role?: RoleEnum.FILE_MAKER;
@ApiPropertyOptional({
type: [ExpertLocationDto],
description:
"Fanavaran Location entries (id = OpBUId). Used for Parsian V4/V5 submits.",
})
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => ExpertLocationDto)
locations?: ExpertLocationDto[];
} }
export class CreateFileReviewerByInsurerDto extends CreateInsurerExpertDto { export class CreateFileReviewerByInsurerDto extends CreateInsurerExpertDto {
@@ -96,4 +129,15 @@ export class CreateFileReviewerByInsurerDto extends CreateInsurerExpertDto {
default: RoleEnum.FILE_REVIEWER, default: RoleEnum.FILE_REVIEWER,
}) })
role?: RoleEnum.FILE_REVIEWER; role?: RoleEnum.FILE_REVIEWER;
@ApiPropertyOptional({
type: [ExpertLocationDto],
description:
"Fanavaran Location entries. Must overlap FileMaker locations on Parsian V4/V5 cases.",
})
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => ExpertLocationDto)
locations?: ExpertLocationDto[];
} }

View File

@@ -2018,6 +2018,19 @@ export class ExpertInsurerService {
return rest; return rest;
} }
private normalizeExpertLocations(
locations?: Array<{ id?: string; name?: string }> | null,
): Array<{ id: string; name: string }> | undefined {
if (!Array.isArray(locations) || locations.length === 0) return undefined;
const normalized = locations
.map((entry) => ({
id: String(entry?.id ?? "").trim(),
name: String(entry?.name ?? "").trim(),
}))
.filter((entry) => entry.id && entry.name);
return normalized.length > 0 ? normalized : undefined;
}
private async createExpertForInsurer( private async createExpertForInsurer(
insurerClientKey: string, insurerClientKey: string,
payload: CreateInsurerExpertDto, payload: CreateInsurerExpertDto,
@@ -2132,6 +2145,7 @@ export class ExpertInsurerService {
} }
const hashedPassword = await this.hashService.hash(payload.password); const hashedPassword = await this.hashService.hash(payload.password);
const locations = this.normalizeExpertLocations(payload.locations);
const created = await this.fileMakerDbService.create({ const created = await this.fileMakerDbService.create({
...payload, ...payload,
email, email,
@@ -2140,6 +2154,7 @@ export class ExpertInsurerService {
role: RoleEnum.FILE_MAKER, role: RoleEnum.FILE_MAKER,
clientKey: clientObjectId, clientKey: clientObjectId,
branchId: new Types.ObjectId(payload.branchId), branchId: new Types.ObjectId(payload.branchId),
...(locations ? { locations } : {}),
}); });
return { return {
@@ -2184,6 +2199,7 @@ export class ExpertInsurerService {
} }
const hashedPassword = await this.hashService.hash(payload.password); const hashedPassword = await this.hashService.hash(payload.password);
const locations = this.normalizeExpertLocations(payload.locations);
const created = await this.fileReviewerDbService.create({ const created = await this.fileReviewerDbService.create({
...payload, ...payload,
email, email,
@@ -2192,6 +2208,7 @@ export class ExpertInsurerService {
role: RoleEnum.FILE_REVIEWER, role: RoleEnum.FILE_REVIEWER,
clientKey: clientObjectId, clientKey: clientObjectId,
branchId: new Types.ObjectId(payload.branchId), branchId: new Types.ObjectId(payload.branchId),
...(locations ? { locations } : {}),
}); });
return { return {

View File

@@ -249,6 +249,8 @@ export class FanavaranAuthService {
options?: { options?: {
auditSession?: FanavaranAuditSession; auditSession?: FanavaranAuditSession;
forceRefresh?: boolean; forceRefresh?: boolean;
/** Per-request Location override (does not affect token fingerprint). */
locationOverride?: string;
}, },
): Promise<{ ): Promise<{
authenticationToken: string; authenticationToken: string;
@@ -261,11 +263,12 @@ export class FanavaranAuthService {
clientKey, clientKey,
options, options,
); );
const locationOverride = options?.locationOverride?.trim();
return { return {
authenticationToken, authenticationToken,
CorpId: profile.auth.corpId, CorpId: profile.auth.corpId,
ContractId: profile.auth.contractId, ContractId: profile.auth.contractId,
Location: profile.auth.location, Location: locationOverride || profile.auth.location,
}; };
} }

View File

@@ -0,0 +1,139 @@
import { ForbiddenException, Injectable, Logger } from "@nestjs/common";
import {
FanavaranClientKey,
getFanavaranClientProfile,
resolveFanavaranClientKey,
} from "src/core/config/fanavaran-client.config";
import { FileMakerDbService } from "src/users/entities/db-service/file-maker.db.service";
import { FileReviewerDbService } from "src/users/entities/db-service/file-reviewer.db.service";
type LocationEntry = { id?: string; name?: string };
/**
* Parsian V4/V5 FileMaker flows: resolve Fanavaran HTTP `Location` from
* file-maker / file-reviewer `locations[].id`, and enforce maker/reviewer match.
*/
@Injectable()
export class FanavaranLocationService {
private readonly logger = new Logger(FanavaranLocationService.name);
constructor(
private readonly fileMakerDbService: FileMakerDbService,
private readonly fileReviewerDbService: FileReviewerDbService,
) {}
isParsianClient(clientKey?: FanavaranClientKey): boolean {
return (clientKey ?? resolveFanavaranClientKey()) === "parsian";
}
extractLocationIds(locations?: LocationEntry[] | null): string[] {
if (!Array.isArray(locations) || locations.length === 0) return [];
const ids: string[] = [];
for (const entry of locations) {
const id = String(entry?.id ?? "").trim();
if (id && !ids.includes(id)) ids.push(id);
}
return ids;
}
primaryLocationId(locations?: LocationEntry[] | null): string | undefined {
return this.extractLocationIds(locations)[0];
}
/**
* Maker primary location → reviewer primary → tenant auth.location.
* Non-parsian or non-FileMaker flows always use the tenant default.
*/
async resolveBusinessLocation(input: {
clientKey: FanavaranClientKey;
isMadeByFileMaker?: boolean;
fileMakerId?: string | null;
fileReviewerId?: string | null;
}): Promise<string> {
const defaultLocation = getFanavaranClientProfile(input.clientKey).auth
.location;
if (input.clientKey !== "parsian" || input.isMadeByFileMaker !== true) {
return defaultLocation;
}
const makerLocation = await this.loadPrimaryLocationId(input.fileMakerId, "maker");
if (makerLocation) {
this.logger.debug(
`Fanavaran Location from file-maker id=${input.fileMakerId} → ${makerLocation}`,
);
return makerLocation;
}
const reviewerLocation = await this.loadPrimaryLocationId(
input.fileReviewerId,
"reviewer",
);
if (reviewerLocation) {
this.logger.debug(
`Fanavaran Location from file-reviewer id=${input.fileReviewerId} → ${reviewerLocation}`,
);
return reviewerLocation;
}
this.logger.debug(
`Fanavaran Location fallback to tenant default → ${defaultLocation}`,
);
return defaultLocation;
}
/**
* When both maker and reviewer have locations, they must share at least one id.
* No-op for non-parsian deployments or when either side has no locations.
*/
async assertMakerReviewerLocationCompatible(input: {
fileMakerId?: string | null;
fileReviewerId?: string | null;
clientKey?: FanavaranClientKey;
}): Promise<void> {
if (!this.isParsianClient(input.clientKey)) {
return;
}
const makerIds = await this.loadLocationIds(input.fileMakerId, "maker");
const reviewerIds = await this.loadLocationIds(
input.fileReviewerId,
"reviewer",
);
if (makerIds.length === 0 || reviewerIds.length === 0) {
return;
}
const shared = makerIds.some((id) => reviewerIds.includes(id));
if (!shared) {
throw new ForbiddenException(
"FileReviewer location must match the FileMaker location for this case.",
);
}
}
private async loadPrimaryLocationId(
userId: string | null | undefined,
kind: "maker" | "reviewer",
): Promise<string | undefined> {
const ids = await this.loadLocationIds(userId, kind);
return ids[0];
}
private async loadLocationIds(
userId: string | null | undefined,
kind: "maker" | "reviewer",
): Promise<string[]> {
if (!userId) return [];
try {
const doc =
kind === "maker"
? await this.fileMakerDbService.findById(String(userId))
: await this.fileReviewerDbService.findById(String(userId));
return this.extractLocationIds((doc as any)?.locations);
} catch {
return [];
}
}
}

View File

@@ -7,9 +7,11 @@ import {
LookupModel, LookupModel,
LookupSchema, LookupSchema,
} from "src/lookups/entities/schema/lookup.schema"; } from "src/lookups/entities/schema/lookup.schema";
import { UsersModule } from "src/users/users.module";
import { FanavaranAuditModule } from "./fanavaran-audit.module"; import { FanavaranAuditModule } from "./fanavaran-audit.module";
import { FanavaranAuthService } from "./fanavaran-auth.service"; import { FanavaranAuthService } from "./fanavaran-auth.service";
import { FanavaranClientConfigService } from "./fanavaran-client-config.service"; import { FanavaranClientConfigService } from "./fanavaran-client-config.service";
import { FanavaranLocationService } from "./fanavaran-location.service";
import { FanavaranLookupService } from "./fanavaran-lookup.service"; import { FanavaranLookupService } from "./fanavaran-lookup.service";
import { import {
FanavaranAuthToken, FanavaranAuthToken,
@@ -36,16 +38,19 @@ import {
{ name: LookupModel.name, schema: LookupSchema }, { name: LookupModel.name, schema: LookupSchema },
]), ]),
FanavaranAuditModule, FanavaranAuditModule,
UsersModule,
], ],
providers: [ providers: [
FanavaranClientConfigService, FanavaranClientConfigService,
FanavaranAuthService, FanavaranAuthService,
FanavaranLookupService, FanavaranLookupService,
FanavaranLocationService,
], ],
exports: [ exports: [
FanavaranClientConfigService, FanavaranClientConfigService,
FanavaranAuthService, FanavaranAuthService,
FanavaranLookupService, FanavaranLookupService,
FanavaranLocationService,
], ],
}) })
export class FanavaranLookupModule {} export class FanavaranLookupModule {}

View File

@@ -9,6 +9,7 @@ import { RequestManagementDbService } from "src/request-management/entities/db-s
import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service"; import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service";
import { SandHubModule } from "src/sand-hub/sand-hub.module"; import { SandHubModule } from "src/sand-hub/sand-hub.module";
import { UsersModule } from "src/users/users.module"; import { UsersModule } from "src/users/users.module";
import { FanavaranLookupModule } from "src/fanavaran/fanavaran-lookup.module";
import { CronModule } from "src/utils/cron/cron.module"; import { CronModule } from "src/utils/cron/cron.module";
import { PublicIdModule } from "src/utils/public-id/public-id.module"; import { PublicIdModule } from "src/utils/public-id/public-id.module";
import { HashModule } from "src/utils/hash/hash.module"; import { HashModule } from "src/utils/hash/hash.module";
@@ -64,6 +65,7 @@ import { CallCenterBlameV6Controller } from "./call-center-blame-v6.controller";
PlateNormalizerModule, PlateNormalizerModule,
WorkflowStepManagementModule, WorkflowStepManagementModule,
PlatesModule, PlatesModule,
FanavaranLookupModule,
MulterModule.register({ MulterModule.register({
dest: "./files/video", dest: "./files/video",
}), }),

View File

@@ -48,6 +48,7 @@ import { ReqBlameStatus } from "src/Types&Enums/blame-request-management/status.
import { StepsEnum } from "src/Types&Enums/blame-request-management/steps.enum"; import { StepsEnum } from "src/Types&Enums/blame-request-management/steps.enum";
import { ExpertDbService } from "src/users/entities/db-service/expert.db.service"; import { ExpertDbService } from "src/users/entities/db-service/expert.db.service";
import { UserDbService } from "src/users/entities/db-service/user.db.service"; import { UserDbService } from "src/users/entities/db-service/user.db.service";
import { FanavaranLocationService } from "src/fanavaran/fanavaran-location.service";
import { isOtpExpiryActive } from "src/helpers/user-otp-expiry"; import { isOtpExpiryActive } from "src/helpers/user-otp-expiry";
import { parseIranLocalDateTime } from "src/helpers/iran-datetime"; import { parseIranLocalDateTime } from "src/helpers/iran-datetime";
import { applyListQueryV2 } from "src/helpers/list-query-v2"; import { applyListQueryV2 } from "src/helpers/list-query-v2";
@@ -617,6 +618,7 @@ export class RequestManagementService {
private readonly workflowStepDbService: WorkflowStepDbService, private readonly workflowStepDbService: WorkflowStepDbService,
private readonly hashService: HashService, private readonly hashService: HashService,
private readonly userAuthService: UserAuthService, private readonly userAuthService: UserAuthService,
private readonly fanavaranLocationService: FanavaranLocationService,
) {} ) {}
/** /**
@@ -4707,6 +4709,12 @@ export class RequestManagementService {
if (!assignedId) { if (!assignedId) {
// Atomically claim — ignore if another reviewer won the race (they would have // Atomically claim — ignore if another reviewer won the race (they would have
// been caught by the assignedId check above on their own first call). // been caught by the assignedId check above on their own first call).
await this.fanavaranLocationService.assertMakerReviewerLocationCompatible({
fileMakerId: req?.initiatedByFieldExpertId
? String(req.initiatedByFieldExpertId)
: null,
fileReviewerId: String(expert.sub),
});
await this.blameRequestDbService.findOneAndUpdate( await this.blameRequestDbService.findOneAndUpdate(
{ {
_id: req._id, _id: req._id,
@@ -4718,6 +4726,14 @@ export class RequestManagementService {
{ $set: { assignedFileReviewerId: new Types.ObjectId(String(expert.sub)) } }, { $set: { assignedFileReviewerId: new Types.ObjectId(String(expert.sub)) } },
{ new: false }, { new: false },
); );
} else {
// Already assigned to this reviewer — re-check location compatibility.
await this.fanavaranLocationService.assertMakerReviewerLocationCompatible({
fileMakerId: req?.initiatedByFieldExpertId
? String(req.initiatedByFieldExpertId)
: null,
fileReviewerId: String(expert.sub),
});
} }
return; return;
} }