forked from Yara724/api
Compare commits
5 Commits
f3c7f6a7e0
...
e2a9232523
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2a9232523 | ||
|
|
dc006735ba | ||
|
|
f92cee0575 | ||
|
|
493be68b80 | ||
|
|
edf027acd3 |
@@ -346,6 +346,14 @@ export class ClaimRequestManagementService {
|
|||||||
: this.isRequiredDocumentUploadedOnClaim(claimCase, k);
|
: this.isRequiredDocumentUploadedOnClaim(claimCase, k);
|
||||||
if (!ok) return false;
|
if (!ok) return false;
|
||||||
}
|
}
|
||||||
|
if (isCarBody) {
|
||||||
|
const g = ClaimRequiredDocumentType.CAR_GREEN_CARD;
|
||||||
|
const ok =
|
||||||
|
assumeUploadedKey === g
|
||||||
|
? true
|
||||||
|
: this.isRequiredDocumentUploadedOnClaim(claimCase, g);
|
||||||
|
if (!ok) return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,6 +370,14 @@ export class ClaimRequestManagementService {
|
|||||||
: this.isRequiredDocumentUploadedOnClaim(claimCase, k);
|
: this.isRequiredDocumentUploadedOnClaim(claimCase, k);
|
||||||
if (!ok) n++;
|
if (!ok) n++;
|
||||||
}
|
}
|
||||||
|
if (isCarBody) {
|
||||||
|
const g = ClaimRequiredDocumentType.CAR_GREEN_CARD;
|
||||||
|
const ok =
|
||||||
|
assumeUploadedKey === g
|
||||||
|
? true
|
||||||
|
: this.isRequiredDocumentUploadedOnClaim(claimCase, g);
|
||||||
|
if (!ok) n++;
|
||||||
|
}
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6281,8 +6297,8 @@ export class ClaimRequestManagementService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial flow: each document once. Expert resend: allow replacement.
|
// Initial flow: each document once. Expert resend and v3 in-person: allow replacement.
|
||||||
if (!isResendUpload) {
|
if (!isResendUpload && !options?.v3InPersonFlow) {
|
||||||
const existingDoc =
|
const existingDoc =
|
||||||
(claimCase.requiredDocuments as any)?.get?.(body.documentKey) ??
|
(claimCase.requiredDocuments as any)?.get?.(body.documentKey) ??
|
||||||
(claimCase.requiredDocuments as any)?.[body.documentKey];
|
(claimCase.requiredDocuments as any)?.[body.documentKey];
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
UploadRequiredDocumentV2Dto,
|
UploadRequiredDocumentV2Dto,
|
||||||
UploadRequiredDocumentV2ResponseDto,
|
UploadRequiredDocumentV2ResponseDto,
|
||||||
} from "src/claim-request-management/dto/upload-document-v2.dto";
|
} from "src/claim-request-management/dto/upload-document-v2.dto";
|
||||||
|
import { GetCaptureRequirementsV2ResponseDto } from "src/claim-request-management/dto/capture-requirements-v2.dto";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* V4 FileMaker flow — first half of the split blame workflow.
|
* V4 FileMaker flow — first half of the split blame workflow.
|
||||||
@@ -365,4 +366,24 @@ export class FileMakerBlameV4Controller {
|
|||||||
fileMaker,
|
fileMaker,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get("capture-requirements/:claimRequestId")
|
||||||
|
@ApiParam({ name: "claimRequestId" })
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Capture requirements (step-aware)",
|
||||||
|
description:
|
||||||
|
"Initial documents phase: pre-capture docs only (no chassis/engine/metal plate). " +
|
||||||
|
"CAPTURE_PART_DAMAGES phase: damaged parts + angles via capture-part, then chassis/engine/metal plate via upload-document. " +
|
||||||
|
"Use `captureSequencePhase` and `captureSequenceHint` to drive the UI.",
|
||||||
|
})
|
||||||
|
async getCaptureRequirements(
|
||||||
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
@CurrentUser() fileMaker: any,
|
||||||
|
): Promise<GetCaptureRequirementsV2ResponseDto> {
|
||||||
|
return this.claimRequestManagementService.getCaptureRequirementsV3(
|
||||||
|
claimRequestId,
|
||||||
|
fileMaker.sub,
|
||||||
|
fileMaker,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { StepsEnum } from "src/Types&Enums/blame-request-management/steps.enum";
|
||||||
import { ExpertDbService } from "src/users/entities/db-service/expert.db.service";
|
import { ExpertDbService } from "src/users/entities/db-service/expert.db.service";
|
||||||
import { UserDbService } from "src/users/entities/db-service/user.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 { parseIranLocalDateTime } from "src/helpers/iran-datetime";
|
||||||
import { applyListQueryV2 } from "src/helpers/list-query-v2";
|
import { applyListQueryV2 } from "src/helpers/list-query-v2";
|
||||||
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
||||||
@@ -2091,14 +2092,32 @@ export class RequestManagementService {
|
|||||||
(req.parties[secondIdx].person as any).phoneNumber = phoneNumber;
|
(req.parties[secondIdx].person as any).phoneNumber = phoneNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a valid OTP is already active for this number, return 200 immediately —
|
||||||
|
// no duplicate SMS, no unnecessary write, no error shown to the expert.
|
||||||
|
const canonicalPhoneAdv = normalizeIranMobile(phoneNumber) ?? phoneNumber;
|
||||||
|
const existingUserAdv = await this.userDbService.findOne(
|
||||||
|
buildUserLookupByPhone(canonicalPhoneAdv),
|
||||||
|
);
|
||||||
|
if (existingUserAdv && isOtpExpiryActive(existingUserAdv.otpExpire)) {
|
||||||
|
return {
|
||||||
|
requestId: req._id,
|
||||||
|
publicId: req.publicId,
|
||||||
|
workflow: req.workflow,
|
||||||
|
message: `OTP already sent to ${phoneNumber} and is still valid. Collect the code and call verify-party-otp.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Send OTP to second party — expert collects it from them in person
|
// Send OTP to second party — expert collects it from them in person
|
||||||
try {
|
try {
|
||||||
await this.userAuthService.sendOtpRequest(phoneNumber);
|
await this.userAuthService.sendOtpRequest(phoneNumber);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e?.message?.includes("Wait for expiry")) {
|
if (e?.message?.includes("Wait for expiry")) {
|
||||||
throw new BadRequestException(
|
return {
|
||||||
`(${phoneNumber}): OTP was recently sent. Wait for the expiry time before requesting again.`,
|
requestId: req._id,
|
||||||
);
|
publicId: req.publicId,
|
||||||
|
workflow: req.workflow,
|
||||||
|
message: `OTP already sent to ${phoneNumber} and is still valid. Collect the code and call verify-party-otp.`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@@ -4547,29 +4566,32 @@ export class RequestManagementService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const sent: string[] = [];
|
const sent: string[] = [];
|
||||||
|
|
||||||
|
// Helper: send OTP or silently succeed when a valid OTP already exists.
|
||||||
|
const sendOrSkipIfActive = async (phone: string): Promise<void> => {
|
||||||
|
const canonical = normalizeIranMobile(phone) ?? phone;
|
||||||
|
const existing = await this.userDbService.findOne(
|
||||||
|
buildUserLookupByPhone(canonical),
|
||||||
|
);
|
||||||
|
if (existing && isOtpExpiryActive(existing.otpExpire)) {
|
||||||
|
sent.push(phone); // already has a live OTP — treat as sent
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await this.userAuthService.sendOtpRequest(dto.firstPartyPhoneNumber);
|
await this.userAuthService.sendOtpRequest(phone);
|
||||||
sent.push(dto.firstPartyPhoneNumber);
|
sent.push(phone);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e?.message?.includes("Wait for expiry")) {
|
if (e?.message?.includes("Wait for expiry")) {
|
||||||
throw new BadRequestException(
|
sent.push(phone); // race: sendOtpRequest found it active too — treat as sent
|
||||||
`First party (${dto.firstPartyPhoneNumber}): OTP was recently sent. Wait for the expiry time before requesting again.`,
|
return;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await sendOrSkipIfActive(dto.firstPartyPhoneNumber);
|
||||||
if (dto.secondPartyPhoneNumber) {
|
if (dto.secondPartyPhoneNumber) {
|
||||||
try {
|
await sendOrSkipIfActive(dto.secondPartyPhoneNumber);
|
||||||
await this.userAuthService.sendOtpRequest(dto.secondPartyPhoneNumber);
|
|
||||||
sent.push(dto.secondPartyPhoneNumber);
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e?.message?.includes("Wait for expiry")) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
`Second party (${dto.secondPartyPhoneNumber}): OTP was recently sent. Wait for the expiry time before requesting again.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
sent: true,
|
sent: true,
|
||||||
@@ -4735,6 +4757,7 @@ export class RequestManagementService {
|
|||||||
const phone = (dto?.phoneNumber || "").trim();
|
const phone = (dto?.phoneNumber || "").trim();
|
||||||
if (!phone) throw new BadRequestException("phoneNumber is required");
|
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 twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000);
|
||||||
const recentlySentToPhone = (req.history || [])
|
const recentlySentToPhone = (req.history || [])
|
||||||
.slice()
|
.slice()
|
||||||
@@ -4747,18 +4770,34 @@ export class RequestManagementService {
|
|||||||
new Date(event.timestamp) > twoMinutesAgo,
|
new Date(event.timestamp) > twoMinutesAgo,
|
||||||
);
|
);
|
||||||
if (recentlySentToPhone) {
|
if (recentlySentToPhone) {
|
||||||
throw new BadRequestException(
|
return {
|
||||||
`(${phone}): OTP was sent recently. Please wait 2 minutes before requesting again.`,
|
sent: true,
|
||||||
|
message: `OTP already sent to ${phone} and is still valid. Collect the code from the party, then call verify-party-otp.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 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 return 200 immediately — no duplicate SMS, no DB writes.
|
||||||
|
const canonicalPhone = normalizeIranMobile(phone) ?? phone;
|
||||||
|
const existingUser = await this.userDbService.findOne(
|
||||||
|
buildUserLookupByPhone(canonicalPhone),
|
||||||
);
|
);
|
||||||
|
if (existingUser && isOtpExpiryActive(existingUser.otpExpire)) {
|
||||||
|
return {
|
||||||
|
sent: true,
|
||||||
|
message: `OTP already sent to ${phone} and is still valid. Collect the code from the party, then call verify-party-otp.`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.userAuthService.sendOtpRequest(phone);
|
await this.userAuthService.sendOtpRequest(phone);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e?.message?.includes("Wait for expiry")) {
|
if (e?.message?.includes("Wait for expiry")) {
|
||||||
throw new BadRequestException(
|
return {
|
||||||
`(${phone}): OTP was recently sent. Wait for the expiry time before requesting again.`,
|
sent: true,
|
||||||
);
|
message: `OTP already sent to ${phone} and is still valid. Collect the code from the party, then call verify-party-otp.`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user