Compare commits

..

21 Commits

Author SHA1 Message Date
5e4897f609 Fix empty Rocket.Chat notify (Woodpecker ${} escaping) 2026-07-15 17:06:27 +03:30
a79c3ca05f Fix git pull SSH in pipeline (host keys + known_hosts) 2026-07-15 16:59:46 +03:30
36fa1c552e Allow git in bind-mounted workspace (safe.directory) 2026-07-15 16:43:31 +03:30
9742fecc11 Update .woodpecker.yml 2026-07-15 16:35:40 +03:30
8007c30efd Fix path typo of workspace 2026-07-15 16:26:06 +03:30
144f8f3e2a Update .woodpecker.yml docker repositories 2026-07-15 16:05:01 +03:30
8674dd8762 Merge pull request 'Fixed v4/v5 car capture flow' (#194) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#194
2026-07-15 16:00:01 +03:30
SepehrYahyaee
f39c50aeb0 Fixed v4/v5 car capture flow 2026-07-15 15:59:21 +03:30
2fda740171 Update .woodpecker.yml 2026-07-15 15:54:28 +03:30
72e5bd616c Update .woodpecker.yml 2026-07-15 15:51:29 +03:30
4b41a60f64 Add .woodpecker.yml 2026-07-15 15:47:53 +03:30
9310285bd4 Merge pull request 'FIX v5 flow' (#193) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#193
2026-07-15 14:57:38 +03:30
SepehrYahyaee
2a8b66bc16 FIX v5 flow 2026-07-15 14:56:58 +03:30
9168a6bdcd Merge pull request 'Added fonts for PDF' (#192) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#192
2026-07-15 13:24:17 +03:30
SepehrYahyaee
057bedeb0c Added fonts for PDF 2026-07-15 13:23:43 +03:30
808a3b8526 Merge pull request 'YARA-994 and fixed metal plate bug' (#191) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#191
2026-07-15 10:34:34 +03:30
SepehrYahyaee
da7a4f8890 YARA-994 and fixed metal plate bug 2026-07-15 10:33:50 +03:30
21e55012be Merge pull request 'Fixed file maker retrieving files' (#190) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#190
2026-07-14 14:31:20 +03:30
SepehrYahyaee
385757c3a0 Fixed file maker retrieving files 2026-07-14 14:30:41 +03:30
a1b122a33b Merge pull request 'Fixed file maker view files error' (#189) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#189
2026-07-14 13:53:02 +03:30
SepehrYahyaee
aec9e76918 Fixed file maker view files error 2026-07-14 13:52:12 +03:30
12 changed files with 305 additions and 60 deletions

142
.woodpecker.yml Normal file
View File

@@ -0,0 +1,142 @@
# yara724/api — development deployment (Deploy-Develop)
# Manual tasks: see ci-cd/TASK.md
# Requires: repo marked Trusted in Woodpecker (host volume mounts)
when:
- event: push
branch: main
- event: manual
skip_clone: true
variables:
- &host_workspace /data/1-deploy/gitea/yara724/api
- &workspace_volume /data/1-deploy/gitea/yara724/api:/workspace
- &host_ssh /home/talieh/.ssh:/root/.ssh:ro
- &pipeline_env
WORKSPACE: *host_workspace
PROJECT_NAME: Yara724 API
ENVIRONMENT: Development
APPLICATION_URL: https://y724-user.ittalie.ir/api
GIT_COMMIT_URL: https://git.ittalie.com/Yara724/api/commit/
GIT_BRANCH: main
COMPOSE_FILE: docker-compose.yml
SUCCESS_COLOR: "#36a64f"
FAILURE_COLOR: "#dc3545"
steps:
pull:
image: docker.arvancloud.ir/alpine/git:latest
environment:
<<: *pipeline_env
GIT_SSH_COMMAND: ssh -o UserKnownHostsFile=/tmp/known_hosts -o StrictHostKeyChecking=yes
volumes:
- *workspace_volume
- *host_ssh
commands:
- git config --global --add safe.directory /workspace
- mkdir -p /tmp && ssh-keyscan -H git.ittalie.com >> /tmp/known_hosts
- cd /workspace
- git pull origin main
- date +%s > /workspace/.wp-deploy-start
deploy:
image: docker.arvancloud.ir/docker:24-cli
environment:
<<: *pipeline_env
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- *workspace_volume
commands:
- cd /workspace
- docker compose -f docker-compose.yml up -d --build
notify-success:
image: docker.arvancloud.ir/alpine:3.20
environment:
<<: *pipeline_env
ROCKETCHAT_WEBHOOK:
from_secret: rocketchat_webhook
volumes:
- *workspace_volume
when:
- status: success
commands:
- apk add --no-cache curl jq git > /dev/null
- |
set -euo pipefail
git config --global --add safe.directory /workspace
cd /workspace
COMMIT_HASH="$(git rev-parse --short HEAD)"
DEPLOY_START="$(cat /workspace/.wp-deploy-start)"
DEPLOY_END="$(date +%s)"
DEPLOY_DURATION="$((DEPLOY_END - DEPLOY_START))"
COMMIT_1="$(git log -1 --pretty=format:'%s')"
COMMIT_2="$(git log -2 --pretty=format:'%s' | tail -n1)"
COMMIT_3="$(git log -3 --pretty=format:'%s' | tail -n1)"
TITLE="✅ $PROJECT_NAME - $ENVIRONMENT ✅"
TEXT="🌐 **URL**: $APPLICATION_URL
🔖 **Commit Hash**: [$COMMIT_HASH]($GIT_COMMIT_URL$COMMIT_HASH)
📝 **Recent Changes**:
• $COMMIT_1
• $COMMIT_2
• $COMMIT_3
⏱️ **Deployment Duration**: $${DEPLOY_DURATION}s"
payload="$(jq -n \
--arg title "$TITLE" \
--arg text "$TEXT" \
--arg color "$SUCCESS_COLOR" \
'{text: $title, attachments: [{text: $text, color: $color}]}')"
curl -fsS -H "Content-Type: application/json" -d "$payload" "$ROCKETCHAT_WEBHOOK" >/dev/null
rm -f /workspace/.wp-deploy-start
notify-failure:
image: docker.arvancloud.ir/alpine:3.20
environment:
<<: *pipeline_env
ROCKETCHAT_WEBHOOK:
from_secret: rocketchat_webhook
volumes:
- *workspace_volume
when:
- status: failure
commands:
- apk add --no-cache curl jq git > /dev/null
- |
set -euo pipefail
git config --global --add safe.directory /workspace
cd /workspace
COMMIT_HASH="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
TITLE="💥 $PROJECT_NAME - $ENVIRONMENT 💥"
TEXT="━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🌐 **Application URL**
$APPLICATION_URL
🔖 **Commit**
\`$COMMIT_HASH\`
⚠️ **Pipeline failed** — check Woodpecker for the failing step.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
payload="$(jq -n \
--arg title "$TITLE" \
--arg text "$TEXT" \
--arg color "$FAILURE_COLOR" \
'{text: $title, attachments: [{text: $text, color: $color}]}')"
curl -fsS -H "Content-Type: application/json" -d "$payload" "$ROCKETCHAT_WEBHOOK" >/dev/null || true
rm -f /workspace/.wp-deploy-start

BIN
assets/Vazirmatn-Bold.ttf Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -8139,7 +8139,7 @@ export class ClaimRequestManagementService {
file: Express.Multer.File, file: Express.Multer.File,
currentUserId: string, currentUserId: string,
actor?: { sub: string; role?: string }, actor?: { sub: string; role?: string },
options?: { v3InPersonFlow?: boolean; skipMetalPlate?: boolean }, options?: { v3InPersonFlow?: boolean; skipMetalPlate?: boolean; requiresFileMakerApproval?: boolean },
): Promise<UploadRequiredDocumentV2ResponseDto> { ): Promise<UploadRequiredDocumentV2ResponseDto> {
try { try {
const claimCase = await this.claimCaseDbService.findById(claimRequestId); const claimCase = await this.claimCaseDbService.findById(claimRequestId);
@@ -8334,12 +8334,14 @@ export class ClaimRequestManagementService {
progressAfterDoc.capturePhaseDocsComplete progressAfterDoc.capturePhaseDocsComplete
) { ) {
captureStepCompletedOnThisUpload = true; captureStepCompletedOnThisUpload = true;
// Advance to walk-around video step (UPLOADING_REQUIRED_DOCUMENTS /
// UPLOAD_REQUIRED_DOCUMENTS). For V4/V5 the claim is finalised in
// setVideoCaptureV3 (car-capture), which is the last FileReviewer
// action before handing off to the damage expert.
$set["status"] = ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS; $set["status"] = ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS;
$set["workflow.currentStep"] = $set["workflow.currentStep"] = ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS;
ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS; $set["workflow.nextStep"] = ClaimWorkflowStep.USER_SUBMISSION_COMPLETE;
$set["workflow.nextStep"] = options?.v3InPersonFlow
? ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS
: ClaimWorkflowStep.USER_SUBMISSION_COMPLETE;
completedStepsEntries.push( completedStepsEntries.push(
ClaimWorkflowStep.CAPTURE_PART_DAMAGES, ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
@@ -8354,8 +8356,9 @@ export class ClaimRequestManagementService {
timestamp: new Date(), timestamp: new Date(),
metadata: { metadata: {
stepKey: ClaimWorkflowStep.CAPTURE_PART_DAMAGES, stepKey: ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
description: description: options?.v3InPersonFlow
"Angles, damaged parts, and capture-phase vehicle evidence (chassis, engine, metal plate) are complete. Please upload remaining required documents.", ? "V4/V5 capture docs complete. Upload walk-around video to finalise."
: "Angles, damaged parts, and capture-phase vehicle evidence are complete.",
}, },
}); });
} }
@@ -8518,7 +8521,9 @@ export class ClaimRequestManagementService {
const message = isCapturePhaseDocUpload const message = isCapturePhaseDocUpload
? captureStepCompletedOnThisUpload ? captureStepCompletedOnThisUpload
? "Capture-phase documents and all damage captures are complete. Please proceed to upload the remaining required documents." ? options?.v3InPersonFlow
? "Capture-phase documents and all damage captures are complete. Upload the walk-around video (car-capture) to finalise."
: "Capture-phase documents and all damage captures are complete. Please proceed to upload the remaining required documents."
: remaining > 0 : remaining > 0
? `Document saved. ${remaining} capture-phase document(s) still required (chassis, engine, metal plate).` ? `Document saved. ${remaining} capture-phase document(s) still required (chassis, engine, metal plate).`
: capturePhaseSequenceMessage( : capturePhaseSequenceMessage(
@@ -10205,6 +10210,18 @@ export class ClaimRequestManagementService {
base: GetCaptureRequirementsV2ResponseDto, base: GetCaptureRequirementsV2ResponseDto,
): GetCaptureRequirementsV2ResponseDto { ): GetCaptureRequirementsV2ResponseDto {
const skipMetalPlate = !!(blame as any)?.isMadeByFileMaker; const skipMetalPlate = !!(blame as any)?.isMadeByFileMaker;
// For V4/V5 files strip metal-plate keys from the base response entirely
// so they never appear in any phase — the front-end should not show them.
if (skipMetalPlate) {
base = {
...base,
requiredDocuments: base.requiredDocuments.filter(
(d) => !OPTIONAL_CAPTURE_PHASE_DOC_KEYS_V4V5.includes(d.key as any),
),
};
}
const step = claimCase.workflow?.currentStep; const step = claimCase.workflow?.currentStep;
const capturePartDone = this.claimV3StepCompleted( const capturePartDone = this.claimV3StepCompleted(
claimCase, claimCase,
@@ -10355,7 +10372,7 @@ export class ClaimRequestManagementService {
file, file,
currentUserId, currentUserId,
actor, actor,
{ v3InPersonFlow: true, skipMetalPlate }, { v3InPersonFlow: true, skipMetalPlate, requiresFileMakerApproval: !!(claimCase as any).requiresFileMakerApproval },
); );
} }
@@ -10550,7 +10567,7 @@ export class ClaimRequestManagementService {
ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS
) { ) {
throw new BadRequestException( throw new BadRequestException(
`Complete all part/angle captures first. Expected ${ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS}, but current step is ${claimCase.workflow?.currentStep}`, `Complete capture-phase documents (chassis/engine) first. Expected ${ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS}, but current step is ${claimCase.workflow?.currentStep}`,
); );
} }
@@ -10573,12 +10590,53 @@ export class ClaimRequestManagementService {
); );
} }
return this.setVideoCaptureV2( // Store the walk-around video (shared V2 logic).
const result = await this.setVideoCaptureV2(
claimRequestId, claimRequestId,
fileDetail, fileDetail,
currentUserId, currentUserId,
actor, actor,
); );
// V4/V5 (in-person FileReviewer flow): car-capture is the last step.
// Both variants land at WAITING_FOR_DAMAGE_EXPERT here.
// For V5 (requiresFileMakerApproval=true), autoSubmitToFanavaranV2OnClaimCompleted
// intercepts COMPLETED → WAITING_FOR_FILE_MAKER_APPROVAL after expert review.
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
$set: {
status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT,
claimStatus: ClaimStatus.PENDING,
"workflow.currentStep": ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
"workflow.nextStep": ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
},
$push: {
"workflow.completedSteps": ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
history: {
type: "STEP_COMPLETED",
actor: {
actorId: new Types.ObjectId(currentUserId),
actorName: claimCase.owner?.fullName || "User",
actorType: actor?.role === RoleEnum.FILE_REVIEWER ? "file_reviewer" : "user",
},
timestamp: new Date(),
metadata: {
stepKey: ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
description: "V4/V5 walk-around video uploaded. Claim ready for damage expert review.",
v3InPersonFlow: true,
},
},
},
});
// Mark the linked blame file as COMPLETED.
if (claimCase.blameRequestId) {
await this.blameRequestDbService.findByIdAndUpdate(
String(claimCase.blameRequestId),
{ $set: { status: BlameCaseStatus.COMPLETED } },
);
}
return result;
} }
async capturePartV3( async capturePartV3(

View File

@@ -131,6 +131,13 @@ export class ClaimDetailV2ResponseDto {
}) })
awaitingFactorValidation?: boolean; awaitingFactorValidation?: boolean;
@ApiPropertyOptional({
description:
"True for V5 files — the FileMaker must approve or reject the claim before it is submitted to fanavaran. False (or absent) for V4 files which go straight through.",
example: true,
})
requiresFileMakerApproval?: boolean;
@ApiPropertyOptional({ @ApiPropertyOptional({
description: description:
"Slice of `claim.evaluation` exposed to the damage expert. " + "Slice of `claim.evaluation` exposed to the damage expert. " +

View File

@@ -70,6 +70,13 @@ export class ClaimListItemV2Dto {
"True in the expert repair-factor validation queue: `status=EXPERT_VALIDATING_REPAIR_FACTORS` (or legacy `WAITING_FOR_INSURER_APPROVAL`) with `claimStatus=UNDER_REVIEW` and `currentStep=EXPERT_COST_EVALUATION`.", "True in the expert repair-factor validation queue: `status=EXPERT_VALIDATING_REPAIR_FACTORS` (or legacy `WAITING_FOR_INSURER_APPROVAL`) with `claimStatus=UNDER_REVIEW` and `currentStep=EXPERT_COST_EVALUATION`.",
}) })
awaitingFactorValidation?: boolean; awaitingFactorValidation?: boolean;
@ApiPropertyOptional({
description:
"True for V5 files — the FileMaker must approve or reject the claim before it is submitted to fanavaran. False (or absent) for V4 files which go straight through.",
example: true,
})
requiresFileMakerApproval?: boolean;
} }
export class GetClaimListV2ResponseDto { export class GetClaimListV2ResponseDto {

View File

@@ -4309,6 +4309,7 @@ export class ExpertClaimService {
blameRequestId: c.blameRequestId?.toString(), blameRequestId: c.blameRequestId?.toString(),
createdAt: c.createdAt, createdAt: c.createdAt,
awaitingFactorValidation: claimIsAwaitingExpertFactorValidationV2(c), awaitingFactorValidation: claimIsAwaitingExpertFactorValidationV2(c),
requiresFileMakerApproval: !!(c as any).requiresFileMakerApproval,
needsFileReviewerCompletion: needsFileReviewerCompletion:
String(blame?.status ?? "") === "WAITING_FOR_FILE_REVIEWER" || String(blame?.status ?? "") === "WAITING_FOR_FILE_REVIEWER" ||
(!!blame?.isMadeByFileMaker && (!!blame?.isMadeByFileMaker &&
@@ -4692,8 +4693,8 @@ export class ExpertClaimService {
const isFactorValidationPending = const isFactorValidationPending =
claimIsAwaitingExpertFactorValidationV2(claim); claimIsAwaitingExpertFactorValidationV2(claim);
// FileMaker: can only view V5 files (requiresFileMakerApproval: true) that they created. // FileMaker: can view any file they created (both during filing and approval phase).
// V4 files do not require FileMaker approval and must not appear in their panel. // The list endpoint already filters to V5-only; detail access is scoped by ownership.
if (actor.role === RoleEnum.FILE_MAKER) { if (actor.role === RoleEnum.FILE_MAKER) {
const isOwn = claimCaseInitiatedByFieldExpert(claim, actor, linkedBlame); const isOwn = claimCaseInitiatedByFieldExpert(claim, actor, linkedBlame);
if (!isOwn) { if (!isOwn) {
@@ -4701,11 +4702,6 @@ export class ExpertClaimService {
"FileMakers can only view files they created.", "FileMakers can only view files they created.",
); );
} }
if (!(claim as any).requiresFileMakerApproval) {
throw new ForbiddenException(
"This file does not require FileMaker approval.",
);
}
// Fall through to the detail build below // Fall through to the detail build below
} else if ( } else if (
// Field experts and FileReviewers can view IN_PERSON expert-initiated claims // Field experts and FileReviewers can view IN_PERSON expert-initiated claims
@@ -5044,6 +5040,7 @@ export class ExpertClaimService {
carAngles, carAngles,
damagedParts, damagedParts,
awaitingFactorValidation: isFactorValidationPending, awaitingFactorValidation: isFactorValidationPending,
requiresFileMakerApproval: !!(claim as any).requiresFileMakerApproval,
evaluation: evaluation:
(evaluationForApi as (evaluationForApi as
| ClaimDetailV2ResponseDto["evaluation"] | ClaimDetailV2ResponseDto["evaluation"]

View File

@@ -22,20 +22,18 @@ const LABEL_COLUMN_X = COLON_X + 10;
function resolveFontFile(variant: "regular" | "bold"): string { function resolveFontFile(variant: "regular" | "bold"): string {
const file = const file =
variant === "bold" variant === "bold" ? "Vazirmatn-Bold.ttf" : "Vazirmatn-Regular.ttf";
? "NotoKufiArabic-Bold.ttf"
: "NotoKufiArabic-Regular.ttf";
const candidates = [ const candidates = [
join(process.cwd(), "assets", file),
join(process.cwd(), "assets", "fonts", file), join(process.cwd(), "assets", "fonts", file),
join(process.cwd(), "dist", "fonts", file), join(process.cwd(), "dist", "assets", file),
join(process.cwd(), "dist", "assets", "fonts", file), join(process.cwd(), "dist", "assets", "fonts", file),
join("/usr/share/fonts/truetype/noto", file),
]; ];
for (const path of candidates) { for (const path of candidates) {
if (existsSync(path)) return path; if (existsSync(path)) return path;
} }
throw new Error( throw new Error(
`Persian PDF font not found (${file}). Place it under assets/fonts/ or install noto fonts on the host.`, `Persian PDF font not found (${file}). Place it under assets/ or assets/fonts/.`,
); );
} }
@@ -46,6 +44,16 @@ export type PersianPdfDocument = InstanceType<typeof PDFDocument> & {
addBlank: (lines?: number) => void; addBlank: (lines?: number) => void;
}; };
/** Ensure at least `neededPts` of vertical space remain; add a page if not. */
function ensureSpace(
doc: InstanceType<typeof PDFDocument>,
neededPts: number,
): void {
if (doc.y + neededPts > doc.page.maxY()) {
doc.addPage();
}
}
function drawRtlLine( function drawRtlLine(
doc: InstanceType<typeof PDFDocument>, doc: InstanceType<typeof PDFDocument>,
text: string, text: string,
@@ -69,6 +77,9 @@ function drawKeyValueRow(
label: string, label: string,
value: string, value: string,
): void { ): void {
// Reserve one row height (font 10pt ≈ 14 pts with leading) before drawing.
ensureSpace(doc, 16);
const y = doc.y; const y = doc.y;
const { label: faLabel, value: faValue } = pdfKitRtlKeyValue(label, value); const { label: faLabel, value: faValue } = pdfKitRtlKeyValue(label, value);
@@ -118,6 +129,9 @@ export function createPersianPdfDocument(): PersianPdfDocument {
}; };
doc.addSection = (text: string) => { doc.addSection = (text: string) => {
// Reserve space for the gap + section heading + at least one data row below it,
// so the heading is never stranded alone at the bottom of a page.
ensureSpace(doc, 60);
doc.moveDown(0.5); doc.moveDown(0.5);
drawRtlLine(doc, text, 13, true); drawRtlLine(doc, text, 13, true);
doc.font("FaRegular").fontSize(10); doc.font("FaRegular").fontSize(10);

View File

@@ -8,18 +8,31 @@ export function normalizePersianPdfText(text: string): string {
.trim(); .trim();
} }
/**
* pdfkit renders glyphs strictly left-to-right.
* fontkit already reshapes each word's glyphs into visual (LTR) order,
* but it does NOT reorder words — so a multi-word RTL string like
* "نام صاحب خودرو" has its words placed LTR on the page and reads
* backwards. Reversing the word sequence here makes pdfkit emit the
* words in the correct visual order for a right-to-left PDF reader.
*/
function reverseRtlWords(text: string): string {
if (!/[\u0600-\u06FF]/.test(text)) return text;
return text.split(" ").reverse().join(" ");
}
export function pdfKitRtlKeyValue( export function pdfKitRtlKeyValue(
label: string, label: string,
value: string, value: string,
): { label: string; value: string } { ): { label: string; value: string } {
return { return {
label: normalizePersianPdfText(label), label: reverseRtlWords(normalizePersianPdfText(label)),
value: normalizePersianPdfText(value), value: reverseRtlWords(normalizePersianPdfText(value)),
}; };
} }
export function pdfKitRtlParagraph(text: string): string { export function pdfKitRtlParagraph(text: string): string {
return normalizePersianPdfText(text); return reverseRtlWords(normalizePersianPdfText(text));
} }
export function isMostlyAscii(text: string): boolean { export function isMostlyAscii(text: string): boolean {

View File

@@ -51,8 +51,7 @@ import { GetCaptureRequirementsV2ResponseDto } from "src/claim-request-managemen
* *
* The FileReviewer picks up a sealed file (both signatures collected by the * The FileReviewer picks up a sealed file (both signatures collected by the
* FileMaker) and completes the damage-assessment portion: accident fields, * FileMaker) and completes the damage-assessment portion: accident fields,
* documents, part selection, photo capture, walk-around video, and the final * documents, part selection, photo capture, and walk-around video (final step).
* blame accident video that moves the file to WAITING_FOR_EXPERT.
* *
* Sequence: * Sequence:
* accident-fields * accident-fields
@@ -61,9 +60,11 @@ import { GetCaptureRequirementsV2ResponseDto } from "src/claim-request-managemen
* → select-outer-parts * → select-outer-parts
* → select-other-parts * → select-other-parts
* → capture-part (damaged-part photos + four angles) * → capture-part (damaged-part photos + four angles)
* → upload-document (chassis / engine / metal plate — capture phase) * → upload-document (chassis / engine — capture phase)
* → car-capture (walk-around video) * → car-capture (walk-around video — FINAL FileReviewer step)
* → upload-video (blame accident video — final, moves to WAITING_FOR_EXPERT) * ↳ claim → WAITING_FOR_DAMAGE_EXPERT, blame → COMPLETED
*
* NOTE: upload-video is a no-op for V4 (blame is already COMPLETED by car-capture).
*/ */
@ApiTags("v4 FileReviewer — blame file review & capture") @ApiTags("v4 FileReviewer — blame file review & capture")
@Controller("v4/file-reviewer/blame-request-management") @Controller("v4/file-reviewer/blame-request-management")
@@ -341,9 +342,10 @@ export class FileReviewerBlameV4Controller {
}, },
}) })
@ApiOperation({ @ApiOperation({
summary: "Walk-around video", summary: "Walk-around video (final FileReviewer step — V4)",
description: description:
"Upload after capture-part is complete (parts, angles, capture-phase docs). Required before the final blame accident video.", "Last FileReviewer action. Upload after capture-part is complete (parts, angles, chassis/engine docs). " +
"Finalises the claim → WAITING_FOR_DAMAGE_EXPERT and marks the blame as COMPLETED.",
}) })
async carCapture( async carCapture(
@Param("claimRequestId") claimRequestId: string, @Param("claimRequestId") claimRequestId: string,
@@ -384,10 +386,10 @@ export class FileReviewerBlameV4Controller {
@Post("upload-video/:requestId") @Post("upload-video/:requestId")
@ApiParam({ name: "requestId" }) @ApiParam({ name: "requestId" })
@ApiOperation({ @ApiOperation({
summary: "Blame accident video (FileReviewer final step — optional)", summary: "Blame accident video (no-op for V4 — car-capture is the final step)",
description: description:
"Last step of the V4 flow. Requires claim walk-around video and completed capture. " + "Deprecated for V4. The blame is already COMPLETED by car-capture. " +
"File is optional — omit it to complete the blame without attaching a video.", "Calling this endpoint returns an idempotent success response.",
}) })
async uploadBlameVideo( async uploadBlameVideo(
@Param("requestId") requestId: string, @Param("requestId") requestId: string,

View File

@@ -47,11 +47,9 @@ import {
import { GetCaptureRequirementsV2ResponseDto } from "src/claim-request-management/dto/capture-requirements-v2.dto"; import { GetCaptureRequirementsV2ResponseDto } from "src/claim-request-management/dto/capture-requirements-v2.dto";
/** /**
* V5 FileReviewer flow — same as V4 except the final blame accident video moves * V5 FileReviewer flow — same as V4 except after the damage expert completes
* the file to WAITING_FOR_FINANCIAL_EXPERT instead of WAITING_FOR_EXPERT. * their review, the claim goes to WAITING_FOR_FILE_MAKER_APPROVAL instead of
* * being submitted directly to fanavaran.
* A FinancialExpert must then approve the file (→ WAITING_FOR_EXPERT) or
* reject it back (→ FINANCIAL_EXPERT_REJECTED) for the FileReviewer to correct.
* *
* Sequence: * Sequence:
* accident-fields * accident-fields
@@ -60,9 +58,12 @@ import { GetCaptureRequirementsV2ResponseDto } from "src/claim-request-managemen
* → select-outer-parts * → select-outer-parts
* → select-other-parts * → select-other-parts
* → capture-part (damaged-part photos + four angles) * → capture-part (damaged-part photos + four angles)
* → upload-document (chassis / engine / metal plate — capture phase) * → upload-document (chassis / engine — capture phase)
* → car-capture (walk-around video) * → car-capture (walk-around video — FINAL FileReviewer step)
* → upload-video (blame accident video — final, moves to WAITING_FOR_FINANCIAL_EXPERT) * ↳ claim → WAITING_FOR_DAMAGE_EXPERT, blame → COMPLETED
* ↳ after expert review: claim → WAITING_FOR_FILE_MAKER_APPROVAL
*
* NOTE: upload-video is a no-op for V5 (blame is already COMPLETED by car-capture).
*/ */
@ApiTags("v5 FileReviewer — blame file review & capture (with FinancialExpert approval)") @ApiTags("v5 FileReviewer — blame file review & capture (with FinancialExpert approval)")
@Controller("v5/file-reviewer/blame-request-management") @Controller("v5/file-reviewer/blame-request-management")
@@ -340,9 +341,11 @@ export class FileReviewerBlameV5Controller {
}, },
}) })
@ApiOperation({ @ApiOperation({
summary: "Walk-around video", summary: "Walk-around video (final FileReviewer step — V5)",
description: description:
"Upload after capture-part is complete (parts, angles, capture-phase docs). Required before the final blame accident video.", "Last FileReviewer action. Upload after capture-part is complete (parts, angles, chassis/engine docs). " +
"Finalises the claim → WAITING_FOR_DAMAGE_EXPERT and marks the blame as COMPLETED. " +
"After the damage expert review completes the claim moves to WAITING_FOR_FILE_MAKER_APPROVAL.",
}) })
async carCapture( async carCapture(
@Param("claimRequestId") claimRequestId: string, @Param("claimRequestId") claimRequestId: string,
@@ -360,7 +363,6 @@ export class FileReviewerBlameV5Controller {
// ─── Final blame video ───────────────────────────────────────────────────────── // ─── Final blame video ─────────────────────────────────────────────────────────
// //
// V5 difference: moves file to WAITING_FOR_FINANCIAL_EXPERT, not WAITING_FOR_EXPERT.
@ApiBody({ @ApiBody({
schema: { schema: {
@@ -385,10 +387,10 @@ export class FileReviewerBlameV5Controller {
@Post("upload-video/:requestId") @Post("upload-video/:requestId")
@ApiParam({ name: "requestId" }) @ApiParam({ name: "requestId" })
@ApiOperation({ @ApiOperation({
summary: "Blame accident video (FileReviewer final step — V5, optional)", summary: "Blame accident video (no-op for V5 — car-capture is the final step)",
description: description:
"Last step of the V5 FileReviewer flow. Requires claim walk-around video and completed capture. " + "Deprecated for V5. The blame is already COMPLETED by car-capture. " +
"File is optional — omit it to complete the blame without attaching a video.", "Calling this endpoint returns an idempotent success response.",
}) })
async uploadBlameVideo( async uploadBlameVideo(
@Param("requestId") requestId: string, @Param("requestId") requestId: string,

View File

@@ -8814,6 +8814,14 @@ export class RequestManagementService {
}, },
} }
: {}), : {}),
// V5 (isMadeByFileMaker): mark approval gate at claim creation so the flag
// is always present regardless of whether upload-video is called.
...((req as any).isMadeByFileMaker
? {
requiresFileMakerApproval: true,
fileMakerApprovalActorId: new Types.ObjectId(actor.sub),
}
: {}),
history: [ history: [
{ {
type: "CLAIM_CREATED", type: "CLAIM_CREATED",
@@ -9816,19 +9824,15 @@ export class RequestManagementService {
} }
await (req as any).save(); await (req as any).save();
// Advance the claim into the damage-expert queue AND mark FileMaker approval // Advance the claim into the damage-expert queue.
// required before fanavaran submission. // requiresFileMakerApproval / fileMakerApprovalActorId were already set at
const fileMakerActorId = req.initiatedByFieldExpertId ?? null; // claim creation time (createV3ClaimFromBlame) so no need to repeat here.
await this.claimCaseDbService.findByIdAndUpdate(String(claim._id), { await this.claimCaseDbService.findByIdAndUpdate(String(claim._id), {
$set: { $set: {
status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT, status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT,
claimStatus: ClaimStatus.PENDING, claimStatus: ClaimStatus.PENDING,
"workflow.currentStep": ClaimWorkflowStep.USER_SUBMISSION_COMPLETE, "workflow.currentStep": ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
"workflow.nextStep": ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT, "workflow.nextStep": ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
requiresFileMakerApproval: true,
...(fileMakerActorId
? { fileMakerApprovalActorId: new Types.ObjectId(String(fileMakerActorId)) }
: {}),
}, },
$push: { $push: {
"workflow.completedSteps": ClaimWorkflowStep.USER_SUBMISSION_COMPLETE, "workflow.completedSteps": ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
@@ -9845,7 +9849,6 @@ export class RequestManagementService {
description: description:
"V5 field workflow complete. Claim ready for damage expert review (FileMaker approval required before fanavaran).", "V5 field workflow complete. Claim ready for damage expert review (FileMaker approval required before fanavaran).",
v5InPersonFlow: true, v5InPersonFlow: true,
fileMakerActorId: fileMakerActorId ? String(fileMakerActorId) : null,
}, },
}, },
}, },