forked from Yara724/api
188 lines
5.8 KiB
TypeScript
188 lines
5.8 KiB
TypeScript
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.",
|
|
);
|
|
}
|
|
}
|
|
|
|
async fileMakerBranchId(
|
|
fileMakerId: string | null | undefined,
|
|
): Promise<string | undefined> {
|
|
if (!fileMakerId) return undefined;
|
|
const doc = await this.fileMakerDbService.findById(String(fileMakerId));
|
|
const branchId = (doc as any)?.branchId;
|
|
return branchId ? String(branchId) : undefined;
|
|
}
|
|
|
|
async fileReviewerBranchId(
|
|
fileReviewerId: string | null | undefined,
|
|
): Promise<string | undefined> {
|
|
if (!fileReviewerId) return undefined;
|
|
const doc = await this.fileReviewerDbService.findById(
|
|
String(fileReviewerId),
|
|
);
|
|
const branchId = (doc as any)?.branchId;
|
|
return branchId ? String(branchId) : undefined;
|
|
}
|
|
|
|
/**
|
|
* V4/V5 case visibility is branch-scoped by the FileMaker who created it.
|
|
* Missing branch assignments are denied instead of widening visibility.
|
|
*/
|
|
async assertMakerReviewerBranchCompatible(input: {
|
|
fileMakerId?: string | null;
|
|
fileReviewerId?: string | null;
|
|
caseBranchId?: string | null;
|
|
}): Promise<void> {
|
|
const reviewerBranchId = await this.fileReviewerBranchId(
|
|
input.fileReviewerId,
|
|
);
|
|
const caseBranchId =
|
|
(input.caseBranchId ? String(input.caseBranchId) : undefined) ??
|
|
(await this.fileMakerBranchId(input.fileMakerId));
|
|
|
|
if (!reviewerBranchId) {
|
|
throw new ForbiddenException(
|
|
"FileReviewer account is not assigned to a branch.",
|
|
);
|
|
}
|
|
if (!caseBranchId || caseBranchId !== reviewerBranchId) {
|
|
throw new ForbiddenException(
|
|
"This file belongs to another branch.",
|
|
);
|
|
}
|
|
}
|
|
|
|
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 [];
|
|
}
|
|
}
|
|
}
|