Merge pull request 'YARA-821' (#33) from s.yahyaee/yara724-api:main into main

Reviewed-on: Yara724/api#33
This commit is contained in:
2026-04-22 10:30:05 +03:30
6 changed files with 211 additions and 32 deletions

View File

@@ -4987,14 +4987,14 @@ export class ClaimRequestManagementService {
data && (data instanceof Map ? data.get(key) : data[key]);
const requiredDocs = claim.requiredDocuments as any;
const requiredDocumentsStatus: Record<string, { uploaded: boolean; fileId?: string }> = {};
const requiredDocumentsStatus: Record<string, { uploaded: boolean; fileUrl?: string }> = {};
if (requiredDocs) {
const keys = requiredDocs instanceof Map ? Array.from(requiredDocs.keys()) : Object.keys(requiredDocs);
for (const k of keys) {
const doc = requiredDocs instanceof Map ? requiredDocs.get(k) : requiredDocs[k];
requiredDocumentsStatus[k] = {
uploaded: !!doc?.uploaded,
fileId: doc?.fileId?.toString(),
fileUrl: doc?.filePath ? buildFileLink(doc.filePath) : undefined,
};
}
}
@@ -5041,6 +5041,25 @@ export class ClaimRequestManagementService {
s ? s.replace(/^(.{4})(.*)(.{4})$/, 'IR$1************$3') : undefined;
const maskNationalCode = (s?: string) =>
s ? s.replace(/^(.{2})(.*)(.{2})$/, '$1******$3') : undefined;
const isExpertViewer = actor?.role === RoleEnum.FIELD_EXPERT;
const ownerData = claim.owner
? {
userId: claim.owner.userId?.toString(),
fullName: claim.owner.fullName,
}
: undefined;
const moneyForUser = claim.money
? {
sheba: maskSheba(claim.money.sheba),
nationalCodeOfOwner: maskNationalCode(claim.money.nationalCodeOfInsurer),
}
: undefined;
const moneyForExpert = claim.money
? {
sheba: claim.money.sheba,
nationalCodeOfOwner: claim.money.nationalCodeOfInsurer,
}
: undefined;
return {
claimRequestId: claim._id.toString(),
@@ -5052,34 +5071,28 @@ export class ClaimRequestManagementService {
nextStep: claim.workflow?.nextStep,
blameRequestId: claim.blameRequestId?.toString(),
blameRequestNo: claim.blameRequestNo,
owner: claim.owner
? {
userId: claim.owner.userId?.toString(),
fullName: claim.owner.fullName,
}
: undefined,
...(isExpertViewer ? { owner: ownerData } : {}),
vehicle: claim.vehicle,
selectedParts: claim.damage?.selectedParts,
otherParts: claim.damage?.otherParts,
money: claim.money
? {
sheba: maskSheba(claim.money.sheba),
nationalCodeOfOwner: maskNationalCode(claim.money.nationalCodeOfInsurer),
}
: undefined,
money: isExpertViewer ? moneyForExpert : moneyForUser,
requiredDocuments: Object.keys(requiredDocumentsStatus).length > 0 ? requiredDocumentsStatus : undefined,
carAngles,
damagedParts,
expertResend,
userRating: claim.userRating
...(isExpertViewer
? {
progressSpeed: claim.userRating.progressSpeed,
registrationEase: claim.userRating.registrationEase,
overallEvaluation: claim.userRating.overallEvaluation,
comment: claim.userRating.comment,
createdAt: claim.userRating.createdAt,
userRating: claim.userRating
? {
progressSpeed: claim.userRating.progressSpeed,
registrationEase: claim.userRating.registrationEase,
overallEvaluation: claim.userRating.overallEvaluation,
comment: claim.userRating.comment,
createdAt: claim.userRating.createdAt,
}
: undefined,
}
: undefined,
: {}),
createdAt: (claim as any).createdAt,
updatedAt: (claim as any).updatedAt,
};

View File

@@ -75,7 +75,8 @@ export class ClaimRequestManagementV2Controller {
})
@ApiOperation({
summary: "Get Claim Details (V2)",
description: "Get full details of a claim request. Only the claim owner can access.",
description:
"Get claim details for current actor. USER receives only minimal own-needed payload (no extra owner/security fields). FIELD_EXPERT keeps full view for expert workflows.",
})
@ApiResponse({
status: 200,
@@ -560,8 +561,7 @@ Returns status of each item (uploaded/captured or not).
description: `
**Workflow Step:** UPLOAD_REQUIRED_DOCUMENTS (Step 5 of Claim)
**Upload one of the required documents** (13 for THIRD_PARTY; CAR_BODY may require fewer — see capture-requirements):
- car_green_card
**Upload one of the required documents** (12 for THIRD_PARTY; CAR_BODY may require fewer — see capture-requirements):
- damaged_driving_license_front/back
- damaged_chassis_number, damaged_engine_photo
- damaged_car_card_front/back, damaged_metal_plate

View File

@@ -69,8 +69,8 @@ export class ClaimDetailsV2ResponseDto {
nationalCodeOfOwner?: string;
};
@ApiPropertyOptional({ description: 'Required documents status' })
requiredDocuments?: Record<string, { uploaded: boolean; fileId?: string }>;
@ApiPropertyOptional({ description: 'Required documents status (link instead of id)' })
requiredDocuments?: Record<string, { uploaded: boolean; fileUrl?: string }>;
@ApiPropertyOptional({ description: 'Car angles captured' })
carAngles?: Record<string, { captured: boolean; url?: string }>;

View File

@@ -46,15 +46,24 @@ function writeFaTimestampsOnUpdate(query: Query<any, any>): void {
update.$set = {};
}
if (update.$set.createdAtFa == null && update.createdAtFa == null) {
update.$set.createdAtFa = nowFa;
}
update.$set.updatedAtFa = nowFa;
// Keep createdAtFa immutable on regular updates; set only for upsert inserts.
const opts = (query as any).getOptions?.() ?? {};
if (opts.upsert) {
if (!update.$setOnInsert || typeof update.$setOnInsert !== "object") {
update.$setOnInsert = {};
}
if (update.$setOnInsert.createdAtFa == null) {
update.$setOnInsert.createdAtFa = nowFa;
}
}
query.setUpdate(update);
}
/**
* Adds `createdAtFa` / `updatedAtFa` (Iran timezone, UTC+03:30 offset string)
* Adds `createdAtFa` / `updatedAtFa` (Iran timezone tuple: [date, time])
* to all schemas that use mongoose timestamps.
*/
export function applyIranFaTimestampPlugin(connection: Connection): void {

View File

@@ -79,6 +79,7 @@ import {
buildMutualAgreementExpertDecision,
MUTUAL_AGREEMENT_EXPERT_DECISION_FIELDS,
} from "src/helpers/blame-party-agreement-decision";
import { buildFileLink } from "src/helpers/urlCreator";
@Injectable()
export class RequestManagementService {
@@ -4117,6 +4118,133 @@ export class RequestManagementService {
return none;
}
/** Party payload for user blame detail: no national codes, licenses, phone, birthdays, or vehicle inquiry blobs. */
private async sanitizePartyForBlameUserView(
party: any,
): Promise<Record<string, unknown>> {
if (!party || typeof party !== "object") {
return {};
}
const person = party.person;
const safePerson = person
? {
userId:
person.userId != null ? String(person.userId) : undefined,
fullName: person.fullName,
clientId:
person.clientId != null ? String(person.clientId) : undefined,
}
: undefined;
const vehicle = party.vehicle
? {
plateId: party.vehicle.plateId,
name: party.vehicle.name,
model: party.vehicle.model,
type: party.vehicle.type,
isNew: party.vehicle.isNew,
}
: undefined;
const evidenceRaw = party.evidence as Record<string, unknown> | undefined;
const evidence: Record<string, unknown> | undefined = evidenceRaw
? { ...evidenceRaw }
: undefined;
if (evidence) {
if (party.role === PartyRole.FIRST && evidence.videoId) {
const videoDoc = await this.blameVideoDbService.findById(
String(evidence.videoId),
);
if (videoDoc?.path) {
evidence.videoUrl = buildFileLink(videoDoc.path);
}
}
delete evidence.videoId;
const voiceIds = evidence.voices;
if (Array.isArray(voiceIds)) {
const voiceUrls: string[] = [];
for (const voiceId of voiceIds) {
const voiceDoc = await this.blameVoiceDbService.findById(
String(voiceId),
);
if (voiceDoc?.path) {
voiceUrls.push(buildFileLink(voiceDoc.path));
}
}
evidence.voiceUrls = voiceUrls;
}
delete evidence.voices;
}
return {
role: party.role,
person: safePerson,
carBodyFirstForm: party.carBodyFirstForm,
location: party.location,
vehicle,
insurance: party.insurance,
statement: party.statement,
evidence,
confirmation: party.confirmation,
};
}
private async resolveBlameExpertDisplayNameFromPlain(
expert: Record<string, unknown>,
): Promise<string | undefined> {
const decision = expert.decision as Record<string, unknown> | undefined;
const decided = decision?.decidedByExpertId;
const assigned = expert.assignedExpertId;
const raw = decided ?? assigned;
if (raw == null || raw === "") return undefined;
const sid = String(raw);
if (!Types.ObjectId.isValid(sid)) return undefined;
const doc = await this.expertDbService.findOne({
_id: new Types.ObjectId(sid),
});
if (!doc) return undefined;
return `${doc.firstName || ""} ${doc.lastName || ""}`.trim() || undefined;
}
/** Expert subdocument for user view: string ids, trimmed snapshot (name only), plus `expertName`. */
private async buildExpertForBlameUserView(
expertRaw: any,
): Promise<Record<string, unknown> | undefined> {
if (!expertRaw || typeof expertRaw !== "object") return undefined;
const out = JSON.parse(JSON.stringify(expertRaw)) as Record<string, unknown>;
const decision = out.decision as Record<string, unknown> | undefined;
let expertName: string | undefined;
if (decision) {
if (decision.guiltyPartyId != null) {
decision.guiltyPartyId = String(decision.guiltyPartyId);
}
if (decision.decidedByExpertId != null) {
decision.decidedByExpertId = String(decision.decidedByExpertId);
}
const snap = decision.expertProfileSnapshot as
| Record<string, unknown>
| undefined;
if (snap) {
expertName =
`${snap.firstName ?? ""} ${snap.lastName ?? ""}`.trim() || undefined;
decision.expertProfileSnapshot = {
firstName: snap.firstName,
lastName: snap.lastName,
};
}
}
if (out.assignedExpertId != null) {
out.assignedExpertId = String(out.assignedExpertId);
}
if (!expertName) {
expertName = await this.resolveBlameExpertDisplayNameFromPlain(out);
}
return { ...out, expertName };
}
/**
* V2: Get one blame request by id. Access allowed if current user is a party (by userId or phone)
* or the initiating field expert. For expert-initiated LINK, party access by phone is sufficient.
@@ -4165,7 +4293,36 @@ export class RequestManagementService {
typeof (req as any).toObject === "function"
? (req as any).toObject({ versionKey: false })
: { ...(req as any) };
return { ...plain, claimCreation };
const allParties = Array.isArray(plain.parties) ? plain.parties : [];
const visibleParties =
isFieldExpertOwner || isRegistrarOwner
? allParties
: allParties.filter((p: any) => {
const pid = p?.person?.userId ? String(p.person.userId) : null;
const phone = p?.person?.phoneNumber;
return (
(pid && pid === String(user?.sub)) ||
(phone && phone === user?.username)
);
});
const parties = await Promise.all(
visibleParties.map((p: any) => this.sanitizePartyForBlameUserView(p)),
);
const expert = await this.buildExpertForBlameUserView(plain.expert);
return {
requestNo: plain.requestNo,
publicId: plain.publicId,
type: plain.type,
status: plain.status,
blameStatus: plain.blameStatus,
workflow: plain.workflow,
parties,
expert,
carBodyInsuranceDetail: plain.carBodyInsuranceDetail,
claimCreation,
};
}
/**

View File

@@ -81,7 +81,7 @@ export class RequestManagementV2Controller {
@ApiOperation({
summary: "Get one blame request (v2)",
description:
"Response is the blame document plus **claimCreation** ({ hasClaim, shouldGuideToCreateClaim }) — computed for the current user so the app can route the damaged party to create-claim when the blame is COMPLETED and no v2 claim exists yet.",
"Returns a minimal, user-safe payload: requestNo, publicId, type, status, blameStatus, workflow, parties (PII stripped), expert (with **expertName**), carBodyInsuranceDetail, plus **claimCreation** ({ hasClaim, shouldGuideToCreateClaim }). History and other internal fields are omitted.",
})
@ApiParam({ name: "requestId", description: "Blame request ID" })
@Roles(RoleEnum.USER, RoleEnum.FIELD_EXPERT)