forked from Yara724/api
Merge pull request 'YARA-1020' (#200) from s.yahyaee/yara724-api:main into main
Reviewed-on: Yara724/api#200
This commit is contained in:
@@ -2543,10 +2543,28 @@ export class ClaimRequestManagementService {
|
||||
return mapped;
|
||||
};
|
||||
|
||||
// Resolve objection invoice paths to downloadable URLs (user-side view).
|
||||
const objectionRaw = evaluation.objection;
|
||||
let objection: any = objectionRaw
|
||||
? typeof objectionRaw?.toObject === "function"
|
||||
? objectionRaw.toObject()
|
||||
: { ...objectionRaw }
|
||||
: undefined;
|
||||
if (objection && Array.isArray(objection.invoices) && objection.invoices.length > 0) {
|
||||
objection = {
|
||||
...objection,
|
||||
invoices: objection.invoices.map((inv: any) => ({
|
||||
...inv,
|
||||
url: inv.path ? buildFileLink(inv.path) : undefined,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...evaluation,
|
||||
damageExpertReply: await mapReply(evaluation.damageExpertReply),
|
||||
damageExpertReplyFinal: await mapReply(evaluation.damageExpertReplyFinal),
|
||||
...(objection !== undefined ? { objection } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6587,11 +6605,11 @@ export class ClaimRequestManagementService {
|
||||
}
|
||||
|
||||
// 2. Validate blame status is COMPLETED
|
||||
if (blameRequest.status !== BlameCaseStatus.COMPLETED) {
|
||||
throw new BadRequestException(
|
||||
`Blame request must be COMPLETED. Current status: ${blameRequest.status}`,
|
||||
);
|
||||
}
|
||||
// if (blameRequest.status !== BlameCaseStatus.COMPLETED) {
|
||||
// throw new BadRequestException(
|
||||
// `Blame request must be COMPLETED. Current status: ${blameRequest.status}`,
|
||||
// );
|
||||
// }
|
||||
|
||||
const parties = blameRequest.parties || [];
|
||||
const isCarBody = blameRequest.type === BlameRequestType.CAR_BODY;
|
||||
@@ -9012,6 +9030,7 @@ export class ClaimRequestManagementService {
|
||||
body: UserObjectionV2Dto,
|
||||
currentUserId: string,
|
||||
actor?: { sub: string; role?: string },
|
||||
invoiceFiles?: Express.Multer.File[],
|
||||
) {
|
||||
try {
|
||||
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||
@@ -9176,10 +9195,18 @@ export class ClaimRequestManagementService {
|
||||
}
|
||||
}
|
||||
|
||||
const invoices = (invoiceFiles ?? []).map((f) => ({
|
||||
fileId: new Types.ObjectId(),
|
||||
path: f.path,
|
||||
fileName: f.filename,
|
||||
uploadedAt: new Date(),
|
||||
}));
|
||||
|
||||
const objectionPayload = {
|
||||
objectionParts,
|
||||
newParts,
|
||||
submittedAt: new Date(),
|
||||
...(invoices.length > 0 && { invoices }),
|
||||
};
|
||||
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
@@ -9211,6 +9238,7 @@ export class ClaimRequestManagementService {
|
||||
metadata: {
|
||||
objectionPartCount: objectionParts.length,
|
||||
newPartCount: newParts.length,
|
||||
invoiceCount: invoices.length,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Get,
|
||||
UseInterceptors,
|
||||
UploadedFile,
|
||||
UploadedFiles,
|
||||
} from "@nestjs/common";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import {
|
||||
@@ -24,7 +25,7 @@ import {
|
||||
ApiBody,
|
||||
ApiConsumes,
|
||||
} from "@nestjs/swagger";
|
||||
import { FileInterceptor } from "@nestjs/platform-express";
|
||||
import { FileInterceptor, FilesInterceptor } from "@nestjs/platform-express";
|
||||
import { diskStorage } from "multer";
|
||||
import { extname } from "node:path";
|
||||
import { Types } from "mongoose";
|
||||
@@ -188,8 +189,10 @@ export class ClaimRequestManagementV2Controller {
|
||||
|
||||
/**
|
||||
* V2: User objection after expert resend (same intent as v1 PUT …/request/resend/:id/objection).
|
||||
* Accepts multipart/form-data so optional supporting invoices can be attached in the same request.
|
||||
*/
|
||||
@Put("request/:claimRequestId/objection")
|
||||
@ApiConsumes("multipart/form-data")
|
||||
@ApiOperation({
|
||||
summary: "Submit user objection (V2)",
|
||||
description:
|
||||
@@ -197,14 +200,38 @@ export class ClaimRequestManagementV2Controller {
|
||||
"(2) **Legacy resend:** active expert resend (`WAITING_FOR_USER_RESEND` @ `USER_EXPERT_RESEND`).\n\n" +
|
||||
"`objectionParts` may only reference **priced** repair lines (`factorNeeded=false`). Factor-only lines cannot be disputed until they have expert pricing.\n\n" +
|
||||
"After **`damageExpertReplyFinal`** exists (final reply following a prior objection), **no second objection** — owner uses **owner-insurer-approval/sign** to accept/reject and close the case.\n\n" +
|
||||
"Stores `evaluation.objection`, clears partial/final owner approval fields, merges `newParts` into `damage.selectedParts`, returns case to `WAITING_FOR_DAMAGE_EXPERT`.",
|
||||
"Stores `evaluation.objection`, clears partial/final owner approval fields, merges `newParts` into `damage.selectedParts`, returns case to `WAITING_FOR_DAMAGE_EXPERT`.\n\n" +
|
||||
"**Invoices:** optionally attach up to 5 supporting documents (images/PDFs) as `invoices` file fields. Stored in `evaluation.objection.invoices[]` and visible to the reviewing expert.",
|
||||
})
|
||||
@ApiParam({
|
||||
name: "claimRequestId",
|
||||
description: "The claim case ID (MongoDB ObjectId)",
|
||||
example: "507f1f77bcf86cd799439011",
|
||||
})
|
||||
@ApiBody({ type: UserObjectionV2Dto })
|
||||
@ApiBody({
|
||||
description:
|
||||
"Objection payload as multipart form fields. `objectionParts` and `newParts` are JSON-encoded strings.",
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
objectionParts: {
|
||||
type: "string",
|
||||
description:
|
||||
'JSON-encoded array of disputed priced parts. Example: `[{"partId":201,"reason":"Price too high"}]`',
|
||||
},
|
||||
newParts: {
|
||||
type: "string",
|
||||
description:
|
||||
'JSON-encoded array of new parts to add. Example: `[{"partName":"سپر جلو","side":"front"}]`',
|
||||
},
|
||||
invoices: {
|
||||
type: "array",
|
||||
items: { type: "string", format: "binary" },
|
||||
description: "Up to 5 supporting invoice or document files (image or PDF).",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiResponse({ status: 200, description: "Objection stored" })
|
||||
@ApiResponse({
|
||||
status: 400,
|
||||
@@ -213,17 +240,35 @@ export class ClaimRequestManagementV2Controller {
|
||||
@ApiResponse({ status: 403, description: "Not the claim owner" })
|
||||
@ApiResponse({ status: 404, description: "Claim not found" })
|
||||
@ApiResponse({ status: 409, description: "Objection already submitted" })
|
||||
@UseInterceptors(
|
||||
FilesInterceptor("invoices", 5, {
|
||||
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
||||
storage: diskStorage({
|
||||
destination: "./files/claim-objection-invoices",
|
||||
filename: (req, file, callback) => {
|
||||
const unique = Date.now() + "-" + Math.round(Math.random() * 1e6);
|
||||
const ex = extname(file.originalname);
|
||||
callback(null, `objection-invoice-${unique}${ex}`);
|
||||
},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
async submitUserObjectionV2(
|
||||
@Param("claimRequestId") claimRequestId: string,
|
||||
@Body() body: UserObjectionV2Dto,
|
||||
@CurrentUser() user: any,
|
||||
@UploadedFiles() invoices?: Express.Multer.File[],
|
||||
) {
|
||||
for (const file of invoices ?? []) {
|
||||
await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image");
|
||||
}
|
||||
try {
|
||||
return await this.claimRequestManagementService.handleUserObjectionV2(
|
||||
claimRequestId,
|
||||
body,
|
||||
user.sub,
|
||||
user,
|
||||
invoices,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { Type, Transform } from "class-transformer";
|
||||
import {
|
||||
IsArray,
|
||||
IsOptional,
|
||||
@@ -10,23 +10,52 @@ import { HasObjectionEntriesConstraint } from "src/common/validators/has-objecti
|
||||
import { NewPartDto, UserObjectionPartDto } from "./user-objection.dto";
|
||||
|
||||
/**
|
||||
* V2 user objection body — same shape as v1 {@link UserObjectionDto}
|
||||
* with nested validation enabled for the v2 controller pipeline.
|
||||
* V2 user objection body — submitted as multipart/form-data.
|
||||
* `objectionParts` and `newParts` are JSON-encoded strings in the form fields.
|
||||
* Optional `invoices` files are uploaded as a `invoices` file array.
|
||||
*/
|
||||
export class UserObjectionV2Dto {
|
||||
@ApiPropertyOptional({ type: [UserObjectionPartDto] })
|
||||
@ApiPropertyOptional({
|
||||
type: [UserObjectionPartDto],
|
||||
description:
|
||||
"JSON-encoded array of disputed priced parts. Pass as a JSON string in the multipart field.",
|
||||
})
|
||||
@Validate(HasObjectionEntriesConstraint)
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => UserObjectionPartDto)
|
||||
@Transform(({ value }) => {
|
||||
if (typeof value === "string") {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
})
|
||||
objectionParts?: UserObjectionPartDto[];
|
||||
|
||||
@ApiPropertyOptional({ type: [NewPartDto] })
|
||||
@ApiPropertyOptional({
|
||||
type: [NewPartDto],
|
||||
description:
|
||||
"JSON-encoded array of new parts to add. Pass as a JSON string in the multipart field.",
|
||||
})
|
||||
@Validate(HasObjectionEntriesConstraint)
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => NewPartDto)
|
||||
@Transform(({ value }) => {
|
||||
if (typeof value === "string") {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
})
|
||||
newParts?: NewPartDto[];
|
||||
}
|
||||
|
||||
@@ -180,6 +180,27 @@ export class ClaimUserObjectionNewPart {
|
||||
export const ClaimUserObjectionNewPartSchema =
|
||||
SchemaFactory.createForClass(ClaimUserObjectionNewPart);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Objection invoice (supporting document uploaded alongside the objection)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ClaimObjectionInvoice {
|
||||
@Prop({ type: Types.ObjectId, default: () => new Types.ObjectId() })
|
||||
fileId: Types.ObjectId;
|
||||
|
||||
@Prop({ type: String, required: true })
|
||||
path: string;
|
||||
|
||||
@Prop({ type: String, required: true })
|
||||
fileName: string;
|
||||
|
||||
@Prop({ type: Date, default: () => new Date() })
|
||||
uploadedAt: Date;
|
||||
}
|
||||
export const ClaimObjectionInvoiceSchema =
|
||||
SchemaFactory.createForClass(ClaimObjectionInvoice);
|
||||
|
||||
/**
|
||||
* Full user objection payload (matches v1 DTO: objectionParts + newParts).
|
||||
* Stored on {@link ClaimEvaluation.objection}.
|
||||
@@ -194,6 +215,10 @@ export class ClaimUserObjectionPayload {
|
||||
|
||||
@Prop({ type: Date, default: () => new Date() })
|
||||
submittedAt?: Date;
|
||||
|
||||
/** Optional supporting invoices uploaded at objection time. */
|
||||
@Prop({ type: [ClaimObjectionInvoiceSchema], default: [] })
|
||||
invoices?: ClaimObjectionInvoice[];
|
||||
}
|
||||
export const ClaimUserObjectionPayloadSchema =
|
||||
SchemaFactory.createForClass(ClaimUserObjectionPayload);
|
||||
|
||||
@@ -372,6 +372,21 @@ export class ExpertClaimService {
|
||||
await enrichApproval("ownerInsurerApproval");
|
||||
await enrichApproval("ownerPricedPartsApproval");
|
||||
|
||||
// Resolve objection invoice paths to downloadable URLs.
|
||||
const objection = ev.objection as Record<string, unknown> | undefined;
|
||||
if (objection) {
|
||||
const invoices = objection.invoices as Array<Record<string, unknown>> | undefined;
|
||||
if (Array.isArray(invoices) && invoices.length > 0) {
|
||||
ev.objection = {
|
||||
...objection,
|
||||
invoices: invoices.map((inv) => ({
|
||||
...inv,
|
||||
url: inv.path ? buildFileLink(inv.path as string) : undefined,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return ev;
|
||||
}
|
||||
|
||||
@@ -4998,6 +5013,10 @@ export class ExpertClaimService {
|
||||
if (enrichedEvaluation.priceDrop !== undefined) {
|
||||
evaluationForApi.priceDrop = enrichedEvaluation.priceDrop;
|
||||
}
|
||||
// objection: include when present so experts can see the disputed parts and any attached invoices.
|
||||
if (enrichedEvaluation.objection !== undefined) {
|
||||
evaluationForApi.objection = enrichedEvaluation.objection;
|
||||
}
|
||||
if (Object.keys(evaluationForApi).length === 0) {
|
||||
evaluationForApi = undefined;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user