diff --git a/src/request-management/request-management.service.ts b/src/request-management/request-management.service.ts index bcb4bef..a4b8168 100644 --- a/src/request-management/request-management.service.ts +++ b/src/request-management/request-management.service.ts @@ -47,6 +47,7 @@ import { ReqBlameStatus } from "src/Types&Enums/blame-request-management/status. import { StepsEnum } from "src/Types&Enums/blame-request-management/steps.enum"; import { ExpertDbService } from "src/users/entities/db-service/expert.db.service"; import { UserDbService } from "src/users/entities/db-service/user.db.service"; +import { isOtpExpiryActive } from "src/helpers/user-otp-expiry"; import { parseIranLocalDateTime } from "src/helpers/iran-datetime"; import { applyListQueryV2 } from "src/helpers/list-query-v2"; import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto"; @@ -4735,6 +4736,7 @@ export class RequestManagementService { const phone = (dto?.phoneNumber || "").trim(); if (!phone) throw new BadRequestException("phoneNumber is required"); + // --- Cooldown guard (in-memory history, catches already-persisted sends) --- const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000); const recentlySentToPhone = (req.history || []) .slice() @@ -4752,6 +4754,20 @@ export class RequestManagementService { ); } + // --- DB-level cooldown guard (catches burst/concurrent requests before history is saved) --- + // Read the user's otpExpire directly from the users collection. If a live + // OTP already exists we return 400 immediately — no SMS call, no DB writes. + const canonicalPhone = + normalizeIranMobile(phone) ?? phone; + const existingUser = await this.userDbService.findOne( + buildUserLookupByPhone(canonicalPhone), + ); + if (existingUser && isOtpExpiryActive(existingUser.otpExpire)) { + throw new BadRequestException( + `(${phone}): OTP was sent recently. Please wait before requesting again.`, + ); + } + try { await this.userAuthService.sendOtpRequest(phone); } catch (e: any) {