forked from Yara724/api
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:
@@ -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 = {
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
139
src/fanavaran/fanavaran-location.service.ts
Normal file
139
src/fanavaran/fanavaran-location.service.ts
Normal 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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user