1
0
forked from Yara724/api

Compare commits

...

5 Commits

Author SHA1 Message Date
SepehrYahyaee
e2a9232523 YARA-1058 2026-07-01 12:23:34 +03:30
SepehrYahyaee
dc006735ba Duplicated capture-requirements endpoint for file-maker 2026-07-01 12:15:22 +03:30
SepehrYahyaee
f92cee0575 YARA-1075 2026-07-01 12:11:19 +03:30
SepehrYahyaee
493be68b80 YARA-1025 2026-07-01 12:04:39 +03:30
SepehrYahyaee
edf027acd3 YARA-1076 2026-07-01 12:00:29 +03:30
3 changed files with 103 additions and 27 deletions

View File

@@ -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];

View File

@@ -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,
);
}
} }

View File

@@ -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[] = [];
try {
await this.userAuthService.sendOtpRequest(dto.firstPartyPhoneNumber); // Helper: send OTP or silently succeed when a valid OTP already exists.
sent.push(dto.firstPartyPhoneNumber); const sendOrSkipIfActive = async (phone: string): Promise<void> => {
} catch (e: any) { const canonical = normalizeIranMobile(phone) ?? phone;
if (e?.message?.includes("Wait for expiry")) { const existing = await this.userDbService.findOne(
throw new BadRequestException( buildUserLookupByPhone(canonical),
`First party (${dto.firstPartyPhoneNumber}): OTP was recently sent. Wait for the expiry time before requesting again.`, );
); if (existing && isOtpExpiryActive(existing.otpExpire)) {
sent.push(phone); // already has a live OTP — treat as sent
return;
} }
throw e;
}
if (dto.secondPartyPhoneNumber) {
try { try {
await this.userAuthService.sendOtpRequest(dto.secondPartyPhoneNumber); await this.userAuthService.sendOtpRequest(phone);
sent.push(dto.secondPartyPhoneNumber); 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
`Second party (${dto.secondPartyPhoneNumber}): OTP was recently sent. Wait for the expiry time before requesting again.`, return;
);
} }
throw e; throw e;
} }
};
await sendOrSkipIfActive(dto.firstPartyPhoneNumber);
if (dto.secondPartyPhoneNumber) {
await sendOrSkipIfActive(dto.secondPartyPhoneNumber);
} }
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;
} }