locations dynamically in fanavaran

This commit is contained in:
2026-09-02 15:31:55 +03:30
parent ebfeef9e01
commit 5c2b660600
10 changed files with 307 additions and 4 deletions

View File

@@ -249,6 +249,8 @@ export class FanavaranAuthService {
options?: {
auditSession?: FanavaranAuditSession;
forceRefresh?: boolean;
/** Per-request Location override (does not affect token fingerprint). */
locationOverride?: string;
},
): Promise<{
authenticationToken: string;
@@ -261,11 +263,12 @@ export class FanavaranAuthService {
clientKey,
options,
);
const locationOverride = options?.locationOverride?.trim();
return {
authenticationToken,
CorpId: profile.auth.corpId,
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,
LookupSchema,
} from "src/lookups/entities/schema/lookup.schema";
import { UsersModule } from "src/users/users.module";
import { FanavaranAuditModule } from "./fanavaran-audit.module";
import { FanavaranAuthService } from "./fanavaran-auth.service";
import { FanavaranClientConfigService } from "./fanavaran-client-config.service";
import { FanavaranLocationService } from "./fanavaran-location.service";
import { FanavaranLookupService } from "./fanavaran-lookup.service";
import {
FanavaranAuthToken,
@@ -36,16 +38,19 @@ import {
{ name: LookupModel.name, schema: LookupSchema },
]),
FanavaranAuditModule,
UsersModule,
],
providers: [
FanavaranClientConfigService,
FanavaranAuthService,
FanavaranLookupService,
FanavaranLocationService,
],
exports: [
FanavaranClientConfigService,
FanavaranAuthService,
FanavaranLookupService,
FanavaranLocationService,
],
})
export class FanavaranLookupModule {}