added offline inquiry in system setting also fix some bugs

This commit is contained in:
2026-08-03 11:49:24 +03:30
parent ab4f667c8d
commit 7f672541ae
19 changed files with 965 additions and 26 deletions

View File

@@ -13,12 +13,38 @@ export class ExternalApisSettingsDto {
sandHubUseLiveApi?: boolean;
}
export class OfflineInquirySettingsDto {
@ApiPropertyOptional({
description:
"Master switch for seeded offline plate inquiries. When false, `offlineInquiries` hits are skipped.",
example: true,
})
@IsOptional()
@IsBoolean()
enabled?: boolean;
}
export class UpdateSystemSettingsDto {
@ApiPropertyOptional({ type: ExternalApisSettingsDto })
@IsOptional()
@ValidateNested()
@Type(() => ExternalApisSettingsDto)
externalApis?: ExternalApisSettingsDto;
@ApiPropertyOptional({ type: OfflineInquirySettingsDto })
@IsOptional()
@ValidateNested()
@Type(() => OfflineInquirySettingsDto)
offlineInquiry?: OfflineInquirySettingsDto;
}
export class UpdateOfflineInquirySettingsDto {
@ApiProperty({
description: "Enable or disable offline seeded plate inquiries globally",
example: true,
})
@IsBoolean()
enabled: boolean;
}
export class ExternalApisSettingsViewDto {
@@ -30,6 +56,15 @@ export class ExternalApisSettingsViewDto {
sandHubUseLiveApi: boolean;
}
export class OfflineInquirySettingsViewDto {
@ApiProperty({
description:
"Stored in `system_settings.offlineInquiry.enabled` — when false, offline seeds are ignored",
example: true,
})
enabled: boolean;
}
export class SystemSettingsResponseDto {
@ApiProperty({ example: "global" })
key: string;
@@ -37,6 +72,9 @@ export class SystemSettingsResponseDto {
@ApiProperty({ type: ExternalApisSettingsViewDto })
externalApis: ExternalApisSettingsViewDto;
@ApiProperty({ type: OfflineInquirySettingsViewDto })
offlineInquiry: OfflineInquirySettingsViewDto;
@ApiProperty({
description: "Human-readable mode for operators",
example: "mock",

View File

@@ -17,6 +17,16 @@ export class ExternalApisSettings {
sandHubUseLiveApi?: boolean;
}
/**
* Master switch for seeded offline plate inquiries (`offlineInquiries` collection).
* When false, lookup is skipped and live/mock inquiry paths run as usual.
* Default true when unset (seeds remain usable without an explicit migrate).
*/
export class OfflineInquirySettings {
@Prop({ type: Boolean, required: false, default: true })
enabled?: boolean;
}
@Schema({ collection: "system_settings", versionKey: false })
export class SystemSettingsModel {
@Prop({ required: true, unique: true, default: SYSTEM_SETTINGS_GLOBAL_KEY })
@@ -24,6 +34,9 @@ export class SystemSettingsModel {
@Prop({ type: ExternalApisSettings, required: false, default: {} })
externalApis?: ExternalApisSettings;
@Prop({ type: OfflineInquirySettings, required: false, default: {} })
offlineInquiry?: OfflineInquirySettings;
}
export const SystemSettingsSchema =

View File

@@ -1,4 +1,4 @@
import { Body, Controller, Get, Patch, UseGuards } from "@nestjs/common";
import { Body, Controller, Get, Patch, Post, UseGuards } from "@nestjs/common";
import {
ApiBearerAuth,
ApiOperation,
@@ -10,11 +10,23 @@ import { RolesGuard } from "src/auth/guards/role.guard";
import { Roles } from "src/decorators/roles.decorator";
import { RoleEnum } from "src/Types&Enums/role.enum";
import {
OfflineInquirySettingsViewDto,
SystemSettingsResponseDto,
UpdateOfflineInquirySettingsDto,
UpdateSystemSettingsDto,
} from "./dto/system-settings.dto";
import { SystemSettingsService } from "./system-settings.service";
const EXPERT_AND_ADMIN_ROLES = [
RoleEnum.ADMIN,
RoleEnum.SUPER_ADMIN,
RoleEnum.EXPERT,
RoleEnum.DAMAGE_EXPERT,
RoleEnum.FIELD_EXPERT,
RoleEnum.FILE_MAKER,
RoleEnum.FILE_REVIEWER,
] as const;
@ApiTags("system-settings")
@ApiBearerAuth()
@Controller("system-settings")
@@ -27,7 +39,7 @@ export class SystemSettingsController {
@ApiOperation({
summary: "Get global system settings (external API toggles)",
description:
"Shows whether SandHub/Tejarat live HTTP is enabled. When disabled, the API uses mocks so flows continue without external connectivity.",
"Shows whether SandHub/Tejarat live HTTP is enabled, and whether offline seeded inquiries are enabled.",
})
@ApiResponse({ status: 200, type: SystemSettingsResponseDto })
getSettings(): Promise<SystemSettingsResponseDto> {
@@ -40,7 +52,7 @@ export class SystemSettingsController {
@ApiOperation({
summary: "Update global system settings",
description:
"Set `externalApis.sandHubUseLiveApi` to true for live inquiries, false for mock/offline mode.",
"Set `externalApis.sandHubUseLiveApi` and/or `offlineInquiry.enabled`.",
})
@ApiResponse({ status: 200, type: SystemSettingsResponseDto })
updateSettings(
@@ -48,4 +60,55 @@ export class SystemSettingsController {
): Promise<SystemSettingsResponseDto> {
return this.systemSettingsService.updateSettings(body);
}
@Post("fanavaran-config/reload")
@UseGuards(SettingsJwtGuard, RolesGuard)
@Roles(...EXPERT_AND_ADMIN_ROLES)
@ApiOperation({
summary: "Reload Fanavaran client config cache from Mongo",
description:
"Re-reads `fanavaranClientConfigs` into the runtime profile cache. If auth fields changed, cached Fanavaran tokens are invalidated so the next call re-logins. Allowed for admin and expert roles.",
})
@ApiResponse({
status: 200,
description: "Cache reloaded",
schema: {
type: "object",
properties: {
ok: { type: "boolean", example: true },
message: { type: "string" },
},
},
})
reloadFanavaranConfigCache(): Promise<{ ok: true; message: string }> {
return this.systemSettingsService.reloadFanavaranConfigCache();
}
@Get("offline-inquiry")
@UseGuards(SettingsJwtGuard, RolesGuard)
@Roles(...EXPERT_AND_ADMIN_ROLES)
@ApiOperation({
summary: "Get offline inquiry master switch",
description:
"When enabled, matching `offlineInquiries` seeds short-circuit live plate inquiry. When disabled, live/mock paths run as usual.",
})
@ApiResponse({ status: 200, type: OfflineInquirySettingsViewDto })
getOfflineInquirySettings(): Promise<OfflineInquirySettingsViewDto> {
return this.systemSettingsService.getOfflineInquirySettings();
}
@Patch("offline-inquiry")
@UseGuards(SettingsJwtGuard, RolesGuard)
@Roles(...EXPERT_AND_ADMIN_ROLES)
@ApiOperation({
summary: "Enable or disable offline seeded plate inquiries",
description:
"Global master switch stored in `system_settings.offlineInquiry.enabled`. Per-seed `enabled` flags still apply when this is on.",
})
@ApiResponse({ status: 200, type: OfflineInquirySettingsViewDto })
updateOfflineInquirySettings(
@Body() body: UpdateOfflineInquirySettingsDto,
): Promise<OfflineInquirySettingsViewDto> {
return this.systemSettingsService.updateOfflineInquirySettings(body);
}
}

View File

@@ -1,5 +1,6 @@
import { Module } from "@nestjs/common";
import { MongooseModule } from "@nestjs/mongoose";
import { FanavaranLookupModule } from "src/fanavaran/fanavaran-lookup.module";
import { SystemSettingsDbService } from "./entities/db-service/system-settings.db.service";
import {
SystemSettingsModel,
@@ -13,6 +14,7 @@ import { SystemSettingsService } from "./system-settings.service";
MongooseModule.forFeature([
{ name: SystemSettingsModel.name, schema: SystemSettingsSchema },
]),
FanavaranLookupModule,
],
controllers: [SystemSettingsController],
providers: [SystemSettingsService, SystemSettingsDbService],

View File

@@ -5,12 +5,18 @@ describe("SystemSettingsService", () => {
findGlobal: jest.fn(),
upsertGlobal: jest.fn(),
};
const fanavaranClientConfigService = {
reloadCache: jest.fn().mockResolvedValue(undefined),
};
let service: SystemSettingsService;
beforeEach(() => {
jest.clearAllMocks();
service = new SystemSettingsService(db as any);
service = new SystemSettingsService(
db as any,
fanavaranClientConfigService as any,
);
});
it("defaults to mock when DB field is false", async () => {
@@ -28,4 +34,41 @@ describe("SystemSettingsService", () => {
});
await expect(service.isSandHubLiveEnabled()).resolves.toBe(true);
});
it("treats unset offlineInquiry as enabled", async () => {
db.findGlobal.mockResolvedValue({
key: "global",
externalApis: { sandHubUseLiveApi: false },
});
await expect(service.isOfflineInquiryEnabled()).resolves.toBe(true);
});
it("disables offline inquiry when explicitly false", async () => {
db.findGlobal.mockResolvedValue({
key: "global",
offlineInquiry: { enabled: false },
});
await expect(service.isOfflineInquiryEnabled()).resolves.toBe(false);
});
it("updates offline inquiry master switch", async () => {
db.upsertGlobal.mockResolvedValue({
key: "global",
offlineInquiry: { enabled: false },
});
await expect(
service.updateOfflineInquirySettings({ enabled: false }),
).resolves.toEqual({ enabled: false });
expect(db.upsertGlobal).toHaveBeenCalledWith({
$set: { "offlineInquiry.enabled": false },
});
});
it("reloads Fanavaran client config cache", async () => {
await expect(service.reloadFanavaranConfigCache()).resolves.toEqual({
ok: true,
message: "Fanavaran client config cache reloaded",
});
expect(fanavaranClientConfigService.reloadCache).toHaveBeenCalled();
});
});

View File

@@ -1,6 +1,9 @@
import { Injectable, Logger } from "@nestjs/common";
import { FanavaranClientConfigService } from "src/fanavaran/fanavaran-client-config.service";
import {
OfflineInquirySettingsViewDto,
SystemSettingsResponseDto,
UpdateOfflineInquirySettingsDto,
UpdateSystemSettingsDto,
} from "./dto/system-settings.dto";
import { SystemSettingsDbService } from "./entities/db-service/system-settings.db.service";
@@ -12,7 +15,10 @@ export class SystemSettingsService {
private cacheAt = 0;
private readonly cacheTtlMs = 15_000;
constructor(private readonly db: SystemSettingsDbService) {}
constructor(
private readonly db: SystemSettingsDbService,
private readonly fanavaranClientConfigService: FanavaranClientConfigService,
) {}
private async loadGlobal(): Promise<Record<string, unknown>> {
const now = Date.now();
@@ -22,10 +28,13 @@ export class SystemSettingsService {
let doc = await this.db.findGlobal();
if (!doc) {
doc = await this.db.upsertGlobal({
$set: { externalApis: { sandHubUseLiveApi: false } },
$set: {
externalApis: { sandHubUseLiveApi: false },
offlineInquiry: { enabled: true },
},
});
this.logger.log(
"Created default system_settings document (sandHubUseLiveApi=false)",
"Created default system_settings document (sandHubUseLiveApi=false, offlineInquiry.enabled=true)",
);
}
this.cache = doc;
@@ -38,6 +47,14 @@ export class SystemSettingsService {
this.cacheAt = 0;
}
private readOfflineInquiryEnabled(doc: Record<string, unknown>): boolean {
const offlineInquiry = doc.offlineInquiry as
| { enabled?: boolean }
| undefined;
// Unset → enabled (preserve prior always-on seed behavior).
return offlineInquiry?.enabled !== false;
}
/**
* Whether SandHub/Tejarat live HTTP should run (`system_settings` collection).
*/
@@ -49,6 +66,18 @@ export class SystemSettingsService {
return externalApis?.sandHubUseLiveApi === true;
}
/**
* Whether seeded offline plate inquiries may short-circuit live/mock inquiry.
*/
async isOfflineInquiryEnabled(): Promise<boolean> {
const doc = await this.loadGlobal();
return this.readOfflineInquiryEnabled(doc);
}
async getOfflineInquirySettings(): Promise<OfflineInquirySettingsViewDto> {
return { enabled: await this.isOfflineInquiryEnabled() };
}
async getSettingsView(): Promise<SystemSettingsResponseDto> {
const doc = await this.loadGlobal();
const externalApis = doc.externalApis as
@@ -58,6 +87,7 @@ export class SystemSettingsService {
return {
key: String(doc.key ?? "global"),
externalApis: { sandHubUseLiveApi },
offlineInquiry: { enabled: this.readOfflineInquiryEnabled(doc) },
sandHubMode: sandHubUseLiveApi ? "live" : "mock",
};
}
@@ -69,6 +99,9 @@ export class SystemSettingsService {
if (body.externalApis?.sandHubUseLiveApi !== undefined) {
$set["externalApis.sandHubUseLiveApi"] = body.externalApis.sandHubUseLiveApi;
}
if (body.offlineInquiry?.enabled !== undefined) {
$set["offlineInquiry.enabled"] = body.offlineInquiry.enabled;
}
if (Object.keys($set).length > 0) {
await this.db.upsertGlobal({ $set });
this.invalidateCache();
@@ -76,4 +109,30 @@ export class SystemSettingsService {
}
return this.getSettingsView();
}
async updateOfflineInquirySettings(
body: UpdateOfflineInquirySettingsDto,
): Promise<OfflineInquirySettingsViewDto> {
await this.db.upsertGlobal({
$set: { "offlineInquiry.enabled": body.enabled },
});
this.invalidateCache();
this.logger.log(
`Offline inquiry master switch set to enabled=${body.enabled}`,
);
return { enabled: body.enabled };
}
/**
* Re-read `fanavaranClientConfigs` into the in-memory profile cache.
* Auth fingerprint changes invalidate cached Fanavaran tokens.
*/
async reloadFanavaranConfigCache(): Promise<{ ok: true; message: string }> {
await this.fanavaranClientConfigService.reloadCache();
this.logger.log("Fanavaran client config cache reloaded via system-settings API");
return {
ok: true,
message: "Fanavaran client config cache reloaded",
};
}
}