forked from Yara724/api
Compare commits
21 Commits
7c59c2407e
...
5e4897f609
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e4897f609 | |||
| a79c3ca05f | |||
| 36fa1c552e | |||
| 9742fecc11 | |||
| 8007c30efd | |||
| 144f8f3e2a | |||
| 8674dd8762 | |||
|
|
f39c50aeb0 | ||
| 2fda740171 | |||
| 72e5bd616c | |||
| 4b41a60f64 | |||
| 9310285bd4 | |||
|
|
2a8b66bc16 | ||
| 9168a6bdcd | |||
|
|
057bedeb0c | ||
| 808a3b8526 | |||
|
|
da7a4f8890 | ||
| 21e55012be | |||
|
|
385757c3a0 | ||
| a1b122a33b | |||
|
|
aec9e76918 |
142
.woodpecker.yml
Normal file
142
.woodpecker.yml
Normal 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
BIN
assets/Vazirmatn-Bold.ttf
Normal file
Binary file not shown.
BIN
assets/Vazirmatn-Regular.ttf
Normal file
BIN
assets/Vazirmatn-Regular.ttf
Normal file
Binary file not shown.
@@ -8139,7 +8139,7 @@ export class ClaimRequestManagementService {
|
||||
file: Express.Multer.File,
|
||||
currentUserId: string,
|
||||
actor?: { sub: string; role?: string },
|
||||
options?: { v3InPersonFlow?: boolean; skipMetalPlate?: boolean },
|
||||
options?: { v3InPersonFlow?: boolean; skipMetalPlate?: boolean; requiresFileMakerApproval?: boolean },
|
||||
): Promise<UploadRequiredDocumentV2ResponseDto> {
|
||||
try {
|
||||
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||
@@ -8334,12 +8334,14 @@ export class ClaimRequestManagementService {
|
||||
progressAfterDoc.capturePhaseDocsComplete
|
||||
) {
|
||||
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["workflow.currentStep"] =
|
||||
ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS;
|
||||
$set["workflow.nextStep"] = options?.v3InPersonFlow
|
||||
? ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS
|
||||
: ClaimWorkflowStep.USER_SUBMISSION_COMPLETE;
|
||||
$set["workflow.currentStep"] = ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS;
|
||||
$set["workflow.nextStep"] = ClaimWorkflowStep.USER_SUBMISSION_COMPLETE;
|
||||
|
||||
completedStepsEntries.push(
|
||||
ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
||||
@@ -8354,8 +8356,9 @@ export class ClaimRequestManagementService {
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
stepKey: ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
||||
description:
|
||||
"Angles, damaged parts, and capture-phase vehicle evidence (chassis, engine, metal plate) are complete. Please upload remaining required documents.",
|
||||
description: options?.v3InPersonFlow
|
||||
? "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
|
||||
? 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
|
||||
? `Document saved. ${remaining} capture-phase document(s) still required (chassis, engine, metal plate).`
|
||||
: capturePhaseSequenceMessage(
|
||||
@@ -10205,6 +10210,18 @@ export class ClaimRequestManagementService {
|
||||
base: GetCaptureRequirementsV2ResponseDto,
|
||||
): GetCaptureRequirementsV2ResponseDto {
|
||||
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 capturePartDone = this.claimV3StepCompleted(
|
||||
claimCase,
|
||||
@@ -10355,7 +10372,7 @@ export class ClaimRequestManagementService {
|
||||
file,
|
||||
currentUserId,
|
||||
actor,
|
||||
{ v3InPersonFlow: true, skipMetalPlate },
|
||||
{ v3InPersonFlow: true, skipMetalPlate, requiresFileMakerApproval: !!(claimCase as any).requiresFileMakerApproval },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10550,7 +10567,7 @@ export class ClaimRequestManagementService {
|
||||
ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS
|
||||
) {
|
||||
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,
|
||||
fileDetail,
|
||||
currentUserId,
|
||||
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(
|
||||
|
||||
@@ -131,6 +131,13 @@ export class ClaimDetailV2ResponseDto {
|
||||
})
|
||||
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({
|
||||
description:
|
||||
"Slice of `claim.evaluation` exposed to the damage expert. " +
|
||||
|
||||
@@ -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`.",
|
||||
})
|
||||
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 {
|
||||
|
||||
@@ -4309,6 +4309,7 @@ export class ExpertClaimService {
|
||||
blameRequestId: c.blameRequestId?.toString(),
|
||||
createdAt: c.createdAt,
|
||||
awaitingFactorValidation: claimIsAwaitingExpertFactorValidationV2(c),
|
||||
requiresFileMakerApproval: !!(c as any).requiresFileMakerApproval,
|
||||
needsFileReviewerCompletion:
|
||||
String(blame?.status ?? "") === "WAITING_FOR_FILE_REVIEWER" ||
|
||||
(!!blame?.isMadeByFileMaker &&
|
||||
@@ -4692,8 +4693,8 @@ export class ExpertClaimService {
|
||||
const isFactorValidationPending =
|
||||
claimIsAwaitingExpertFactorValidationV2(claim);
|
||||
|
||||
// FileMaker: can only view V5 files (requiresFileMakerApproval: true) that they created.
|
||||
// V4 files do not require FileMaker approval and must not appear in their panel.
|
||||
// FileMaker: can view any file they created (both during filing and approval phase).
|
||||
// The list endpoint already filters to V5-only; detail access is scoped by ownership.
|
||||
if (actor.role === RoleEnum.FILE_MAKER) {
|
||||
const isOwn = claimCaseInitiatedByFieldExpert(claim, actor, linkedBlame);
|
||||
if (!isOwn) {
|
||||
@@ -4701,11 +4702,6 @@ export class ExpertClaimService {
|
||||
"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
|
||||
} else if (
|
||||
// Field experts and FileReviewers can view IN_PERSON expert-initiated claims
|
||||
@@ -5044,6 +5040,7 @@ export class ExpertClaimService {
|
||||
carAngles,
|
||||
damagedParts,
|
||||
awaitingFactorValidation: isFactorValidationPending,
|
||||
requiresFileMakerApproval: !!(claim as any).requiresFileMakerApproval,
|
||||
evaluation:
|
||||
(evaluationForApi as
|
||||
| ClaimDetailV2ResponseDto["evaluation"]
|
||||
|
||||
@@ -22,20 +22,18 @@ const LABEL_COLUMN_X = COLON_X + 10;
|
||||
|
||||
function resolveFontFile(variant: "regular" | "bold"): string {
|
||||
const file =
|
||||
variant === "bold"
|
||||
? "NotoKufiArabic-Bold.ttf"
|
||||
: "NotoKufiArabic-Regular.ttf";
|
||||
variant === "bold" ? "Vazirmatn-Bold.ttf" : "Vazirmatn-Regular.ttf";
|
||||
const candidates = [
|
||||
join(process.cwd(), "assets", 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("/usr/share/fonts/truetype/noto", file),
|
||||
];
|
||||
for (const path of candidates) {
|
||||
if (existsSync(path)) return path;
|
||||
}
|
||||
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;
|
||||
};
|
||||
|
||||
/** 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(
|
||||
doc: InstanceType<typeof PDFDocument>,
|
||||
text: string,
|
||||
@@ -69,6 +77,9 @@ function drawKeyValueRow(
|
||||
label: string,
|
||||
value: string,
|
||||
): void {
|
||||
// Reserve one row height (font 10pt ≈ 14 pts with leading) before drawing.
|
||||
ensureSpace(doc, 16);
|
||||
|
||||
const y = doc.y;
|
||||
const { label: faLabel, value: faValue } = pdfKitRtlKeyValue(label, value);
|
||||
|
||||
@@ -118,6 +129,9 @@ export function createPersianPdfDocument(): PersianPdfDocument {
|
||||
};
|
||||
|
||||
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);
|
||||
drawRtlLine(doc, text, 13, true);
|
||||
doc.font("FaRegular").fontSize(10);
|
||||
|
||||
@@ -8,18 +8,31 @@ export function normalizePersianPdfText(text: string): string {
|
||||
.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(
|
||||
label: string,
|
||||
value: string,
|
||||
): { label: string; value: string } {
|
||||
return {
|
||||
label: normalizePersianPdfText(label),
|
||||
value: normalizePersianPdfText(value),
|
||||
label: reverseRtlWords(normalizePersianPdfText(label)),
|
||||
value: reverseRtlWords(normalizePersianPdfText(value)),
|
||||
};
|
||||
}
|
||||
|
||||
export function pdfKitRtlParagraph(text: string): string {
|
||||
return normalizePersianPdfText(text);
|
||||
return reverseRtlWords(normalizePersianPdfText(text));
|
||||
}
|
||||
|
||||
export function isMostlyAscii(text: string): boolean {
|
||||
|
||||
@@ -51,8 +51,7 @@ import { GetCaptureRequirementsV2ResponseDto } from "src/claim-request-managemen
|
||||
*
|
||||
* The FileReviewer picks up a sealed file (both signatures collected by the
|
||||
* FileMaker) and completes the damage-assessment portion: accident fields,
|
||||
* documents, part selection, photo capture, walk-around video, and the final
|
||||
* blame accident video that moves the file to WAITING_FOR_EXPERT.
|
||||
* documents, part selection, photo capture, and walk-around video (final step).
|
||||
*
|
||||
* Sequence:
|
||||
* accident-fields
|
||||
@@ -61,9 +60,11 @@ import { GetCaptureRequirementsV2ResponseDto } from "src/claim-request-managemen
|
||||
* → select-outer-parts
|
||||
* → select-other-parts
|
||||
* → capture-part (damaged-part photos + four angles)
|
||||
* → upload-document (chassis / engine / metal plate — capture phase)
|
||||
* → car-capture (walk-around video)
|
||||
* → upload-video (blame accident video — final, moves to WAITING_FOR_EXPERT)
|
||||
* → upload-document (chassis / engine — capture phase)
|
||||
* → car-capture (walk-around video — FINAL FileReviewer step)
|
||||
* ↳ 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")
|
||||
@Controller("v4/file-reviewer/blame-request-management")
|
||||
@@ -341,9 +342,10 @@ export class FileReviewerBlameV4Controller {
|
||||
},
|
||||
})
|
||||
@ApiOperation({
|
||||
summary: "Walk-around video",
|
||||
summary: "Walk-around video (final FileReviewer step — V4)",
|
||||
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(
|
||||
@Param("claimRequestId") claimRequestId: string,
|
||||
@@ -384,10 +386,10 @@ export class FileReviewerBlameV4Controller {
|
||||
@Post("upload-video/:requestId")
|
||||
@ApiParam({ name: "requestId" })
|
||||
@ApiOperation({
|
||||
summary: "Blame accident video (FileReviewer final step — optional)",
|
||||
summary: "Blame accident video (no-op for V4 — car-capture is the final step)",
|
||||
description:
|
||||
"Last step of the V4 flow. Requires claim walk-around video and completed capture. " +
|
||||
"File is optional — omit it to complete the blame without attaching a video.",
|
||||
"Deprecated for V4. The blame is already COMPLETED by car-capture. " +
|
||||
"Calling this endpoint returns an idempotent success response.",
|
||||
})
|
||||
async uploadBlameVideo(
|
||||
@Param("requestId") requestId: string,
|
||||
|
||||
@@ -47,11 +47,9 @@ import {
|
||||
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
|
||||
* the file to WAITING_FOR_FINANCIAL_EXPERT instead of WAITING_FOR_EXPERT.
|
||||
*
|
||||
* A FinancialExpert must then approve the file (→ WAITING_FOR_EXPERT) or
|
||||
* reject it back (→ FINANCIAL_EXPERT_REJECTED) for the FileReviewer to correct.
|
||||
* V5 FileReviewer flow — same as V4 except after the damage expert completes
|
||||
* their review, the claim goes to WAITING_FOR_FILE_MAKER_APPROVAL instead of
|
||||
* being submitted directly to fanavaran.
|
||||
*
|
||||
* Sequence:
|
||||
* accident-fields
|
||||
@@ -60,9 +58,12 @@ import { GetCaptureRequirementsV2ResponseDto } from "src/claim-request-managemen
|
||||
* → select-outer-parts
|
||||
* → select-other-parts
|
||||
* → capture-part (damaged-part photos + four angles)
|
||||
* → upload-document (chassis / engine / metal plate — capture phase)
|
||||
* → car-capture (walk-around video)
|
||||
* → upload-video (blame accident video — final, moves to WAITING_FOR_FINANCIAL_EXPERT)
|
||||
* → upload-document (chassis / engine — capture phase)
|
||||
* → car-capture (walk-around video — FINAL FileReviewer step)
|
||||
* ↳ 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)")
|
||||
@Controller("v5/file-reviewer/blame-request-management")
|
||||
@@ -340,9 +341,11 @@ export class FileReviewerBlameV5Controller {
|
||||
},
|
||||
})
|
||||
@ApiOperation({
|
||||
summary: "Walk-around video",
|
||||
summary: "Walk-around video (final FileReviewer step — V5)",
|
||||
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(
|
||||
@Param("claimRequestId") claimRequestId: string,
|
||||
@@ -360,7 +363,6 @@ export class FileReviewerBlameV5Controller {
|
||||
|
||||
// ─── Final blame video ─────────────────────────────────────────────────────────
|
||||
//
|
||||
// V5 difference: moves file to WAITING_FOR_FINANCIAL_EXPERT, not WAITING_FOR_EXPERT.
|
||||
|
||||
@ApiBody({
|
||||
schema: {
|
||||
@@ -385,10 +387,10 @@ export class FileReviewerBlameV5Controller {
|
||||
@Post("upload-video/:requestId")
|
||||
@ApiParam({ name: "requestId" })
|
||||
@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:
|
||||
"Last step of the V5 FileReviewer flow. Requires claim walk-around video and completed capture. " +
|
||||
"File is optional — omit it to complete the blame without attaching a video.",
|
||||
"Deprecated for V5. The blame is already COMPLETED by car-capture. " +
|
||||
"Calling this endpoint returns an idempotent success response.",
|
||||
})
|
||||
async uploadBlameVideo(
|
||||
@Param("requestId") requestId: string,
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
type: "CLAIM_CREATED",
|
||||
@@ -9816,19 +9824,15 @@ export class RequestManagementService {
|
||||
}
|
||||
await (req as any).save();
|
||||
|
||||
// Advance the claim into the damage-expert queue AND mark FileMaker approval
|
||||
// required before fanavaran submission.
|
||||
const fileMakerActorId = req.initiatedByFieldExpertId ?? null;
|
||||
// Advance the claim into the damage-expert queue.
|
||||
// requiresFileMakerApproval / fileMakerApprovalActorId were already set at
|
||||
// claim creation time (createV3ClaimFromBlame) so no need to repeat here.
|
||||
await this.claimCaseDbService.findByIdAndUpdate(String(claim._id), {
|
||||
$set: {
|
||||
status: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT,
|
||||
claimStatus: ClaimStatus.PENDING,
|
||||
"workflow.currentStep": ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
||||
"workflow.nextStep": ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
||||
requiresFileMakerApproval: true,
|
||||
...(fileMakerActorId
|
||||
? { fileMakerApprovalActorId: new Types.ObjectId(String(fileMakerActorId)) }
|
||||
: {}),
|
||||
},
|
||||
$push: {
|
||||
"workflow.completedSteps": ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
||||
@@ -9845,7 +9849,6 @@ export class RequestManagementService {
|
||||
description:
|
||||
"V5 field workflow complete. Claim ready for damage expert review (FileMaker approval required before fanavaran).",
|
||||
v5InPersonFlow: true,
|
||||
fileMakerActorId: fileMakerActorId ? String(fileMakerActorId) : null,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user