diff --git a/src/client/client.service.ts b/src/client/client.service.ts index 6c20a7c..73f4d08 100644 --- a/src/client/client.service.ts +++ b/src/client/client.service.ts @@ -114,6 +114,7 @@ export class ClientService { ) {} async addClient(client: ClientDto): Promise { try { + const smsApiKey = client.property?.smsApiKey?.trim(); const newClient = await this.clientDbService.create({ clientCode: client.clientCode, @@ -129,9 +130,7 @@ export class ClientService { : (client.clientName?.english ?? null), }, - property: { - smsApiKey: client.property?.smsApiKey ?? null, - }, + ...(smsApiKey ? { property: { smsApiKey } } : {}), useExpertMode: client.useExpertMode ?? null, }); diff --git a/src/client/dto/create-client.dto.ts b/src/client/dto/create-client.dto.ts index 9a25ef0..5e21fcc 100644 --- a/src/client/dto/create-client.dto.ts +++ b/src/client/dto/create-client.dto.ts @@ -23,8 +23,9 @@ class ClientName { } class Property { @ApiPropertyOptional({}) + @IsOptional() @IsString() - smsApiKey: string; + smsApiKey?: string; } export class ClientDto { diff --git a/src/client/entities/db-service/client.db.service.ts b/src/client/entities/db-service/client.db.service.ts index 24978f7..5276d7a 100644 --- a/src/client/entities/db-service/client.db.service.ts +++ b/src/client/entities/db-service/client.db.service.ts @@ -1,15 +1,49 @@ -import { Injectable } from "@nestjs/common"; +import { Injectable, Logger, OnModuleInit } from "@nestjs/common"; import { InjectModel } from "@nestjs/mongoose"; import { FilterQuery, Model, UpdateQuery } from "mongoose"; import { ClientModel, ClientDocument } from "../schema/client.schema"; @Injectable() -export class ClientDbService { +export class ClientDbService implements OnModuleInit { + private readonly logger = new Logger(ClientDbService.name); + constructor( @InjectModel(ClientModel.name) private readonly clientModel: Model, ) {} + async onModuleInit(): Promise { + await this.dropLegacyPropertyUniqueIndex(); + } + + /** + * `property` was once declared `unique: true` in the schema. Production keeps + * `autoIndex: false`, so the stale unique index remained and rejected a second + * client with a missing/null SMS key (E11000 duplicate key). + */ + private async dropLegacyPropertyUniqueIndex(): Promise { + try { + const indexes = await this.clientModel.collection.indexes(); + for (const index of indexes) { + if (!index.unique || !index.name || index.name === "_id_") continue; + + const keys = Object.keys(index.key ?? {}); + const isLegacyPropertyIndex = + keys.length === 1 && + (keys[0] === "property" || keys[0] === "property.smsApiKey"); + + if (!isLegacyPropertyIndex) continue; + + await this.clientModel.collection.dropIndex(index.name); + this.logger.warn( + `Dropped legacy unique index on clients.${keys[0]}: ${index.name}`, + ); + } + } catch (err) { + this.logger.error("Failed to drop legacy client property index", err); + } + } + async create(client: ClientModel): Promise { return await this.clientModel.create(client); } diff --git a/src/client/entities/schema/client.schema.ts b/src/client/entities/schema/client.schema.ts index 1e11d52..ec79296 100644 --- a/src/client/entities/schema/client.schema.ts +++ b/src/client/entities/schema/client.schema.ts @@ -96,9 +96,9 @@ export class ClientModel { english: string; }; - @Prop({ required: false, unique: false, type: Object }) - property: { - smsApiKey: string; + @Prop({ required: false, type: Object }) + property?: { + smsApiKey?: string; }; @Prop({ required: true, unique: false }) diff --git a/src/request-management/request-management.service.ts b/src/request-management/request-management.service.ts index 5d94ddf..70f6f59 100644 --- a/src/request-management/request-management.service.ts +++ b/src/request-management/request-management.service.ts @@ -4061,9 +4061,6 @@ export class RequestManagementService { persian: data.clientName, english: null, }, - property: { - smsApiKey: null, - }, useExpertMode: "genuine", }); } catch (err) { diff --git a/src/sand-hub/sand-hub.service.ts b/src/sand-hub/sand-hub.service.ts index 3df17c4..99d342b 100644 --- a/src/sand-hub/sand-hub.service.ts +++ b/src/sand-hub/sand-hub.service.ts @@ -20,6 +20,9 @@ import { firstValueFrom } from "rxjs"; @Injectable() export class SandHubService { + private static readonly ESG_INQUIRY_UNAVAILABLE_MESSAGE = + "استعلام در دسترس نیست"; + private readonly logger = new Logger(SandHubService.name); private loginToken: string | null = null; private tokenExpiry: Date | null = null; @@ -528,17 +531,13 @@ export class SandHubService { if (!raw) return raw; if (raw?.success === false) { + this.logger.warn( + "ESG policyByPlate inquiry returned success=false", + raw, + ); return { Error: { - Message: - raw?.error?.message || - raw?.message || - "ESG policyByPlate inquiry returned an error", - Code: raw?.error?.code || raw?.error?.providerCode || "ESG_INQUIRY_ERROR", - ProviderMessage: raw?.error?.providerMessage, - ProviderCode: raw?.error?.providerCode, - TrackingCode: raw?.trackingCode, - Conflict: raw?.error?.conflict, + Message: SandHubService.ESG_INQUIRY_UNAVAILABLE_MESSAGE, }, }; } @@ -622,10 +621,9 @@ export class SandHubService { private mapEsgPersonInquiryToOldFormat(raw: any): Record { if (raw?.success === false) { - throw new NotFoundException( - raw?.error?.message || - raw?.message || - "Personal inquiry failed: Record not found for the given national code and birth date.", + this.logger.warn("ESG person inquiry returned success=false", raw); + throw new BadRequestException( + SandHubService.ESG_INQUIRY_UNAVAILABLE_MESSAGE, ); } @@ -648,10 +646,9 @@ export class SandHubService { private mapEsgShebaInquiryToOldFormat(raw: any): Record { if (raw?.success === false) { + this.logger.warn("ESG sheba inquiry returned success=false", raw); throw new BadRequestException( - raw?.error?.message || - raw?.message || - "Sheba ID validation failed. The provided Sheba ID does not match the national ID.", + SandHubService.ESG_INQUIRY_UNAVAILABLE_MESSAGE, ); }