Compare commits

..

6 Commits

2 changed files with 80 additions and 47 deletions

View File

@@ -244,6 +244,32 @@ interface FanavaranAttachmentCandidate {
submittedFileId?: unknown; submittedFileId?: unknown;
} }
/**
* Returns today's Jalali date as a packed integer YYYYMMDD for cheap comparison.
* Uses the browser/Node `fa-IR` locale which is available everywhere Node runs.
*/
function jalaliDateToInt(d: Date): number {
const parts = d
.toLocaleDateString("fa-IR", { year: "numeric", month: "2-digit", day: "2-digit" })
.split("/")
.map((p) => parseInt(p.replace(/[۰-۹]/g, (c) => String(c.charCodeAt(0) - 0x06f0)), 10));
return parts[0] * 10000 + parts[1] * 100 + parts[2];
}
/**
* Parses a Jalali date string in "YYYY/MM/DD" or "YYYY-MM-DD" form to the same
* packed integer YYYYMMDD. Returns null when the string cannot be parsed.
*/
function jalaliStringToInt(raw: string): number | null {
const m = raw.match(/(\d{4})[\/\-](\d{1,2})[\/\-](\d{1,2})/);
if (!m) return null;
const y = parseInt(m[1], 10);
const mo = parseInt(m[2], 10);
const d = parseInt(m[3], 10);
if (!y || !mo || !d) return null;
return y * 10000 + mo * 100 + d;
}
const FANAVARAN_ACCIDENT_LOCATION_ADDRESS = "استان تهران شهر تهران"; const FANAVARAN_ACCIDENT_LOCATION_ADDRESS = "استان تهران شهر تهران";
const FANAVARAN_DEFAULT_ACCIDENT_CAUSE_ID = 6; const FANAVARAN_DEFAULT_ACCIDENT_CAUSE_ID = 6;
const FANAVARAN_DEFAULT_ACCIDENT_LEVEL = 5456; const FANAVARAN_DEFAULT_ACCIDENT_LEVEL = 5456;
@@ -577,6 +603,9 @@ export class ClaimRequestManagementService {
async getOuterPartsCatalogV2(): Promise<OuterPartCatalogItemDto[]> { async getOuterPartsCatalogV2(): Promise<OuterPartCatalogItemDto[]> {
const clientKey = resolveFanavaranClientKey(); const clientKey = resolveFanavaranClientKey();
const rows = await this.getFanavaranLookupRows(clientKey, "car-components"); const rows = await this.getFanavaranLookupRows(clientKey, "car-components");
// Build today's Jalali date as a comparable integer (YYYYMMDD) so we can
// filter expired rows without any Jalali→Gregorian conversion risk.
const todayJalaliInt = jalaliDateToInt(new Date());
return rows return rows
.filter( .filter(
(r): r is { Id: number; Caption: string } => (r): r is { Id: number; Caption: string } =>
@@ -585,6 +614,13 @@ export class ClaimRequestManagementService {
typeof (r as any).Id === "number" && typeof (r as any).Id === "number" &&
typeof (r as any).Caption === "string", typeof (r as any).Caption === "string",
) )
.filter((r) => {
const toDate = (r as any).ToDate;
if (!toDate) return true;
const toDateInt = jalaliStringToInt(String(toDate));
if (toDateInt === null) return true;
return toDateInt >= todayJalaliInt;
})
.map((r) => ({ id: r.Id, label_fa: r.Caption })); .map((r) => ({ id: r.Id, label_fa: r.Caption }));
} }
@@ -8198,12 +8234,8 @@ export class ClaimRequestManagementService {
); );
} }
// 4. Validate outer parts haven't been selected yet. // 4. Validate outer parts haven't been selected yet
// Skip this guard when FileMaker rejected the claim and the FileReviewer
// is updating (overwriting) the previous selection.
const isFileMakerRejectedRerun = !!(claimCase as any).fileMakerRejectedPendingReview;
if ( if (
!isFileMakerRejectedRerun &&
claimCase.damage?.selectedParts && claimCase.damage?.selectedParts &&
claimCase.damage.selectedParts.length > 0 claimCase.damage.selectedParts.length > 0
) { ) {
@@ -8270,9 +8302,7 @@ export class ClaimRequestManagementService {
})); }));
const selectedPartIds = selectedItems.map((p) => p.id); const selectedPartIds = selectedItems.map((p) => p.id);
// 6. Update claim case with selected parts and move to next step. // 6. Update claim case with selected parts and move to next step
// Also clear fileMakerRejectedPendingReview now that the reviewer has
// actively re-submitted outer parts.
const updatedClaim = await this.claimCaseDbService.findByIdAndUpdate( const updatedClaim = await this.claimCaseDbService.findByIdAndUpdate(
claimRequestId, claimRequestId,
{ {
@@ -8282,10 +8312,7 @@ export class ClaimRequestManagementService {
status: ClaimCaseStatus.SELECTING_OTHER_PARTS, status: ClaimCaseStatus.SELECTING_OTHER_PARTS,
"workflow.currentStep": ClaimWorkflowStep.SELECT_OTHER_PARTS, "workflow.currentStep": ClaimWorkflowStep.SELECT_OTHER_PARTS,
"workflow.nextStep": ClaimWorkflowStep.CAPTURE_PART_DAMAGES, "workflow.nextStep": ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
$unset: { $unset: { "damage.selectedOuterParts": "" },
"damage.selectedOuterParts": "",
"fileMakerRejectedPendingReview": "",
},
$push: { $push: {
"workflow.completedSteps": ClaimWorkflowStep.SELECT_OUTER_PARTS, "workflow.completedSteps": ClaimWorkflowStep.SELECT_OUTER_PARTS,
history: { history: {
@@ -8410,11 +8437,8 @@ 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
// Skip when FileMaker rejected the claim — the FileReviewer can update bank info.
const isFileMakerRejectedRerun = !!(claimCase as any).fileMakerRejectedPendingReview;
if ( if (
!isFileMakerRejectedRerun &&
claimCase.money?.sheba && claimCase.money?.sheba &&
claimCase.money?.nationalCodeOfInsurer && claimCase.money?.nationalCodeOfInsurer &&
claimCase.workflow?.currentStep !== ClaimWorkflowStep.USER_EXPERT_RESEND claimCase.workflow?.currentStep !== ClaimWorkflowStep.USER_EXPERT_RESEND

View File

@@ -10615,51 +10615,30 @@ export class RequestManagementService {
const newRejectionCount = currentRejections + 1; const newRejectionCount = currentRejections + 1;
const actorName = `${fileMaker.firstName || ""} ${fileMaker.lastName || ""}`.trim(); const actorName = `${fileMaker.firstName || ""} ${fileMaker.lastName || ""}`.trim();
// Steps that belong to the re-run assessment cycle.
// Pulled from completedSteps so workflow ordering guards pass on re-run.
const assessmentSteps = [
ClaimWorkflowStep.SELECT_OUTER_PARTS,
ClaimWorkflowStep.SELECT_OTHER_PARTS,
ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
];
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, { await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
$set: { $set: {
status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT, status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT,
claimStatus: ClaimStatus.NEEDS_REVISION, claimStatus: ClaimStatus.NEEDS_REVISION,
fileMakerRejectionCount: newRejectionCount, fileMakerRejectionCount: newRejectionCount,
fileMakerRejectionReason: reason ?? null, fileMakerRejectionReason: reason ?? null,
// Land directly at SELECT_OUTER_PARTS: the FileReviewer sees the "workflow.currentStep": ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
// existing parts pre-populated and can adjust or confirm them. "workflow.nextStep": ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
"workflow.currentStep": ClaimWorkflowStep.SELECT_OUTER_PARTS,
"workflow.nextStep": ClaimWorkflowStep.SELECT_OTHER_PARTS,
"workflow.locked": false, "workflow.locked": false,
// Flag consumed by selectOuterPartsV2 / selectOtherPartsV2 to allow
// overwriting previously submitted data instead of throwing ConflictException.
"fileMakerRejectedPendingReview": true,
}, },
// Clear stale lock/evaluation fields and media that must be re-captured. // Clear stale lock fields and the owner-cycle evaluation data.
// Part selections (damage.selectedParts, damage.otherParts) are intentionally // evaluation.damageExpertReply is intentionally preserved so the FileReviewer
// preserved as a pre-populated starting point for the FileReviewer to adjust. // sees their previous prices / daghi / severity pre-populated in the claim
// detail and can adjust them. submitExpertReplyV2 overwrites it with $set.
// - assignedForReviewBy intentionally kept so the same reviewer
// is still scoped to this file on re-lock.
$unset: { $unset: {
"workflow.lockedAt": "", "workflow.lockedAt": "",
"workflow.expiredAt": "", "workflow.expiredAt": "",
"workflow.lockedBy": "", "workflow.lockedBy": "",
"workflow.preLockQueueSnapshot": "", "workflow.preLockQueueSnapshot": "",
"evaluation.damageExpertReply": "",
"evaluation.damageExpertResend": "", "evaluation.damageExpertResend": "",
"evaluation.ownerInsurerApproval": "", "evaluation.ownerInsurerApproval": "",
"evaluation.ownerPricedPartsApproval": "", "evaluation.ownerPricedPartsApproval": "",
// Photos and walk-around video must be re-taken for the new part set.
"media.damagedParts": "",
"media.captures": "",
"media.videoCaptureId": "",
},
// Remove assessment-cycle steps from completedSteps so workflow ordering
// guards pass when the FileReviewer re-runs each step.
$pull: {
"workflow.completedSteps": { $in: assessmentSteps },
}, },
$push: { $push: {
history: { history: {
@@ -11102,6 +11081,9 @@ export class RequestManagementService {
: { ...(req as any) }; : { ...(req as any) };
// Attach linked claim ID if present // Attach linked claim ID if present
const claim = await this.claimCaseDbService.findOne({ blameRequestId: (req as any)._id }); const claim = await this.claimCaseDbService.findOne({ blameRequestId: (req as any)._id });
const claimPlain = claim && typeof (claim as any).toObject === "function"
? (claim as any).toObject({ versionKey: false })
: claim ? { ...(claim as any) } : null;
return { return {
_id: plain._id, _id: plain._id,
publicId: plain.publicId, publicId: plain.publicId,
@@ -11136,7 +11118,19 @@ export class RequestManagementService {
inquiryComplete: !!(p.person?.inquiriesCompleted), inquiryComplete: !!(p.person?.inquiriesCompleted),
hasSigned: p.confirmation != null, hasSigned: p.confirmation != null,
})), })),
linkedClaimId: claim ? String((claim as any)._id) : null, linkedClaimId: claimPlain ? String(claimPlain._id) : null,
...(claimPlain ? {
claimStatus: claimPlain.status,
claimWorkflow: claimPlain.workflow,
fileMakerRejectionCount: claimPlain.fileMakerRejectionCount ?? 0,
fileMakerRejectionReason: claimPlain.fileMakerRejectionReason ?? null,
evaluation: claimPlain.evaluation
? {
damageExpertReply: claimPlain.evaluation.damageExpertReply ?? null,
damageExpertReplyFinal: claimPlain.evaluation.damageExpertReplyFinal ?? null,
}
: null,
} : {}),
createdAt: plain.createdAt, createdAt: plain.createdAt,
updatedAt: plain.updatedAt, updatedAt: plain.updatedAt,
}; };
@@ -11190,6 +11184,9 @@ export class RequestManagementService {
? (req as any).toObject({ versionKey: false }) ? (req as any).toObject({ versionKey: false })
: { ...(req as any) }; : { ...(req as any) };
const claim = await this.claimCaseDbService.findOne({ blameRequestId: (req as any)._id }); const claim = await this.claimCaseDbService.findOne({ blameRequestId: (req as any)._id });
const claimPlain = claim && typeof (claim as any).toObject === "function"
? (claim as any).toObject({ versionKey: false })
: claim ? { ...(claim as any) } : null;
return { return {
_id: plain._id, _id: plain._id,
publicId: plain.publicId, publicId: plain.publicId,
@@ -11237,7 +11234,19 @@ export class RequestManagementService {
} : undefined, } : undefined,
accidentFields: plain.expert.accidentFields, accidentFields: plain.expert.accidentFields,
} : undefined, } : undefined,
linkedClaimId: claim ? String((claim as any)._id) : null, linkedClaimId: claimPlain ? String(claimPlain._id) : null,
...(claimPlain ? {
claimStatus: claimPlain.status,
claimWorkflow: claimPlain.workflow,
fileMakerRejectionCount: claimPlain.fileMakerRejectionCount ?? 0,
fileMakerRejectionReason: claimPlain.fileMakerRejectionReason ?? null,
evaluation: claimPlain.evaluation
? {
damageExpertReply: claimPlain.evaluation.damageExpertReply ?? null,
damageExpertReplyFinal: claimPlain.evaluation.damageExpertReplyFinal ?? null,
}
: null,
} : {}),
createdAt: plain.createdAt, createdAt: plain.createdAt,
updatedAt: plain.updatedAt, updatedAt: plain.updatedAt,
}; };