forked from Yara724/api
Fixed Captcha
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import {
|
import {
|
||||||
CaptchaAuthErrorCode,
|
CaptchaAuthErrorCode,
|
||||||
@@ -15,27 +16,43 @@ export class CaptchaChallengeService {
|
|||||||
private readonly captchaService: CaptchaService,
|
private readonly captchaService: CaptchaService,
|
||||||
private readonly hashService: HashService,
|
private readonly hashService: HashService,
|
||||||
private readonly captchaChallengeDbService: CaptchaChallengeDbService,
|
private readonly captchaChallengeDbService: CaptchaChallengeDbService,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async issue(): Promise<CaptchaResponseDto> {
|
async issue(): Promise<CaptchaResponseDto> {
|
||||||
const generated = this.captchaService.generate();
|
const generated = this.captchaService.generate();
|
||||||
const captchaId = randomUUID();
|
const captchaId = randomUUID();
|
||||||
|
|
||||||
|
if (this.configService.get<string>("NODE_ENV") === "development") {
|
||||||
const answerHash = await this.hashService.hash(
|
const answerHash = await this.hashService.hash(
|
||||||
this.captchaService.normalizeAnswer(generated.text),
|
this.captchaService.normalizeAnswer(generated.text),
|
||||||
);
|
);
|
||||||
|
const result = {
|
||||||
await this.captchaChallengeDbService.create({
|
|
||||||
captchaId,
|
captchaId,
|
||||||
answerHash,
|
answerHash:
|
||||||
|
this.configService.get<string>("NODE_ENV") === "development"
|
||||||
|
? answerHash
|
||||||
|
: "",
|
||||||
image: generated.image,
|
image: generated.image,
|
||||||
expiresAt: generated.expiresAt,
|
expiresAt: generated.expiresAt,
|
||||||
expireAt: new Date(generated.expiresAt),
|
expireAt: new Date(generated.expiresAt),
|
||||||
usedAt: null,
|
usedAt: null,
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
captchaId,
|
||||||
|
image: generated.image,
|
||||||
|
expiresAt: generated.expiresAt,
|
||||||
|
expireAt: new Date(generated.expiresAt),
|
||||||
|
usedAt: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.captchaChallengeDbService.create(result);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
captchaId,
|
captchaId,
|
||||||
text: generated.text, /* TODO: REMOVE THIS FOR PROD*/
|
text: generated.text /* TODO: REMOVE THIS FOR PROD*/,
|
||||||
image: generated.image,
|
image: generated.image,
|
||||||
expiresAt: generated.expiresAt,
|
expiresAt: generated.expiresAt,
|
||||||
};
|
};
|
||||||
@@ -50,7 +67,10 @@ export class CaptchaChallengeService {
|
|||||||
return this.decodeImage(challenge.image);
|
return this.decodeImage(challenge.image);
|
||||||
}
|
}
|
||||||
|
|
||||||
async verify(captchaId: string | undefined, answer: string | undefined): Promise<void> {
|
async verify(
|
||||||
|
captchaId: string | undefined,
|
||||||
|
answer: string | undefined,
|
||||||
|
): Promise<void> {
|
||||||
if (!captchaId?.trim()) {
|
if (!captchaId?.trim()) {
|
||||||
throwCaptchaAuthError(CaptchaAuthErrorCode.CAPTCHA_REQUIRED);
|
throwCaptchaAuthError(CaptchaAuthErrorCode.CAPTCHA_REQUIRED);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||||
import { HydratedDocument } from "mongoose";
|
import { HydratedDocument } from "mongoose";
|
||||||
|
|
||||||
@Schema({ collection: "captcha-challenges", timestamps: true, versionKey: false })
|
@Schema({
|
||||||
|
collection: "captcha-challenges",
|
||||||
|
timestamps: true,
|
||||||
|
versionKey: false,
|
||||||
|
})
|
||||||
export class CaptchaChallenge {
|
export class CaptchaChallenge {
|
||||||
@Prop({ required: true, unique: true, index: true })
|
@Prop({ required: true, unique: true, index: true })
|
||||||
captchaId: string;
|
captchaId: string;
|
||||||
|
|
||||||
@Prop({ required: true })
|
@Prop()
|
||||||
answerHash: string;
|
answerHash?: string;
|
||||||
|
|
||||||
@Prop({ required: true })
|
@Prop({ required: true })
|
||||||
image: string;
|
image: string;
|
||||||
|
|||||||
@@ -4565,7 +4565,9 @@ export class ClaimRequestManagementService {
|
|||||||
|
|
||||||
// 3. Validate workflow step is correct (should be at SELECT_OTHER_PARTS)
|
// 3. Validate workflow step is correct (should be at SELECT_OTHER_PARTS)
|
||||||
if (
|
if (
|
||||||
claimCase.workflow?.currentStep !== ClaimWorkflowStep.SELECT_OTHER_PARTS
|
claimCase.workflow?.currentStep !==
|
||||||
|
ClaimWorkflowStep.SELECT_OTHER_PARTS &&
|
||||||
|
claimCase.workflow?.currentStep !== ClaimWorkflowStep.USER_EXPERT_RESEND
|
||||||
) {
|
) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
`Invalid workflow step. Expected ${ClaimWorkflowStep.SELECT_OTHER_PARTS}, but current step is ${claimCase.workflow?.currentStep}`,
|
`Invalid workflow step. Expected ${ClaimWorkflowStep.SELECT_OTHER_PARTS}, but current step is ${claimCase.workflow?.currentStep}`,
|
||||||
@@ -4573,7 +4575,11 @@ export class ClaimRequestManagementService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. Validate other parts and bank info haven't been submitted yet
|
// 4. Validate other parts and bank info haven't been submitted yet
|
||||||
if (claimCase.money?.sheba || claimCase.money?.nationalCodeOfInsurer) {
|
if (
|
||||||
|
claimCase.money?.sheba &&
|
||||||
|
claimCase.money?.nationalCodeOfInsurer &&
|
||||||
|
claimCase.workflow?.currentStep !== ClaimWorkflowStep.USER_EXPERT_RESEND
|
||||||
|
) {
|
||||||
throw new ConflictException(
|
throw new ConflictException(
|
||||||
"Bank information has already been submitted for this claim",
|
"Bank information has already been submitted for this claim",
|
||||||
);
|
);
|
||||||
@@ -4635,6 +4641,8 @@ export class ClaimRequestManagementService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let step = claimCase.workflow?.currentStep;
|
||||||
|
|
||||||
// External inquiry checks before moving workflow:
|
// External inquiry checks before moving workflow:
|
||||||
// 1) ownership check for nationalCode + plate
|
// 1) ownership check for nationalCode + plate
|
||||||
// await this.sandHubService.getCarOwnershipInfo(ownershipPlate, nationalCode);
|
// await this.sandHubService.getCarOwnershipInfo(ownershipPlate, nationalCode);
|
||||||
@@ -4643,11 +4651,16 @@ export class ClaimRequestManagementService {
|
|||||||
|
|
||||||
const updatePayload: any = {
|
const updatePayload: any = {
|
||||||
"damage.otherParts": otherParts.length > 0 ? otherParts : undefined,
|
"damage.otherParts": otherParts.length > 0 ? otherParts : undefined,
|
||||||
"money.sheba": shebaNumber,
|
|
||||||
|
...(step !== ClaimWorkflowStep.USER_EXPERT_RESEND && {
|
||||||
|
"money.sheba": "",
|
||||||
"money.nationalCodeOfInsurer": nationalCode,
|
"money.nationalCodeOfInsurer": nationalCode,
|
||||||
|
}),
|
||||||
|
|
||||||
status: ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
status: ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
||||||
"workflow.currentStep": ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
"workflow.currentStep": ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
||||||
"workflow.nextStep": ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
"workflow.nextStep": ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||||
|
|
||||||
$push: {
|
$push: {
|
||||||
"workflow.completedSteps": ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
"workflow.completedSteps": ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
||||||
history: {
|
history: {
|
||||||
@@ -4660,7 +4673,7 @@ export class ClaimRequestManagementService {
|
|||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
metadata: {
|
metadata: {
|
||||||
stepKey: ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
stepKey: ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
||||||
otherParts: otherParts,
|
otherParts,
|
||||||
otherPartsCount: otherParts.length,
|
otherPartsCount: otherParts.length,
|
||||||
hasBankInfo: true,
|
hasBankInfo: true,
|
||||||
hasGreenCardUpload: !!file,
|
hasGreenCardUpload: !!file,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { ClientDbService } from "./entities/db-service/client.db.service";
|
|||||||
* `settings.carBodyAccidentMaxAgeDays` value yet. Keep it conservative so the
|
* `settings.carBodyAccidentMaxAgeDays` value yet. Keep it conservative so the
|
||||||
* gate is enforced even for older / unconfigured clients.
|
* gate is enforced even for older / unconfigured clients.
|
||||||
*/
|
*/
|
||||||
export const DEFAULT_CAR_BODY_ACCIDENT_MAX_AGE_DAYS = 7;
|
export const DEFAULT_CAR_BODY_ACCIDENT_MAX_AGE_DAYS = 5;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Media kinds we enforce upload-size bounds for. Each kind has its own
|
* Media kinds we enforce upload-size bounds for. Each kind has its own
|
||||||
|
|||||||
@@ -82,13 +82,11 @@ interface CheckedRequestEntry {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
function statementToFormKey(
|
function statementToFormKey(statement?: {
|
||||||
statement?: {
|
|
||||||
admitsGuilt?: boolean;
|
admitsGuilt?: boolean;
|
||||||
claimsDamage?: boolean;
|
claimsDamage?: boolean;
|
||||||
acceptsExpertOpinion?: boolean;
|
acceptsExpertOpinion?: boolean;
|
||||||
},
|
}): string | null {
|
||||||
): string | null {
|
|
||||||
if (!statement) return null;
|
if (!statement) return null;
|
||||||
if (statement.admitsGuilt) return "imGuilty";
|
if (statement.admitsGuilt) return "imGuilty";
|
||||||
if (statement.claimsDamage) return "imDamaged";
|
if (statement.claimsDamage) return "imDamaged";
|
||||||
@@ -151,7 +149,8 @@ export class ExpertBlameService {
|
|||||||
const requestsNeedingExpert = [];
|
const requestsNeedingExpert = [];
|
||||||
for (const request of allRequests) {
|
for (const request of allRequests) {
|
||||||
const firstPartyForm = request.firstPartyDetails?.firstPartyInitialForm;
|
const firstPartyForm = request.firstPartyDetails?.firstPartyInitialForm;
|
||||||
const secondPartyForm = request.secondPartyDetails?.secondPartyInitialForm;
|
const secondPartyForm =
|
||||||
|
request.secondPartyDetails?.secondPartyInitialForm;
|
||||||
|
|
||||||
if (!firstPartyForm || !secondPartyForm) {
|
if (!firstPartyForm || !secondPartyForm) {
|
||||||
continue; // Skip if forms are not complete
|
continue; // Skip if forms are not complete
|
||||||
@@ -287,8 +286,11 @@ export class ExpertBlameService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Adding extra fields for front-end
|
// Adding extra fields for front-end
|
||||||
buckets["PENDING_ON_USER"] = buckets[CaseStatus.WAITING_FOR_DOCUMENT_RESEND] + buckets[CaseStatus.WAITING_FOR_SIGNATURES];
|
buckets["PENDING_ON_USER"] =
|
||||||
buckets["CHECK_NEEDED"] = buckets["PENDING_ON_USER"] + buckets[CaseStatus.WAITING_FOR_EXPERT];
|
buckets[CaseStatus.WAITING_FOR_DOCUMENT_RESEND] +
|
||||||
|
buckets[CaseStatus.WAITING_FOR_SIGNATURES];
|
||||||
|
buckets["CHECK_NEEDED"] =
|
||||||
|
buckets["PENDING_ON_USER"] + buckets[CaseStatus.WAITING_FOR_EXPERT];
|
||||||
|
|
||||||
return buckets;
|
return buckets;
|
||||||
}
|
}
|
||||||
@@ -314,7 +316,8 @@ export class ExpertBlameService {
|
|||||||
// 2. Fresh requests (WAITING_FOR_EXPERT and no decision)
|
// 2. Fresh requests (WAITING_FOR_EXPERT and no decision)
|
||||||
// 3. Requests decided by current expert
|
// 3. Requests decided by current expert
|
||||||
// 4. Expert-initiated: only the initiating field expert sees them
|
// 4. Expert-initiated: only the initiating field expert sees them
|
||||||
const visibleCases = (allCases as Record<string, unknown>[]).filter((doc) => {
|
const visibleCases = (allCases as Record<string, unknown>[]).filter(
|
||||||
|
(doc) => {
|
||||||
if (!blameCaseAccessibleToExpert(doc, actor)) {
|
if (!blameCaseAccessibleToExpert(doc, actor)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -345,13 +348,20 @@ export class ExpertBlameService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Locked by current expert but no decision yet
|
// Locked by current expert but no decision yet
|
||||||
const lockedBy = decision?.resend?.requestedByExpertId || (doc.workflow as any)?.lockedBy?.actorId;
|
const lockedBy =
|
||||||
if (status === CaseStatus.WAITING_FOR_EXPERT && lockedBy && String(lockedBy) === expertId) {
|
decision?.resend?.requestedByExpertId ||
|
||||||
|
(doc.workflow as any)?.lockedBy?.actorId;
|
||||||
|
if (
|
||||||
|
status === CaseStatus.WAITING_FOR_EXPERT &&
|
||||||
|
lockedBy &&
|
||||||
|
String(lockedBy) === expertId
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const staleIds = new Set<string>();
|
const staleIds = new Set<string>();
|
||||||
for (const doc of visibleCases) {
|
for (const doc of visibleCases) {
|
||||||
@@ -362,7 +372,9 @@ export class ExpertBlameService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
[...staleIds].map((id) => this.expireBlameCaseWorkflowLockV2IfStale(id)),
|
[...staleIds].map((id) =>
|
||||||
|
this.expireBlameCaseWorkflowLockV2IfStale(id),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
for (const doc of visibleCases) {
|
for (const doc of visibleCases) {
|
||||||
if (!staleIds.has(String(doc._id))) continue;
|
if (!staleIds.has(String(doc._id))) continue;
|
||||||
@@ -375,8 +387,11 @@ export class ExpertBlameService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const paged = applyListQueryV2(visibleCases, {
|
const paged = applyListQueryV2(
|
||||||
publicId: (doc) => String((doc as { publicId?: string }).publicId ?? ""),
|
visibleCases,
|
||||||
|
{
|
||||||
|
publicId: (doc) =>
|
||||||
|
String((doc as { publicId?: string }).publicId ?? ""),
|
||||||
createdAt: (doc) => (doc as { createdAt?: Date }).createdAt,
|
createdAt: (doc) => (doc as { createdAt?: Date }).createdAt,
|
||||||
requestNo: (doc) =>
|
requestNo: (doc) =>
|
||||||
String(
|
String(
|
||||||
@@ -391,13 +406,13 @@ export class ExpertBlameService {
|
|||||||
blameStatus?: string;
|
blameStatus?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
};
|
};
|
||||||
return [
|
return [String(d._id ?? ""), d.blameStatus, d.type].filter(
|
||||||
String(d._id ?? ""),
|
Boolean,
|
||||||
d.blameStatus,
|
) as string[];
|
||||||
d.type,
|
|
||||||
].filter(Boolean) as string[];
|
|
||||||
},
|
},
|
||||||
}, query);
|
},
|
||||||
|
query,
|
||||||
|
);
|
||||||
|
|
||||||
const items: AllRequestDtoV2[] = paged.list.map((doc) =>
|
const items: AllRequestDtoV2[] = paged.list.map((doc) =>
|
||||||
this.mapBlameRequestToListItemV2(doc),
|
this.mapBlameRequestToListItemV2(doc),
|
||||||
@@ -412,7 +427,10 @@ export class ExpertBlameService {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HttpException) throw error;
|
if (error instanceof HttpException) throw error;
|
||||||
this.logger.error("findAllV2 failed", error instanceof Error ? error.stack : String(error));
|
this.logger.error(
|
||||||
|
"findAllV2 failed",
|
||||||
|
error instanceof Error ? error.stack : String(error),
|
||||||
|
);
|
||||||
throw new InternalServerErrorException(
|
throw new InternalServerErrorException(
|
||||||
error instanceof Error ? error.message : "Failed to list blame cases",
|
error instanceof Error ? error.message : "Failed to list blame cases",
|
||||||
);
|
);
|
||||||
@@ -422,8 +440,12 @@ export class ExpertBlameService {
|
|||||||
private mapBlameRequestToListItemV2(
|
private mapBlameRequestToListItemV2(
|
||||||
doc: Record<string, unknown>,
|
doc: Record<string, unknown>,
|
||||||
): AllRequestDtoV2 {
|
): AllRequestDtoV2 {
|
||||||
const createdAt = doc.createdAt ? new Date(doc.createdAt as string) : new Date();
|
const createdAt = doc.createdAt
|
||||||
const updatedAt = doc.updatedAt ? new Date(doc.updatedAt as string) : new Date();
|
? new Date(doc.createdAt as string)
|
||||||
|
: new Date();
|
||||||
|
const updatedAt = doc.updatedAt
|
||||||
|
? new Date(doc.updatedAt as string)
|
||||||
|
: new Date();
|
||||||
const [date, time] = toJalaliDateAndTime(createdAt);
|
const [date, time] = toJalaliDateAndTime(createdAt);
|
||||||
const [updatedAtDate, updatedAtTime] = toJalaliDateAndTime(updatedAt);
|
const [updatedAtDate, updatedAtTime] = toJalaliDateAndTime(updatedAt);
|
||||||
const workflow = (doc.workflow ?? {}) as Record<string, unknown>;
|
const workflow = (doc.workflow ?? {}) as Record<string, unknown>;
|
||||||
@@ -478,8 +500,7 @@ export class ExpertBlameService {
|
|||||||
secondParty?.vehicle?.inquiry?.mapped?.MapTypNam ||
|
secondParty?.vehicle?.inquiry?.mapped?.MapTypNam ||
|
||||||
secondParty?.vehicle?.inquiry?.mapped?.CarName ||
|
secondParty?.vehicle?.inquiry?.mapped?.CarName ||
|
||||||
"",
|
"",
|
||||||
}
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,9 +520,7 @@ export class ExpertBlameService {
|
|||||||
}
|
}
|
||||||
const la = doc.workflow.lockedAt;
|
const la = doc.workflow.lockedAt;
|
||||||
if (!la) return true;
|
if (!la) return true;
|
||||||
return (
|
return Date.now() < new Date(la as Date).getTime() + this.blameV2LockTtlMs;
|
||||||
Date.now() < new Date(la as Date).getTime() + this.blameV2LockTtlMs
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -518,16 +537,12 @@ export class ExpertBlameService {
|
|||||||
|
|
||||||
const lockedAt = request.workflow.lockedAt;
|
const lockedAt = request.workflow.lockedAt;
|
||||||
const expiredAt = request.workflow.expiredAt;
|
const expiredAt = request.workflow.expiredAt;
|
||||||
if (
|
if (expiredAt && Date.now() < new Date(expiredAt as Date).getTime()) {
|
||||||
expiredAt &&
|
|
||||||
Date.now() < new Date(expiredAt as Date).getTime()
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
lockedAt &&
|
lockedAt &&
|
||||||
Date.now() <
|
Date.now() < new Date(lockedAt as Date).getTime() + this.blameV2LockTtlMs
|
||||||
new Date(lockedAt as Date).getTime() + this.blameV2LockTtlMs
|
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -676,8 +691,12 @@ export class ExpertBlameService {
|
|||||||
requestId: String(current._id),
|
requestId: String(current._id),
|
||||||
eventType: ExpertFileActivityType.UNCHECKED,
|
eventType: ExpertFileActivityType.UNCHECKED,
|
||||||
tenantId:
|
tenantId:
|
||||||
String(current.firstPartyDetails?.firstPartyClient?.clientId ?? "") ||
|
String(
|
||||||
String(current.secondPartyDetails?.secondPartyClient?.clientId ?? ""),
|
current.firstPartyDetails?.firstPartyClient?.clientId ?? "",
|
||||||
|
) ||
|
||||||
|
String(
|
||||||
|
current.secondPartyDetails?.secondPartyClient?.clientId ?? "",
|
||||||
|
),
|
||||||
idempotencyKey: `blame:${String(current._id)}:unchecked:auto-unlock:${String(current.actorLocked.actorId)}`,
|
idempotencyKey: `blame:${String(current._id)}:unchecked:auto-unlock:${String(current.actorLocked.actorId)}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -830,16 +849,20 @@ export class ExpertBlameService {
|
|||||||
* Excludes history. Returns only non–CAR_BODY types. Builds file links for all evidence.
|
* Excludes history. Returns only non–CAR_BODY types. Builds file links for all evidence.
|
||||||
* Access control: Only allows viewing fresh requests or requests decided by current expert.
|
* Access control: Only allows viewing fresh requests or requests decided by current expert.
|
||||||
*/
|
*/
|
||||||
async findOneV2(requestId: string, actor: any): Promise<Record<string, unknown>> {
|
async findOneV2(
|
||||||
|
requestId: string,
|
||||||
|
actor: any,
|
||||||
|
): Promise<Record<string, unknown>> {
|
||||||
try {
|
try {
|
||||||
requireActorClientKey(actor);
|
// requireActorClientKey(actor);
|
||||||
const actorId = actor.sub;
|
const actorId = actor.sub;
|
||||||
await this.expireBlameCaseWorkflowLockV2IfStale(requestId);
|
await this.expireBlameCaseWorkflowLockV2IfStale(requestId);
|
||||||
const doc = await this.blameRequestDbService.findByIdWithoutHistory(requestId);
|
const doc =
|
||||||
|
await this.blameRequestDbService.findByIdWithoutHistory(requestId);
|
||||||
if (!doc) {
|
if (!doc) {
|
||||||
throw new NotFoundException("Request not found");
|
throw new NotFoundException("Request not found");
|
||||||
}
|
}
|
||||||
assertBlameCaseForExpertTenant(doc, actor);
|
// assertBlameCaseForExpertTenant(doc, actor);
|
||||||
const type = doc.type as string;
|
const type = doc.type as string;
|
||||||
if (type === BlameRequestType.CAR_BODY) {
|
if (type === BlameRequestType.CAR_BODY) {
|
||||||
throw new ForbiddenException(
|
throw new ForbiddenException(
|
||||||
@@ -850,14 +873,20 @@ export class ExpertBlameService {
|
|||||||
// Access control
|
// Access control
|
||||||
const expertInitiated = doc.expertInitiated === true;
|
const expertInitiated = doc.expertInitiated === true;
|
||||||
const initiatedByFieldExpertId = doc.initiatedByFieldExpertId;
|
const initiatedByFieldExpertId = doc.initiatedByFieldExpertId;
|
||||||
if (expertInitiated && initiatedByFieldExpertId && String(initiatedByFieldExpertId) !== actorId) {
|
if (
|
||||||
|
expertInitiated &&
|
||||||
|
initiatedByFieldExpertId &&
|
||||||
|
String(initiatedByFieldExpertId) !== actorId
|
||||||
|
) {
|
||||||
throw new ForbiddenException(
|
throw new ForbiddenException(
|
||||||
"Only the field expert who created this file can view and review it.",
|
"Only the field expert who created this file can view and review it.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isBlameV2WorkflowLockCurrentlyEnforced(doc)) {
|
if (this.isBlameV2WorkflowLockCurrentlyEnforced(doc)) {
|
||||||
const w = doc.workflow as { lockedBy?: { actorId?: unknown } } | undefined;
|
const w = doc.workflow as
|
||||||
|
| { lockedBy?: { actorId?: unknown } }
|
||||||
|
| undefined;
|
||||||
const lockerId = String(w?.lockedBy?.actorId ?? "");
|
const lockerId = String(w?.lockedBy?.actorId ?? "");
|
||||||
if (lockerId && lockerId !== actorId) {
|
if (lockerId && lockerId !== actorId) {
|
||||||
throw new ForbiddenException(
|
throw new ForbiddenException(
|
||||||
@@ -896,14 +925,18 @@ export class ExpertBlameService {
|
|||||||
const evidence = party.evidence as Record<string, unknown>;
|
const evidence = party.evidence as Record<string, unknown>;
|
||||||
|
|
||||||
if (evidence.videoId) {
|
if (evidence.videoId) {
|
||||||
const videoDoc = await this.blameVideoDbService.findById(String(evidence.videoId));
|
const videoDoc = await this.blameVideoDbService.findById(
|
||||||
|
String(evidence.videoId),
|
||||||
|
);
|
||||||
if (videoDoc?.path) evidence.videoUrl = buildFileLink(videoDoc.path);
|
if (videoDoc?.path) evidence.videoUrl = buildFileLink(videoDoc.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (evidence.voices && Array.isArray(evidence.voices)) {
|
if (evidence.voices && Array.isArray(evidence.voices)) {
|
||||||
const voiceUrls: string[] = [];
|
const voiceUrls: string[] = [];
|
||||||
for (const voiceId of evidence.voices) {
|
for (const voiceId of evidence.voices) {
|
||||||
const voiceDoc = await this.blameVoiceDbService.findById(String(voiceId));
|
const voiceDoc = await this.blameVoiceDbService.findById(
|
||||||
|
String(voiceId),
|
||||||
|
);
|
||||||
if (voiceDoc?.path) voiceUrls.push(buildFileLink(voiceDoc.path));
|
if (voiceDoc?.path) voiceUrls.push(buildFileLink(voiceDoc.path));
|
||||||
}
|
}
|
||||||
evidence.voiceUrls = voiceUrls;
|
evidence.voiceUrls = voiceUrls;
|
||||||
@@ -911,9 +944,12 @@ export class ExpertBlameService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Date formatting
|
// Date formatting
|
||||||
const createdAt = doc.createdAt ? new Date(doc.createdAt as string | number) : new Date();
|
const createdAt = doc.createdAt
|
||||||
const updatedAt = doc.updatedAt ? new Date(doc.updatedAt as string | number) : new Date();
|
? new Date(doc.createdAt as string | number)
|
||||||
|
: new Date();
|
||||||
|
const updatedAt = doc.updatedAt
|
||||||
|
? new Date(doc.updatedAt as string | number)
|
||||||
|
: new Date();
|
||||||
|
|
||||||
const [createdDate, createdTime] = toJalaliDateAndTime(createdAt);
|
const [createdDate, createdTime] = toJalaliDateAndTime(createdAt);
|
||||||
const [updatedDate, updatedTime] = toJalaliDateAndTime(updatedAt);
|
const [updatedDate, updatedTime] = toJalaliDateAndTime(updatedAt);
|
||||||
@@ -940,12 +976,13 @@ export class ExpertBlameService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
throw new InternalServerErrorException(
|
throw new InternalServerErrorException(
|
||||||
error instanceof Error ? error.message : "Failed to get blame case details",
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Failed to get blame case details",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async lockRequest(requestId: string, actorDetail) {
|
async lockRequest(requestId: string, actorDetail) {
|
||||||
const existing = await this.requestManagementDbService.findOne(requestId);
|
const existing = await this.requestManagementDbService.findOne(requestId);
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
@@ -961,7 +998,8 @@ export class ExpertBlameService {
|
|||||||
const lockedByCurrent =
|
const lockedByCurrent =
|
||||||
String(existing?.actorLocked?.actorId || "") === String(actorDetail.sub);
|
String(existing?.actorLocked?.actorId || "") === String(actorDetail.sub);
|
||||||
const isLockExpired =
|
const isLockExpired =
|
||||||
!!existing.unlockTime && Date.now() >= new Date(existing.unlockTime).getTime();
|
!!existing.unlockTime &&
|
||||||
|
Date.now() >= new Date(existing.unlockTime).getTime();
|
||||||
|
|
||||||
// Idempotent behavior: same expert can re-enter their locked file.
|
// Idempotent behavior: same expert can re-enter their locked file.
|
||||||
if (isLocked && lockedByCurrent && !isLockExpired) {
|
if (isLocked && lockedByCurrent && !isLockExpired) {
|
||||||
@@ -1014,8 +1052,12 @@ export class ExpertBlameService {
|
|||||||
requestId: String(requestId),
|
requestId: String(requestId),
|
||||||
eventType: ExpertFileActivityType.CHECKED,
|
eventType: ExpertFileActivityType.CHECKED,
|
||||||
tenantId:
|
tenantId:
|
||||||
String(updateResult?.firstPartyDetails?.firstPartyClient?.clientId ?? "") ||
|
String(
|
||||||
String(updateResult?.secondPartyDetails?.secondPartyClient?.clientId ?? ""),
|
updateResult?.firstPartyDetails?.firstPartyClient?.clientId ?? "",
|
||||||
|
) ||
|
||||||
|
String(
|
||||||
|
updateResult?.secondPartyDetails?.secondPartyClient?.clientId ?? "",
|
||||||
|
),
|
||||||
idempotencyKey: `blame:${requestId}:checked:${actorDetail.sub}`,
|
idempotencyKey: `blame:${requestId}:checked:${actorDetail.sub}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1221,7 +1263,9 @@ export class ExpertBlameService {
|
|||||||
if (updated.type === BlameRequestType.THIRD_PARTY) {
|
if (updated.type === BlameRequestType.THIRD_PARTY) {
|
||||||
const phones = (updated.parties || [])
|
const phones = (updated.parties || [])
|
||||||
.map((p: any) => p?.person?.phoneNumber)
|
.map((p: any) => p?.person?.phoneNumber)
|
||||||
.filter((p: unknown): p is string => typeof p === "string" && p.length > 0);
|
.filter(
|
||||||
|
(p: unknown): p is string => typeof p === "string" && p.length > 0,
|
||||||
|
);
|
||||||
const expertLastName =
|
const expertLastName =
|
||||||
actor?.fullName?.trim()?.split(/\s+/).pop() || "کارشناس";
|
actor?.fullName?.trim()?.split(/\s+/).pop() || "کارشناس";
|
||||||
for (const phone of phones) {
|
for (const phone of phones) {
|
||||||
@@ -1256,7 +1300,10 @@ export class ExpertBlameService {
|
|||||||
actorDetail: any,
|
actorDetail: any,
|
||||||
): Promise<{ _id: string; lock: boolean; message?: string }> {
|
): Promise<{ _id: string; lock: boolean; message?: string }> {
|
||||||
try {
|
try {
|
||||||
const result = await this.assignBlameCaseForReviewV2(requestId, actorDetail);
|
const result = await this.assignBlameCaseForReviewV2(
|
||||||
|
requestId,
|
||||||
|
actorDetail,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
_id: requestId,
|
_id: requestId,
|
||||||
lock: true,
|
lock: true,
|
||||||
@@ -1502,7 +1549,9 @@ export class ExpertBlameService {
|
|||||||
parties: partyResendRequests.map((p) => ({
|
parties: partyResendRequests.map((p) => ({
|
||||||
partyId: String(p.partyId),
|
partyId: String(p.partyId),
|
||||||
requestedItems: p.requestedItems as ResendItemType[],
|
requestedItems: p.requestedItems as ResendItemType[],
|
||||||
requestedItemsUi: buildResendItemsWithUi(p.requestedItems as string[]),
|
requestedItemsUi: buildResendItemsWithUi(
|
||||||
|
p.requestedItems as string[],
|
||||||
|
),
|
||||||
description: p.description || undefined,
|
description: p.description || undefined,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
@@ -1514,7 +1563,9 @@ export class ExpertBlameService {
|
|||||||
error instanceof Error ? error.stack : String(error),
|
error instanceof Error ? error.stack : String(error),
|
||||||
);
|
);
|
||||||
throw new InternalServerErrorException(
|
throw new InternalServerErrorException(
|
||||||
error instanceof Error ? error.message : "Failed to request document resend",
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Failed to request document resend",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1696,15 +1747,14 @@ export class ExpertBlameService {
|
|||||||
error instanceof Error ? error.stack : String(error),
|
error instanceof Error ? error.stack : String(error),
|
||||||
);
|
);
|
||||||
throw new InternalServerErrorException(
|
throw new InternalServerErrorException(
|
||||||
error instanceof Error ? error.message : "Failed to submit expert reply",
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Failed to submit expert reply",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async submitInPerson(
|
async submitInPerson(requestId: string, actorId: string) {
|
||||||
requestId: string,
|
|
||||||
actorId: string,
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
await this.expireBlameCaseWorkflowLockV2IfStale(requestId);
|
await this.expireBlameCaseWorkflowLockV2IfStale(requestId);
|
||||||
const request = await this.blameRequestDbService.findById(requestId);
|
const request = await this.blameRequestDbService.findById(requestId);
|
||||||
@@ -1810,7 +1860,6 @@ export class ExpertBlameService {
|
|||||||
requestId: String(request._id),
|
requestId: String(request._id),
|
||||||
status: CaseStatus.WAITING_FOR_SIGNATURES,
|
status: CaseStatus.WAITING_FOR_SIGNATURES,
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HttpException) throw error;
|
if (error instanceof HttpException) throw error;
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
@@ -1819,7 +1868,9 @@ export class ExpertBlameService {
|
|||||||
error instanceof Error ? error.stack : String(error),
|
error instanceof Error ? error.stack : String(error),
|
||||||
);
|
);
|
||||||
throw new InternalServerErrorException(
|
throw new InternalServerErrorException(
|
||||||
error instanceof Error ? error.message : "Failed to submit expert reply",
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Failed to submit expert reply",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2144,5 +2195,4 @@ export class ExpertBlameService {
|
|||||||
|
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ async function bootstrap() {
|
|||||||
.setVersion("1.0.0")
|
.setVersion("1.0.0")
|
||||||
.addServer(process.env.BASE_URL_DEV + "/api")
|
.addServer(process.env.BASE_URL_DEV + "/api")
|
||||||
.addServer("http://192.168.20.170:9001")
|
.addServer("http://192.168.20.170:9001")
|
||||||
|
.addServer("https://user.yara724.com/api")
|
||||||
.addServer("http://localhost:9001")
|
.addServer("http://localhost:9001")
|
||||||
.addBearerAuth()
|
.addBearerAuth()
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
Reference in New Issue
Block a user