import { BadGatewayException, GoneException, Injectable } from "@nestjs/common"; import { Types } from "mongoose"; import { ClientDto, ClientDtoRs, ClientLists, } from "src/client/dto/create-client.dto"; import { ClientDbService } from "./entities/db-service/client.db.service"; /** * System-wide default applied when a client document has no * `settings.carBodyAccidentMaxAgeDays` value yet. Keep it conservative so the * gate is enforced even for older / unconfigured clients. */ export const DEFAULT_CAR_BODY_ACCIDENT_MAX_AGE_DAYS = 7; /** * Media kinds we enforce upload-size bounds for. Each kind has its own * default window (see {@link DEFAULT_MEDIA_LIMITS}) and an optional * per-client override under `settings.media.`. */ export type MediaKind = "video" | "image" | "voice"; export interface MediaLimits { /** Inclusive lower bound. `0` allows any size from zero up. */ minBytes: number; /** Inclusive upper bound. */ maxBytes: number; } /** * System defaults applied when a client (or kind) has no explicit override. * * Important: each upload route also has a `multer.limits.fileSize` hard * ceiling — those route ceilings are the absolute maximum the API can * receive. Per-client `maxBytes` cannot legitimately go above its route's * multer ceiling, so the defaults below are kept at-or-below those. */ export const DEFAULT_MEDIA_LIMITS: Record = { video: { minBytes: 256 * 1024, maxBytes: 20 * 1024 * 1024 }, image: { minBytes: 5 * 1024, maxBytes: 8 * 1024 * 1024 }, voice: { minBytes: 5 * 1024, maxBytes: 8 * 1024 * 1024 }, }; @Injectable() export class ClientService { constructor(private readonly clientDbService: ClientDbService) {} async addClient(client: ClientDto): Promise { try { const newClient = await this.clientDbService.create({ clientCode: client.clientCode, clientName: { persian: client.clientName.persian, english: client.clientName.english || null, }, property: { smsApiKey: client.property.smsApiKey || null, }, useExpertMode: client.useExpertMode || null, }); if (newClient) return new ClientDtoRs(newClient); else throw new GoneException("database not connected"); } catch (er) { throw new BadGatewayException(er.errors); } } findOne(filter) { return this.clientDbService.findOne(filter); } async findClientWithPersianName(filter: string) { return await this.clientDbService.find({ "clientName.persian": filter }); } async findClientWithCompanyCode(companyCode: number) { return await this.clientDbService.find({ clientCode: companyCode }); } async getClients(): Promise { const clients = await this.clientDbService.findAll(); const show = clients.map((c) => new ClientDtoRs(c)); return show; } async getClientList(): Promise { const client = await this.clientDbService.findAll(); const list = client.map((element) => new ClientLists(element)); return list; } /** * Resolve the per-client byte bounds for a media kind. Missing bounds fall * back to {@link DEFAULT_MEDIA_LIMITS}. If no clientId is supplied (or it * doesn't resolve to a client document) the defaults are returned. * * The returned object is always fully populated — callers can compare * directly against `file.size`. */ async getMediaLimits( clientId: string | Types.ObjectId | null | undefined, kind: MediaKind, ): Promise { const defaults = DEFAULT_MEDIA_LIMITS[kind]; if (!clientId) return { ...defaults }; const idString = String(clientId); if (!Types.ObjectId.isValid(idString)) return { ...defaults }; const client = await this.clientDbService.findOne({ _id: new Types.ObjectId(idString), }); const override = client?.settings?.media?.[kind]; const minBytes = typeof override?.minBytes === "number" && Number.isFinite(override.minBytes) && override.minBytes >= 0 ? override.minBytes : defaults.minBytes; const maxBytes = typeof override?.maxBytes === "number" && Number.isFinite(override.maxBytes) && override.maxBytes > 0 ? override.maxBytes : defaults.maxBytes; return { minBytes, maxBytes }; } /** * Resolve the per-client CAR_BODY accident-age window (in days). * * Falls back to {@link DEFAULT_CAR_BODY_ACCIDENT_MAX_AGE_DAYS} when: * - no client id is supplied, * - the id is malformed, * - the client document is missing, * - the client has no `settings.carBodyAccidentMaxAgeDays` configured, * - or the configured value is not a positive finite number. */ async getCarBodyAccidentMaxAgeDays( clientId?: string | Types.ObjectId | null, ): Promise { if (!clientId) return DEFAULT_CAR_BODY_ACCIDENT_MAX_AGE_DAYS; const idString = String(clientId); if (!Types.ObjectId.isValid(idString)) { return DEFAULT_CAR_BODY_ACCIDENT_MAX_AGE_DAYS; } const client = await this.clientDbService.findOne({ _id: new Types.ObjectId(idString), }); const configured = client?.settings?.carBodyAccidentMaxAgeDays; if (typeof configured === "number" && Number.isFinite(configured) && configured > 0) { return configured; } return DEFAULT_CAR_BODY_ACCIDENT_MAX_AGE_DAYS; } }