forked from Yara724/api
Compare commits
8 Commits
44f7ce5b54
...
d84bd24682
| Author | SHA1 | Date | |
|---|---|---|---|
| d84bd24682 | |||
|
|
ed2b6948cf | ||
|
|
c6b417ced7 | ||
| 16d3c54613 | |||
|
|
db569db9d1 | ||
| 83e82ea68e | |||
|
|
8f29bb564c | ||
| 89e715b0c9 |
@@ -60,6 +60,7 @@ SMS_PROVIDER =
|
|||||||
SMS_API_KEY =
|
SMS_API_KEY =
|
||||||
AUTH_SMS_TEMPLATE =
|
AUTH_SMS_TEMPLATE =
|
||||||
EXP_OTP_TIME =
|
EXP_OTP_TIME =
|
||||||
|
FAKE_OTP =
|
||||||
|
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
# ⚙️ Application Features / Flags
|
# ⚙️ Application Features / Flags
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
} from "src/helpers/iran-mobile";
|
} from "src/helpers/iran-mobile";
|
||||||
import {
|
import {
|
||||||
computeOtpExpireMs,
|
computeOtpExpireMs,
|
||||||
|
FAKE_OTP_CODE,
|
||||||
|
isFakeOtpEnabled,
|
||||||
isOtpExpiryActive,
|
isOtpExpiryActive,
|
||||||
readOtpExpireMinutesFromEnv,
|
readOtpExpireMinutesFromEnv,
|
||||||
} from "src/helpers/user-otp-expiry";
|
} from "src/helpers/user-otp-expiry";
|
||||||
@@ -109,7 +111,7 @@ export class UserAuthService {
|
|||||||
const userExist = await this.userDbService.findOne(
|
const userExist = await this.userDbService.findOne(
|
||||||
buildUserLookupByPhone(canonicalMobile),
|
buildUserLookupByPhone(canonicalMobile),
|
||||||
);
|
);
|
||||||
const otp = this.otpCreator.create();
|
const otp = this.createOtpForRequest();
|
||||||
const hashOtp = await this.hashService.hash(otp);
|
const hashOtp = await this.hashService.hash(otp);
|
||||||
const expireMinutes = readOtpExpireMinutesFromEnv();
|
const expireMinutes = readOtpExpireMinutesFromEnv();
|
||||||
const nowMs = Date.now();
|
const nowMs = Date.now();
|
||||||
@@ -158,7 +160,23 @@ export class UserAuthService {
|
|||||||
return new LoginDtoRs(userExist);
|
return new LoginDtoRs(userExist);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createOtpForRequest(): string {
|
||||||
|
if (isFakeOtpEnabled()) {
|
||||||
|
this.logger.warn(
|
||||||
|
"FAKE_OTP=true — using fixed dev OTP; SMS provider is not called",
|
||||||
|
);
|
||||||
|
return FAKE_OTP_CODE;
|
||||||
|
}
|
||||||
|
return this.otpCreator.create();
|
||||||
|
}
|
||||||
|
|
||||||
private async smsSender(otp: string, mobile: string) {
|
private async smsSender(otp: string, mobile: string) {
|
||||||
|
if (isFakeOtpEnabled()) {
|
||||||
|
this.logger.log(
|
||||||
|
`FAKE_OTP=true — skipped SMS for phone=${mobile} (use OTP ${FAKE_OTP_CODE})`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const ok = await this.smsOrchestrationService.sendAuthOtp(
|
const ok = await this.smsOrchestrationService.sendAuthOtp(
|
||||||
mobile,
|
mobile,
|
||||||
otp,
|
otp,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
ApiQuery,
|
ApiQuery,
|
||||||
ApiTags,
|
ApiTags,
|
||||||
ApiOperation,
|
ApiOperation,
|
||||||
|
ApiExcludeController,
|
||||||
} from "@nestjs/swagger";
|
} from "@nestjs/swagger";
|
||||||
import { diskStorage } from "multer";
|
import { diskStorage } from "multer";
|
||||||
import { ClaimAccessGuard } from "src/auth/guards/claim-access.guard";
|
import { ClaimAccessGuard } from "src/auth/guards/claim-access.guard";
|
||||||
@@ -39,8 +40,9 @@ import { UserObjectionDto } from "./dto/user-objection.dto";
|
|||||||
import { InPersonVisitDto } from "./dto/in-person-visit.dto";
|
import { InPersonVisitDto } from "./dto/in-person-visit.dto";
|
||||||
import { UserRatingDto } from "./dto/user-rating.dto";
|
import { UserRatingDto } from "./dto/user-rating.dto";
|
||||||
|
|
||||||
|
@ApiExcludeController()
|
||||||
@Controller("claim-request-management")
|
@Controller("claim-request-management")
|
||||||
@ApiTags("claim-request-management")
|
// @ApiTags("claim-request-management")
|
||||||
@Roles(RoleEnum.USER, RoleEnum.EXPERT, RoleEnum.DAMAGE_EXPERT)
|
@Roles(RoleEnum.USER, RoleEnum.EXPERT, RoleEnum.DAMAGE_EXPERT)
|
||||||
@UseGuards(ClaimAccessGuard, RolesGuard)
|
@UseGuards(ClaimAccessGuard, RolesGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@@ -49,8 +51,8 @@ export class ClaimRequestManagementController {
|
|||||||
private readonly claimRequestManagementService: ClaimRequestManagementService,
|
private readonly claimRequestManagementService: ClaimRequestManagementService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ApiParam({ name: "blameId" })
|
// @ApiParam({ name: "blameId" })
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Post("/:blameId")
|
@Post("/:blameId")
|
||||||
async createClaimRequest(
|
async createClaimRequest(
|
||||||
@Param("blameId") requestId: string,
|
@Param("blameId") requestId: string,
|
||||||
@@ -63,10 +65,10 @@ export class ClaimRequestManagementController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiBody({ type: CarDamagePartDto })
|
// @ApiBody({ type: CarDamagePartDto })
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Patch("/car-part-damage/:claimRequestID")
|
@Patch("/car-part-damage/:claimRequestID")
|
||||||
@ApiParam({ name: "claimRequestID" })
|
// @ApiParam({ name: "claimRequestID" })
|
||||||
async carPartDamage(
|
async carPartDamage(
|
||||||
@Param("claimRequestID") requestId: string,
|
@Param("claimRequestID") requestId: string,
|
||||||
@Body() body: CarDamagePartDto,
|
@Body() body: CarDamagePartDto,
|
||||||
@@ -79,7 +81,7 @@ export class ClaimRequestManagementController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Get("/car-other-part")
|
@Get("/car-other-part")
|
||||||
async getCarOtherParts() {
|
async getCarOtherParts() {
|
||||||
const carOtherPart = await readFile(
|
const carOtherPart = await readFile(
|
||||||
@@ -89,8 +91,8 @@ export class ClaimRequestManagementController {
|
|||||||
return carOtherPart;
|
return carOtherPart;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiBody({ type: OtherCarDamagePartDto })
|
// @ApiBody({ type: OtherCarDamagePartDto })
|
||||||
@ApiParam({ name: "claimRequestID" })
|
// @ApiParam({ name: "claimRequestID" })
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
FileInterceptor("file", {
|
FileInterceptor("file", {
|
||||||
limits: {
|
limits: {
|
||||||
@@ -108,7 +110,7 @@ export class ClaimRequestManagementController {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ApiConsumes("multipart/form-data")
|
@ApiConsumes("multipart/form-data")
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Patch("/car-other-part-damage/:claimRequestID")
|
@Patch("/car-other-part-damage/:claimRequestID")
|
||||||
async carOtherPartDamage(
|
async carOtherPartDamage(
|
||||||
@Param("claimRequestID") requestId: string,
|
@Param("claimRequestID") requestId: string,
|
||||||
@@ -124,32 +126,32 @@ export class ClaimRequestManagementController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Get("required-documents-status/:claimRequestID")
|
@Get("required-documents-status/:claimRequestID")
|
||||||
@ApiParam({ name: "claimRequestID" })
|
// @ApiParam({ name: "claimRequestID" })
|
||||||
async getRequiredDocumentsStatus(@Param("claimRequestID") requestId: string) {
|
async getRequiredDocumentsStatus(@Param("claimRequestID") requestId: string) {
|
||||||
return await this.claimRequestManagementService.getRequiredDocumentsStatus(
|
return await this.claimRequestManagementService.getRequiredDocumentsStatus(
|
||||||
requestId,
|
requestId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Get("car-part-image-required/:claimRequestID")
|
@Get("car-part-image-required/:claimRequestID")
|
||||||
@ApiParam({ name: "claimRequestID" })
|
// @ApiParam({ name: "claimRequestID" })
|
||||||
async getImageRequired(@Param("claimRequestID") requestId) {
|
async getImageRequired(@Param("claimRequestID") requestId) {
|
||||||
return await this.claimRequestManagementService.getImageRequiredList(
|
return await this.claimRequestManagementService.getImageRequiredList(
|
||||||
requestId,
|
requestId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiBody({
|
// @ApiBody({
|
||||||
schema: {
|
// schema: {
|
||||||
type: "object",
|
// type: "object",
|
||||||
properties: {
|
// properties: {
|
||||||
file: { type: "string", format: "binary" },
|
// file: { type: "string", format: "binary" },
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
})
|
// })
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
FileInterceptor("file", {
|
FileInterceptor("file", {
|
||||||
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
||||||
@@ -169,14 +171,14 @@ export class ClaimRequestManagementController {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ApiConsumes("multipart/form-data")
|
// @ApiConsumes("multipart/form-data")
|
||||||
@ApiParam({ name: "claimRequestID" })
|
// // @ApiParam({ name: "claimRequestID" })
|
||||||
@ApiQuery({
|
// @ApiQuery({
|
||||||
name: "documentType",
|
// name: "documentType",
|
||||||
enum: ClaimRequiredDocumentType,
|
// enum: ClaimRequiredDocumentType,
|
||||||
description: "Type of required document to upload",
|
// description: "Type of required document to upload",
|
||||||
})
|
// })
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Patch("upload-required-document/:claimRequestID")
|
@Patch("upload-required-document/:claimRequestID")
|
||||||
async uploadRequiredDocument(
|
async uploadRequiredDocument(
|
||||||
@Param("claimRequestID") requestId: string,
|
@Param("claimRequestID") requestId: string,
|
||||||
@@ -195,14 +197,14 @@ export class ClaimRequestManagementController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiBody({
|
// @ApiBody({
|
||||||
schema: {
|
// schema: {
|
||||||
type: "object",
|
// type: "object",
|
||||||
properties: {
|
// properties: {
|
||||||
file: { type: "string", format: "binary" },
|
// file: { type: "string", format: "binary" },
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
})
|
// })
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
FileInterceptor("file", {
|
FileInterceptor("file", {
|
||||||
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
||||||
@@ -223,13 +225,13 @@ export class ClaimRequestManagementController {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ApiConsumes("multipart/form-data")
|
// @ApiConsumes("multipart/form-data")
|
||||||
@ApiParam({ name: "claimRequestID" })
|
// @ApiParam({ name: "claimRequestID" })
|
||||||
@ApiParam({
|
// @ApiParam({
|
||||||
name: "partId",
|
// name: "partId",
|
||||||
description: "The ID of the specific car part being photographed.",
|
// description: "The ID of the specific car part being photographed.",
|
||||||
})
|
// })
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Patch("capture-car-part-damage/:claimRequestID/:partId")
|
@Patch("capture-car-part-damage/:claimRequestID/:partId")
|
||||||
async captureCarPartDamage(
|
async captureCarPartDamage(
|
||||||
@Param("partId") partId: string,
|
@Param("partId") partId: string,
|
||||||
@@ -246,14 +248,14 @@ export class ClaimRequestManagementController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiBody({
|
// @ApiBody({
|
||||||
schema: {
|
// schema: {
|
||||||
type: "object",
|
// type: "object",
|
||||||
properties: {
|
// properties: {
|
||||||
file: { type: "string", format: "binary" },
|
// file: { type: "string", format: "binary" },
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
})
|
// })
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
FileInterceptor("file", {
|
FileInterceptor("file", {
|
||||||
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
||||||
@@ -268,9 +270,9 @@ export class ClaimRequestManagementController {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ApiConsumes("multipart/form-data")
|
// @ApiConsumes("multipart/form-data")
|
||||||
@ApiParam({ name: "claimRequestID" })
|
// @ApiParam({ name: "claimRequestID" })
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Patch("car-capture/:claimRequestID")
|
@Patch("car-capture/:claimRequestID")
|
||||||
async captureVideoCapture(
|
async captureVideoCapture(
|
||||||
@Param("claimRequestID") requestId: string,
|
@Param("claimRequestID") requestId: string,
|
||||||
@@ -282,22 +284,22 @@ export class ClaimRequestManagementController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Get("requests/")
|
@Get("requests/")
|
||||||
async getRequest(@CurrentUser() currentUser) {
|
async getRequest(@CurrentUser() currentUser) {
|
||||||
return await this.claimRequestManagementService.myRequests(currentUser);
|
return await this.claimRequestManagementService.myRequests(currentUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Get("request/:claimRequestId")
|
@Get("request/:claimRequestId")
|
||||||
@ApiParam({ name: "claimRequestId" })
|
// @ApiParam({ name: "claimRequestId" })
|
||||||
myRequests(@Param("claimRequestId") requestId: string, @CurrentUser() user) {
|
myRequests(@Param("claimRequestId") requestId: string, @CurrentUser() user) {
|
||||||
return this.claimRequestManagementService.requestDetails(requestId, user);
|
return this.claimRequestManagementService.requestDetails(requestId, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Put("request/reply/:claimRequestId")
|
@Put("request/reply/:claimRequestId")
|
||||||
@ApiParam({ name: "claimRequestId" })
|
// @ApiParam({ name: "claimRequestId" })
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
FileInterceptor("file", {
|
FileInterceptor("file", {
|
||||||
limits: {
|
limits: {
|
||||||
@@ -314,12 +316,12 @@ export class ClaimRequestManagementController {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ApiBody({
|
// @ApiBody({
|
||||||
type: UserCommentDto,
|
// type: UserCommentDto,
|
||||||
description: "if partId null , you can upload video capture",
|
// description: "if partId null , you can upload video capture",
|
||||||
})
|
// })
|
||||||
@ApiConsumes("multipart/form-data")
|
// @ApiConsumes("multipart/form-data")
|
||||||
@ApiParam({ name: "claimRequestId" })
|
// @ApiParam({ name: "claimRequestId" })
|
||||||
async submitReply(
|
async submitReply(
|
||||||
@Param("claimRequestId") requestId,
|
@Param("claimRequestId") requestId,
|
||||||
@Body() body,
|
@Body() body,
|
||||||
@@ -334,14 +336,14 @@ export class ClaimRequestManagementController {
|
|||||||
// );
|
// );
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Put("request/resend/:claimRequestId/objection")
|
@Put("request/resend/:claimRequestId/objection")
|
||||||
@ApiParam({ name: "claimRequestId" })
|
// @ApiParam({ name: "claimRequestId" })
|
||||||
@ApiConsumes("application/json")
|
// @ApiConsumes("application/json")
|
||||||
@ApiBody({
|
// @ApiBody({
|
||||||
type: UserObjectionDto,
|
// type: UserObjectionDto,
|
||||||
description: "Objection details with optional new parts",
|
// description: "Objection details with optional new parts",
|
||||||
})
|
// })
|
||||||
async handleUserObjection(
|
async handleUserObjection(
|
||||||
@Param("claimRequestId") claimRequestId: string,
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
@Body() userObjectionDto: UserObjectionDto,
|
@Body() userObjectionDto: UserObjectionDto,
|
||||||
@@ -352,25 +354,25 @@ export class ClaimRequestManagementController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Patch("request/resend/:claimRequestId")
|
@Patch("request/resend/:claimRequestId")
|
||||||
@ApiConsumes("multipart/form-data")
|
// @ApiConsumes("multipart/form-data")
|
||||||
@ApiParam({ name: "claimRequestId" })
|
// @ApiParam({ name: "claimRequestId" })
|
||||||
@ApiQuery({ name: "fields", enum: ["resendDocuments", "resendCarParts"] })
|
// @ApiQuery({ name: "fields", enum: ["resendDocuments", "resendCarParts"] })
|
||||||
@ApiQuery({ name: "partId", required: false })
|
// @ApiQuery({ name: "partId", required: false })
|
||||||
@ApiQuery({ name: "documentName", required: false })
|
// @ApiQuery({ name: "documentName", required: false })
|
||||||
@ApiQuery({ name: "side", required: false })
|
// @ApiQuery({ name: "side", required: false })
|
||||||
@ApiBody({
|
// @ApiBody({
|
||||||
schema: {
|
// schema: {
|
||||||
type: "object",
|
// type: "object",
|
||||||
properties: {
|
// properties: {
|
||||||
file: {
|
// file: {
|
||||||
type: "string",
|
// type: "string",
|
||||||
format: "binary",
|
// format: "binary",
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
})
|
// })
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
FileInterceptor("file", {
|
FileInterceptor("file", {
|
||||||
storage: diskStorage({
|
storage: diskStorage({
|
||||||
@@ -403,10 +405,10 @@ export class ClaimRequestManagementController {
|
|||||||
* User satisfaction rating for a completed claim file.
|
* User satisfaction rating for a completed claim file.
|
||||||
* Only the damaged user (claim owner) can rate their claim after it is closed.
|
* Only the damaged user (claim owner) can rate their claim after it is closed.
|
||||||
*/
|
*/
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Put("request/:claimRequestId/user-rating")
|
@Put("request/:claimRequestId/user-rating")
|
||||||
@ApiParam({ name: "claimRequestId" })
|
// @ApiParam({ name: "claimRequestId" })
|
||||||
@ApiBody({ type: UserRatingDto })
|
// @ApiBody({ type: UserRatingDto })
|
||||||
async addUserRating(
|
async addUserRating(
|
||||||
@Param("claimRequestId") claimRequestId: string,
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
@Body() ratingDto: UserRatingDto,
|
@Body() ratingDto: UserRatingDto,
|
||||||
@@ -419,22 +421,22 @@ export class ClaimRequestManagementController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Patch("request/reply/:claimRequestId/:partId/upload-factor")
|
@Patch("request/reply/:claimRequestId/:partId/upload-factor")
|
||||||
@ApiConsumes("multipart/form-data")
|
// @ApiConsumes("multipart/form-data")
|
||||||
@ApiParam({ name: "claimRequestId" })
|
// @ApiParam({ name: "claimRequestId" })
|
||||||
@ApiParam({ name: "partId" })
|
// @ApiParam({ name: "partId" })
|
||||||
@ApiBody({
|
// @ApiBody({
|
||||||
schema: {
|
// schema: {
|
||||||
type: "object",
|
// type: "object",
|
||||||
properties: {
|
// properties: {
|
||||||
file: {
|
// file: {
|
||||||
type: "string",
|
// type: "string",
|
||||||
format: "binary",
|
// format: "binary",
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
})
|
// })
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
FileInterceptor("file", {
|
FileInterceptor("file", {
|
||||||
storage: diskStorage({
|
storage: diskStorage({
|
||||||
@@ -462,9 +464,9 @@ export class ClaimRequestManagementController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiBody({ type: InPersonVisitDto })
|
// @ApiBody({ type: InPersonVisitDto })
|
||||||
@ApiParam({ name: "id" })
|
// @ApiParam({ name: "id" })
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Patch(":id/visit")
|
@Patch(":id/visit")
|
||||||
async inPersonVisit(
|
async inPersonVisit(
|
||||||
@Param("id") requestId: string,
|
@Param("id") requestId: string,
|
||||||
@@ -479,26 +481,25 @@ export class ClaimRequestManagementController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Get("branches/:insuranceId")
|
@Get("branches/:insuranceId")
|
||||||
|
// @ApiParam({ name: "insuranceId" })
|
||||||
async insuranceBranches(@Param("insuranceId") insuranceId: string) {
|
async insuranceBranches(@Param("insuranceId") insuranceId: string) {
|
||||||
return await this.claimRequestManagementService.retrieveInsuranceBranches(
|
return await this.claimRequestManagementService.retrieveInsuranceBranches(insuranceId);
|
||||||
insuranceId,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Get("fanavaran-submit/:claimRequestId")
|
@Get("fanavaran-submit/:claimRequestId")
|
||||||
@ApiParam({ name: "claimRequestId" })
|
// @ApiParam({ name: "claimRequestId" })
|
||||||
async fanavaranSubmit(@Param("claimRequestId") claimRequestId: string) {
|
async fanavaranSubmit(@Param("claimRequestId") claimRequestId: string) {
|
||||||
return await this.claimRequestManagementService.fanavaranSubmit(
|
return await this.claimRequestManagementService.fanavaranSubmit(
|
||||||
claimRequestId,
|
claimRequestId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Post("fanavaran-submit/:claimRequestId")
|
@Post("fanavaran-submit/:claimRequestId")
|
||||||
@ApiParam({ name: "claimRequestId" })
|
// @ApiParam({ name: "claimRequestId" })
|
||||||
async submitToFanavaran(@Param("claimRequestId") claimRequestId: string) {
|
async submitToFanavaran(@Param("claimRequestId") claimRequestId: string) {
|
||||||
return await this.claimRequestManagementService.submitToFanavaran(
|
return await this.claimRequestManagementService.submitToFanavaran(
|
||||||
claimRequestId,
|
claimRequestId,
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
SelectOtherPartsV2Dto,
|
SelectOtherPartsV2Dto,
|
||||||
SelectOtherPartsV2ResponseDto,
|
SelectOtherPartsV2ResponseDto,
|
||||||
} from "./dto/select-other-parts-v2.dto";
|
} from "./dto/select-other-parts-v2.dto";
|
||||||
|
import { SelectOtherPartsV3Dto } from "./dto/select-other-parts-v3.dto";
|
||||||
import { GetCaptureRequirementsV2ResponseDto } from "./dto/capture-requirements-v2.dto";
|
import { GetCaptureRequirementsV2ResponseDto } from "./dto/capture-requirements-v2.dto";
|
||||||
import {
|
import {
|
||||||
UploadRequiredDocumentV2Dto,
|
UploadRequiredDocumentV2Dto,
|
||||||
@@ -82,6 +83,7 @@ import { ClaimRequiredDocumentType } from "src/Types&Enums/claim-request-managem
|
|||||||
import { ClaimStatus } from "src/Types&Enums/claim-request-management/claimStatus.enum";
|
import { ClaimStatus } from "src/Types&Enums/claim-request-management/claimStatus.enum";
|
||||||
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
|
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
|
||||||
import { CaseStatus as BlameCaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
|
import { CaseStatus as BlameCaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
|
||||||
|
import { WorkflowStep } from "src/Types&Enums/blame-request-management/blameWorkflow-steps.enum";
|
||||||
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||||
import { ClaimSignDbService } from "./entites/db-service/claim-sign.db.service";
|
import { ClaimSignDbService } from "./entites/db-service/claim-sign.db.service";
|
||||||
import { DamageImageDbService } from "./entites/db-service/damage-image.db.service";
|
import { DamageImageDbService } from "./entites/db-service/damage-image.db.service";
|
||||||
@@ -307,6 +309,44 @@ export class ClaimRequestManagementService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** V3 initial document step — excludes capture-phase vehicle evidence. */
|
||||||
|
private v3PreCaptureDocumentKeys(isCarBody: boolean): string[] {
|
||||||
|
return this.requiredDocumentKeysV2(isCarBody).filter(
|
||||||
|
(k) => !isCapturePhaseDamagedPartyDocKey(k),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private allV3PreCaptureDocumentsComplete(
|
||||||
|
claimCase: any,
|
||||||
|
isCarBody: boolean,
|
||||||
|
assumeUploadedKey?: string,
|
||||||
|
): boolean {
|
||||||
|
for (const k of this.v3PreCaptureDocumentKeys(isCarBody)) {
|
||||||
|
const ok =
|
||||||
|
k === assumeUploadedKey
|
||||||
|
? true
|
||||||
|
: this.isRequiredDocumentUploadedOnClaim(claimCase, k);
|
||||||
|
if (!ok) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private countRemainingV3PreCaptureDocuments(
|
||||||
|
claimCase: any,
|
||||||
|
isCarBody: boolean,
|
||||||
|
assumeUploadedKey?: string,
|
||||||
|
): number {
|
||||||
|
let n = 0;
|
||||||
|
for (const k of this.v3PreCaptureDocumentKeys(isCarBody)) {
|
||||||
|
const ok =
|
||||||
|
k === assumeUploadedKey
|
||||||
|
? true
|
||||||
|
: this.isRequiredDocumentUploadedOnClaim(claimCase, k);
|
||||||
|
if (!ok) n++;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
private countRemainingV2OwnerDocuments(
|
private countRemainingV2OwnerDocuments(
|
||||||
claimCase: any,
|
claimCase: any,
|
||||||
isCarBody: boolean,
|
isCarBody: boolean,
|
||||||
@@ -5060,6 +5100,101 @@ export class ClaimRequestManagementService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V2: Registrar creates a claim from a registrar-initiated IN_PERSON completed blame.
|
||||||
|
* Mirrors createClaimFromBlameForRegistrarV1 but uses V2 naming conventions.
|
||||||
|
*/
|
||||||
|
async createClaimFromBlameForRegistrarV2(
|
||||||
|
blameRequestId: string,
|
||||||
|
registrar: { sub: string; firstName?: string; lastName?: string },
|
||||||
|
): Promise<CreateClaimFromBlameResponseDto> {
|
||||||
|
const blameRequest =
|
||||||
|
await this.blameRequestDbService.findById(blameRequestId);
|
||||||
|
if (!blameRequest) throw new NotFoundException("Blame request not found");
|
||||||
|
if (blameRequest.status !== BlameCaseStatus.COMPLETED) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Blame request must be COMPLETED. Current status: ${blameRequest.status}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!blameRequest.registrarInitiated ||
|
||||||
|
blameRequest.creationMethod !== "IN_PERSON" ||
|
||||||
|
!blameRequest.initiatedByRegistrarId
|
||||||
|
) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"This endpoint is only for registrar-initiated IN_PERSON blame files.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (String(blameRequest.initiatedByRegistrarId) !== String(registrar.sub)) {
|
||||||
|
throw new ForbiddenException(
|
||||||
|
"Only the registrar who created this blame file can create the claim.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const ownerFields = resolveClaimOwnerFieldsFromBlame(blameRequest);
|
||||||
|
if (!ownerFields) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Could not resolve claim owner (damaged party) from blame",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const existingClaim = await this.claimCaseDbService.findOne({
|
||||||
|
blameRequestId: new Types.ObjectId(blameRequestId),
|
||||||
|
});
|
||||||
|
if (existingClaim)
|
||||||
|
throw new ConflictException("A claim for this blame case already exists");
|
||||||
|
const claimNo = await this.generateUniqueClaimNumber();
|
||||||
|
const newClaim = await this.claimCaseDbService.create({
|
||||||
|
requestNo: claimNo,
|
||||||
|
publicId: blameRequest.publicId,
|
||||||
|
blameRequestId: new Types.ObjectId(blameRequestId),
|
||||||
|
blameRequestNo: blameRequest.requestNo,
|
||||||
|
status: ClaimCaseStatus.SELECTING_OUTER_PARTS,
|
||||||
|
claimStatus: ClaimStatus.PENDING,
|
||||||
|
inquiries: (blameRequest as any).inquiries ?? {},
|
||||||
|
workflow: {
|
||||||
|
currentStep: ClaimWorkflowStep.SELECT_OUTER_PARTS,
|
||||||
|
nextStep: ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
||||||
|
completedSteps: [ClaimWorkflowStep.CLAIM_CREATED],
|
||||||
|
locked: false,
|
||||||
|
},
|
||||||
|
damagedPartyUserId: new Types.ObjectId(ownerFields.userId),
|
||||||
|
owner: {
|
||||||
|
userId: new Types.ObjectId(ownerFields.userId),
|
||||||
|
userRole: ownerFields.userRole as any,
|
||||||
|
...(ownerFields.clientId
|
||||||
|
? {
|
||||||
|
clientId: new Types.ObjectId(ownerFields.clientId),
|
||||||
|
userClientKey: ownerFields.clientId,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
createdByRegistrarId: new Types.ObjectId(registrar.sub),
|
||||||
|
history: [
|
||||||
|
{
|
||||||
|
type: "CLAIM_CREATED",
|
||||||
|
actor: {
|
||||||
|
actorId: new Types.ObjectId(registrar.sub),
|
||||||
|
actorName:
|
||||||
|
`${registrar.firstName || ""} ${registrar.lastName || ""}`.trim(),
|
||||||
|
actorType: "registrar",
|
||||||
|
},
|
||||||
|
timestamp: new Date(),
|
||||||
|
metadata: {
|
||||||
|
blameRequestId,
|
||||||
|
blamePublicId: blameRequest.publicId,
|
||||||
|
createdByRegistrar: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as any);
|
||||||
|
return {
|
||||||
|
claimRequestId: String(newClaim._id),
|
||||||
|
publicId: blameRequest.publicId,
|
||||||
|
status: ClaimCaseStatus.SELECTING_OUTER_PARTS,
|
||||||
|
message:
|
||||||
|
"Claim created successfully. Registrar can now fill claim data on behalf of the damaged party.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* V2 API: Select damaged outer car parts for claim (Step 2 of claim workflow)
|
* V2 API: Select damaged outer car parts for claim (Step 2 of claim workflow)
|
||||||
*
|
*
|
||||||
@@ -5888,6 +6023,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 },
|
||||||
): Promise<UploadRequiredDocumentV2ResponseDto> {
|
): Promise<UploadRequiredDocumentV2ResponseDto> {
|
||||||
try {
|
try {
|
||||||
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
@@ -5945,6 +6081,21 @@ export class ClaimRequestManagementService {
|
|||||||
`Invalid workflow step. Expected ${ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS}, ${ClaimWorkflowStep.CAPTURE_PART_DAMAGES} (capture-phase documents only), or ${ClaimWorkflowStep.USER_EXPERT_RESEND}, but current step is ${claimCase.workflow?.currentStep}`,
|
`Invalid workflow step. Expected ${ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS}, ${ClaimWorkflowStep.CAPTURE_PART_DAMAGES} (capture-phase documents only), or ${ClaimWorkflowStep.USER_EXPERT_RESEND}, but current step is ${claimCase.workflow?.currentStep}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (options?.v3InPersonFlow) {
|
||||||
|
if (body.documentKey === ClaimRequiredDocumentType.CAR_GREEN_CARD) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Car green card must be uploaded during other-parts selection, not in the documents step.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
step === ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS &&
|
||||||
|
isCapturePhaseDamagedPartyDocKey(body.documentKey)
|
||||||
|
) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Chassis, engine, and metal plate photos must be uploaded during capture (after parts and angles), not in the initial documents step (${CAPTURE_PHASE_DAMAGED_PARTY_DOC_KEYS.join(", ")}).`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isResendUpload) {
|
if (isResendUpload) {
|
||||||
@@ -6063,8 +6214,9 @@ export class ClaimRequestManagementService {
|
|||||||
$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"] =
|
$set["workflow.nextStep"] = options?.v3InPersonFlow
|
||||||
ClaimWorkflowStep.USER_SUBMISSION_COMPLETE;
|
? ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS
|
||||||
|
: ClaimWorkflowStep.USER_SUBMISSION_COMPLETE;
|
||||||
|
|
||||||
completedStepsEntries.push(
|
completedStepsEntries.push(
|
||||||
ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
||||||
@@ -6086,30 +6238,46 @@ export class ClaimRequestManagementService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
allDocumentsUploaded = this.allV2OwnerDocumentsComplete(
|
const v3InitialDocsComplete =
|
||||||
claimCase,
|
!!options?.v3InPersonFlow &&
|
||||||
isCarBodyUpload,
|
step === ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS &&
|
||||||
body.documentKey,
|
this.allV3PreCaptureDocumentsComplete(
|
||||||
);
|
claimCase,
|
||||||
remaining = this.countRemainingV2OwnerDocuments(
|
isCarBodyUpload,
|
||||||
claimCase,
|
body.documentKey,
|
||||||
isCarBodyUpload,
|
);
|
||||||
body.documentKey,
|
|
||||||
);
|
allDocumentsUploaded = options?.v3InPersonFlow
|
||||||
|
? v3InitialDocsComplete
|
||||||
|
: this.allV2OwnerDocumentsComplete(
|
||||||
|
claimCase,
|
||||||
|
isCarBodyUpload,
|
||||||
|
body.documentKey,
|
||||||
|
);
|
||||||
|
remaining = options?.v3InPersonFlow
|
||||||
|
? this.countRemainingV3PreCaptureDocuments(
|
||||||
|
claimCase,
|
||||||
|
isCarBodyUpload,
|
||||||
|
body.documentKey,
|
||||||
|
)
|
||||||
|
: this.countRemainingV2OwnerDocuments(
|
||||||
|
claimCase,
|
||||||
|
isCarBodyUpload,
|
||||||
|
body.documentKey,
|
||||||
|
);
|
||||||
|
|
||||||
if (allDocumentsUploaded) {
|
if (allDocumentsUploaded) {
|
||||||
$set["status"] = ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT;
|
if (options?.v3InPersonFlow) {
|
||||||
$set["claimStatus"] = ClaimStatus.PENDING;
|
$set["status"] = ClaimCaseStatus.SELECTING_OUTER_PARTS;
|
||||||
$set["workflow.currentStep"] =
|
$set["workflow.currentStep"] =
|
||||||
ClaimWorkflowStep.USER_SUBMISSION_COMPLETE;
|
ClaimWorkflowStep.SELECT_OUTER_PARTS;
|
||||||
$set["workflow.nextStep"] =
|
$set["workflow.nextStep"] =
|
||||||
ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT;
|
ClaimWorkflowStep.SELECT_OTHER_PARTS;
|
||||||
|
|
||||||
completedStepsEntries.push(
|
completedStepsEntries.push(
|
||||||
ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||||
);
|
);
|
||||||
historyEntries.push(
|
historyEntries.push({
|
||||||
{
|
|
||||||
type: "STEP_COMPLETED",
|
type: "STEP_COMPLETED",
|
||||||
actor: {
|
actor: {
|
||||||
actorId: new Types.ObjectId(currentUserId),
|
actorId: new Types.ObjectId(currentUserId),
|
||||||
@@ -6119,24 +6287,52 @@ export class ClaimRequestManagementService {
|
|||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
metadata: {
|
metadata: {
|
||||||
stepKey: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
stepKey: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||||
description: "All required documents uploaded",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "STEP_COMPLETED",
|
|
||||||
actor: {
|
|
||||||
actorId: new Types.ObjectId(currentUserId),
|
|
||||||
actorName: claimCase.owner?.fullName || "User",
|
|
||||||
actorType: "user",
|
|
||||||
},
|
|
||||||
timestamp: new Date(),
|
|
||||||
metadata: {
|
|
||||||
stepKey: ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
|
||||||
description:
|
description:
|
||||||
"User submission complete. Claim ready for damage expert review.",
|
"Initial required documents uploaded. Proceed to outer part selection.",
|
||||||
|
v3InPersonFlow: true,
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
);
|
} else {
|
||||||
|
$set["status"] = ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT;
|
||||||
|
$set["claimStatus"] = ClaimStatus.PENDING;
|
||||||
|
$set["workflow.currentStep"] =
|
||||||
|
ClaimWorkflowStep.USER_SUBMISSION_COMPLETE;
|
||||||
|
$set["workflow.nextStep"] =
|
||||||
|
ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT;
|
||||||
|
|
||||||
|
completedStepsEntries.push(
|
||||||
|
ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||||
|
);
|
||||||
|
historyEntries.push(
|
||||||
|
{
|
||||||
|
type: "STEP_COMPLETED",
|
||||||
|
actor: {
|
||||||
|
actorId: new Types.ObjectId(currentUserId),
|
||||||
|
actorName: claimCase.owner?.fullName || "User",
|
||||||
|
actorType: "user",
|
||||||
|
},
|
||||||
|
timestamp: new Date(),
|
||||||
|
metadata: {
|
||||||
|
stepKey: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||||
|
description: "All required documents uploaded",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "STEP_COMPLETED",
|
||||||
|
actor: {
|
||||||
|
actorId: new Types.ObjectId(currentUserId),
|
||||||
|
actorName: claimCase.owner?.fullName || "User",
|
||||||
|
actorType: "user",
|
||||||
|
},
|
||||||
|
timestamp: new Date(),
|
||||||
|
metadata: {
|
||||||
|
stepKey: ClaimWorkflowStep.USER_SUBMISSION_COMPLETE,
|
||||||
|
description:
|
||||||
|
"User submission complete. Claim ready for damage expert review.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6198,7 +6394,9 @@ export class ClaimRequestManagementService {
|
|||||||
}).sequencePhase,
|
}).sequencePhase,
|
||||||
)
|
)
|
||||||
: allDocumentsUploaded
|
: allDocumentsUploaded
|
||||||
? "All documents uploaded successfully. Your claim is now ready for damage expert review."
|
? options?.v3InPersonFlow
|
||||||
|
? "Initial required documents uploaded. Proceed to outer part selection."
|
||||||
|
: "All documents uploaded successfully. Your claim is now ready for damage expert review."
|
||||||
: `Document uploaded successfully. ${remaining} documents remaining.`;
|
: `Document uploaded successfully. ${remaining} documents remaining.`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -6211,7 +6409,9 @@ export class ClaimRequestManagementService {
|
|||||||
? ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS
|
? ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS
|
||||||
: ClaimWorkflowStep.CAPTURE_PART_DAMAGES
|
: ClaimWorkflowStep.CAPTURE_PART_DAMAGES
|
||||||
: allDocumentsUploaded
|
: allDocumentsUploaded
|
||||||
? ClaimWorkflowStep.USER_SUBMISSION_COMPLETE
|
? options?.v3InPersonFlow
|
||||||
|
? ClaimWorkflowStep.SELECT_OUTER_PARTS
|
||||||
|
: ClaimWorkflowStep.USER_SUBMISSION_COMPLETE
|
||||||
: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||||
message,
|
message,
|
||||||
};
|
};
|
||||||
@@ -7660,4 +7860,412 @@ export class ClaimRequestManagementService {
|
|||||||
const randomPart = Math.floor(10000 + Math.random() * 90000); // 5 digits
|
const randomPart = Math.floor(10000 + Math.random() * 90000); // 5 digits
|
||||||
return `${prefix}${randomPart}`;
|
return `${prefix}${randomPart}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async loadBlameForV3Claim(claimCase: any) {
|
||||||
|
if (!claimCase?.blameRequestId) return null;
|
||||||
|
return this.blameRequestDbService.findById(
|
||||||
|
claimCase.blameRequestId.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private hasPartyInquiriesOnBlame(blame: any, role: "FIRST" | "SECOND"): boolean {
|
||||||
|
const step =
|
||||||
|
role === "FIRST"
|
||||||
|
? WorkflowStep.FIRST_INITIAL_FORM
|
||||||
|
: WorkflowStep.SECOND_INITIAL_FORM;
|
||||||
|
return (blame.workflow?.completedSteps ?? []).includes(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
private partyHasSignedOnBlame(blame: any, role: "FIRST" | "SECOND"): boolean {
|
||||||
|
const party = (blame.parties ?? []).find((p: any) => p?.role === role);
|
||||||
|
return !!party?.confirmation;
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertV3ClaimDocumentsPhase(blame: any): void {
|
||||||
|
if (!(blame.expert?.decision as any)?.fields?.accidentWay) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Submit accident fields before uploading required documents.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const isCarBody =
|
||||||
|
blame?.type === BlameRequestType.CAR_BODY || blame?.type === "CAR_BODY";
|
||||||
|
if (isCarBody) {
|
||||||
|
if (!this.partyHasSignedOnBlame(blame, "FIRST")) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Upload documents after the party has signed.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!this.partyHasSignedOnBlame(blame, "FIRST") ||
|
||||||
|
!this.partyHasSignedOnBlame(blame, "SECOND")
|
||||||
|
) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Both parties must sign before uploading required documents.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertV3ClaimPartsPhase(blame: any): void {
|
||||||
|
if (!(blame.expert?.decision as any)?.fields?.accidentWay) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Submit accident fields before part selection.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const isCarBody =
|
||||||
|
blame?.type === BlameRequestType.CAR_BODY || blame?.type === "CAR_BODY";
|
||||||
|
if (isCarBody) {
|
||||||
|
if (!this.partyHasSignedOnBlame(blame, "FIRST")) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Part selection requires the party signature.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!this.partyHasSignedOnBlame(blame, "FIRST") ||
|
||||||
|
!this.partyHasSignedOnBlame(blame, "SECOND")
|
||||||
|
) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Both parties must sign before part selection.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private claimV3StepCompleted(
|
||||||
|
claimCase: any,
|
||||||
|
step: ClaimWorkflowStep,
|
||||||
|
): boolean {
|
||||||
|
return (claimCase.workflow?.completedSteps ?? []).includes(step);
|
||||||
|
}
|
||||||
|
|
||||||
|
private assertV3ClaimWorkflowStep(
|
||||||
|
claimCase: any,
|
||||||
|
expected: ClaimWorkflowStep | ClaimWorkflowStep[],
|
||||||
|
hint?: string,
|
||||||
|
): void {
|
||||||
|
const allowed = Array.isArray(expected) ? expected : [expected];
|
||||||
|
const current = claimCase.workflow?.currentStep;
|
||||||
|
if (!allowed.includes(current)) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
hint ??
|
||||||
|
`Invalid workflow step. Expected ${allowed.join(" or ")}, but current step is ${current}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private v3PartsFlowNotStarted(claimCase: any): boolean {
|
||||||
|
const parts = claimCase.damage?.selectedParts;
|
||||||
|
return !Array.isArray(parts) || parts.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async advanceV3ClaimToOuterPartsIfReady(
|
||||||
|
claimCase: any,
|
||||||
|
blame: any,
|
||||||
|
): Promise<void> {
|
||||||
|
if (claimCase.workflow?.currentStep === ClaimWorkflowStep.SELECT_OUTER_PARTS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const step = claimCase.workflow?.currentStep;
|
||||||
|
const partsNotStarted = this.v3PartsFlowNotStarted(claimCase);
|
||||||
|
const prematurelyCompleted =
|
||||||
|
step === ClaimWorkflowStep.USER_SUBMISSION_COMPLETE && partsNotStarted;
|
||||||
|
const canAdvance =
|
||||||
|
step === ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS ||
|
||||||
|
prematurelyCompleted;
|
||||||
|
|
||||||
|
if (!canAdvance) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`Complete required documents before selecting outer parts. Current step: ${step}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!(blame.expert?.decision as any)?.fields?.accidentWay) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Submit accident fields before selecting outer parts.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.claimCaseDbService.findByIdAndUpdate(String(claimCase._id), {
|
||||||
|
$set: {
|
||||||
|
status: ClaimCaseStatus.SELECTING_OUTER_PARTS,
|
||||||
|
claimStatus: ClaimStatus.PENDING,
|
||||||
|
"workflow.currentStep": ClaimWorkflowStep.SELECT_OUTER_PARTS,
|
||||||
|
"workflow.nextStep": ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async assertV3InPersonClaim(claimCase: any) {
|
||||||
|
const blame = await this.loadBlameForV3Claim(claimCase);
|
||||||
|
if (!blame) {
|
||||||
|
throw new BadRequestException("V3 claim flow requires a linked blame file.");
|
||||||
|
}
|
||||||
|
if (blame.creationMethod !== CreationMethod.IN_PERSON) {
|
||||||
|
throw new BadRequestException("V3 claim flow is only for IN_PERSON blame files.");
|
||||||
|
}
|
||||||
|
if (!blame.expertInitiated) {
|
||||||
|
throw new BadRequestException("V3 claim flow requires an expert-initiated blame file.");
|
||||||
|
}
|
||||||
|
return blame;
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadRequiredDocumentV3(
|
||||||
|
claimRequestId: string,
|
||||||
|
body: UploadRequiredDocumentV2Dto,
|
||||||
|
file: Express.Multer.File,
|
||||||
|
currentUserId: string,
|
||||||
|
actor?: { sub: string; role?: string },
|
||||||
|
): Promise<UploadRequiredDocumentV2ResponseDto> {
|
||||||
|
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
|
if (!claimCase) {
|
||||||
|
throw new NotFoundException(`Claim case with ID ${claimRequestId} not found`);
|
||||||
|
}
|
||||||
|
const blame = await this.assertV3InPersonClaim(claimCase);
|
||||||
|
this.assertV3ClaimDocumentsPhase(blame);
|
||||||
|
|
||||||
|
return this.uploadRequiredDocumentV2(
|
||||||
|
claimRequestId,
|
||||||
|
body,
|
||||||
|
file,
|
||||||
|
currentUserId,
|
||||||
|
actor,
|
||||||
|
{ v3InPersonFlow: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCaptureRequirementsV3(
|
||||||
|
claimRequestId: string,
|
||||||
|
currentUserId: string,
|
||||||
|
actor?: { sub: string; role?: string },
|
||||||
|
): Promise<GetCaptureRequirementsV2ResponseDto> {
|
||||||
|
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
|
if (!claimCase) {
|
||||||
|
throw new NotFoundException(`Claim case with ID ${claimRequestId} not found`);
|
||||||
|
}
|
||||||
|
const blame = await this.assertV3InPersonClaim(claimCase);
|
||||||
|
this.assertV3ClaimDocumentsPhase(blame);
|
||||||
|
return this.getCaptureRequirementsV2(claimRequestId, currentUserId, actor);
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectOuterPartsV3(
|
||||||
|
claimRequestId: string,
|
||||||
|
body: SelectOuterPartsV2Dto,
|
||||||
|
currentUserId: string,
|
||||||
|
actor?: { sub: string; role?: string },
|
||||||
|
): Promise<SelectOuterPartsV2ResponseDto> {
|
||||||
|
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
|
if (!claimCase) {
|
||||||
|
throw new NotFoundException(`Claim case with ID ${claimRequestId} not found`);
|
||||||
|
}
|
||||||
|
const blame = await this.assertV3InPersonClaim(claimCase);
|
||||||
|
this.assertV3ClaimPartsPhase(blame);
|
||||||
|
await this.advanceV3ClaimToOuterPartsIfReady(claimCase, blame);
|
||||||
|
|
||||||
|
const refreshed = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
|
this.assertV3ClaimWorkflowStep(
|
||||||
|
refreshed,
|
||||||
|
ClaimWorkflowStep.SELECT_OUTER_PARTS,
|
||||||
|
"Select outer parts after accident fields and required documents.",
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.selectOuterPartsV2(claimRequestId, body, currentUserId, actor);
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectOtherPartsV3(
|
||||||
|
claimRequestId: string,
|
||||||
|
body: SelectOtherPartsV3Dto,
|
||||||
|
currentUserId: string,
|
||||||
|
actor?: { sub: string; role?: string },
|
||||||
|
file?: Express.Multer.File,
|
||||||
|
): Promise<SelectOtherPartsV2ResponseDto> {
|
||||||
|
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
|
if (!claimCase) {
|
||||||
|
throw new NotFoundException(`Claim case with ID ${claimRequestId} not found`);
|
||||||
|
}
|
||||||
|
const blame = await this.assertV3InPersonClaim(claimCase);
|
||||||
|
this.assertV3ClaimPartsPhase(blame);
|
||||||
|
this.assertV3ClaimWorkflowStep(
|
||||||
|
claimCase,
|
||||||
|
ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
||||||
|
"Select outer parts before other parts.",
|
||||||
|
);
|
||||||
|
if (!this.claimV3StepCompleted(claimCase, ClaimWorkflowStep.SELECT_OUTER_PARTS)) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Complete outer part selection before selecting other parts.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let otherParts = Array.isArray((body as any)?.otherParts)
|
||||||
|
? (body as any).otherParts
|
||||||
|
: [];
|
||||||
|
if (
|
||||||
|
!Array.isArray((body as any)?.otherParts) &&
|
||||||
|
typeof (body as any)?.otherParts === "string"
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse((body as any).otherParts);
|
||||||
|
otherParts = Array.isArray(parsed) ? parsed : [];
|
||||||
|
} catch {
|
||||||
|
otherParts = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const shebaNumber = claimCase.money?.sheba;
|
||||||
|
const nationalCode = claimCase.money?.nationalCodeOfInsurer;
|
||||||
|
if (!shebaNumber || !nationalCode) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Bank information is missing on the claim. Complete run-inquiries for both parties first.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePayload: any = {
|
||||||
|
"damage.otherParts": otherParts.length > 0 ? otherParts : undefined,
|
||||||
|
status: ClaimCaseStatus.CAPTURING_PART_DAMAGES,
|
||||||
|
"workflow.currentStep": ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
||||||
|
"workflow.nextStep": ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||||
|
$push: {
|
||||||
|
"workflow.completedSteps": ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
||||||
|
history: {
|
||||||
|
type: "V3_STEP_COMPLETED",
|
||||||
|
actor: {
|
||||||
|
actorId: new Types.ObjectId(currentUserId),
|
||||||
|
actorName: claimCase.owner?.fullName || "Actor",
|
||||||
|
actorType: actor?.role === RoleEnum.REGISTRAR ? "registrar" : "field_expert",
|
||||||
|
},
|
||||||
|
timestamp: new Date(),
|
||||||
|
metadata: {
|
||||||
|
stepKey: ClaimWorkflowStep.SELECT_OTHER_PARTS,
|
||||||
|
otherParts,
|
||||||
|
v3SelectionOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
const docRef = await this.claimRequiredDocumentDbService.create({
|
||||||
|
path: file.path,
|
||||||
|
fileName: file.filename,
|
||||||
|
claimId: new Types.ObjectId(claimRequestId),
|
||||||
|
documentType: ClaimRequiredDocumentType.CAR_GREEN_CARD,
|
||||||
|
uploadedAt: new Date(),
|
||||||
|
});
|
||||||
|
updatePayload[`requiredDocuments.${ClaimRequiredDocumentType.CAR_GREEN_CARD}`] = {
|
||||||
|
fileId: docRef._id,
|
||||||
|
filePath: file.path,
|
||||||
|
fileName: file.filename,
|
||||||
|
uploaded: true,
|
||||||
|
uploadedAt: new Date(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedClaim = await this.claimCaseDbService.findByIdAndUpdate(
|
||||||
|
claimRequestId,
|
||||||
|
updatePayload,
|
||||||
|
);
|
||||||
|
if (!updatedClaim) {
|
||||||
|
throw new InternalServerErrorException("Failed to update claim case");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
claimRequestId: updatedClaim._id.toString(),
|
||||||
|
publicId: updatedClaim.publicId,
|
||||||
|
otherParts,
|
||||||
|
shebaNumber: String(shebaNumber).replace(/^(.{4})(.*)(.{4})$/, "IR$1************$3"),
|
||||||
|
nationalCodeOfOwner: String(nationalCode).replace(/^(.{2})(.*)(.{2})$/, "$1******$3"),
|
||||||
|
currentStep: ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
||||||
|
nextStep: ClaimWorkflowStep.UPLOAD_REQUIRED_DOCUMENTS,
|
||||||
|
message:
|
||||||
|
"Other parts saved. Proceed to damaged-part photos and car angles (capture-part).",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async setVideoCaptureV3(
|
||||||
|
claimRequestId: string,
|
||||||
|
fileDetail: Express.Multer.File,
|
||||||
|
currentUserId: string,
|
||||||
|
actor?: { sub: string; role?: string },
|
||||||
|
): Promise<VideoCaptureV2ResponseDto> {
|
||||||
|
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
|
if (!claimCase) {
|
||||||
|
throw new NotFoundException(`Claim case with ID ${claimRequestId} not found`);
|
||||||
|
}
|
||||||
|
await this.assertV3InPersonClaim(claimCase);
|
||||||
|
|
||||||
|
if (!fileDetail) {
|
||||||
|
throw new BadRequestException("Video file is required.");
|
||||||
|
}
|
||||||
|
if (claimCase.media?.videoCaptureId) {
|
||||||
|
throw new ConflictException(
|
||||||
|
"A walk-around video has already been uploaded for this claim.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
claimCase.workflow?.currentStep !== 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}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const captureProgress = getClaimCaptureProgress(claimCase);
|
||||||
|
if (!captureProgress.partsComplete) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Upload photos for all selected damaged parts before the walk-around video.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!captureProgress.anglesComplete) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Capture all four car angles before the walk-around video.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!isClaimCaptureStepComplete(claimCase)) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Upload capture-phase vehicle documents (chassis, engine, metal plate) before the walk-around video.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.setVideoCaptureV2(
|
||||||
|
claimRequestId,
|
||||||
|
fileDetail,
|
||||||
|
currentUserId,
|
||||||
|
actor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async capturePartV3(
|
||||||
|
claimRequestId: string,
|
||||||
|
body: CapturePartV2Dto,
|
||||||
|
file: Express.Multer.File,
|
||||||
|
currentUserId: string,
|
||||||
|
actor?: { sub: string; role?: string },
|
||||||
|
): Promise<CapturePartV2ResponseDto> {
|
||||||
|
const claimCase = await this.claimCaseDbService.findById(claimRequestId);
|
||||||
|
if (!claimCase) {
|
||||||
|
throw new NotFoundException(`Claim case with ID ${claimRequestId} not found`);
|
||||||
|
}
|
||||||
|
const blame = await this.assertV3InPersonClaim(claimCase);
|
||||||
|
this.assertV3ClaimPartsPhase(blame);
|
||||||
|
this.assertV3ClaimWorkflowStep(
|
||||||
|
claimCase,
|
||||||
|
ClaimWorkflowStep.CAPTURE_PART_DAMAGES,
|
||||||
|
"Select other parts before capturing damaged parts and angles.",
|
||||||
|
);
|
||||||
|
if (!this.claimV3StepCompleted(claimCase, ClaimWorkflowStep.SELECT_OTHER_PARTS)) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Complete other part selection before capture.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.capturePartV2(
|
||||||
|
claimRequestId,
|
||||||
|
body,
|
||||||
|
file,
|
||||||
|
currentUserId,
|
||||||
|
actor,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import {
|
||||||
|
ArrayMaxSize,
|
||||||
|
IsArray,
|
||||||
|
IsEnum,
|
||||||
|
IsOptional,
|
||||||
|
} from "class-validator";
|
||||||
|
import { OtherCarPart } from "./select-other-parts-v2.dto";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V3 in-person expert flow: other parts only (sheba/national code collected during run-inquiries).
|
||||||
|
*/
|
||||||
|
export class SelectOtherPartsV3Dto {
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Array of selected other damaged parts (non-body parts)",
|
||||||
|
example: ["engine", "suspension", "headlight"],
|
||||||
|
enum: OtherCarPart,
|
||||||
|
isArray: true,
|
||||||
|
maxItems: 11,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray({ message: "otherParts must be an array" })
|
||||||
|
@ArrayMaxSize(11, { message: "Maximum 11 other parts can be selected" })
|
||||||
|
@IsEnum(OtherCarPart, {
|
||||||
|
each: true,
|
||||||
|
message: "Invalid part name. Must be one of the valid other car parts",
|
||||||
|
})
|
||||||
|
otherParts?: OtherCarPart[];
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { Body, Controller, Get, Param, Patch, Post, UploadedFile, UseGuards, Use
|
|||||||
import { FileInterceptor } from "@nestjs/platform-express";
|
import { FileInterceptor } from "@nestjs/platform-express";
|
||||||
import { diskStorage } from "multer";
|
import { diskStorage } from "multer";
|
||||||
import { extname } from "path";
|
import { extname } from "path";
|
||||||
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiOperation, ApiParam, ApiTags } from "@nestjs/swagger";
|
import { ApiBearerAuth, ApiConsumes, ApiExcludeController } from "@nestjs/swagger";
|
||||||
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
||||||
import { RolesGuard } from "src/auth/guards/role.guard";
|
import { RolesGuard } from "src/auth/guards/role.guard";
|
||||||
import { Roles } from "src/decorators/roles.decorator";
|
import { Roles } from "src/decorators/roles.decorator";
|
||||||
@@ -15,7 +15,8 @@ import { UploadRequiredDocumentV2Dto } from "./dto/upload-document-v2.dto";
|
|||||||
import { CapturePartV2Dto } from "./dto/capture-part-v2.dto";
|
import { CapturePartV2Dto } from "./dto/capture-part-v2.dto";
|
||||||
import { ClaimRequestManagementService } from "./claim-request-management.service";
|
import { ClaimRequestManagementService } from "./claim-request-management.service";
|
||||||
|
|
||||||
@ApiTags("registrar-claim (v1)")
|
@ApiExcludeController()
|
||||||
|
// @ApiTags("registrar-claim (v1)")
|
||||||
@Controller("registrar-claim")
|
@Controller("registrar-claim")
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
||||||
@@ -24,7 +25,7 @@ export class RegistrarClaimV1Controller {
|
|||||||
constructor(private readonly claimRequestManagementService: ClaimRequestManagementService) {}
|
constructor(private readonly claimRequestManagementService: ClaimRequestManagementService) {}
|
||||||
|
|
||||||
@Post("create-from-blame/:blameRequestId")
|
@Post("create-from-blame/:blameRequestId")
|
||||||
@ApiParam({ name: "blameRequestId" })
|
// @ApiParam({ name: "blameRequestId" })
|
||||||
createFromBlame(@Param("blameRequestId") blameRequestId: string, @CurrentUser() registrar: any) {
|
createFromBlame(@Param("blameRequestId") blameRequestId: string, @CurrentUser() registrar: any) {
|
||||||
return this.claimRequestManagementService.createClaimFromBlameForRegistrarV1(
|
return this.claimRequestManagementService.createClaimFromBlameForRegistrarV1(
|
||||||
blameRequestId,
|
blameRequestId,
|
||||||
@@ -42,7 +43,7 @@ export class RegistrarClaimV1Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch("select-outer-parts/:claimRequestId")
|
@Patch("select-outer-parts/:claimRequestId")
|
||||||
@ApiBody({ type: SelectOuterPartsV2Dto })
|
// @ApiBody({ type: SelectOuterPartsV2Dto })
|
||||||
selectOuter(
|
selectOuter(
|
||||||
@Param("claimRequestId") claimRequestId: string,
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
@Body() body: SelectOuterPartsV2Dto,
|
@Body() body: SelectOuterPartsV2Dto,
|
||||||
@@ -57,7 +58,7 @@ export class RegistrarClaimV1Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch("select-other-parts/:claimRequestId")
|
@Patch("select-other-parts/:claimRequestId")
|
||||||
@ApiBody({ type: SelectOtherPartsV2Dto })
|
// @ApiBody({ type: SelectOtherPartsV2Dto })
|
||||||
selectOther(
|
selectOther(
|
||||||
@Param("claimRequestId") claimRequestId: string,
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
@Body() body: SelectOtherPartsV2Dto,
|
@Body() body: SelectOtherPartsV2Dto,
|
||||||
@@ -91,7 +92,7 @@ export class RegistrarClaimV1Controller {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ApiOperation({ summary: "Registrar uploads required claim document" })
|
// @ApiOperation({ summary: "Registrar uploads required claim document" })
|
||||||
uploadDoc(
|
uploadDoc(
|
||||||
@Param("claimRequestId") claimRequestId: string,
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
@Body() body: UploadRequiredDocumentV2Dto,
|
@Body() body: UploadRequiredDocumentV2Dto,
|
||||||
@@ -108,7 +109,7 @@ export class RegistrarClaimV1Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post("capture-part/:claimRequestId")
|
@Post("capture-part/:claimRequestId")
|
||||||
@ApiConsumes("multipart/form-data")
|
// @ApiConsumes("multipart/form-data")
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
FileInterceptor("file", {
|
FileInterceptor("file", {
|
||||||
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import {
|
|||||||
MaxLength,
|
MaxLength,
|
||||||
Min,
|
Min,
|
||||||
} from "class-validator";
|
} from "class-validator";
|
||||||
|
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||||
|
import { UNIFIED_FILE_STATUS_KEYS } from "src/helpers/unified-file-status";
|
||||||
|
|
||||||
/** Allowed sort keys for V2 list endpoints (claims / blame). */
|
/** Allowed sort keys for V2 list endpoints (claims / blame / insurer files). */
|
||||||
export const LIST_SORT_BY_V2 = [
|
export const LIST_SORT_BY_V2 = [
|
||||||
"publicId",
|
"publicId",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
@@ -20,9 +22,16 @@ export const LIST_SORT_BY_V2 = [
|
|||||||
|
|
||||||
export type ListSortByV2 = (typeof LIST_SORT_BY_V2)[number];
|
export type ListSortByV2 = (typeof LIST_SORT_BY_V2)[number];
|
||||||
|
|
||||||
|
export const LIST_FILE_TYPE_V2 = [
|
||||||
|
BlameRequestType.THIRD_PARTY,
|
||||||
|
BlameRequestType.CAR_BODY,
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type ListFileTypeV2 = (typeof LIST_FILE_TYPE_V2)[number];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional query params for V2 lists. If neither `page` nor `limit` is sent,
|
* Optional query params for V2 expert/insurer file lists.
|
||||||
* the full filtered+sorted list is returned (backward compatible).
|
* If neither `page` nor `limit` is sent, the full filtered+sorted list is returned.
|
||||||
*/
|
*/
|
||||||
export class ListQueryV2Dto {
|
export class ListQueryV2Dto {
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
@@ -67,7 +76,7 @@ export class ListQueryV2Dto {
|
|||||||
|
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description:
|
description:
|
||||||
"Case-insensitive substring match on `publicId` and `requestNo` (and other endpoint-specific fields).",
|
"Case-insensitive substring match on `publicId`, `requestNo`, status, and other endpoint-specific fields.",
|
||||||
example: "A142",
|
example: "A142",
|
||||||
maxLength: 64,
|
maxLength: 64,
|
||||||
})
|
})
|
||||||
@@ -75,4 +84,22 @@ export class ListQueryV2Dto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@MaxLength(64)
|
@MaxLength(64)
|
||||||
search?: string;
|
search?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
enum: UNIFIED_FILE_STATUS_KEYS,
|
||||||
|
description:
|
||||||
|
"Filter by calculated unified file status (blame + claim combined).",
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn([...UNIFIED_FILE_STATUS_KEYS])
|
||||||
|
unifiedStatus?: (typeof UNIFIED_FILE_STATUS_KEYS)[number];
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
enum: LIST_FILE_TYPE_V2,
|
||||||
|
description:
|
||||||
|
"Filter by blame file type: third-party liability (`THIRD_PARTY`) or car body (`CAR_BODY`).",
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn([...LIST_FILE_TYPE_V2])
|
||||||
|
fileType?: ListFileTypeV2;
|
||||||
}
|
}
|
||||||
|
|||||||
68
src/common/dto/unified-file-status-report.dto.ts
Normal file
68
src/common/dto/unified-file-status-report.dto.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { IsIn, IsISO8601, IsOptional } from "class-validator";
|
||||||
|
import {
|
||||||
|
LIST_FILE_TYPE_V2,
|
||||||
|
ListFileTypeV2,
|
||||||
|
} from "src/common/dto/list-query-v2.dto";
|
||||||
|
import {
|
||||||
|
UNIFIED_FILE_STATUS_KEYS,
|
||||||
|
UnifiedFileStatusDefinition,
|
||||||
|
UnifiedFileStatusKey,
|
||||||
|
} from "src/helpers/unified-file-status";
|
||||||
|
|
||||||
|
export class UnifiedFileStatusReportQueryDto {
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Optional start datetime (ISO 8601) for portfolio date filter.",
|
||||||
|
example: "2026-01-01T00:00:00.000Z",
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsISO8601({ strict: false })
|
||||||
|
from?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Optional end datetime (ISO 8601) for portfolio date filter.",
|
||||||
|
example: "2026-12-31T23:59:59.999Z",
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsISO8601({ strict: false })
|
||||||
|
to?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
enum: LIST_FILE_TYPE_V2,
|
||||||
|
description:
|
||||||
|
"Count only files of this blame type (`THIRD_PARTY` or `CAR_BODY`).",
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn([...LIST_FILE_TYPE_V2])
|
||||||
|
fileType?: ListFileTypeV2;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedFileStatusDefinitionDto implements UnifiedFileStatusDefinition {
|
||||||
|
@ApiProperty({ enum: UNIFIED_FILE_STATUS_KEYS })
|
||||||
|
key: UnifiedFileStatusKey;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
labelEn: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
labelFa: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedFileStatusReportDto {
|
||||||
|
@ApiProperty({ type: [UnifiedFileStatusDefinitionDto] })
|
||||||
|
statuses: UnifiedFileStatusDefinitionDto[];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: "Counts per unified status key plus `all`",
|
||||||
|
example: {
|
||||||
|
all: 42,
|
||||||
|
IN_PROGRESS: 10,
|
||||||
|
WAITING_FOR_DAMAGE_EXPERT: 5,
|
||||||
|
COMPLETED: 12,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
counts: Record<string, number>;
|
||||||
|
}
|
||||||
@@ -117,6 +117,8 @@ export class AllRequestDtoV2 {
|
|||||||
lockTime: string | null;
|
lockTime: string | null;
|
||||||
type: string;
|
type: string;
|
||||||
blameStatus: string;
|
blameStatus: string;
|
||||||
|
/** Calculated blame + linked claim lifecycle status */
|
||||||
|
unifiedFileStatus?: string;
|
||||||
partiesInitialForms: { firstParty: string; secondParty: string };
|
partiesInitialForms: { firstParty: string; secondParty: string };
|
||||||
partiesVehicles: { firstPartyVehicle: string; secondPartyVehicle: string };
|
partiesVehicles: { firstPartyVehicle: string; secondPartyVehicle: string };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
ApiProduces,
|
ApiProduces,
|
||||||
ApiTags,
|
ApiTags,
|
||||||
ApiOperation,
|
ApiOperation,
|
||||||
|
ApiExcludeController,
|
||||||
} from "@nestjs/swagger";
|
} from "@nestjs/swagger";
|
||||||
import { Response, Request } from "express";
|
import { Response, Request } from "express";
|
||||||
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
||||||
@@ -35,7 +36,8 @@ import {
|
|||||||
} from "./dto/reply.dto";
|
} from "./dto/reply.dto";
|
||||||
import { ExpertBlameService } from "./expert-blame.service";
|
import { ExpertBlameService } from "./expert-blame.service";
|
||||||
|
|
||||||
@ApiTags("expert-blame-panel")
|
@ApiExcludeController()
|
||||||
|
// @ApiTags("expert-blame-panel")
|
||||||
@Controller("expert-blame")
|
@Controller("expert-blame")
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
||||||
@@ -45,35 +47,35 @@ export class ExpertBlameController {
|
|||||||
|
|
||||||
// TODO role guard for expert fix
|
// TODO role guard for expert fix
|
||||||
@Roles(RoleEnum.EXPERT)
|
@Roles(RoleEnum.EXPERT)
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Get()
|
@Get()
|
||||||
async findAll(@CurrentUser() actor, @ClientKey() client) {
|
async findAll(@CurrentUser() actor, @ClientKey() client) {
|
||||||
return await this.expertBlameService.findAll(actor);
|
return await this.expertBlameService.findAll(actor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Roles(RoleEnum.EXPERT)
|
@Roles(RoleEnum.EXPERT)
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Get(":id")
|
@Get(":id")
|
||||||
async findOne(@Param("id") id: string, @CurrentUser() actor) {
|
async findOne(@Param("id") id: string, @CurrentUser() actor) {
|
||||||
return await this.expertBlameService.findOne(id, actor.sub);
|
return await this.expertBlameService.findOne(id, actor.sub);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Roles(RoleEnum.EXPERT)
|
@Roles(RoleEnum.EXPERT)
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Put("lock/:id")
|
@Put("lock/:id")
|
||||||
async lockRequest(@Param("id") id: string, @CurrentUser() actor) {
|
async lockRequest(@Param("id") id: string, @CurrentUser() actor) {
|
||||||
return await this.expertBlameService.lockRequest(id, actor);
|
return await this.expertBlameService.lockRequest(id, actor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Roles(RoleEnum.EXPERT)
|
@Roles(RoleEnum.EXPERT)
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Get("request/accident-fields")
|
@Get("request/accident-fields")
|
||||||
async getAccidentFields() {
|
async getAccidentFields() {
|
||||||
return await this.expertBlameService.getAccidentField();
|
return await this.expertBlameService.getAccidentField();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Roles(RoleEnum.EXPERT)
|
@Roles(RoleEnum.EXPERT)
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Put("reply/submit/:id")
|
@Put("reply/submit/:id")
|
||||||
@ApiBody({ type: SubmitReplyDto })
|
@ApiBody({ type: SubmitReplyDto })
|
||||||
@ApiParam({ name: "id" })
|
@ApiParam({ name: "id" })
|
||||||
@@ -100,10 +102,10 @@ export class ExpertBlameController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Roles(RoleEnum.EXPERT)
|
@Roles(RoleEnum.EXPERT)
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Put("reply/resend/first/:id")
|
@Put("reply/resend/first/:id")
|
||||||
@ApiBody({ type: ResendFirstPartyDto })
|
// @ApiBody({ type: ResendFirstPartyDto })
|
||||||
@ApiParam({ name: "id" })
|
// @ApiParam({ name: "id" })
|
||||||
async resendFirstParty(
|
async resendFirstParty(
|
||||||
@Param("id") id: string,
|
@Param("id") id: string,
|
||||||
@Body() body: SendAginIF,
|
@Body() body: SendAginIF,
|
||||||
@@ -113,11 +115,11 @@ export class ExpertBlameController {
|
|||||||
return this.handleResendRequest(id, body, actor.sub, req);
|
return this.handleResendRequest(id, body, actor.sub, req);
|
||||||
}
|
}
|
||||||
@Roles(RoleEnum.EXPERT)
|
@Roles(RoleEnum.EXPERT)
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Put("reply/resend/second/:id")
|
@Put("reply/resend/second/:id")
|
||||||
@Roles(RoleEnum.EXPERT)
|
@Roles(RoleEnum.EXPERT)
|
||||||
@ApiBody({ type: ResendSecondPartyDto })
|
// @ApiBody({ type: ResendSecondPartyDto })
|
||||||
@ApiParam({ name: "id" })
|
// @ApiParam({ name: "id" })
|
||||||
async resendSecondParty(
|
async resendSecondParty(
|
||||||
@Param("id") id: string,
|
@Param("id") id: string,
|
||||||
@Body() body: SendAginIF,
|
@Body() body: SendAginIF,
|
||||||
@@ -127,25 +129,25 @@ export class ExpertBlameController {
|
|||||||
return this.handleResendRequest(id, body, actor.sub, req);
|
return this.handleResendRequest(id, body, actor.sub, req);
|
||||||
}
|
}
|
||||||
@Roles(RoleEnum.EXPERT)
|
@Roles(RoleEnum.EXPERT)
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Get("stream/:requestId")
|
@Get("stream/:requestId")
|
||||||
@ApiParam({ name: "requestId" })
|
@ApiParam({ name: "requestId" })
|
||||||
async streamVideo(@Param("requestId") requestId: string) {
|
async streamVideo(@Param("requestId") requestId: string) {
|
||||||
return this.expertBlameService.streamVideo(requestId);
|
return this.expertBlameService.streamVideo(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Get("voice/:requestId/:voiceId")
|
@Get("voice/:requestId/:voiceId")
|
||||||
@Roles(RoleEnum.EXPERT, RoleEnum.DAMAGE_EXPERT)
|
@Roles(RoleEnum.EXPERT, RoleEnum.DAMAGE_EXPERT)
|
||||||
@ApiParam({ name: "requestId" })
|
// @ApiParam({ name: "requestId" })
|
||||||
@ApiParam({ name: "voiceId" })
|
// @ApiParam({ name: "voiceId" })
|
||||||
@ApiOkResponse({
|
// @ApiOkResponse({
|
||||||
schema: {
|
// schema: {
|
||||||
type: "string",
|
// type: "string",
|
||||||
format: "binary",
|
// format: "binary",
|
||||||
},
|
// },
|
||||||
})
|
// })
|
||||||
@ApiProduces("mp3")
|
// @ApiProduces("mp3")
|
||||||
async downloadVoice(
|
async downloadVoice(
|
||||||
@Param("voiceId") voiceId,
|
@Param("voiceId") voiceId,
|
||||||
@Param("requestId") requestId: string,
|
@Param("requestId") requestId: string,
|
||||||
@@ -156,8 +158,8 @@ export class ExpertBlameController {
|
|||||||
return await this.expertBlameService.streamVoice(requestId, voiceId);
|
return await this.expertBlameService.streamVoice(requestId, voiceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiParam({ name: "id" })
|
// @ApiParam({ name: "id" })
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Patch(":id/visit")
|
@Patch(":id/visit")
|
||||||
async inPersonVisit(@Param("id") requestId: string, @CurrentUser() actor) {
|
async inPersonVisit(@Param("id") requestId: string, @CurrentUser() actor) {
|
||||||
return await this.expertBlameService.inPersonVisit(requestId, actor);
|
return await this.expertBlameService.inPersonVisit(requestId, actor);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { ExpertBlameV2Controller } from "./expert-blame.v2.controller";
|
|||||||
import { ExpertBlameService } from "./expert-blame.service";
|
import { ExpertBlameService } from "./expert-blame.service";
|
||||||
import { RequestManagementModule } from "src/request-management/request-management.module";
|
import { RequestManagementModule } from "src/request-management/request-management.module";
|
||||||
import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.module";
|
import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.module";
|
||||||
|
import { ClaimRequestManagementModule } from "src/claim-request-management/claim-request-management.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -15,6 +16,7 @@ import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.
|
|||||||
RequestManagementModule,
|
RequestManagementModule,
|
||||||
PlatesModule,
|
PlatesModule,
|
||||||
SmsOrchestrationModule,
|
SmsOrchestrationModule,
|
||||||
|
ClaimRequestManagementModule,
|
||||||
],
|
],
|
||||||
controllers: [ExpertBlameController, ExpertBlameV2Controller],
|
controllers: [ExpertBlameController, ExpertBlameV2Controller],
|
||||||
providers: [ExpertBlameService],
|
providers: [ExpertBlameService],
|
||||||
|
|||||||
@@ -23,8 +23,18 @@ import {
|
|||||||
expertPortfolioFileIdsFromActivityEvents,
|
expertPortfolioFileIdsFromActivityEvents,
|
||||||
objectIdsFromStringSet,
|
objectIdsFromStringSet,
|
||||||
} from "src/helpers/expert-portfolio";
|
} from "src/helpers/expert-portfolio";
|
||||||
import { applyListQueryV2 } from "src/helpers/list-query-v2";
|
import {
|
||||||
|
countUnifiedFileStatuses,
|
||||||
|
getUnifiedFileStatusCatalog,
|
||||||
|
resolveUnifiedFileStatus,
|
||||||
|
} from "src/helpers/unified-file-status";
|
||||||
|
import { applyListQueryV2, isInListDateRange, parseListDateRange } from "src/helpers/list-query-v2";
|
||||||
|
import { ClaimCaseDbService } from "src/claim-request-management/entites/db-service/claim-case.db.service";
|
||||||
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
||||||
|
import {
|
||||||
|
UnifiedFileStatusReportDto,
|
||||||
|
UnifiedFileStatusReportQueryDto,
|
||||||
|
} from "src/common/dto/unified-file-status-report.dto";
|
||||||
import {
|
import {
|
||||||
ExpertFileAssignResultDto,
|
ExpertFileAssignResultDto,
|
||||||
ExpertFileAssignStatus,
|
ExpertFileAssignStatus,
|
||||||
@@ -115,8 +125,126 @@ export class ExpertBlameService {
|
|||||||
private readonly requestManagementService: RequestManagementService,
|
private readonly requestManagementService: RequestManagementService,
|
||||||
private readonly smsOrchestrationService: SmsOrchestrationService,
|
private readonly smsOrchestrationService: SmsOrchestrationService,
|
||||||
private readonly expertFileActivityDbService: ExpertFileActivityDbService,
|
private readonly expertFileActivityDbService: ExpertFileActivityDbService,
|
||||||
|
private readonly claimCaseDbService: ClaimCaseDbService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
private async loadClaimsByPublicIds(
|
||||||
|
publicIds: string[],
|
||||||
|
): Promise<Map<string, { status?: string }>> {
|
||||||
|
const unique = [...new Set(publicIds.filter(Boolean))];
|
||||||
|
if (unique.length === 0) return new Map();
|
||||||
|
const claims = (await this.claimCaseDbService.find(
|
||||||
|
{ publicId: { $in: unique } },
|
||||||
|
{ lean: true, select: "publicId status" },
|
||||||
|
)) as Array<{ publicId?: string; status?: string }>;
|
||||||
|
return new Map(
|
||||||
|
claims.map((c) => [String(c.publicId), { status: c.status }]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private filterBlameDocsByUnifiedStatus(
|
||||||
|
docs: Record<string, unknown>[],
|
||||||
|
claimByPublicId: Map<string, { status?: string }>,
|
||||||
|
unifiedStatus?: string,
|
||||||
|
): Record<string, unknown>[] {
|
||||||
|
if (!unifiedStatus) return docs;
|
||||||
|
return docs.filter((doc) => {
|
||||||
|
const publicId = String((doc as { publicId?: string }).publicId ?? "");
|
||||||
|
const claim = claimByPublicId.get(publicId);
|
||||||
|
return (
|
||||||
|
resolveUnifiedFileStatus({
|
||||||
|
blameStatus: String((doc as { status?: string }).status ?? ""),
|
||||||
|
claimStatus: claim?.status,
|
||||||
|
}) === unifiedStatus
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUnifiedFileStatusReportV2(
|
||||||
|
actor: any,
|
||||||
|
query: UnifiedFileStatusReportQueryDto = {},
|
||||||
|
): Promise<UnifiedFileStatusReportDto> {
|
||||||
|
let rows = await this.collectBlamePortfolioDocs(actor);
|
||||||
|
const { fromDate, toDate } = parseListDateRange(query.from, query.to);
|
||||||
|
rows = rows.filter((doc) =>
|
||||||
|
isInListDateRange(
|
||||||
|
(doc as { createdAt?: Date }).createdAt,
|
||||||
|
fromDate,
|
||||||
|
toDate,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (query.fileType) {
|
||||||
|
rows = rows.filter(
|
||||||
|
(doc) =>
|
||||||
|
String((doc as { type?: string }).type ?? "") === query.fileType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const claimByPublicId = await this.loadClaimsByPublicIds(
|
||||||
|
rows.map((d) => String((d as { publicId?: string }).publicId ?? "")),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
statuses: getUnifiedFileStatusCatalog(),
|
||||||
|
counts: countUnifiedFileStatuses(
|
||||||
|
rows.map((doc) => {
|
||||||
|
const publicId = String((doc as { publicId?: string }).publicId ?? "");
|
||||||
|
const claim = claimByPublicId.get(publicId);
|
||||||
|
return {
|
||||||
|
blameStatus: String((doc as { status?: string }).status ?? ""),
|
||||||
|
claimStatus: claim?.status,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async collectBlamePortfolioDocs(
|
||||||
|
actor: any,
|
||||||
|
): Promise<Record<string, unknown>[]> {
|
||||||
|
if (actor.role === RoleEnum.FIELD_EXPERT) {
|
||||||
|
return (await this.blameRequestDbService.find(
|
||||||
|
{
|
||||||
|
expertInitiated: true,
|
||||||
|
initiatedByFieldExpertId: new Types.ObjectId(actor.sub),
|
||||||
|
},
|
||||||
|
{ lean: true },
|
||||||
|
)) as Record<string, unknown>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
requireActorClientKey(actor);
|
||||||
|
const expertId = String(actor.sub);
|
||||||
|
const expertOid = new Types.ObjectId(expertId);
|
||||||
|
const activityEvents = await this.expertFileActivityDbService.findByExpert(
|
||||||
|
expertId,
|
||||||
|
ExpertFileKind.BLAME,
|
||||||
|
);
|
||||||
|
const activityFileIds =
|
||||||
|
expertPortfolioFileIdsFromActivityEvents(activityEvents);
|
||||||
|
const orClauses: Record<string, unknown>[] = [
|
||||||
|
{ initiatedByFieldExpertId: expertOid },
|
||||||
|
{ "expert.decision.decidedByExpertId": expertOid },
|
||||||
|
{ "expert.resend.requestedByExpertId": expertOid },
|
||||||
|
{ "workflow.lockedBy.actorId": expertOid },
|
||||||
|
];
|
||||||
|
const activityOids = objectIdsFromStringSet(activityFileIds);
|
||||||
|
if (activityOids.length > 0) {
|
||||||
|
orClauses.push({ _id: { $in: activityOids } });
|
||||||
|
}
|
||||||
|
const rows = (await this.blameRequestDbService.find(
|
||||||
|
{ blameStatus: BlameStatus.DISAGREEMENT, $or: orClauses },
|
||||||
|
{ lean: true },
|
||||||
|
)) as Record<string, unknown>[];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const out: Record<string, unknown>[] = [];
|
||||||
|
for (const doc of rows) {
|
||||||
|
const id = String(doc._id ?? "");
|
||||||
|
if (seen.has(id)) continue;
|
||||||
|
if (!blameDocInExpertPortfolio(doc, actor, activityFileIds)) continue;
|
||||||
|
seen.add(id);
|
||||||
|
out.push(doc);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
/** Load immutable profile fields from `expert` for the acting field expert. */
|
/** Load immutable profile fields from `expert` for the acting field expert. */
|
||||||
private async snapshotFieldExpert(actorId: string) {
|
private async snapshotFieldExpert(actorId: string) {
|
||||||
const expertDoc = await this.expertDbService.findOne({
|
const expertDoc = await this.expertDbService.findOne({
|
||||||
@@ -386,44 +514,8 @@ export class ExpertBlameService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const paged = applyListQueryV2(
|
const pagedResult = await this.paginateBlameListV2(visibleCases, query);
|
||||||
visibleCases,
|
return pagedResult;
|
||||||
{
|
|
||||||
publicId: (doc) =>
|
|
||||||
String((doc as { publicId?: string }).publicId ?? ""),
|
|
||||||
createdAt: (doc) => (doc as { createdAt?: Date }).createdAt,
|
|
||||||
requestNo: (doc) =>
|
|
||||||
String(
|
|
||||||
(doc as { requestNo?: string }).requestNo ??
|
|
||||||
(doc as { publicId?: string }).publicId ??
|
|
||||||
"",
|
|
||||||
),
|
|
||||||
status: (doc) => String((doc as { status?: string }).status ?? ""),
|
|
||||||
searchExtras: (doc) => {
|
|
||||||
const d = doc as {
|
|
||||||
_id?: unknown;
|
|
||||||
blameStatus?: string;
|
|
||||||
type?: string;
|
|
||||||
};
|
|
||||||
return [String(d._id ?? ""), d.blameStatus, d.type].filter(
|
|
||||||
Boolean,
|
|
||||||
) as string[];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
query,
|
|
||||||
);
|
|
||||||
|
|
||||||
const items: AllRequestDtoV2[] = paged.list.map((doc) =>
|
|
||||||
this.mapBlameRequestToListItemV2(doc),
|
|
||||||
);
|
|
||||||
|
|
||||||
return new AllRequestDtoRsV2({
|
|
||||||
data: items,
|
|
||||||
total: paged.total,
|
|
||||||
page: paged.page,
|
|
||||||
limit: paged.limit,
|
|
||||||
totalPages: paged.totalPages,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HttpException) throw error;
|
if (error instanceof HttpException) throw error;
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
@@ -480,8 +572,27 @@ export class ExpertBlameService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const paged = applyListQueryV2(
|
const pagedResult = await this.paginateBlameListV2(visibleCases, query);
|
||||||
|
return pagedResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async paginateBlameListV2(
|
||||||
|
visibleCases: Record<string, unknown>[],
|
||||||
|
query: ListQueryV2Dto,
|
||||||
|
): Promise<AllRequestDtoRsV2> {
|
||||||
|
const claimByPublicId = await this.loadClaimsByPublicIds(
|
||||||
|
visibleCases.map((d) =>
|
||||||
|
String((d as { publicId?: string }).publicId ?? ""),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const filtered = this.filterBlameDocsByUnifiedStatus(
|
||||||
visibleCases,
|
visibleCases,
|
||||||
|
claimByPublicId,
|
||||||
|
query.unifiedStatus,
|
||||||
|
);
|
||||||
|
|
||||||
|
const paged = applyListQueryV2(
|
||||||
|
filtered,
|
||||||
{
|
{
|
||||||
publicId: (doc) =>
|
publicId: (doc) =>
|
||||||
String((doc as { publicId?: string }).publicId ?? ""),
|
String((doc as { publicId?: string }).publicId ?? ""),
|
||||||
@@ -492,23 +603,44 @@ export class ExpertBlameService {
|
|||||||
(doc as { publicId?: string }).publicId ??
|
(doc as { publicId?: string }).publicId ??
|
||||||
"",
|
"",
|
||||||
),
|
),
|
||||||
status: (doc) => String((doc as { status?: string }).status ?? ""),
|
status: (doc) => {
|
||||||
|
const publicId = String((doc as { publicId?: string }).publicId ?? "");
|
||||||
|
const claim = claimByPublicId.get(publicId);
|
||||||
|
return resolveUnifiedFileStatus({
|
||||||
|
blameStatus: String((doc as { status?: string }).status ?? ""),
|
||||||
|
claimStatus: claim?.status,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
fileType: (doc) =>
|
||||||
|
String((doc as { type?: string }).type ?? "") || undefined,
|
||||||
searchExtras: (doc) => {
|
searchExtras: (doc) => {
|
||||||
const d = doc as {
|
const d = doc as {
|
||||||
_id?: unknown;
|
_id?: unknown;
|
||||||
blameStatus?: string;
|
blameStatus?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
|
publicId?: string;
|
||||||
|
status?: string;
|
||||||
};
|
};
|
||||||
return [String(d._id ?? ""), d.blameStatus, d.type].filter(
|
const publicId = String(d.publicId ?? "");
|
||||||
Boolean,
|
const claim = claimByPublicId.get(publicId);
|
||||||
) as string[];
|
const unified = resolveUnifiedFileStatus({
|
||||||
|
blameStatus: String(d.status ?? ""),
|
||||||
|
claimStatus: claim?.status,
|
||||||
|
});
|
||||||
|
return [
|
||||||
|
String(d._id ?? ""),
|
||||||
|
d.blameStatus,
|
||||||
|
d.type,
|
||||||
|
unified,
|
||||||
|
claim?.status,
|
||||||
|
].filter(Boolean) as string[];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
query,
|
query,
|
||||||
);
|
);
|
||||||
|
|
||||||
const items: AllRequestDtoV2[] = paged.list.map((doc) =>
|
const items: AllRequestDtoV2[] = paged.list.map((doc) =>
|
||||||
this.mapBlameRequestToListItemV2(doc),
|
this.mapBlameRequestToListItemV2(doc, claimByPublicId),
|
||||||
);
|
);
|
||||||
|
|
||||||
return new AllRequestDtoRsV2({
|
return new AllRequestDtoRsV2({
|
||||||
@@ -522,6 +654,7 @@ export class ExpertBlameService {
|
|||||||
|
|
||||||
private mapBlameRequestToListItemV2(
|
private mapBlameRequestToListItemV2(
|
||||||
doc: Record<string, unknown>,
|
doc: Record<string, unknown>,
|
||||||
|
claimByPublicId?: Map<string, { status?: string }>,
|
||||||
): AllRequestDtoV2 {
|
): AllRequestDtoV2 {
|
||||||
const createdAt = doc.createdAt
|
const createdAt = doc.createdAt
|
||||||
? new Date(doc.createdAt as string)
|
? new Date(doc.createdAt as string)
|
||||||
@@ -552,10 +685,17 @@ export class ExpertBlameService {
|
|||||||
|
|
||||||
const firstParty = parties.find((p) => p.role === "FIRST");
|
const firstParty = parties.find((p) => p.role === "FIRST");
|
||||||
const secondParty = parties.find((p) => p.role === "SECOND");
|
const secondParty = parties.find((p) => p.role === "SECOND");
|
||||||
|
const publicId = String(doc.publicId ?? "");
|
||||||
|
const linkedClaim = claimByPublicId?.get(publicId);
|
||||||
|
const unifiedFileStatus = resolveUnifiedFileStatus({
|
||||||
|
blameStatus: String(doc.status ?? ""),
|
||||||
|
claimStatus: linkedClaim?.status,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
requestId: String(doc._id),
|
requestId: String(doc._id),
|
||||||
status: String(doc.status ?? ""),
|
status: String(doc.status ?? ""),
|
||||||
|
unifiedFileStatus,
|
||||||
userComment: null,
|
userComment: null,
|
||||||
requestCode: String(doc.publicId ?? ""),
|
requestCode: String(doc.publicId ?? ""),
|
||||||
date,
|
date,
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ import { CurrentUser } from "src/decorators/user.decorator";
|
|||||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||||
import { ExpertBlameService } from "./expert-blame.service";
|
import { ExpertBlameService } from "./expert-blame.service";
|
||||||
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
||||||
|
import {
|
||||||
|
UnifiedFileStatusReportDto,
|
||||||
|
UnifiedFileStatusReportQueryDto,
|
||||||
|
} from "src/common/dto/unified-file-status-report.dto";
|
||||||
import { AllRequestDtoRsV2 } from "./dto/all-request.dto";
|
import { AllRequestDtoRsV2 } from "./dto/all-request.dto";
|
||||||
import { SubmitReplyDto } from "./dto/reply.dto";
|
import { SubmitReplyDto } from "./dto/reply.dto";
|
||||||
import { ResendRequestDto } from "./dto/resend.dto";
|
import { ResendRequestDto } from "./dto/resend.dto";
|
||||||
@@ -44,8 +48,9 @@ export class ExpertBlameV2Controller {
|
|||||||
description:
|
description:
|
||||||
"Damage experts (`expert`): tenant-scoped **DISAGREEMENT** queue (available, locked, or decided by you). " +
|
"Damage experts (`expert`): tenant-scoped **DISAGREEMENT** queue (available, locked, or decided by you). " +
|
||||||
"Field experts (`field_expert`): all expert-initiated blame files they created (LINK + IN_PERSON). Use expert-claim for claims after blame completion. " +
|
"Field experts (`field_expert`): all expert-initiated blame files they created (LINK + IN_PERSON). Use expert-claim for claims after blame completion. " +
|
||||||
"Optional query: `search`, `sortBy` (publicId | createdAt | requestNo | status), `sortOrder`, `page`, `limit`.",
|
"Optional query: `search`, `sortBy` (publicId | createdAt | requestNo | status), `sortOrder`, `page`, `limit`, `unifiedStatus`, `fileType` (THIRD_PARTY | CAR_BODY).",
|
||||||
})
|
})
|
||||||
|
@ApiResponse({ status: 200, type: AllRequestDtoRsV2 })
|
||||||
async findAll(
|
async findAll(
|
||||||
@CurrentUser() actor: any,
|
@CurrentUser() actor: any,
|
||||||
@Query() query: ListQueryV2Dto,
|
@Query() query: ListQueryV2Dto,
|
||||||
@@ -60,11 +65,38 @@ export class ExpertBlameV2Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get("report/unified-file-statuses")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Unified file status catalog + counts (blame + linked claim)",
|
||||||
|
description:
|
||||||
|
"Calculatable statuses for this expert's blame portfolio. Filter lists with `unifiedStatus` and `fileType` query params. Optional `from` / `to` ISO dates narrow the counted portfolio.",
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, type: UnifiedFileStatusReportDto })
|
||||||
|
async getUnifiedFileStatusReportV2(
|
||||||
|
@CurrentUser() actor: any,
|
||||||
|
@Query() query: UnifiedFileStatusReportQueryDto,
|
||||||
|
): Promise<UnifiedFileStatusReportDto> {
|
||||||
|
try {
|
||||||
|
return await this.expertBlameService.getUnifiedFileStatusReportV2(
|
||||||
|
actor,
|
||||||
|
query,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Failed to load unified file status report",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Get("report/status-counts")
|
@Get("report/status-counts")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Count blame cases by grouped status bucket (this field expert)",
|
summary: "Count blame cases by grouped status bucket (this field expert)",
|
||||||
|
deprecated: true,
|
||||||
description:
|
description:
|
||||||
"Counts only files in the acting expert’s portfolio: expertFileActivities (checked or handled), workflow lock, expert decision, or expert-initiated blame. IN_PROGRESS groups OPEN and WAITING_FOR_SECOND_PARTY. Other keys match CaseStatus. Does not include the open WAITING_FOR_EXPERT queue unless this expert has touched the file.",
|
"Legacy blame-only buckets. Prefer GET report/unified-file-statuses for blame+claim calculatable statuses.",
|
||||||
})
|
})
|
||||||
async getStatusReportBucketsV2(@CurrentUser() actor: any) {
|
async getStatusReportBucketsV2(@CurrentUser() actor: any) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ export class ClaimListItemV2Dto {
|
|||||||
})
|
})
|
||||||
status: string;
|
status: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Calculated unified blame+claim lifecycle status",
|
||||||
|
example: "WAITING_FOR_DAMAGE_EXPERT",
|
||||||
|
})
|
||||||
|
unifiedFileStatus?: string;
|
||||||
|
|
||||||
@ApiProperty({ description: 'Workflow step', example: 'USER_SUBMISSION_COMPLETE' })
|
@ApiProperty({ description: 'Workflow step', example: 'USER_SUBMISSION_COMPLETE' })
|
||||||
currentStep: string;
|
currentStep: string;
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
ApiQuery,
|
ApiQuery,
|
||||||
ApiTags,
|
ApiTags,
|
||||||
ApiOperation,
|
ApiOperation,
|
||||||
|
ApiExcludeController,
|
||||||
} from "@nestjs/swagger";
|
} from "@nestjs/swagger";
|
||||||
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
||||||
import { RolesGuard } from "src/auth/guards/role.guard";
|
import { RolesGuard } from "src/auth/guards/role.guard";
|
||||||
@@ -33,7 +34,8 @@ import { ParseJsonPipe } from "src/utils/pipes/parse-json.pipe";
|
|||||||
import { ExpertClaimService } from "./expert-claim.service";
|
import { ExpertClaimService } from "./expert-claim.service";
|
||||||
import { FactorValidationDto } from "./dto/factor-validation.dto";
|
import { FactorValidationDto } from "./dto/factor-validation.dto";
|
||||||
|
|
||||||
@ApiTags("expert-claim-panel")
|
@ApiExcludeController()
|
||||||
|
// @ApiTags("expert-claim-panel")
|
||||||
@Controller("expert-claim")
|
@Controller("expert-claim")
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
||||||
@@ -41,20 +43,20 @@ import { FactorValidationDto } from "./dto/factor-validation.dto";
|
|||||||
export class ExpertClaimController {
|
export class ExpertClaimController {
|
||||||
constructor(private readonly expertClaimService: ExpertClaimService) {}
|
constructor(private readonly expertClaimService: ExpertClaimService) {}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Get()
|
@Get()
|
||||||
async getAllClaimRequests(@CurrentUser() actor, @ClientKey() client) {
|
async getAllClaimRequests(@CurrentUser() actor, @ClientKey() client) {
|
||||||
return await this.expertClaimService.getClaimRequestsListForExpert(actor);
|
return await this.expertClaimService.getClaimRequestsListForExpert(actor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Get("/:id")
|
@Get("/:id")
|
||||||
@ApiParam({ name: "id" })
|
// @ApiParam({ name: "id" })
|
||||||
@ApiQuery({
|
// @ApiQuery({
|
||||||
name: "updateDropPrice",
|
// name: "updateDropPrice",
|
||||||
required: false,
|
// required: false,
|
||||||
description: "A stringified JSON object with price drop override values.",
|
// description: "A stringified JSON object with price drop override values.",
|
||||||
})
|
// })
|
||||||
async getClaimRequestPerId(
|
async getClaimRequestPerId(
|
||||||
@Param("id") id: string,
|
@Param("id") id: string,
|
||||||
@CurrentUser() currentUser,
|
@CurrentUser() currentUser,
|
||||||
@@ -67,16 +69,16 @@ export class ExpertClaimController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Get("imageRequired/:id")
|
@Get("imageRequired/:id")
|
||||||
@ApiParam({ name: "id" })
|
// @ApiParam({ name: "id" })
|
||||||
async getImageRequired(@Param("id") id: string, @CurrentUser() currentUser) {
|
async getImageRequired(@Param("id") id: string, @CurrentUser() currentUser) {
|
||||||
return await this.expertClaimService.imageRequired(id, currentUser);
|
return await this.expertClaimService.imageRequired(id, currentUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Get("required-documents/:id")
|
@Get("required-documents/:id")
|
||||||
@ApiParam({ name: "id" })
|
// @ApiParam({ name: "id" })
|
||||||
async getRequiredDocuments(
|
async getRequiredDocuments(
|
||||||
@Param("id") id: string,
|
@Param("id") id: string,
|
||||||
@CurrentUser() currentUser,
|
@CurrentUser() currentUser,
|
||||||
@@ -84,7 +86,7 @@ export class ExpertClaimController {
|
|||||||
return await this.expertClaimService.getRequiredDocuments(id, currentUser);
|
return await this.expertClaimService.getRequiredDocuments(id, currentUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Put("lock/:id")
|
@Put("lock/:id")
|
||||||
async lockClaimRequest(@Param("id") requestId: string, @CurrentUser() actor) {
|
async lockClaimRequest(@Param("id") requestId: string, @CurrentUser() actor) {
|
||||||
return await this.expertClaimService.lockClaimRequest(requestId, actor);
|
return await this.expertClaimService.lockClaimRequest(requestId, actor);
|
||||||
@@ -92,7 +94,7 @@ export class ExpertClaimController {
|
|||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
@ApiOperation({ deprecated: true })
|
||||||
@Put("reply/submit/:id")
|
@Put("reply/submit/:id")
|
||||||
@ApiParam({ name: "id" })
|
// @ApiParam({ name: "id" })
|
||||||
async submitReplyRequest(
|
async submitReplyRequest(
|
||||||
@Param("id") requestId,
|
@Param("id") requestId,
|
||||||
@Body() body: ClaimSubmitReplyDto,
|
@Body() body: ClaimSubmitReplyDto,
|
||||||
@@ -105,9 +107,9 @@ export class ExpertClaimController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Put("resend/submit/:id")
|
@Put("resend/submit/:id")
|
||||||
@ApiParam({ name: "id" })
|
// @ApiParam({ name: "id" })
|
||||||
async submitResend(
|
async submitResend(
|
||||||
@Param("id") requestId,
|
@Param("id") requestId,
|
||||||
@Body() body: ClaimSubmitResendDto,
|
@Body() body: ClaimSubmitResendDto,
|
||||||
@@ -120,12 +122,12 @@ export class ExpertClaimController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiQuery({
|
// @ApiQuery({
|
||||||
name: "query",
|
// name: "query",
|
||||||
enum: ["car-capture", "accident"],
|
//enum: ["car-capture", "accident"],
|
||||||
required: true,
|
//required: true,
|
||||||
})
|
//})
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Get("stream/:id/video")
|
@Get("stream/:id/video")
|
||||||
@Header("Accept-Ranges", "bytes")
|
@Header("Accept-Ranges", "bytes")
|
||||||
@Header("Content-Type", "video/mp4")
|
@Header("Content-Type", "video/mp4")
|
||||||
@@ -143,7 +145,7 @@ export class ExpertClaimController {
|
|||||||
enum: ["car-capture", "accident"],
|
enum: ["car-capture", "accident"],
|
||||||
required: true,
|
required: true,
|
||||||
})
|
})
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Get("link/:id/video")
|
@Get("link/:id/video")
|
||||||
@ApiParam({ name: "id" })
|
@ApiParam({ name: "id" })
|
||||||
async getClaimVideoLink(
|
async getClaimVideoLink(
|
||||||
@@ -153,17 +155,17 @@ export class ExpertClaimController {
|
|||||||
return this.expertClaimService.getVideoLink(id, query);
|
return this.expertClaimService.getVideoLink(id, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Get("stream/:id/voice")
|
@Get("stream/:id/voice")
|
||||||
@ApiParam({ name: "id" })
|
// @ApiParam({ name: "id" })
|
||||||
async getClaimVoiceLink(@Param("id") id: string) {
|
async getClaimVoiceLink(@Param("id") id: string) {
|
||||||
return await this.expertClaimService.getVoiceLink(id);
|
return await this.expertClaimService.getVoiceLink(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Patch("validate-factors/:claimId")
|
@Patch("validate-factors/:claimId")
|
||||||
@ApiParam({ name: "claimId" })
|
// @ApiParam({ name: "claimId" })
|
||||||
@ApiBody({ type: FactorValidationDto })
|
// @ApiBody({ type: FactorValidationDto })
|
||||||
@Roles(RoleEnum.DAMAGE_EXPERT)
|
@Roles(RoleEnum.DAMAGE_EXPERT)
|
||||||
async validateFactors(
|
async validateFactors(
|
||||||
@Param("claimId") claimId: string,
|
@Param("claimId") claimId: string,
|
||||||
@@ -177,16 +179,16 @@ export class ExpertClaimController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiParam({ name: "id" })
|
// @ApiParam({ name: "id" })
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Patch(":id/visit")
|
@Patch(":id/visit")
|
||||||
async inPersonVisit(@Param("id") requestId: string, @CurrentUser() actor) {
|
async inPersonVisit(@Param("id") requestId: string, @CurrentUser() actor) {
|
||||||
return await this.expertClaimService.inPersonVisit(requestId, actor);
|
return await this.expertClaimService.inPersonVisit(requestId, actor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Get("branches/:insuranceId")
|
@Get("branches/:insuranceId")
|
||||||
@ApiParam({ name: "insuranceId" })
|
// @ApiParam({ name: "insuranceId" })
|
||||||
async getInsuranceBranches(@Param("insuranceId") insuranceId: string) {
|
async getInsuranceBranches(@Param("insuranceId") insuranceId: string) {
|
||||||
return await this.expertClaimService.retrieveInsuranceBranches(insuranceId);
|
return await this.expertClaimService.retrieveInsuranceBranches(insuranceId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,7 +141,20 @@ import {
|
|||||||
/** Maximum sum of line `totalPayment` across the claim (Toman; priced parts + factor lines after validation). */
|
/** Maximum sum of line `totalPayment` across the claim (Toman; priced parts + factor lines after validation). */
|
||||||
import { getClaimV2TotalPaymentCapToman } from "src/constants/repair-amount-limits";
|
import { getClaimV2TotalPaymentCapToman } from "src/constants/repair-amount-limits";
|
||||||
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
||||||
import { applyListQueryV2 } from "src/helpers/list-query-v2";
|
import {
|
||||||
|
UnifiedFileStatusReportDto,
|
||||||
|
UnifiedFileStatusReportQueryDto,
|
||||||
|
} from "src/common/dto/unified-file-status-report.dto";
|
||||||
|
import {
|
||||||
|
applyListQueryV2,
|
||||||
|
isInListDateRange,
|
||||||
|
parseListDateRange,
|
||||||
|
} from "src/helpers/list-query-v2";
|
||||||
|
import {
|
||||||
|
countUnifiedFileStatuses,
|
||||||
|
getUnifiedFileStatusCatalog,
|
||||||
|
resolveUnifiedFileStatus,
|
||||||
|
} from "src/helpers/unified-file-status";
|
||||||
import { buildEnrichedDamagedParts } from "./dto/claim-damaged-part.enricher";
|
import { buildEnrichedDamagedParts } from "./dto/claim-damaged-part.enricher";
|
||||||
import { canonicalizeResendDocumentKey } from "src/helpers/claim-resend-document-keys";
|
import { canonicalizeResendDocumentKey } from "src/helpers/claim-resend-document-keys";
|
||||||
|
|
||||||
@@ -3411,6 +3424,169 @@ export class ExpertClaimService {
|
|||||||
return buckets;
|
return buckets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUnifiedFileStatusReportV2(
|
||||||
|
actor: any,
|
||||||
|
query: UnifiedFileStatusReportQueryDto = {},
|
||||||
|
): Promise<UnifiedFileStatusReportDto> {
|
||||||
|
const { claims, blameById } =
|
||||||
|
await this.collectClaimPortfolioWithBlame(actor);
|
||||||
|
const { fromDate, toDate } = parseListDateRange(query.from, query.to);
|
||||||
|
const filteredClaims = claims.filter((c) => {
|
||||||
|
if (
|
||||||
|
!isInListDateRange(
|
||||||
|
c.createdAt as Date | string | undefined,
|
||||||
|
fromDate,
|
||||||
|
toDate,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!query.fileType) return true;
|
||||||
|
const blame = c.blameRequestId
|
||||||
|
? blameById.get(String(c.blameRequestId))
|
||||||
|
: undefined;
|
||||||
|
const type =
|
||||||
|
blame?.type ??
|
||||||
|
(c as { snapshot?: { accident?: { type?: string } } })?.snapshot
|
||||||
|
?.accident?.type;
|
||||||
|
return type === query.fileType;
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
statuses: getUnifiedFileStatusCatalog(),
|
||||||
|
counts: countUnifiedFileStatuses(
|
||||||
|
filteredClaims.map((c) => {
|
||||||
|
const blame = c.blameRequestId
|
||||||
|
? blameById.get(String(c.blameRequestId))
|
||||||
|
: undefined;
|
||||||
|
return {
|
||||||
|
blameStatus: blame?.status,
|
||||||
|
claimStatus: c.status,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async collectClaimPortfolioWithBlame(actor: any): Promise<{
|
||||||
|
claims: any[];
|
||||||
|
blameById: Map<string, any>;
|
||||||
|
}> {
|
||||||
|
let claims: any[] = [];
|
||||||
|
if (actor.role === RoleEnum.FIELD_EXPERT) {
|
||||||
|
const expertOid = new Types.ObjectId(actor.sub);
|
||||||
|
const expertBlameIds = await this.blameRequestDbService
|
||||||
|
.find(
|
||||||
|
{ expertInitiated: true, initiatedByFieldExpertId: expertOid },
|
||||||
|
{ select: "_id", lean: true },
|
||||||
|
)
|
||||||
|
.then((docs) => docs.map((d) => (d as { _id: unknown })._id));
|
||||||
|
const claimOr: Record<string, unknown>[] = [
|
||||||
|
{ initiatedByFieldExpertId: expertOid },
|
||||||
|
];
|
||||||
|
if (expertBlameIds.length > 0) {
|
||||||
|
claimOr.push({ blameRequestId: { $in: expertBlameIds } });
|
||||||
|
}
|
||||||
|
claims = (await this.claimCaseDbService.find({ $or: claimOr })) as any[];
|
||||||
|
} else {
|
||||||
|
const clientKey = requireActorClientKey(actor);
|
||||||
|
const expertId = String(actor.sub);
|
||||||
|
const expertOid = new Types.ObjectId(expertId);
|
||||||
|
const activityEvents = await this.expertFileActivityDbService.findByExpert(
|
||||||
|
expertId,
|
||||||
|
ExpertFileKind.CLAIM,
|
||||||
|
);
|
||||||
|
const activityFileIds =
|
||||||
|
expertPortfolioFileIdsFromActivityEvents(activityEvents);
|
||||||
|
const orClauses: Record<string, unknown>[] = [
|
||||||
|
{ "workflow.lockedBy.actorId": expertOid },
|
||||||
|
{ "evaluation.damageExpertReply.actorDetail.actorId": expertOid },
|
||||||
|
{ "evaluation.damageExpertReplyFinal.actorDetail.actorId": expertOid },
|
||||||
|
{ "evaluation.damageExpertResend.requestedByExpertId": expertOid },
|
||||||
|
];
|
||||||
|
const activityOids = objectIdsFromStringSet(activityFileIds);
|
||||||
|
if (activityOids.length > 0) {
|
||||||
|
orClauses.push({ _id: { $in: activityOids } });
|
||||||
|
}
|
||||||
|
const rows =
|
||||||
|
orClauses.length === 0
|
||||||
|
? []
|
||||||
|
: await this.claimCaseDbService.find({ $or: orClauses }, { lean: true });
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const doc of rows as Record<string, unknown>[]) {
|
||||||
|
const id = String(doc._id ?? "");
|
||||||
|
if (seen.has(id)) continue;
|
||||||
|
if (
|
||||||
|
!claimDocInExpertPortfolio(doc, expertId, activityFileIds, clientKey)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(id);
|
||||||
|
claims.push(doc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const blameIds = [
|
||||||
|
...new Set(
|
||||||
|
claims
|
||||||
|
.map((c) => c.blameRequestId?.toString())
|
||||||
|
.filter((id): id is string => !!id),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
const blames =
|
||||||
|
blameIds.length > 0
|
||||||
|
? ((await this.blameRequestDbService.find(
|
||||||
|
{
|
||||||
|
_id: { $in: blameIds.map((id) => new Types.ObjectId(id)) },
|
||||||
|
},
|
||||||
|
{ lean: true, select: "status publicId" },
|
||||||
|
)) as any[])
|
||||||
|
: [];
|
||||||
|
const blameById = new Map<string, any>(
|
||||||
|
blames.map((b) => [String(b._id), b]),
|
||||||
|
);
|
||||||
|
return { claims, blameById };
|
||||||
|
}
|
||||||
|
|
||||||
|
private paginateClaimListV2(
|
||||||
|
list: ClaimListItemV2Dto[],
|
||||||
|
query: ListQueryV2Dto,
|
||||||
|
): GetClaimListV2ResponseDto {
|
||||||
|
let filtered = list;
|
||||||
|
if (query.unifiedStatus) {
|
||||||
|
filtered = list.filter(
|
||||||
|
(item) => item.unifiedFileStatus === query.unifiedStatus,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const paged = applyListQueryV2(
|
||||||
|
filtered,
|
||||||
|
{
|
||||||
|
publicId: (r) => r.publicId,
|
||||||
|
createdAt: (r) => r.createdAt,
|
||||||
|
requestNo: (r) => r.publicId,
|
||||||
|
status: (r) => r.unifiedFileStatus ?? r.status,
|
||||||
|
fileType: (r) => r.blameRequestType,
|
||||||
|
searchExtras: (r) =>
|
||||||
|
[
|
||||||
|
r.claimRequestId,
|
||||||
|
r.currentStep,
|
||||||
|
r.vehicle?.carName,
|
||||||
|
r.vehicle?.carModel,
|
||||||
|
r.blameRequestType,
|
||||||
|
r.unifiedFileStatus,
|
||||||
|
r.status,
|
||||||
|
].filter(Boolean) as string[],
|
||||||
|
},
|
||||||
|
query,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
list: paged.list,
|
||||||
|
total: paged.total,
|
||||||
|
page: paged.page,
|
||||||
|
limit: paged.limit,
|
||||||
|
totalPages: paged.totalPages,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* V2: Get claim list for damage expert
|
* V2: Get claim list for damage expert
|
||||||
*
|
*
|
||||||
@@ -3581,6 +3757,10 @@ export class ExpertClaimService {
|
|||||||
claimRequestId: c._id.toString(),
|
claimRequestId: c._id.toString(),
|
||||||
publicId: c.publicId,
|
publicId: c.publicId,
|
||||||
status: statusForList,
|
status: statusForList,
|
||||||
|
unifiedFileStatus: resolveUnifiedFileStatus({
|
||||||
|
blameStatus: blame?.status,
|
||||||
|
claimStatus: c.status,
|
||||||
|
}),
|
||||||
currentStep: c.workflow?.currentStep || "",
|
currentStep: c.workflow?.currentStep || "",
|
||||||
locked: lockActive,
|
locked: lockActive,
|
||||||
lockedBy:
|
lockedBy:
|
||||||
@@ -3601,32 +3781,7 @@ export class ExpertClaimService {
|
|||||||
};
|
};
|
||||||
}) as ClaimListItemV2Dto[];
|
}) as ClaimListItemV2Dto[];
|
||||||
|
|
||||||
const paged = applyListQueryV2(
|
return this.paginateClaimListV2(list, query);
|
||||||
list,
|
|
||||||
{
|
|
||||||
publicId: (r) => r.publicId,
|
|
||||||
createdAt: (r) => r.createdAt,
|
|
||||||
requestNo: (r) => r.publicId,
|
|
||||||
status: (r) => r.status,
|
|
||||||
searchExtras: (r) =>
|
|
||||||
[
|
|
||||||
r.claimRequestId,
|
|
||||||
r.currentStep,
|
|
||||||
r.vehicle?.carName,
|
|
||||||
r.vehicle?.carModel,
|
|
||||||
r.blameRequestType,
|
|
||||||
].filter(Boolean) as string[],
|
|
||||||
},
|
|
||||||
query,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
list: paged.list,
|
|
||||||
total: paged.total,
|
|
||||||
page: paged.page,
|
|
||||||
limit: paged.limit,
|
|
||||||
totalPages: paged.totalPages,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -3687,6 +3842,10 @@ export class ExpertClaimService {
|
|||||||
claimRequestId: c._id.toString(),
|
claimRequestId: c._id.toString(),
|
||||||
publicId: c.publicId,
|
publicId: c.publicId,
|
||||||
status: c.status,
|
status: c.status,
|
||||||
|
unifiedFileStatus: resolveUnifiedFileStatus({
|
||||||
|
blameStatus: blame?.status,
|
||||||
|
claimStatus: c.status,
|
||||||
|
}),
|
||||||
currentStep: c.workflow?.currentStep || "",
|
currentStep: c.workflow?.currentStep || "",
|
||||||
locked: lockActive,
|
locked: lockActive,
|
||||||
lockedBy:
|
lockedBy:
|
||||||
@@ -3707,31 +3866,7 @@ export class ExpertClaimService {
|
|||||||
};
|
};
|
||||||
}) as ClaimListItemV2Dto[];
|
}) as ClaimListItemV2Dto[];
|
||||||
|
|
||||||
const paged = applyListQueryV2(
|
return this.paginateClaimListV2(list, query);
|
||||||
list,
|
|
||||||
{
|
|
||||||
publicId: (r) => r.publicId,
|
|
||||||
createdAt: (r) => r.createdAt,
|
|
||||||
requestNo: (r) => r.publicId,
|
|
||||||
status: (r) => r.status,
|
|
||||||
searchExtras: (r) =>
|
|
||||||
[
|
|
||||||
r.claimRequestId,
|
|
||||||
r.currentStep,
|
|
||||||
r.vehicle?.carName,
|
|
||||||
r.vehicle?.carModel,
|
|
||||||
].filter(Boolean) as string[],
|
|
||||||
},
|
|
||||||
query,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
list: paged.list,
|
|
||||||
total: paged.total,
|
|
||||||
page: paged.page,
|
|
||||||
limit: paged.limit,
|
|
||||||
totalPages: paged.totalPages,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ import { RoleEnum } from "src/Types&Enums/role.enum";
|
|||||||
import { ExpertFileAssignResultDto } from "src/common/dto/expert-file-assign-result.dto";
|
import { ExpertFileAssignResultDto } from "src/common/dto/expert-file-assign-result.dto";
|
||||||
import { ExpertClaimService } from "./expert-claim.service";
|
import { ExpertClaimService } from "./expert-claim.service";
|
||||||
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
||||||
|
import {
|
||||||
|
UnifiedFileStatusReportDto,
|
||||||
|
UnifiedFileStatusReportQueryDto,
|
||||||
|
} from "src/common/dto/unified-file-status-report.dto";
|
||||||
import { GetClaimListV2ResponseDto } from "./dto/claim-list-v2.dto";
|
import { GetClaimListV2ResponseDto } from "./dto/claim-list-v2.dto";
|
||||||
import {
|
import {
|
||||||
ClaimSubmitResendV2Dto,
|
ClaimSubmitResendV2Dto,
|
||||||
@@ -65,11 +69,29 @@ export class ExpertClaimV2Controller {
|
|||||||
private readonly claimRequestManagementService: ClaimRequestManagementService,
|
private readonly claimRequestManagementService: ClaimRequestManagementService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@Get("report/unified-file-statuses")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Unified file status catalog + counts (claim + linked blame)",
|
||||||
|
description:
|
||||||
|
"Calculatable statuses for this expert's claim portfolio. Filter lists with `unifiedStatus` and `fileType` query params. Optional `from` / `to` ISO dates narrow the counted portfolio.",
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, type: UnifiedFileStatusReportDto })
|
||||||
|
async getUnifiedFileStatusReportV2(
|
||||||
|
@CurrentUser() actor: any,
|
||||||
|
@Query() query: UnifiedFileStatusReportQueryDto,
|
||||||
|
): Promise<UnifiedFileStatusReportDto> {
|
||||||
|
return await this.expertClaimService.getUnifiedFileStatusReportV2(
|
||||||
|
actor,
|
||||||
|
query,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Get("report/status-counts")
|
@Get("report/status-counts")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Count claim cases by grouped status bucket (this damage expert)",
|
summary: "Count claim cases by grouped status bucket (this damage expert)",
|
||||||
|
deprecated: true,
|
||||||
description:
|
description:
|
||||||
"Counts only files in the acting expert’s portfolio: expertFileActivities (checked or handled), current workflow lock, or a prior damageExpertReply / damageExpertReplyFinal by this expert. IN_PROGRESS groups user-phase statuses before submission is complete (CREATED through CAPTURING_PART_DAMAGES). Other keys match ClaimCaseStatus. Does not include the open tenant queue unless this expert has touched the file.",
|
"Legacy claim-only buckets. Prefer GET report/unified-file-statuses for blame+claim calculatable statuses.",
|
||||||
})
|
})
|
||||||
async getStatusReportBucketsV2(@CurrentUser() actor: any) {
|
async getStatusReportBucketsV2(@CurrentUser() actor: any) {
|
||||||
return await this.expertClaimService.getStatusReportBucketsV2(actor);
|
return await this.expertClaimService.getStatusReportBucketsV2(actor);
|
||||||
@@ -116,7 +138,8 @@ export class ExpertClaimV2Controller {
|
|||||||
summary: "List available claim requests for damage expert",
|
summary: "List available claim requests for damage expert",
|
||||||
description:
|
description:
|
||||||
"Damage experts: tenant queue (`WAITING_FOR_DAMAGE_EXPERT`), locked/in-progress, factor validation. " +
|
"Damage experts: tenant queue (`WAITING_FOR_DAMAGE_EXPERT`), locked/in-progress, factor validation. " +
|
||||||
"Field experts: all claims linked to blame files they initiated (`initiatedByFieldExpertId` or matching `blameRequestId`). Optional query: `search`, `sortBy`, `sortOrder`, `page`, `limit`.",
|
"Field experts: all claims linked to blame files they initiated (`initiatedByFieldExpertId` or matching `blameRequestId`). " +
|
||||||
|
"Optional query: `search`, `sortBy`, `sortOrder`, `page`, `limit`, `unifiedStatus`, `fileType` (THIRD_PARTY | CAR_BODY).",
|
||||||
})
|
})
|
||||||
@ApiResponse({ status: 200, type: GetClaimListV2ResponseDto })
|
@ApiResponse({ status: 200, type: GetClaimListV2ResponseDto })
|
||||||
async getClaimListV2(
|
async getClaimListV2(
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
ApiOperation,
|
ApiOperation,
|
||||||
ApiParam,
|
ApiParam,
|
||||||
ApiQuery,
|
ApiQuery,
|
||||||
|
ApiResponse,
|
||||||
ApiTags,
|
ApiTags,
|
||||||
} from "@nestjs/swagger";
|
} from "@nestjs/swagger";
|
||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
@@ -26,6 +27,11 @@ import { CurrentUser } from "src/decorators/user.decorator";
|
|||||||
import { FileRating } from "src/request-management/entities/schema/request-management.schema";
|
import { FileRating } from "src/request-management/entities/schema/request-management.schema";
|
||||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||||
import { ExpertInsurerService } from "./expert-insurer.service";
|
import { ExpertInsurerService } from "./expert-insurer.service";
|
||||||
|
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
||||||
|
import {
|
||||||
|
UnifiedFileStatusReportDto,
|
||||||
|
UnifiedFileStatusReportQueryDto,
|
||||||
|
} from "src/common/dto/unified-file-status-report.dto";
|
||||||
import { CreateBranchDto } from "src/client/dto/create-branch.dto";
|
import { CreateBranchDto } from "src/client/dto/create-branch.dto";
|
||||||
import {
|
import {
|
||||||
CreateBlameExpertByInsurerDto,
|
CreateBlameExpertByInsurerDto,
|
||||||
@@ -162,9 +168,35 @@ export class ExpertInsurerController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get("files")
|
@Get("files")
|
||||||
async getAllFiles(@CurrentUser() insurer) {
|
@ApiOperation({
|
||||||
|
summary: "List insurer files (blame + claim merged by publicId)",
|
||||||
|
description:
|
||||||
|
"Optional query: `search`, `sortBy` (publicId | createdAt | requestNo | status), `sortOrder`, `page`, `limit`, `unifiedStatus`, `fileType` (THIRD_PARTY | CAR_BODY).",
|
||||||
|
})
|
||||||
|
async getAllFiles(
|
||||||
|
@CurrentUser() insurer,
|
||||||
|
@Query() query: ListQueryV2Dto,
|
||||||
|
) {
|
||||||
return await this.expertInsurerService.retrieveAllFilesOfClient(
|
return await this.expertInsurerService.retrieveAllFilesOfClient(
|
||||||
insurer.clientKey,
|
insurer.clientKey,
|
||||||
|
query,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("report/unified-file-statuses")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Unified file status catalog + counts (blame + claim)",
|
||||||
|
description:
|
||||||
|
"Calculatable unified statuses and per-status counts for this insurer's files. Filter lists with `unifiedStatus` and `fileType`. Optional `from` / `to` ISO dates narrow the counted portfolio.",
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 200, type: UnifiedFileStatusReportDto })
|
||||||
|
async getUnifiedFileStatusReport(
|
||||||
|
@CurrentUser() actor,
|
||||||
|
@Query() query: UnifiedFileStatusReportQueryDto,
|
||||||
|
): Promise<UnifiedFileStatusReportDto> {
|
||||||
|
return await this.expertInsurerService.getInsurerUnifiedFileStatusReport(
|
||||||
|
actor,
|
||||||
|
query,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,6 +297,11 @@ export class ExpertInsurerController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get("report/status-counts")
|
@Get("report/status-counts")
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Legacy status counts (mapped from unified statuses)",
|
||||||
|
deprecated: true,
|
||||||
|
description: "Prefer GET report/unified-file-statuses for full calculatable blame+claim statuses.",
|
||||||
|
})
|
||||||
@ApiQuery({
|
@ApiQuery({
|
||||||
name: "from",
|
name: "from",
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
@@ -39,6 +39,21 @@ import { toJalaliDateAndTime } from "src/helpers/date-jalali";
|
|||||||
import { enrichBlamePartiesForAgreementView } from "src/helpers/blame-party-agreement-decision";
|
import { enrichBlamePartiesForAgreementView } from "src/helpers/blame-party-agreement-decision";
|
||||||
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||||
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
|
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
|
||||||
|
import { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
||||||
|
import {
|
||||||
|
UnifiedFileStatusReportDto,
|
||||||
|
UnifiedFileStatusReportQueryDto,
|
||||||
|
} from "src/common/dto/unified-file-status-report.dto";
|
||||||
|
import {
|
||||||
|
applyListQueryV2,
|
||||||
|
isInListDateRange,
|
||||||
|
parseListDateRange,
|
||||||
|
} from "src/helpers/list-query-v2";
|
||||||
|
import {
|
||||||
|
countUnifiedFileStatuses,
|
||||||
|
getUnifiedFileStatusCatalog,
|
||||||
|
resolveUnifiedFileStatus,
|
||||||
|
} from "src/helpers/unified-file-status";
|
||||||
import {
|
import {
|
||||||
getClaimCarAngleCaptureBlob,
|
getClaimCarAngleCaptureBlob,
|
||||||
getDamagedPartCaptureBlob,
|
getDamagedPartCaptureBlob,
|
||||||
@@ -1145,13 +1160,10 @@ export class ExpertInsurerService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async retrieveAllFilesOfClient(insurerId: string) {
|
private async buildMergedInsurerFiles(clientObjectId: Types.ObjectId) {
|
||||||
const id = this.getClientId(insurerId);
|
|
||||||
const idStr = String(id);
|
|
||||||
|
|
||||||
const [blameFiles, claimFiles] = await Promise.all([
|
const [blameFiles, claimFiles] = await Promise.all([
|
||||||
this.getClientBlameFiles(id),
|
this.getClientBlameFiles(clientObjectId),
|
||||||
this.getClientClaimFiles(id),
|
this.getClientClaimFiles(clientObjectId),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const byPublicId = new Map<
|
const byPublicId = new Map<
|
||||||
@@ -1192,7 +1204,6 @@ export class ExpertInsurerService {
|
|||||||
}
|
}
|
||||||
>();
|
>();
|
||||||
|
|
||||||
// Index blame files first
|
|
||||||
for (const b of blameFiles) {
|
for (const b of blameFiles) {
|
||||||
const key = String((b as any).publicId || (b as any)._id || "");
|
const key = String((b as any).publicId || (b as any)._id || "");
|
||||||
if (!key) continue;
|
if (!key) continue;
|
||||||
@@ -1216,8 +1227,6 @@ export class ExpertInsurerService {
|
|||||||
byPublicId.set(key, prev);
|
byPublicId.set(key, prev);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For claim files whose blame wasn't in blameFiles (different insurer's blame),
|
|
||||||
// fetch the linked blame directly so we can get vehicle + parties data
|
|
||||||
const claimOnlyBlameIds = claimFiles
|
const claimOnlyBlameIds = claimFiles
|
||||||
.filter((c) => {
|
.filter((c) => {
|
||||||
const key = String((c as any).publicId || (c as any)._id || "");
|
const key = String((c as any).publicId || (c as any)._id || "");
|
||||||
@@ -1243,13 +1252,11 @@ export class ExpertInsurerService {
|
|||||||
linkedBlames.map((b) => [String(b.publicId), b]),
|
linkedBlames.map((b) => [String(b.publicId), b]),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Index claim files
|
|
||||||
for (const c of claimFiles) {
|
for (const c of claimFiles) {
|
||||||
const key = String((c as any).publicId || (c as any)._id || "");
|
const key = String((c as any).publicId || (c as any)._id || "");
|
||||||
if (!key) continue;
|
if (!key) continue;
|
||||||
const prev = byPublicId.get(key) ?? { publicId: key };
|
const prev = byPublicId.get(key) ?? { publicId: key };
|
||||||
|
|
||||||
// If no blame entry yet, use the linked blame for parties/vehicle/fileType
|
|
||||||
if (!prev.blame) {
|
if (!prev.blame) {
|
||||||
const linkedBlame = linkedBlameByPublicId.get(key);
|
const linkedBlame = linkedBlameByPublicId.get(key);
|
||||||
if (linkedBlame) {
|
if (linkedBlame) {
|
||||||
@@ -1293,26 +1300,88 @@ export class ExpertInsurerService {
|
|||||||
byPublicId.set(key, prev);
|
byPublicId.set(key, prev);
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = Array.from(byPublicId.values())
|
return Array.from(byPublicId.values()).map((f) => ({
|
||||||
.map((f) => ({
|
publicId: f.publicId,
|
||||||
publicId: f.publicId,
|
fileType: f.fileType,
|
||||||
fileType: f.fileType,
|
hasClaim: !!f.claim,
|
||||||
hasClaim: !!f.claim,
|
unifiedFileStatus: resolveUnifiedFileStatus({
|
||||||
parties: f.parties,
|
blameStatus: f.blame?.status,
|
||||||
blame: f.blame,
|
claimStatus: f.claim?.status,
|
||||||
claim: f.claim,
|
}),
|
||||||
createdAt: f.createdAt,
|
parties: f.parties,
|
||||||
updatedAt: f.updatedAt,
|
blame: f.blame,
|
||||||
}))
|
claim: f.claim,
|
||||||
.sort((a, b) => {
|
createdAt: f.createdAt,
|
||||||
const at = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
|
updatedAt: f.updatedAt,
|
||||||
const bt = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
|
}));
|
||||||
return bt - at;
|
}
|
||||||
});
|
|
||||||
|
async retrieveAllFilesOfClient(
|
||||||
|
insurerId: string,
|
||||||
|
query: ListQueryV2Dto = {},
|
||||||
|
) {
|
||||||
|
const id = this.getClientId(insurerId);
|
||||||
|
let files = await this.buildMergedInsurerFiles(id);
|
||||||
|
|
||||||
|
if (query.unifiedStatus) {
|
||||||
|
files = files.filter(
|
||||||
|
(f) => f.unifiedFileStatus === query.unifiedStatus,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const paged = applyListQueryV2(
|
||||||
|
files,
|
||||||
|
{
|
||||||
|
publicId: (f) => f.publicId,
|
||||||
|
createdAt: (f) => f.updatedAt ?? f.createdAt,
|
||||||
|
requestNo: (f) => f.publicId,
|
||||||
|
status: (f) => f.unifiedFileStatus,
|
||||||
|
fileType: (f) => f.fileType,
|
||||||
|
searchExtras: (f) =>
|
||||||
|
[
|
||||||
|
f.fileType,
|
||||||
|
f.blame?.requestNo,
|
||||||
|
f.claim?.requestNo,
|
||||||
|
f.blame?.status,
|
||||||
|
f.claim?.status,
|
||||||
|
f.unifiedFileStatus,
|
||||||
|
].filter(Boolean) as string[],
|
||||||
|
},
|
||||||
|
query,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
total: files.length,
|
total: paged.total,
|
||||||
files,
|
page: paged.page,
|
||||||
|
limit: paged.limit,
|
||||||
|
totalPages: paged.totalPages,
|
||||||
|
files: paged.list,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getInsurerUnifiedFileStatusReport(
|
||||||
|
actor: any,
|
||||||
|
query: UnifiedFileStatusReportQueryDto = {},
|
||||||
|
): Promise<UnifiedFileStatusReportDto> {
|
||||||
|
const clientObjectId = this.getClientId(actor);
|
||||||
|
const { fromDate, toDate } = parseListDateRange(query.from, query.to);
|
||||||
|
|
||||||
|
const files = await this.buildMergedInsurerFiles(clientObjectId);
|
||||||
|
let inRange = files.filter((f) =>
|
||||||
|
isInListDateRange(f.createdAt, fromDate, toDate),
|
||||||
|
);
|
||||||
|
if (query.fileType) {
|
||||||
|
inRange = inRange.filter((f) => f.fileType === query.fileType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
statuses: getUnifiedFileStatusCatalog(),
|
||||||
|
counts: countUnifiedFileStatuses(
|
||||||
|
inRange.map((f) => ({
|
||||||
|
blameStatus: f.blame?.status,
|
||||||
|
claimStatus: f.claim?.status,
|
||||||
|
})),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1888,72 +1957,20 @@ export class ExpertInsurerService {
|
|||||||
under_review: number;
|
under_review: number;
|
||||||
waiting_for_documents_resend: number;
|
waiting_for_documents_resend: number;
|
||||||
}> {
|
}> {
|
||||||
const clientObjectId = this.getClientId(actor);
|
const report = await this.getInsurerUnifiedFileStatusReport(actor, {
|
||||||
const { fromDate, toDate } = this.parseDateRange(from, to);
|
from,
|
||||||
|
to,
|
||||||
const [blameFilesRaw, claimFilesRaw] = await Promise.all([
|
});
|
||||||
this.getClientBlameFiles(clientObjectId),
|
const counts = report.counts;
|
||||||
this.getClientClaimFiles(clientObjectId),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const blameFiles = blameFilesRaw.filter((f) =>
|
|
||||||
this.isInDateRange((f as any)?.createdAt, fromDate, toDate),
|
|
||||||
);
|
|
||||||
const claimFiles = claimFilesRaw.filter((f) =>
|
|
||||||
this.isInDateRange((f as any)?.createdAt, fromDate, toDate),
|
|
||||||
);
|
|
||||||
|
|
||||||
const fileMap = new Map<string, { blame?: any; claim?: any }>();
|
|
||||||
|
|
||||||
for (const b of blameFiles) {
|
|
||||||
const key = String((b as any).publicId || (b as any)._id || "");
|
|
||||||
if (!key) continue;
|
|
||||||
const prev = fileMap.get(key) ?? {};
|
|
||||||
prev.blame = b;
|
|
||||||
fileMap.set(key, prev);
|
|
||||||
}
|
|
||||||
for (const c of claimFiles) {
|
|
||||||
const key = String((c as any).publicId || (c as any)._id || "");
|
|
||||||
if (!key) continue;
|
|
||||||
const prev = fileMap.get(key) ?? {};
|
|
||||||
prev.claim = c;
|
|
||||||
fileMap.set(key, prev);
|
|
||||||
}
|
|
||||||
|
|
||||||
let completed = 0;
|
|
||||||
let underReview = 0;
|
|
||||||
let waitingForDocumentsResend = 0;
|
|
||||||
|
|
||||||
for (const entry of fileMap.values()) {
|
|
||||||
const blameStatus = entry.blame?.status;
|
|
||||||
const claimStatus = entry.claim?.status;
|
|
||||||
|
|
||||||
if (claimStatus === ClaimCaseStatus.COMPLETED) {
|
|
||||||
completed += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const blameUnderReview = blameStatus === CaseStatus.WAITING_FOR_EXPERT;
|
|
||||||
const claimUnderReview =
|
|
||||||
claimStatus === ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT ||
|
|
||||||
claimStatus === ClaimCaseStatus.EXPERT_REVIEWING;
|
|
||||||
if (blameUnderReview || claimUnderReview) {
|
|
||||||
underReview += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const blameResend =
|
|
||||||
blameStatus === CaseStatus.WAITING_FOR_DOCUMENT_RESEND;
|
|
||||||
const claimResend =
|
|
||||||
claimStatus === ClaimCaseStatus.WAITING_FOR_USER_RESEND;
|
|
||||||
if (blameResend || claimResend) {
|
|
||||||
waitingForDocumentsResend += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
all: fileMap.size,
|
all: counts.all ?? 0,
|
||||||
completed,
|
completed: counts.COMPLETED ?? 0,
|
||||||
under_review: underReview,
|
under_review:
|
||||||
waiting_for_documents_resend: waitingForDocumentsResend,
|
(counts.WAITING_FOR_BLAME_EXPERT ?? 0) +
|
||||||
|
(counts.WAITING_FOR_DAMAGE_EXPERT ?? 0) +
|
||||||
|
(counts.EXPERT_REVIEWING ?? 0) +
|
||||||
|
(counts.EXPERT_VALIDATING_FACTORS ?? 0),
|
||||||
|
waiting_for_documents_resend: counts.WAITING_FOR_DOCUMENT_RESEND ?? 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Body, Controller, Post } from "@nestjs/common";
|
import { Body, Controller, Post } from "@nestjs/common";
|
||||||
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
|
import { ApiBearerAuth, ApiExcludeController, ApiTags } from "@nestjs/swagger";
|
||||||
import { FieldExpertLoginDto } from "./dtos/login.dto";
|
import { FieldExpertLoginDto } from "./dtos/login.dto";
|
||||||
import { FieldExpertService } from "./expert-initiated.service";
|
import { FieldExpertService } from "./expert-initiated.service";
|
||||||
|
|
||||||
|
@ApiExcludeController()
|
||||||
@Controller("expert-initiated")
|
@Controller("expert-initiated")
|
||||||
@ApiTags("Expert Initiated Flow (Field Expert)")
|
@ApiTags("Expert Initiated Flow (Field Expert)")
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
import type { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
||||||
|
import { BadRequestException } from "@nestjs/common";
|
||||||
|
|
||||||
const MAX_LIMIT = 100;
|
const MAX_LIMIT = 100;
|
||||||
const DEFAULT_LIMIT_WHEN_PAGE_SET = 20;
|
const DEFAULT_LIMIT_WHEN_PAGE_SET = 20;
|
||||||
@@ -8,10 +9,45 @@ export type ListItemGetters<T> = {
|
|||||||
createdAt: (row: T) => Date | string | number | undefined;
|
createdAt: (row: T) => Date | string | number | undefined;
|
||||||
requestNo?: (row: T) => string | undefined;
|
requestNo?: (row: T) => string | undefined;
|
||||||
status: (row: T) => string | undefined;
|
status: (row: T) => string | undefined;
|
||||||
|
/** Blame file type (`THIRD_PARTY` | `CAR_BODY`) when available. */
|
||||||
|
fileType?: (row: T) => string | undefined;
|
||||||
/** Extra strings to match against `search` (e.g. claimRequestId, vehicle). */
|
/** Extra strings to match against `search` (e.g. claimRequestId, vehicle). */
|
||||||
searchExtras?: (row: T) => string[];
|
searchExtras?: (row: T) => string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function parseListDateRange(
|
||||||
|
from?: string,
|
||||||
|
to?: string,
|
||||||
|
): { fromDate?: Date; toDate?: Date } {
|
||||||
|
const fromDate = from ? new Date(from) : undefined;
|
||||||
|
const toDate = to ? new Date(to) : undefined;
|
||||||
|
|
||||||
|
if (fromDate && Number.isNaN(fromDate.getTime())) {
|
||||||
|
throw new BadRequestException("Invalid 'from' date");
|
||||||
|
}
|
||||||
|
if (toDate && Number.isNaN(toDate.getTime())) {
|
||||||
|
throw new BadRequestException("Invalid 'to' date");
|
||||||
|
}
|
||||||
|
if (fromDate && toDate && fromDate > toDate) {
|
||||||
|
throw new BadRequestException("'from' must be before or equal to 'to'");
|
||||||
|
}
|
||||||
|
return { fromDate, toDate };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isInListDateRange(
|
||||||
|
value: Date | string | number | undefined,
|
||||||
|
fromDate?: Date,
|
||||||
|
toDate?: Date,
|
||||||
|
): boolean {
|
||||||
|
if (!fromDate && !toDate) return true;
|
||||||
|
if (value == null) return false;
|
||||||
|
const d = value instanceof Date ? value : new Date(value);
|
||||||
|
if (Number.isNaN(d.getTime())) return false;
|
||||||
|
if (fromDate && d < fromDate) return false;
|
||||||
|
if (toDate && d > toDate) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeSearch(raw?: string): string | undefined {
|
function normalizeSearch(raw?: string): string | undefined {
|
||||||
const t = raw?.trim().toLowerCase();
|
const t = raw?.trim().toLowerCase();
|
||||||
return t || undefined;
|
return t || undefined;
|
||||||
@@ -40,8 +76,13 @@ export function applyListQueryV2<T>(
|
|||||||
} {
|
} {
|
||||||
const search = normalizeSearch(query.search);
|
const search = normalizeSearch(query.search);
|
||||||
let filtered = rows;
|
let filtered = rows;
|
||||||
|
if (query.fileType && getters.fileType) {
|
||||||
|
filtered = filtered.filter(
|
||||||
|
(row) => getters.fileType!(row) === query.fileType,
|
||||||
|
);
|
||||||
|
}
|
||||||
if (search) {
|
if (search) {
|
||||||
filtered = rows.filter((row) => {
|
filtered = filtered.filter((row) => {
|
||||||
const chunks: string[] = [];
|
const chunks: string[] = [];
|
||||||
const pid = getters.publicId(row);
|
const pid = getters.publicId(row);
|
||||||
if (pid) chunks.push(pid);
|
if (pid) chunks.push(pid);
|
||||||
|
|||||||
46
src/helpers/unified-file-status.spec.ts
Normal file
46
src/helpers/unified-file-status.spec.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import {
|
||||||
|
countUnifiedFileStatuses,
|
||||||
|
resolveUnifiedFileStatus,
|
||||||
|
} from "./unified-file-status";
|
||||||
|
import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
|
||||||
|
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
|
||||||
|
|
||||||
|
describe("resolveUnifiedFileStatus", () => {
|
||||||
|
it("prefers claim completed over blame in progress", () => {
|
||||||
|
expect(
|
||||||
|
resolveUnifiedFileStatus({
|
||||||
|
blameStatus: CaseStatus.WAITING_FOR_EXPERT,
|
||||||
|
claimStatus: ClaimCaseStatus.COMPLETED,
|
||||||
|
}),
|
||||||
|
).toBe("COMPLETED");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps blame expert queue", () => {
|
||||||
|
expect(
|
||||||
|
resolveUnifiedFileStatus({
|
||||||
|
blameStatus: CaseStatus.WAITING_FOR_EXPERT,
|
||||||
|
}),
|
||||||
|
).toBe("WAITING_FOR_BLAME_EXPERT");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("maps claim damage expert queue", () => {
|
||||||
|
expect(
|
||||||
|
resolveUnifiedFileStatus({
|
||||||
|
blameStatus: CaseStatus.COMPLETED,
|
||||||
|
claimStatus: ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT,
|
||||||
|
}),
|
||||||
|
).toBe("WAITING_FOR_DAMAGE_EXPERT");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("counts merged entries", () => {
|
||||||
|
const counts = countUnifiedFileStatuses([
|
||||||
|
{ blameStatus: CaseStatus.OPEN },
|
||||||
|
{
|
||||||
|
blameStatus: CaseStatus.COMPLETED,
|
||||||
|
claimStatus: ClaimCaseStatus.CAPTURING_PART_DAMAGES,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(counts.all).toBe(2);
|
||||||
|
expect(counts.IN_PROGRESS).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
221
src/helpers/unified-file-status.ts
Normal file
221
src/helpers/unified-file-status.ts
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
|
||||||
|
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
|
||||||
|
|
||||||
|
/** Calculated lifecycle status for a publicId (blame + claim combined). */
|
||||||
|
export const UNIFIED_FILE_STATUS_KEYS = [
|
||||||
|
"IN_PROGRESS",
|
||||||
|
"WAITING_FOR_SECOND_PARTY",
|
||||||
|
"WAITING_FOR_BLAME_EXPERT",
|
||||||
|
"WAITING_FOR_SIGNATURES",
|
||||||
|
"WAITING_FOR_DOCUMENT_RESEND",
|
||||||
|
"WAITING_FOR_DAMAGE_EXPERT",
|
||||||
|
"EXPERT_REVIEWING",
|
||||||
|
"EXPERT_VALIDATING_FACTORS",
|
||||||
|
"INSURER_REVIEW",
|
||||||
|
"COMPLETED",
|
||||||
|
"CANCELLED",
|
||||||
|
"REJECTED",
|
||||||
|
"STOPPED",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type UnifiedFileStatusKey = (typeof UNIFIED_FILE_STATUS_KEYS)[number];
|
||||||
|
|
||||||
|
export type UnifiedFileStatusInput = {
|
||||||
|
blameStatus?: string | null;
|
||||||
|
claimStatus?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CLAIM_USER_PHASE = new Set<string>([
|
||||||
|
ClaimCaseStatus.CREATED,
|
||||||
|
ClaimCaseStatus.SELECTING_OUTER_PARTS,
|
||||||
|
ClaimCaseStatus.SELECTING_OTHER_PARTS,
|
||||||
|
ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS,
|
||||||
|
ClaimCaseStatus.CAPTURING_PART_DAMAGES,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const INSURER_REVIEW_CLAIM_STATUSES = new Set<string>([
|
||||||
|
ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL,
|
||||||
|
ClaimCaseStatus.INSURER_REVIEW_AWAITING_OWNER_SIGN,
|
||||||
|
ClaimCaseStatus.INSURER_REVIEW_MIXED_FACTORS_PENDING,
|
||||||
|
ClaimCaseStatus.OWNER_REPAIR_FACTOR_UPLOAD_PENDING,
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type UnifiedFileStatusDefinition = {
|
||||||
|
key: UnifiedFileStatusKey;
|
||||||
|
labelEn: string;
|
||||||
|
labelFa: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UNIFIED_FILE_STATUS_CATALOG: UnifiedFileStatusDefinition[] = [
|
||||||
|
{
|
||||||
|
key: "IN_PROGRESS",
|
||||||
|
labelEn: "In progress (user)",
|
||||||
|
labelFa: "در حال تکمیل توسط کاربر",
|
||||||
|
description:
|
||||||
|
"Parties are still filling blame steps and/or the damaged party is completing the claim (parts, documents, capture).",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "WAITING_FOR_SECOND_PARTY",
|
||||||
|
labelEn: "Waiting for second party",
|
||||||
|
labelFa: "در انتظار طرف دوم",
|
||||||
|
description: "Blame file is waiting for the second party to join or complete steps.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "WAITING_FOR_BLAME_EXPERT",
|
||||||
|
labelEn: "Waiting for blame expert",
|
||||||
|
labelFa: "در انتظار کارشناس مقصر",
|
||||||
|
description: "Blame case is in the expert disagreement / guilt-decision queue.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "WAITING_FOR_SIGNATURES",
|
||||||
|
labelEn: "Waiting for signatures",
|
||||||
|
labelFa: "در انتظار امضا",
|
||||||
|
description: "Blame file is waiting for party signatures on the expert decision.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "WAITING_FOR_DOCUMENT_RESEND",
|
||||||
|
labelEn: "Waiting for document resend",
|
||||||
|
labelFa: "در انتظار ارسال مجدد مدارک",
|
||||||
|
description:
|
||||||
|
"Blame or claim is waiting for the user to resubmit documents or photos requested by an expert.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "WAITING_FOR_DAMAGE_EXPERT",
|
||||||
|
labelEn: "Waiting for damage expert",
|
||||||
|
labelFa: "در انتظار کارشناس خسارت",
|
||||||
|
description: "Claim is in the damage-expert review queue (not yet locked).",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "EXPERT_REVIEWING",
|
||||||
|
labelEn: "Damage expert reviewing",
|
||||||
|
labelFa: "در حال بررسی کارشناس خسارت",
|
||||||
|
description: "A damage expert has locked the claim and is assessing damage.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "EXPERT_VALIDATING_FACTORS",
|
||||||
|
labelEn: "Validating repair factors",
|
||||||
|
labelFa: "اعتبارسنجی فاکتور تعمیر",
|
||||||
|
description: "Damage expert is validating uploaded repair-factor documents.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "INSURER_REVIEW",
|
||||||
|
labelEn: "Insurer / owner review",
|
||||||
|
labelFa: "بررسی بیمهگر / مالک",
|
||||||
|
description:
|
||||||
|
"Expert pricing is done; owner signature, factor upload, or insurer approval is pending.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "COMPLETED",
|
||||||
|
labelEn: "Completed",
|
||||||
|
labelFa: "تکمیل شده",
|
||||||
|
description: "Claim (and blame when present) is finished successfully.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "CANCELLED",
|
||||||
|
labelEn: "Cancelled",
|
||||||
|
labelFa: "لغو شده",
|
||||||
|
description: "File was cancelled or auto-closed.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "REJECTED",
|
||||||
|
labelEn: "Rejected",
|
||||||
|
labelFa: "رد شده",
|
||||||
|
description: "Claim was rejected.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "STOPPED",
|
||||||
|
labelEn: "Stopped",
|
||||||
|
labelFa: "متوقف شده",
|
||||||
|
description: "Blame file was stopped.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function initialUnifiedFileStatusCounts(): Record<
|
||||||
|
UnifiedFileStatusKey | "all",
|
||||||
|
number
|
||||||
|
> {
|
||||||
|
const out: Record<string, number> = { all: 0 };
|
||||||
|
for (const key of UNIFIED_FILE_STATUS_KEYS) {
|
||||||
|
out[key] = 0;
|
||||||
|
}
|
||||||
|
return out as Record<UnifiedFileStatusKey | "all", number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single calculatable status for a shared publicId from raw blame + claim statuses.
|
||||||
|
* Claim pipeline takes precedence once a claim exists and has left the user-only phase.
|
||||||
|
*/
|
||||||
|
export function resolveUnifiedFileStatus(
|
||||||
|
input: UnifiedFileStatusInput,
|
||||||
|
): UnifiedFileStatusKey {
|
||||||
|
const blame = input.blameStatus?.trim() || undefined;
|
||||||
|
const claim = input.claimStatus?.trim() || undefined;
|
||||||
|
|
||||||
|
if (claim === ClaimCaseStatus.COMPLETED) return "COMPLETED";
|
||||||
|
if (claim === ClaimCaseStatus.REJECTED) return "REJECTED";
|
||||||
|
if (
|
||||||
|
claim === ClaimCaseStatus.CANCELLED ||
|
||||||
|
blame === CaseStatus.CANCELLED ||
|
||||||
|
blame === CaseStatus.AUTO_CLOSED
|
||||||
|
) {
|
||||||
|
return "CANCELLED";
|
||||||
|
}
|
||||||
|
if (blame === CaseStatus.STOPPED) return "STOPPED";
|
||||||
|
|
||||||
|
if (
|
||||||
|
blame === CaseStatus.WAITING_FOR_DOCUMENT_RESEND ||
|
||||||
|
claim === ClaimCaseStatus.WAITING_FOR_USER_RESEND
|
||||||
|
) {
|
||||||
|
return "WAITING_FOR_DOCUMENT_RESEND";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (claim === ClaimCaseStatus.EXPERT_REVIEWING) return "EXPERT_REVIEWING";
|
||||||
|
if (claim === ClaimCaseStatus.EXPERT_VALIDATING_REPAIR_FACTORS) {
|
||||||
|
return "EXPERT_VALIDATING_FACTORS";
|
||||||
|
}
|
||||||
|
if (claim && INSURER_REVIEW_CLAIM_STATUSES.has(claim)) {
|
||||||
|
return "INSURER_REVIEW";
|
||||||
|
}
|
||||||
|
if (claim === ClaimCaseStatus.WAITING_FOR_DAMAGE_EXPERT) {
|
||||||
|
return "WAITING_FOR_DAMAGE_EXPERT";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blame === CaseStatus.WAITING_FOR_EXPERT) return "WAITING_FOR_BLAME_EXPERT";
|
||||||
|
if (blame === CaseStatus.WAITING_FOR_SIGNATURES) {
|
||||||
|
return "WAITING_FOR_SIGNATURES";
|
||||||
|
}
|
||||||
|
if (blame === CaseStatus.WAITING_FOR_SECOND_PARTY) {
|
||||||
|
return "WAITING_FOR_SECOND_PARTY";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blame === CaseStatus.COMPLETED && !claim) return "COMPLETED";
|
||||||
|
|
||||||
|
if (
|
||||||
|
blame === CaseStatus.OPEN ||
|
||||||
|
(claim && CLAIM_USER_PHASE.has(claim)) ||
|
||||||
|
(blame === CaseStatus.COMPLETED && claim && CLAIM_USER_PHASE.has(claim))
|
||||||
|
) {
|
||||||
|
return "IN_PROGRESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (claim) return "IN_PROGRESS";
|
||||||
|
if (blame) return "IN_PROGRESS";
|
||||||
|
return "IN_PROGRESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function countUnifiedFileStatuses(
|
||||||
|
entries: UnifiedFileStatusInput[],
|
||||||
|
): Record<UnifiedFileStatusKey | "all", number> {
|
||||||
|
const buckets = initialUnifiedFileStatusCounts();
|
||||||
|
for (const entry of entries) {
|
||||||
|
const key = resolveUnifiedFileStatus(entry);
|
||||||
|
buckets.all++;
|
||||||
|
buckets[key]++;
|
||||||
|
}
|
||||||
|
return buckets;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUnifiedFileStatusCatalog(): UnifiedFileStatusDefinition[] {
|
||||||
|
return UNIFIED_FILE_STATUS_CATALOG;
|
||||||
|
}
|
||||||
@@ -41,3 +41,10 @@ export function readOtpExpireMinutesFromEnv(): number {
|
|||||||
const raw = Number(process.env.EXP_OTP_TIME ?? "2");
|
const raw = Number(process.env.EXP_OTP_TIME ?? "2");
|
||||||
return Number.isFinite(raw) && raw > 0 ? raw : 2;
|
return Number.isFinite(raw) && raw > 0 ? raw : 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Dev-only: skip SMS provider and accept a fixed OTP for any mobile. */
|
||||||
|
export const FAKE_OTP_CODE = "12345";
|
||||||
|
|
||||||
|
export function isFakeOtpEnabled(): boolean {
|
||||||
|
return process.env.FAKE_OTP === "true";
|
||||||
|
}
|
||||||
|
|||||||
91
src/request-management/dto/run-inquiries-v3.dto.ts
Normal file
91
src/request-management/dto/run-inquiries-v3.dto.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import {
|
||||||
|
IsBoolean,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
ValidateNested,
|
||||||
|
} from "class-validator";
|
||||||
|
import { Type } from "class-transformer";
|
||||||
|
|
||||||
|
class PlateV3Dto {
|
||||||
|
@ApiProperty({ example: "44", description: "Left two digits" })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
leftDigits: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: "ب", description: "Center alphabet letter" })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
centerAlphabet: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: "111", description: "Center three digits" })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
centerDigits: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: "22", description: "Right two digits (Iran region code)" })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
ir: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Body for `POST run-inquiries/:requestId`.
|
||||||
|
* First call = guilty party (+ auto claim). Second call = damaged party (THIRD_PARTY only).
|
||||||
|
*/
|
||||||
|
export class RunInquiriesV3Dto {
|
||||||
|
@ApiProperty({
|
||||||
|
type: PlateV3Dto,
|
||||||
|
description: "Plate segments — Tejarat block / third-party inquiry.",
|
||||||
|
})
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => PlateV3Dto)
|
||||||
|
plate: PlateV3Dto;
|
||||||
|
|
||||||
|
@ApiProperty({ example: "1234567890", description: "National code of the policyholder (insurer)" })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
nationalCodeOfInsurer: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: "1234567890", description: "National code of the driver" })
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
nationalCodeOfDriver: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: true, description: "Whether the driver is the same person as the insurer" })
|
||||||
|
@IsBoolean()
|
||||||
|
driverIsInsurer: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 13780624, description: "Insurer birth date (Jalali)" })
|
||||||
|
insurerBirthday: number | string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ example: 13780624, description: "Driver birth date (Jalali). Required when driverIsInsurer is false." })
|
||||||
|
@IsOptional()
|
||||||
|
driverBirthday?: number | string | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
example: "IR123456789012345678901234",
|
||||||
|
description:
|
||||||
|
"Sheba (IBAN). Required on the damaged party call (CAR_BODY first call; THIRD_PARTY second call).",
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
sheba?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
example: "123456789",
|
||||||
|
description: "Driver license (required when driverIsInsurer is false).",
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
driverLicense?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
example: "123456789",
|
||||||
|
description: "Insurer license (required when driverIsInsurer is true).",
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
insurerLicense?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,584 @@
|
|||||||
|
import { extname } from "node:path";
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
UploadedFile,
|
||||||
|
UseGuards,
|
||||||
|
UseInterceptors,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import {
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiBody,
|
||||||
|
ApiConsumes,
|
||||||
|
ApiOperation,
|
||||||
|
ApiParam,
|
||||||
|
ApiTags,
|
||||||
|
} from "@nestjs/swagger";
|
||||||
|
import { FileInterceptor } from "@nestjs/platform-express";
|
||||||
|
import { diskStorage } from "multer";
|
||||||
|
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
||||||
|
import { RolesGuard } from "src/auth/guards/role.guard";
|
||||||
|
import { Roles } from "src/decorators/roles.decorator";
|
||||||
|
import { CurrentUser } from "src/decorators/user.decorator";
|
||||||
|
import { MediaPolicyService } from "src/media-policy/media-policy.service";
|
||||||
|
import { DEFAULT_MEDIA_MAX_BYTES } from "src/client/client.service";
|
||||||
|
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||||
|
import { CreationMethod } from "./entities/schema/request-management.schema";
|
||||||
|
import { CreateExpertInitiatedFileDto } from "./dto/expert-initiated.dto";
|
||||||
|
import { SendPartyOtpDto, VerifyPartyOtpDto } from "./dto/party-otp.dto";
|
||||||
|
import { RunInquiriesV3Dto } from "./dto/run-inquiries-v3.dto";
|
||||||
|
import {
|
||||||
|
CarBodyFormDto,
|
||||||
|
DescriptionDto,
|
||||||
|
LocationDto,
|
||||||
|
} from "./dto/create-request-management.dto";
|
||||||
|
import { ExpertAccidentFieldsDto } from "./dto/expert-accident-fields.dto";
|
||||||
|
import { RequestManagementService } from "./request-management.service";
|
||||||
|
import { ClaimRequestManagementService } from "src/claim-request-management/claim-request-management.service";
|
||||||
|
import { PartyRole } from "./entities/schema/partyRole.enum";
|
||||||
|
import {
|
||||||
|
SelectOuterPartsV2Dto,
|
||||||
|
SelectOuterPartsV2ResponseDto,
|
||||||
|
} from "src/claim-request-management/dto/select-outer-parts-v2.dto";
|
||||||
|
import {
|
||||||
|
SelectOtherPartsV2ResponseDto,
|
||||||
|
} from "src/claim-request-management/dto/select-other-parts-v2.dto";
|
||||||
|
import { SelectOtherPartsV3Dto } from "src/claim-request-management/dto/select-other-parts-v3.dto";
|
||||||
|
import {
|
||||||
|
UploadRequiredDocumentV2Dto,
|
||||||
|
UploadRequiredDocumentV2ResponseDto,
|
||||||
|
} from "src/claim-request-management/dto/upload-document-v2.dto";
|
||||||
|
import {
|
||||||
|
CapturePartV2Dto,
|
||||||
|
CapturePartV2ResponseDto,
|
||||||
|
} from "src/claim-request-management/dto/capture-part-v2.dto";
|
||||||
|
import { GetCaptureRequirementsV2ResponseDto } from "src/claim-request-management/dto/capture-requirements-v2.dto";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V3 field-expert in-person flow (one party at a time, standard workflow steps).
|
||||||
|
*
|
||||||
|
* THIRD_PARTY:
|
||||||
|
* create → OTP/verify (guilty) → [car-body-form if CAR_BODY] → run-inquiries (guilty + claim)
|
||||||
|
* → location/description/voice → sign → OTP/verify (damaged) → run-inquiries (damaged)
|
||||||
|
* → location/description/voice → sign → accident-fields → docs → outer parts → other parts
|
||||||
|
* → capture-part → car-capture → upload-video (blame accident video, last) → WAITING_FOR_EXPERT
|
||||||
|
*
|
||||||
|
* CAR_BODY: same but single party — omit second-party OTP/inquiries/sign steps.
|
||||||
|
*/
|
||||||
|
@ApiTags("expert-initiated blame (mirror v3 in-person)")
|
||||||
|
@Controller("v3/expert-initiated/blame-request-management")
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
||||||
|
@Roles(RoleEnum.FIELD_EXPERT)
|
||||||
|
export class ExpertInitiatedBlameV3MirrorController {
|
||||||
|
constructor(
|
||||||
|
private readonly requestManagementService: RequestManagementService,
|
||||||
|
private readonly claimRequestManagementService: ClaimRequestManagementService,
|
||||||
|
private readonly mediaPolicyService: MediaPolicyService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({ summary: "Create IN_PERSON blame file" })
|
||||||
|
@ApiBody({ type: CreateExpertInitiatedFileDto })
|
||||||
|
async create(
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
@Body() dto: CreateExpertInitiatedFileDto,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.createExpertInitiatedBlameV2(expert, {
|
||||||
|
...dto,
|
||||||
|
creationMethod: CreationMethod.IN_PERSON,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("claim-id/:requestId")
|
||||||
|
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Get linked claim ID",
|
||||||
|
description:
|
||||||
|
"Returns the claim auto-created during guilty-party run-inquiries. Use this claim ID for capture-requirements, upload-document, and part-selection endpoints.",
|
||||||
|
})
|
||||||
|
async getLinkedClaimId(
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.getV3LinkedClaimForExpert(
|
||||||
|
expert,
|
||||||
|
requestId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("send-party-otp/:requestId")
|
||||||
|
@ApiParam({ name: "requestId" })
|
||||||
|
@ApiBody({ type: SendPartyOtpDto })
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Send OTP to one party",
|
||||||
|
description: "Guilty party first; damaged party after guilty party has signed (THIRD_PARTY).",
|
||||||
|
})
|
||||||
|
async sendPartyOtp(
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@Body() dto: SendPartyOtpDto,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.sendPartyOtpV2(expert, requestId, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("verify-party-otp/:requestId")
|
||||||
|
@ApiParam({ name: "requestId" })
|
||||||
|
@ApiBody({ type: VerifyPartyOtpDto })
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Verify one party OTP",
|
||||||
|
description: "FIRST (guilty) then SECOND (damaged) on THIRD_PARTY files.",
|
||||||
|
})
|
||||||
|
async verifyPartyOtp(
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@Body() dto: VerifyPartyOtpDto,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.verifyPartyOtpV2(expert, requestId, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("car-body-form/:requestId")
|
||||||
|
@ApiParam({ name: "requestId" })
|
||||||
|
@ApiBody({ type: CarBodyFormDto })
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "CAR_BODY only — accident type before inquiries",
|
||||||
|
description: "Submit after guilty-party OTP verify, before run-inquiries.",
|
||||||
|
})
|
||||||
|
async carBodyForm(
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@Body() body: CarBodyFormDto,
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.carBodyAccidentTypeFormV3(
|
||||||
|
requestId,
|
||||||
|
body,
|
||||||
|
expert,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("run-inquiries/:requestId")
|
||||||
|
@ApiParam({ name: "requestId" })
|
||||||
|
@ApiBody({ type: RunInquiriesV3Dto })
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Run external inquiries for the current party",
|
||||||
|
description:
|
||||||
|
"1st call = guilty party (+ auto claim). 2nd call = damaged party (THIRD_PARTY, after guilty sign). " +
|
||||||
|
"Advances workflow to FIRST_INITIAL_FORM or SECOND_INITIAL_FORM.",
|
||||||
|
})
|
||||||
|
async runInquiries(
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@Body() dto: RunInquiriesV3Dto,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.runInquiriesV3(requestId, expert, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("add-detail-location/:requestId")
|
||||||
|
@ApiParam({ name: "requestId" })
|
||||||
|
@ApiBody({ type: LocationDto })
|
||||||
|
@ApiOperation({ summary: "Add location for current party (partyRole FIRST or SECOND)" })
|
||||||
|
async addLocation(
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@Body() body: LocationDto,
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
@Body("partyRole") partyRole?: string,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.addPartyLocationV3(
|
||||||
|
requestId,
|
||||||
|
expert,
|
||||||
|
body,
|
||||||
|
partyRole,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("add-detail-description/:requestId")
|
||||||
|
@ApiParam({ name: "requestId" })
|
||||||
|
@ApiBody({ type: DescriptionDto })
|
||||||
|
@ApiOperation({ summary: "Add description for current party" })
|
||||||
|
async addDescription(
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@Body() body: DescriptionDto,
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
@Body("partyRole") partyRole?: string,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.addPartyDescriptionV3(
|
||||||
|
requestId,
|
||||||
|
expert,
|
||||||
|
body,
|
||||||
|
partyRole,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiBody({
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
file: { type: "string", format: "binary" },
|
||||||
|
partyRole: { type: "string", enum: ["FIRST", "SECOND"] },
|
||||||
|
},
|
||||||
|
required: ["file"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiConsumes("multipart/form-data")
|
||||||
|
@UseInterceptors(
|
||||||
|
FileInterceptor("file", {
|
||||||
|
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
||||||
|
storage: diskStorage({
|
||||||
|
destination: "./files/voice",
|
||||||
|
filename: (req, file, callback) => {
|
||||||
|
const unique = Date.now();
|
||||||
|
const ex = extname(file.originalname);
|
||||||
|
callback(null, `v3-expert-voice-${unique}${ex}`);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
@Post("upload-voice/:requestId")
|
||||||
|
@ApiParam({ name: "requestId" })
|
||||||
|
@ApiOperation({ summary: "Upload voice for current party" })
|
||||||
|
async uploadVoice(
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
@UploadedFile() voice: Express.Multer.File,
|
||||||
|
@Body("partyRole") partyRole?: string,
|
||||||
|
) {
|
||||||
|
await this.mediaPolicyService.assertForBlame(voice, requestId, "voice");
|
||||||
|
return this.requestManagementService.addPartyVoiceV3(
|
||||||
|
requestId,
|
||||||
|
expert,
|
||||||
|
voice,
|
||||||
|
partyRole,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put("sign/:requestId")
|
||||||
|
@ApiParam({ name: "requestId" })
|
||||||
|
@ApiConsumes("multipart/form-data")
|
||||||
|
@ApiOperation({ summary: "Party on-site signature" })
|
||||||
|
@ApiBody({
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
required: ["partyRole", "sign"],
|
||||||
|
properties: {
|
||||||
|
partyRole: { type: "string", enum: ["FIRST", "SECOND"] },
|
||||||
|
isAccept: { type: "boolean", default: true },
|
||||||
|
sign: { type: "string", format: "binary" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@UseInterceptors(
|
||||||
|
FileInterceptor("sign", {
|
||||||
|
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
||||||
|
storage: diskStorage({
|
||||||
|
destination: "./files/signs",
|
||||||
|
filename: (req, file, callback) => {
|
||||||
|
const unique = Date.now();
|
||||||
|
const ex = extname(file.originalname);
|
||||||
|
callback(null, `v3-expert-party-${unique}${ex}`);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
async sign(
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@Body() body: { partyRole?: string; isAccept?: string | boolean },
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
@UploadedFile() sign: Express.Multer.File,
|
||||||
|
) {
|
||||||
|
await this.mediaPolicyService.assertForBlame(sign, requestId, "image");
|
||||||
|
const partyRole =
|
||||||
|
body?.partyRole === "SECOND" ? PartyRole.SECOND : PartyRole.FIRST;
|
||||||
|
const isAccept = !(body?.isAccept === false || body?.isAccept === "false");
|
||||||
|
return this.requestManagementService.expertUploadPartySignatureV3(
|
||||||
|
expert,
|
||||||
|
requestId,
|
||||||
|
partyRole,
|
||||||
|
isAccept,
|
||||||
|
sign,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("accident-fields/:requestId")
|
||||||
|
@ApiParam({ name: "requestId" })
|
||||||
|
@ApiBody({ type: ExpertAccidentFieldsDto })
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Accident type / fields (after all signatures)",
|
||||||
|
description:
|
||||||
|
"Saves accident fields only. Does not move to WAITING_FOR_EXPERT — that happens after the final blame accident video.",
|
||||||
|
})
|
||||||
|
async addAccidentFields(
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@Body() fields: ExpertAccidentFieldsDto,
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
) {
|
||||||
|
return this.requestManagementService.expertAddAccidentFieldsForBlameV3(
|
||||||
|
expert,
|
||||||
|
requestId,
|
||||||
|
fields,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("capture-requirements/:claimRequestId")
|
||||||
|
@ApiParam({ name: "claimRequestId" })
|
||||||
|
@ApiOperation({ summary: "List required claim documents" })
|
||||||
|
async getCaptureRequirements(
|
||||||
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
): Promise<GetCaptureRequirementsV2ResponseDto> {
|
||||||
|
return this.claimRequestManagementService.getCaptureRequirementsV3(
|
||||||
|
claimRequestId,
|
||||||
|
expert.sub,
|
||||||
|
expert,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("upload-document/:claimRequestId")
|
||||||
|
@ApiParam({ name: "claimRequestId" })
|
||||||
|
@ApiConsumes("multipart/form-data")
|
||||||
|
@UseInterceptors(
|
||||||
|
FileInterceptor("file", {
|
||||||
|
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
||||||
|
storage: diskStorage({
|
||||||
|
destination: "./files/claim-documents",
|
||||||
|
filename: (req, file, callback) => {
|
||||||
|
const unique = Date.now();
|
||||||
|
const ex = extname(file.originalname);
|
||||||
|
callback(null, `${file.originalname.split(".")[0]}-${unique}${ex}`);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
@ApiOperation({ summary: "Upload one required claim document" })
|
||||||
|
async uploadDocument(
|
||||||
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
@Body() body: UploadRequiredDocumentV2Dto,
|
||||||
|
@UploadedFile() file: Express.Multer.File,
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
): Promise<UploadRequiredDocumentV2ResponseDto> {
|
||||||
|
await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image");
|
||||||
|
return this.claimRequestManagementService.uploadRequiredDocumentV3(
|
||||||
|
claimRequestId,
|
||||||
|
body,
|
||||||
|
file,
|
||||||
|
expert.sub,
|
||||||
|
expert,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch("select-outer-parts/:claimRequestId")
|
||||||
|
@ApiParam({ name: "claimRequestId" })
|
||||||
|
@ApiBody({ type: SelectOuterPartsV2Dto })
|
||||||
|
@ApiOperation({ summary: "Select damaged outer parts" })
|
||||||
|
async selectOuterParts(
|
||||||
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
@Body() body: SelectOuterPartsV2Dto,
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
): Promise<SelectOuterPartsV2ResponseDto> {
|
||||||
|
return this.claimRequestManagementService.selectOuterPartsV3(
|
||||||
|
claimRequestId,
|
||||||
|
body,
|
||||||
|
expert.sub,
|
||||||
|
expert,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch("select-other-parts/:claimRequestId")
|
||||||
|
@ApiParam({ name: "claimRequestId", example: "507f1f77bcf86cd799439011" })
|
||||||
|
@ApiConsumes("multipart/form-data")
|
||||||
|
@UseInterceptors(
|
||||||
|
FileInterceptor("file", {
|
||||||
|
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
||||||
|
storage: diskStorage({
|
||||||
|
destination: "./files/claim-required-document",
|
||||||
|
filename: (req, file, callback) => {
|
||||||
|
const unique = Date.now();
|
||||||
|
const ex = extname(file.originalname);
|
||||||
|
callback(null, `v3-other-parts-${unique}${ex}`);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Select other damaged parts (V3)",
|
||||||
|
description:
|
||||||
|
"Other parts only — sheba and national code were collected during run-inquiries. Optional car green card file.",
|
||||||
|
})
|
||||||
|
@ApiBody({
|
||||||
|
description: "Other parts selection. Bank info is already on the claim from run-inquiries.",
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
otherParts: {
|
||||||
|
oneOf: [
|
||||||
|
{ type: "array", items: { type: "string", enum: ["engine", "suspension", "brake_system", "electrical", "radiator", "transmission", "exhaust", "headlight", "taillight", "mirror", "glass"] } },
|
||||||
|
{ type: "string", description: "JSON string array for multipart" },
|
||||||
|
],
|
||||||
|
example: ["engine", "suspension"],
|
||||||
|
},
|
||||||
|
file: { type: "string", format: "binary", description: "Optional car green card" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
async selectOtherParts(
|
||||||
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
@Body() body: SelectOtherPartsV3Dto,
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
@UploadedFile() file?: Express.Multer.File,
|
||||||
|
): Promise<SelectOtherPartsV2ResponseDto> {
|
||||||
|
if (file) {
|
||||||
|
await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image");
|
||||||
|
}
|
||||||
|
return this.claimRequestManagementService.selectOtherPartsV3(
|
||||||
|
claimRequestId,
|
||||||
|
body,
|
||||||
|
expert.sub,
|
||||||
|
expert,
|
||||||
|
file,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("capture-part/:claimRequestId")
|
||||||
|
@ApiParam({ name: "claimRequestId", example: "507f1f77bcf86cd799439011" })
|
||||||
|
@ApiConsumes("multipart/form-data")
|
||||||
|
@UseInterceptors(
|
||||||
|
FileInterceptor("file", {
|
||||||
|
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
||||||
|
storage: diskStorage({
|
||||||
|
destination: "./files/claim-captures",
|
||||||
|
filename: (req, file, callback) => {
|
||||||
|
const unique = Date.now();
|
||||||
|
const ex = extname(file.originalname);
|
||||||
|
callback(null, `v3-capture-${unique}${ex}`);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Field — car angles and damaged-part photos",
|
||||||
|
description:
|
||||||
|
"CAPTURE_PART_DAMAGES: (1) all damaged-part photos, (2) four car angles, (3) chassis/engine/metal-plate via upload-document. Then car-capture walk-around video.",
|
||||||
|
})
|
||||||
|
@ApiBody({
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
required: ["captureType", "captureKey", "file"],
|
||||||
|
properties: {
|
||||||
|
captureType: {
|
||||||
|
type: "string",
|
||||||
|
enum: ["angle", "part"],
|
||||||
|
example: "angle",
|
||||||
|
},
|
||||||
|
captureKey: {
|
||||||
|
type: "string",
|
||||||
|
example: "front",
|
||||||
|
description:
|
||||||
|
"For angle: front/back/left/right. For part: hood/front_bumper/etc.",
|
||||||
|
},
|
||||||
|
file: { type: "string", format: "binary" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
async capturePart(
|
||||||
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
@Body() body: CapturePartV2Dto,
|
||||||
|
@UploadedFile() file: Express.Multer.File,
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
): Promise<CapturePartV2ResponseDto> {
|
||||||
|
await this.mediaPolicyService.assertForClaim(file, claimRequestId, "image");
|
||||||
|
return this.claimRequestManagementService.capturePartV3(
|
||||||
|
claimRequestId,
|
||||||
|
body,
|
||||||
|
file,
|
||||||
|
expert.sub,
|
||||||
|
expert,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch("car-capture/:claimRequestId")
|
||||||
|
@ApiParam({ name: "claimRequestId", example: "507f1f77bcf86cd799439011" })
|
||||||
|
@ApiConsumes("multipart/form-data")
|
||||||
|
@UseInterceptors(
|
||||||
|
FileInterceptor("file", {
|
||||||
|
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
||||||
|
storage: diskStorage({
|
||||||
|
destination: "./files/car-capture-videos/",
|
||||||
|
filename: (req, file, callback) => {
|
||||||
|
const unique = Date.now();
|
||||||
|
const ex = extname(file.originalname);
|
||||||
|
callback(null, `v3-claim-video-${unique}${ex}`);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
@ApiBody({
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
required: ["file"],
|
||||||
|
properties: { file: { type: "string", format: "binary" } },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Field — walk-around video",
|
||||||
|
description:
|
||||||
|
"Upload after capture-part is complete (parts, angles, capture-phase docs). Required before the final blame accident video.",
|
||||||
|
})
|
||||||
|
async carCapture(
|
||||||
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
@UploadedFile("file") file: Express.Multer.File,
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
) {
|
||||||
|
await this.mediaPolicyService.assertForClaim(file, claimRequestId, "video");
|
||||||
|
return this.claimRequestManagementService.setVideoCaptureV3(
|
||||||
|
claimRequestId,
|
||||||
|
file,
|
||||||
|
expert.sub,
|
||||||
|
expert,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiBody({
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
required: ["file"],
|
||||||
|
properties: { file: { type: "string", format: "binary" } },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiConsumes("multipart/form-data")
|
||||||
|
@UseInterceptors(
|
||||||
|
FileInterceptor("file", {
|
||||||
|
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
||||||
|
storage: diskStorage({
|
||||||
|
destination: "./files/video",
|
||||||
|
filename: (req, file, callback) => {
|
||||||
|
const unique = Date.now();
|
||||||
|
const ex = extname(file.originalname);
|
||||||
|
callback(null, `v3-blame-accident-${unique}${ex}`);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
@Post("upload-video/:requestId")
|
||||||
|
@ApiParam({ name: "requestId" })
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Blame accident video (final step)",
|
||||||
|
description:
|
||||||
|
"Last step of the V3 flow. Requires claim walk-around video and completed capture. Sets blame status to WAITING_FOR_EXPERT.",
|
||||||
|
})
|
||||||
|
async uploadBlameVideo(
|
||||||
|
@Param("requestId") requestId: string,
|
||||||
|
@CurrentUser() expert: any,
|
||||||
|
@UploadedFile() file: Express.Multer.File,
|
||||||
|
) {
|
||||||
|
await this.mediaPolicyService.assertForBlame(file, requestId, "video");
|
||||||
|
return this.requestManagementService.expertUploadBlameVideoV3(
|
||||||
|
expert,
|
||||||
|
requestId,
|
||||||
|
file,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
ApiOperation,
|
ApiOperation,
|
||||||
ApiResponse,
|
ApiResponse,
|
||||||
ApiConsumes,
|
ApiConsumes,
|
||||||
|
ApiExcludeController,
|
||||||
} from "@nestjs/swagger";
|
} from "@nestjs/swagger";
|
||||||
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
||||||
import { RolesGuard } from "src/auth/guards/role.guard";
|
import { RolesGuard } from "src/auth/guards/role.guard";
|
||||||
@@ -36,7 +37,8 @@ import { ExpertAccidentFieldsDto } from "./dto/expert-accident-fields.dto";
|
|||||||
import { ExpertCompleteClaimDataDto } from "./dto/expert-complete-claim-data.dto";
|
import { ExpertCompleteClaimDataDto } from "./dto/expert-complete-claim-data.dto";
|
||||||
import { ClaimRequestManagementService } from "src/claim-request-management/claim-request-management.service";
|
import { ClaimRequestManagementService } from "src/claim-request-management/claim-request-management.service";
|
||||||
|
|
||||||
@ApiTags("expert-initiated-blame")
|
@ApiExcludeController()
|
||||||
|
// @ApiTags("expert-initiated-blame")
|
||||||
@Controller("expert-initiated-blame")
|
@Controller("expert-initiated-blame")
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
||||||
@@ -47,34 +49,26 @@ export class ExpertInitiatedController {
|
|||||||
private readonly claimRequestManagementService: ClaimRequestManagementService,
|
private readonly claimRequestManagementService: ClaimRequestManagementService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Get("my-files")
|
@Get("my-files")
|
||||||
@ApiOperation({
|
// @ApiOperation({
|
||||||
summary: "List my expert-initiated files",
|
// summary: "List my expert-initiated files",
|
||||||
description:
|
// description:
|
||||||
"Returns all blame files created by the current field expert (their panel).",
|
// "Returns all blame files created by the current field expert (their panel).",
|
||||||
})
|
// })
|
||||||
@ApiResponse({ status: 200, description: "List of expert-initiated files" })
|
// @ApiResponse({ status: 200, description: "List of expert-initiated files" })
|
||||||
async getMyFiles(@CurrentUser() expert: any) {
|
async getMyFiles(@CurrentUser() expert: any) {
|
||||||
return await this.requestManagementService.getMyExpertInitiatedFiles(
|
return await this.requestManagementService.getMyExpertInitiatedFiles(expert);
|
||||||
expert,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Post("complete-blame-data/:requestId")
|
@Post("complete-blame-data/:requestId")
|
||||||
@ApiOperation({
|
// @ApiParam({ name: "requestId", description: "ID of the expert-initiated file" })
|
||||||
summary: "Submit all blame-needed data",
|
// @ApiBody({
|
||||||
description:
|
// description:
|
||||||
"Single endpoint to complete blame form: send THIRD_PARTY or CAR_BODY form data according to file type. " +
|
// "Send ExpertCompleteThirdPartyFormDto for THIRD_PARTY files or ExpertCompleteCarBodyFormDto for CAR_BODY files (according to file type).",
|
||||||
"Video and voice can be uploaded separately.",
|
// })
|
||||||
})
|
// @ApiResponse({ status: 200, description: "Blame form completed successfully" })
|
||||||
@ApiParam({ name: "requestId", description: "ID of the expert-initiated file" })
|
|
||||||
@ApiBody({
|
|
||||||
description:
|
|
||||||
"Send ExpertCompleteThirdPartyFormDto for THIRD_PARTY files or ExpertCompleteCarBodyFormDto for CAR_BODY files (according to file type).",
|
|
||||||
})
|
|
||||||
@ApiResponse({ status: 200, description: "Blame form completed successfully" })
|
|
||||||
async completeBlameData(
|
async completeBlameData(
|
||||||
@CurrentUser() expert: any,
|
@CurrentUser() expert: any,
|
||||||
@Param("requestId") requestId: string,
|
@Param("requestId") requestId: string,
|
||||||
@@ -87,20 +81,20 @@ export class ExpertInitiatedController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Post("complete-claim-data/:claimRequestId")
|
@Post("complete-claim-data/:claimRequestId")
|
||||||
@ApiOperation({
|
// @ApiOperation({
|
||||||
summary: "Submit claim-needed data",
|
// summary: "Submit claim-needed data",
|
||||||
description:
|
// description:
|
||||||
"Field expert fills claim data for expert-initiated IN_PERSON files (car part damage, sheba, national code, other parts). " +
|
// "Field expert fills claim data for expert-initiated IN_PERSON files (car part damage, sheba, national code, other parts). " +
|
||||||
"Only the initiating expert can call this.",
|
// "Only the initiating expert can call this.",
|
||||||
})
|
// })
|
||||||
@ApiParam({
|
// @ApiParam({
|
||||||
name: "claimRequestId",
|
// name: "claimRequestId",
|
||||||
description: "ID of the claim file (created from the expert-initiated blame file)",
|
// description: "ID of the claim file (created from the expert-initiated blame file)",
|
||||||
})
|
// })
|
||||||
@ApiBody({ type: ExpertCompleteClaimDataDto })
|
// @ApiBody({ type: ExpertCompleteClaimDataDto })
|
||||||
@ApiResponse({ status: 200, description: "Claim data updated successfully" })
|
// @ApiResponse({ status: 200, description: "Claim data updated successfully" })
|
||||||
async completeClaimData(
|
async completeClaimData(
|
||||||
@CurrentUser() expert: any,
|
@CurrentUser() expert: any,
|
||||||
@Param("claimRequestId") claimRequestId: string,
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
@@ -141,24 +135,11 @@ export class ExpertInitiatedController {
|
|||||||
// );
|
// );
|
||||||
// }
|
// }
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Post("complete-form-third-party/:requestId")
|
@Post("complete-form-third-party/:requestId")
|
||||||
@ApiOperation({
|
// @ApiParam({ name: "requestId" })
|
||||||
summary: "Expert completes all information for IN_PERSON THIRD_PARTY file",
|
// @ApiBody({ type: ExpertCompleteThirdPartyFormDto })
|
||||||
description:
|
// @ApiResponse({ status: 200, description: "Form completed successfully. File is now ready for user signatures." })
|
||||||
"Expert fills all information at once for IN_PERSON THIRD_PARTY files. " +
|
|
||||||
"Includes first party, second party, and guilty party selection. " +
|
|
||||||
"Video and voice uploads are handled separately.",
|
|
||||||
})
|
|
||||||
@ApiParam({
|
|
||||||
name: "requestId",
|
|
||||||
description: "ID of the expert-initiated IN_PERSON THIRD_PARTY file",
|
|
||||||
})
|
|
||||||
@ApiBody({ type: ExpertCompleteThirdPartyFormDto })
|
|
||||||
@ApiResponse({
|
|
||||||
status: 200,
|
|
||||||
description: "Form completed successfully. File is now ready for user signatures.",
|
|
||||||
})
|
|
||||||
async completeThirdPartyForm(
|
async completeThirdPartyForm(
|
||||||
@CurrentUser() expert: any,
|
@CurrentUser() expert: any,
|
||||||
@Param("requestId") requestId: string,
|
@Param("requestId") requestId: string,
|
||||||
@@ -171,24 +152,15 @@ export class ExpertInitiatedController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Post("complete-form-car-body/:requestId")
|
@Post("complete-form-car-body/:requestId")
|
||||||
@ApiOperation({
|
// @ApiOperation({
|
||||||
summary: "Expert completes all information for IN_PERSON CAR_BODY file",
|
// summary: "Expert completes all information for IN_PERSON CAR_BODY file",
|
||||||
description:
|
// description:
|
||||||
"Expert fills all information at once for IN_PERSON CAR_BODY files. " +
|
// "Expert fills all information at once for IN_PERSON CAR_BODY files. " +
|
||||||
"Includes first party data and carBodyForm. " +
|
// "Includes first party data and carBodyForm. " +
|
||||||
"Video and voice uploads are handled separately.",
|
// "Video and voice uploads are handled separately.",
|
||||||
})
|
// })
|
||||||
@ApiParam({
|
|
||||||
name: "requestId",
|
|
||||||
description: "ID of the expert-initiated IN_PERSON CAR_BODY file",
|
|
||||||
})
|
|
||||||
@ApiBody({ type: ExpertCompleteCarBodyFormDto })
|
|
||||||
@ApiResponse({
|
|
||||||
status: 200,
|
|
||||||
description: "Form completed successfully. File is now ready for user signature.",
|
|
||||||
})
|
|
||||||
async completeCarBodyForm(
|
async completeCarBodyForm(
|
||||||
@CurrentUser() expert: any,
|
@CurrentUser() expert: any,
|
||||||
@Param("requestId") requestId: string,
|
@Param("requestId") requestId: string,
|
||||||
@@ -201,31 +173,16 @@ export class ExpertInitiatedController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Post("upload-video/:requestId")
|
@Post("upload-video/:requestId")
|
||||||
@ApiOperation({
|
// @ApiParam({ name: "requestId" })
|
||||||
summary: "Expert uploads video for expert-initiated file",
|
// @ApiConsumes("multipart/form-data")
|
||||||
description:
|
// @ApiBody({
|
||||||
"Expert uploads a single video file for the entire blame file. " +
|
// schema: {
|
||||||
"Only one video is needed per file (not per party). " +
|
// type: "object",
|
||||||
"Only works for expert-initiated files.",
|
// properties: { file: { type: "string", format: "binary" } },
|
||||||
})
|
// },
|
||||||
@ApiParam({
|
// })
|
||||||
name: "requestId",
|
|
||||||
description: "ID of the expert-initiated file",
|
|
||||||
})
|
|
||||||
@ApiConsumes("multipart/form-data")
|
|
||||||
@ApiBody({
|
|
||||||
schema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
file: {
|
|
||||||
type: "string",
|
|
||||||
format: "binary",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
FileInterceptor("file", {
|
FileInterceptor("file", {
|
||||||
limits: {
|
limits: {
|
||||||
@@ -242,10 +199,10 @@ export class ExpertInitiatedController {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ApiResponse({
|
// @ApiResponse({
|
||||||
status: 200,
|
// status: 200,
|
||||||
description: "Video uploaded successfully",
|
// description: "Video uploaded successfully",
|
||||||
})
|
// })
|
||||||
async uploadVideo(
|
async uploadVideo(
|
||||||
@CurrentUser() expert: any,
|
@CurrentUser() expert: any,
|
||||||
@Param("requestId") requestId: string,
|
@Param("requestId") requestId: string,
|
||||||
@@ -258,31 +215,28 @@ export class ExpertInitiatedController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Post("upload-voice/:requestId")
|
@Post("upload-voice/:requestId")
|
||||||
@ApiOperation({
|
// @ApiOperation({
|
||||||
summary: "Expert uploads voice for expert-initiated file",
|
// summary: "Expert uploads voice for expert-initiated file",
|
||||||
description:
|
// description:
|
||||||
"Expert uploads a single voice file for the entire blame file. " +
|
// "Expert uploads a single voice file for the entire blame file. " +
|
||||||
"Only one voice is needed per file (not per party). " +
|
// "Only one voice is needed per file (not per party). " +
|
||||||
"Only works for expert-initiated files.",
|
// "Only works for expert-initiated files.",
|
||||||
})
|
// })
|
||||||
@ApiParam({
|
// @ApiParam({ name: "requestId" })
|
||||||
name: "requestId",
|
// @ApiConsumes("multipart/form-data")
|
||||||
description: "ID of the expert-initiated file",
|
// @ApiBody({
|
||||||
})
|
// schema: {
|
||||||
@ApiConsumes("multipart/form-data")
|
// type: "object",
|
||||||
@ApiBody({
|
// properties: {
|
||||||
schema: {
|
// voice: {
|
||||||
type: "object",
|
// type: "string",
|
||||||
properties: {
|
// format: "binary",
|
||||||
voice: {
|
// },
|
||||||
type: "string",
|
// },
|
||||||
format: "binary",
|
// },
|
||||||
},
|
// })
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
FileInterceptor("voice", {
|
FileInterceptor("voice", {
|
||||||
limits: {
|
limits: {
|
||||||
@@ -300,10 +254,7 @@ export class ExpertInitiatedController {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ApiResponse({
|
// @ApiResponse({ status: 200, description: "Voice uploaded successfully" })
|
||||||
status: 200,
|
|
||||||
description: "Voice uploaded successfully",
|
|
||||||
})
|
|
||||||
async uploadVoice(
|
async uploadVoice(
|
||||||
@CurrentUser() expert: any,
|
@CurrentUser() expert: any,
|
||||||
@Param("requestId") requestId: string,
|
@Param("requestId") requestId: string,
|
||||||
@@ -316,24 +267,18 @@ export class ExpertInitiatedController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ deprecated: true })
|
// @ApiOperation({ deprecated: true })
|
||||||
@Post("add-accident-fields/:requestId")
|
@Post("add-accident-fields/:requestId")
|
||||||
@ApiOperation({
|
// @ApiOperation({
|
||||||
summary: "Expert adds accident fields to expert-initiated file",
|
// summary: "Expert adds accident fields to expert-initiated file",
|
||||||
description:
|
// description:
|
||||||
"Expert adds accident way, reason, and type fields to an expert-initiated file. " +
|
// "Expert adds accident way, reason, and type fields to an expert-initiated file. " +
|
||||||
"Can be used for both IN_PERSON and LINK-based files. " +
|
// "Can be used for both IN_PERSON and LINK-based files. " +
|
||||||
"If expertSubmitReply already exists, updates the fields. Otherwise, creates a new expertSubmitReply.",
|
// "If expertSubmitReply already exists, updates the fields. Otherwise, creates a new expertSubmitReply.",
|
||||||
})
|
// })
|
||||||
@ApiParam({
|
// @ApiParam({ name: "requestId" })
|
||||||
name: "requestId",
|
// @ApiBody({ type: ExpertAccidentFieldsDto })
|
||||||
description: "ID of the expert-initiated file",
|
// @ApiResponse({ status: 200, description: "Accident fields added successfully" })
|
||||||
})
|
|
||||||
@ApiBody({ type: ExpertAccidentFieldsDto })
|
|
||||||
@ApiResponse({
|
|
||||||
status: 200,
|
|
||||||
description: "Accident fields added successfully",
|
|
||||||
})
|
|
||||||
async addAccidentFields(
|
async addAccidentFields(
|
||||||
@CurrentUser() expert: any,
|
@CurrentUser() expert: any,
|
||||||
@Param("requestId") requestId: string,
|
@Param("requestId") requestId: string,
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ import { PartyRole } from "./entities/schema/partyRole.enum";
|
|||||||
* Field experts create files via LINK (send link to user) or IN_PERSON (expert fills on-site).
|
* Field experts create files via LINK (send link to user) or IN_PERSON (expert fills on-site).
|
||||||
* Only the initiating field expert can see/review these files.
|
* Only the initiating field expert can see/review these files.
|
||||||
*/
|
*/
|
||||||
@ApiTags("expert-initiated-blame (v2)")
|
// @ApiTags("expert-initiated-blame (v2)")
|
||||||
@Controller("v2/expert-initiated-blame")
|
@Controller("v2/expert-initiated-blame")
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
||||||
@@ -59,23 +59,23 @@ export class ExpertInitiatedV2Controller {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get("my-files")
|
@Get("my-files")
|
||||||
@ApiOperation({
|
// @ApiOperation({
|
||||||
summary: "[V2] List my expert-initiated blame files",
|
// summary: "[V2] List my expert-initiated blame files",
|
||||||
description:
|
// description:
|
||||||
"Returns all BlameRequest (workflow) files created by the current field expert.",
|
// "Returns all BlameRequest (workflow) files created by the current field expert.",
|
||||||
})
|
// })
|
||||||
@ApiResponse({ status: 200, description: "List of expert-initiated files" })
|
// @ApiResponse({ status: 200, description: "List of expert-initiated files" })
|
||||||
async getMyFilesV2(@CurrentUser() expert: any) {
|
async getMyFilesV2(@CurrentUser() expert: any) {
|
||||||
return this.requestManagementService.getMyExpertInitiatedFilesV2(expert);
|
return this.requestManagementService.getMyExpertInitiatedFilesV2(expert);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("blame/:requestId")
|
@Get("blame/:requestId")
|
||||||
@ApiOperation({
|
// @ApiOperation({
|
||||||
summary: "[V2] Get one expert-initiated blame request",
|
// summary: "[V2] Get one expert-initiated blame request",
|
||||||
description: "Returns a single blame request. Only the initiating field expert can access.",
|
// description: "Returns a single blame request. Only the initiating field expert can access.",
|
||||||
})
|
// })
|
||||||
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
// @ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
@ApiResponse({ status: 200, description: "Blame request" })
|
// @ApiResponse({ status: 200, description: "Blame request" })
|
||||||
async getBlameRequestV2(
|
async getBlameRequestV2(
|
||||||
@Param("requestId") requestId: string,
|
@Param("requestId") requestId: string,
|
||||||
@CurrentUser() expert: any,
|
@CurrentUser() expert: any,
|
||||||
@@ -84,68 +84,45 @@ export class ExpertInitiatedV2Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post("create")
|
@Post("create")
|
||||||
@ApiOperation({
|
// @ApiOperation({
|
||||||
summary: "[V2] Create expert-initiated blame file",
|
// summary: "[V2] Create expert-initiated blame file",
|
||||||
description:
|
// description:
|
||||||
"Creates a BlameRequest with workflow. LINK: parties identified by phone, expert sends link. IN_PERSON: expert fills form later.",
|
// "Creates a BlameRequest with workflow. LINK: parties identified by phone, expert sends link. IN_PERSON: expert fills form later.",
|
||||||
})
|
// })
|
||||||
@ApiBody({
|
// @ApiBody({
|
||||||
type: CreateExpertInitiatedFileDto,
|
// type: CreateExpertInitiatedFileDto,
|
||||||
examples: {
|
// examples: {
|
||||||
thirdPartyInPerson: {
|
// thirdPartyInPerson: {
|
||||||
summary: "Third-party, IN_PERSON (both parties)",
|
// summary: "Third-party, IN_PERSON (both parties)",
|
||||||
description:
|
// description:
|
||||||
"Expert fills form on-site for both parties.",
|
// "Expert fills form on-site for both parties.",
|
||||||
value: {
|
// value: {
|
||||||
type: "THIRD_PARTY",
|
// type: "THIRD_PARTY",
|
||||||
creationMethod: "IN_PERSON",
|
// creationMethod: "LINK",
|
||||||
firstPartyPhoneNumber: "09123456789",
|
// firstPartyPhoneNumber: "09123456789",
|
||||||
secondPartyPhoneNumber: "09187654321",
|
// },
|
||||||
},
|
// },
|
||||||
},
|
// carBodyInPerson: {
|
||||||
thirdPartyLink: {
|
// summary: "Car-body, IN_PERSON",
|
||||||
summary: "Third-party, LINK",
|
// description: "Expert fills form on-site for single party (car body).",
|
||||||
description:
|
// value: {
|
||||||
"Expert sends link to first and second party by phone.",
|
// type: "CAR_BODY",
|
||||||
value: {
|
// creationMethod: "IN_PERSON",
|
||||||
type: "THIRD_PARTY",
|
// firstPartyPhoneNumber: "09123456789",
|
||||||
creationMethod: "LINK",
|
// },
|
||||||
firstPartyPhoneNumber: "09123456789",
|
// },
|
||||||
},
|
// carBodyLink: {
|
||||||
},
|
// summary: "Car-body, LINK",
|
||||||
carBodyInPerson: {
|
// description:
|
||||||
summary: "Car-body, IN_PERSON",
|
// "Expert sends link to party by phone. Only first party phone needed.",
|
||||||
description: "Expert fills form on-site for single party (car body).",
|
// value: {
|
||||||
value: {
|
// type: "CAR_BODY",
|
||||||
type: "CAR_BODY",
|
// creationMethod: "LINK",
|
||||||
creationMethod: "IN_PERSON",
|
// firstPartyPhoneNumber: "09123456789",
|
||||||
firstPartyPhoneNumber: "09123456789",
|
// },
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
carBodyLink: {
|
// })
|
||||||
summary: "Car-body, LINK",
|
|
||||||
description:
|
|
||||||
"Expert sends link to party by phone. Only first party phone needed.",
|
|
||||||
value: {
|
|
||||||
type: "CAR_BODY",
|
|
||||||
creationMethod: "LINK",
|
|
||||||
firstPartyPhoneNumber: "09123456789",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@ApiResponse({
|
|
||||||
status: 201,
|
|
||||||
description: "File created",
|
|
||||||
schema: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
requestId: { type: "string" },
|
|
||||||
publicId: { type: "string" },
|
|
||||||
linkUrl: { type: "string", description: "Present for LINK method" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
async createV2(
|
async createV2(
|
||||||
@CurrentUser() expert: any,
|
@CurrentUser() expert: any,
|
||||||
@Body() dto: CreateExpertInitiatedFileDto,
|
@Body() dto: CreateExpertInitiatedFileDto,
|
||||||
@@ -157,13 +134,13 @@ export class ExpertInitiatedV2Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post("send-link/:requestId")
|
@Post("send-link/:requestId")
|
||||||
@ApiOperation({
|
// @ApiOperation({
|
||||||
summary: "[V2] Send blame link to first party (LINK)",
|
// summary: "[V2] Send blame link to first party (LINK)",
|
||||||
description:
|
// description:
|
||||||
"For expert-initiated LINK files only. Provide the first party phone number; the service stores it, registers the user if needed, and sends the invite link via SMS.",
|
// "For expert-initiated LINK files only. Provide the first party phone number; the service stores it, registers the user if needed, and sends the invite link via SMS.",
|
||||||
})
|
// })
|
||||||
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
// @ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
@ApiBody({ type: SendExpertInitiatedLinkV2Dto })
|
// @ApiBody({ type: SendExpertInitiatedLinkV2Dto })
|
||||||
async sendLinkV2(
|
async sendLinkV2(
|
||||||
@CurrentUser() expert: any,
|
@CurrentUser() expert: any,
|
||||||
@Param("requestId") requestId: string,
|
@Param("requestId") requestId: string,
|
||||||
@@ -173,14 +150,14 @@ export class ExpertInitiatedV2Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post("send-party-otps/:requestId")
|
@Post("send-party-otps/:requestId")
|
||||||
@ApiOperation({
|
// @ApiOperation({
|
||||||
summary: "[V2] Send OTP to party/parties (IN_PERSON)",
|
// summary: "[V2] Send OTP to party/parties (IN_PERSON)",
|
||||||
description:
|
// description:
|
||||||
"Sends OTP via SMS to first party (and second party for THIRD_PARTY) using the same flow as /user/send-otp. Parties receive the code; collect it from them and call verify-party-otps. Call this before filling the blame form.",
|
// "Sends OTP via SMS to first party (and second party for THIRD_PARTY) using the same flow as /user/send-otp. Parties receive the code; collect it from them and call verify-party-otps. Call this before filling the blame form.",
|
||||||
})
|
// })
|
||||||
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
// @ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
@ApiBody({ type: SendPartyOtpsDto })
|
// @ApiBody({ type: SendPartyOtpsDto })
|
||||||
@ApiResponse({ status: 200, description: "OTP(s) sent; collect codes and call verify-party-otps" })
|
// @ApiResponse({ status: 200, description: "OTP(s) sent; collect codes and call verify-party-otps" })
|
||||||
async sendPartyOtpsV2(
|
async sendPartyOtpsV2(
|
||||||
@CurrentUser() expert: any,
|
@CurrentUser() expert: any,
|
||||||
@Param("requestId") requestId: string,
|
@Param("requestId") requestId: string,
|
||||||
@@ -190,14 +167,14 @@ export class ExpertInitiatedV2Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post("verify-party-otps/:requestId")
|
@Post("verify-party-otps/:requestId")
|
||||||
@ApiOperation({
|
// @ApiOperation({
|
||||||
summary: "[V2] Verify party OTPs (IN_PERSON)",
|
// summary: "[V2] Verify party OTPs (IN_PERSON)",
|
||||||
description:
|
// description:
|
||||||
"After send-party-otps, parties receive SMS. They tell you the code; submit it here. Required before complete-blame-data.",
|
// "After send-party-otps, parties receive SMS. They tell you the code; submit it here. Required before complete-blame-data.",
|
||||||
})
|
// })
|
||||||
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
// @ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
@ApiBody({ type: VerifyPartyOtpsDto })
|
// @ApiBody({ type: VerifyPartyOtpsDto })
|
||||||
@ApiResponse({ status: 200, description: "OTPs verified; expert can proceed to complete-blame-data" })
|
// @ApiResponse({ status: 200, description: "OTPs verified; expert can proceed to complete-blame-data" })
|
||||||
async verifyPartyOtpsV2(
|
async verifyPartyOtpsV2(
|
||||||
@CurrentUser() expert: any,
|
@CurrentUser() expert: any,
|
||||||
@Param("requestId") requestId: string,
|
@Param("requestId") requestId: string,
|
||||||
@@ -207,99 +184,99 @@ export class ExpertInitiatedV2Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post("complete-blame-data/:requestId")
|
@Post("complete-blame-data/:requestId")
|
||||||
@ApiOperation({
|
// @ApiOperation({
|
||||||
summary: "[V2] Submit all blame data (IN_PERSON)",
|
// summary: "[V2] Submit all blame data (IN_PERSON)",
|
||||||
description:
|
// description:
|
||||||
"For IN_PERSON files only. Send THIRD_PARTY or CAR_BODY form EXCEPT location. Use one shared expertDescription (not per-party descriptions). After this, call add-locations once to submit scene lat/lon and move workflow to WAITING_FOR_SIGNATURES.",
|
// "For IN_PERSON files only. Send THIRD_PARTY or CAR_BODY form EXCEPT location. Use one shared expertDescription (not per-party descriptions). After this, call add-locations once to submit scene lat/lon and move workflow to WAITING_FOR_SIGNATURES.",
|
||||||
})
|
// })
|
||||||
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
// @ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
@ApiBody({
|
// @ApiBody({
|
||||||
description: "Choose THIRD_PARTY or CAR_BODY example according to the file type. IN_PERSON v2 uses one shared expertDescription for the scene.",
|
// description: "Choose THIRD_PARTY or CAR_BODY example according to the file type. IN_PERSON v2 uses one shared expertDescription for the scene.",
|
||||||
examples: {
|
// examples: {
|
||||||
THIRD_PARTY: {
|
// THIRD_PARTY: {
|
||||||
summary: "THIRD_PARTY",
|
// summary: "THIRD_PARTY",
|
||||||
description: "Use when the blame file type is THIRD_PARTY",
|
// description: "Use when the blame file type is THIRD_PARTY",
|
||||||
value: {
|
// value: {
|
||||||
firstPartyPhoneNumber: "09123456789",
|
// firstPartyPhoneNumber: "09123456789",
|
||||||
firstPartyInitialForm: {
|
// firstPartyInitialForm: {
|
||||||
expertOpinion: false,
|
// expertOpinion: false,
|
||||||
imDamaged: false,
|
// imDamaged: false,
|
||||||
imGuilty: true,
|
// imGuilty: true,
|
||||||
},
|
// },
|
||||||
firstPartyPlate: {
|
// firstPartyPlate: {
|
||||||
nationalCodeOfInsurer: "",
|
// nationalCodeOfInsurer: "",
|
||||||
nationalCodeOfDriver: "",
|
// nationalCodeOfDriver: "",
|
||||||
insurerLicense: "",
|
// insurerLicense: "",
|
||||||
driverLicense: "",
|
// driverLicense: "",
|
||||||
plate: { leftDigits: 12, centerAlphabet: "الف", centerDigits: 345, ir: 22 },
|
// plate: { leftDigits: 12, centerAlphabet: "الف", centerDigits: 345, ir: 22 },
|
||||||
driverIsInsurer: true,
|
// driverIsInsurer: true,
|
||||||
isNewCar: false,
|
// isNewCar: false,
|
||||||
userNoCertificate: false,
|
// userNoCertificate: false,
|
||||||
insurerBirthday: 13770624,
|
// insurerBirthday: 13770624,
|
||||||
driverBirthday: "1370-01-01",
|
// driverBirthday: "1370-01-01",
|
||||||
},
|
// },
|
||||||
expertDescription: { desc: "توضیح حادثه کارشناس (مشترک برای صحنه)" },
|
// expertDescription: { desc: "توضیح حادثه کارشناس (مشترک برای صحنه)" },
|
||||||
secondParty: {
|
// secondParty: {
|
||||||
phoneNumber: "09187654321",
|
// phoneNumber: "09187654321",
|
||||||
initialForm: {
|
// initialForm: {
|
||||||
expertOpinion: false,
|
// expertOpinion: false,
|
||||||
imDamaged: true,
|
// imDamaged: true,
|
||||||
imGuilty: false,
|
// imGuilty: false,
|
||||||
},
|
// },
|
||||||
plate: {
|
// plate: {
|
||||||
nationalCodeOfInsurer: "",
|
// nationalCodeOfInsurer: "",
|
||||||
nationalCodeOfDriver: "",
|
// nationalCodeOfDriver: "",
|
||||||
insurerLicense: "",
|
// insurerLicense: "",
|
||||||
driverLicense: "",
|
// driverLicense: "",
|
||||||
plate: { leftDigits: 91, centerAlphabet: "ن", centerDigits: 174, ir: 79 },
|
// plate: { leftDigits: 91, centerAlphabet: "ن", centerDigits: 174, ir: 79 },
|
||||||
driverIsInsurer: true,
|
// driverIsInsurer: true,
|
||||||
isNewCar: false,
|
// isNewCar: false,
|
||||||
userNoCertificate: false,
|
// userNoCertificate: false,
|
||||||
insurerBirthday: 13700720,
|
// insurerBirthday: 13700720,
|
||||||
driverBirthday: "1370-01-01",
|
// driverBirthday: "1370-01-01",
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
guiltyPartyPhoneNumber: "09123456789",
|
// guiltyPartyPhoneNumber: "09123456789",
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
CAR_BODY: {
|
// CAR_BODY: {
|
||||||
summary: "CAR_BODY",
|
// summary: "CAR_BODY",
|
||||||
description: "Use when the blame file type is CAR_BODY",
|
// description: "Use when the blame file type is CAR_BODY",
|
||||||
value: {
|
// value: {
|
||||||
firstPartyPhoneNumber: "09123456789",
|
// firstPartyPhoneNumber: "09123456789",
|
||||||
firstPartyInitialForm: {
|
// firstPartyInitialForm: {
|
||||||
expertOpinion: false,
|
// expertOpinion: false,
|
||||||
imDamaged: false,
|
// imDamaged: false,
|
||||||
imGuilty: true,
|
// imGuilty: true,
|
||||||
},
|
// },
|
||||||
carBodyForm: { car: true, object: false },
|
// carBodyForm: { car: true, object: false },
|
||||||
firstPartyPlate: {
|
// firstPartyPlate: {
|
||||||
plateId: "",
|
// plateId: "",
|
||||||
nationalCodeOfInsurer: "",
|
// nationalCodeOfInsurer: "",
|
||||||
nationalCodeOfDriver: "",
|
// nationalCodeOfDriver: "",
|
||||||
insurerLicense: "",
|
// insurerLicense: "",
|
||||||
driverLicense: "",
|
// driverLicense: "",
|
||||||
plate: { leftDigits: 12, centerAlphabet: "الف", centerDigits: 345, ir: 22 },
|
// plate: { leftDigits: 12, centerAlphabet: "الف", centerDigits: 345, ir: 22 },
|
||||||
driverIsInsurer: true,
|
// driverIsInsurer: true,
|
||||||
isNewCar: false,
|
// isNewCar: false,
|
||||||
userNoCertificate: false,
|
// userNoCertificate: false,
|
||||||
insurerBirthday: 1370,
|
// insurerBirthday: 1370,
|
||||||
driverBirthday: "1370-01-01",
|
// driverBirthday: "1370-01-01",
|
||||||
},
|
// },
|
||||||
expertDescription: {
|
// expertDescription: {
|
||||||
desc: "توضیح حادثه",
|
// desc: "توضیح حادثه",
|
||||||
accidentDate: "2025-01-15",
|
// accidentDate: "2025-01-15",
|
||||||
accidentTime: "14:30",
|
// accidentTime: "14:30",
|
||||||
weatherCondition: "صاف",
|
// weatherCondition: "صاف",
|
||||||
roadCondition: "خشک",
|
// roadCondition: "خشک",
|
||||||
lightCondition: "روز",
|
// lightCondition: "روز",
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
schema: { type: "object" },
|
// schema: { type: "object" },
|
||||||
})
|
// })
|
||||||
@ApiResponse({ status: 200, description: "Blame form completed; next: add-locations" })
|
// @ApiResponse({ status: 200, description: "Blame form completed; next: add-locations" })
|
||||||
async completeBlameDataV2(
|
async completeBlameDataV2(
|
||||||
@CurrentUser() expert: any,
|
@CurrentUser() expert: any,
|
||||||
@Param("requestId") requestId: string,
|
@Param("requestId") requestId: string,
|
||||||
@@ -313,24 +290,24 @@ export class ExpertInitiatedV2Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post("add-locations/:requestId")
|
@Post("add-locations/:requestId")
|
||||||
@ApiOperation({
|
// @ApiOperation({
|
||||||
summary: "[V2] Submit one scene location for expert-initiated IN_PERSON blame",
|
// summary: "[V2] Submit one scene location for expert-initiated IN_PERSON blame",
|
||||||
description:
|
// description:
|
||||||
"Submit one scene location after complete-blame-data. This transitions workflow to WAITING_FOR_SIGNATURES.",
|
// "Submit one scene location after complete-blame-data. This transitions workflow to WAITING_FOR_SIGNATURES.",
|
||||||
})
|
// })
|
||||||
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
// @ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
@ApiBody({
|
// @ApiBody({
|
||||||
type: ExpertCompleteLocationV2Dto,
|
// type: ExpertCompleteLocationV2Dto,
|
||||||
examples: {
|
// examples: {
|
||||||
scene: {
|
// scene: {
|
||||||
summary: "One scene location (all IN_PERSON types)",
|
// summary: "One scene location (all IN_PERSON types)",
|
||||||
value: {
|
// value: {
|
||||||
location: { lat: 35.6892, lon: 51.389 },
|
// location: { lat: 35.6892, lon: 51.389 },
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
})
|
// })
|
||||||
@ApiResponse({ status: 200, description: "Locations saved; next: upload party signature(s)" })
|
// @ApiResponse({ status: 200, description: "Locations saved; next: upload party signature(s)" })
|
||||||
async addLocationsV2(
|
async addLocationsV2(
|
||||||
@CurrentUser() expert: any,
|
@CurrentUser() expert: any,
|
||||||
@Param("requestId") requestId: string,
|
@Param("requestId") requestId: string,
|
||||||
@@ -344,17 +321,17 @@ export class ExpertInitiatedV2Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post("upload-video/:requestId")
|
@Post("upload-video/:requestId")
|
||||||
@ApiOperation({
|
// @ApiOperation({
|
||||||
summary: "[V2] Expert uploads video for expert-initiated BlameRequest",
|
// summary: "[V2] Expert uploads video for expert-initiated BlameRequest",
|
||||||
})
|
// })
|
||||||
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
// @ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
@ApiConsumes("multipart/form-data")
|
// @ApiConsumes("multipart/form-data")
|
||||||
@ApiBody({
|
// @ApiBody({
|
||||||
schema: {
|
// schema: {
|
||||||
type: "object",
|
// type: "object",
|
||||||
properties: { file: { type: "string", format: "binary" } },
|
// properties: { file: { type: "string", format: "binary" } },
|
||||||
},
|
// },
|
||||||
})
|
// })
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
FileInterceptor("file", {
|
FileInterceptor("file", {
|
||||||
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
||||||
@@ -368,7 +345,7 @@ export class ExpertInitiatedV2Controller {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ApiResponse({ status: 200, description: "Video uploaded" })
|
// @ApiResponse({ status: 200, description: "Video uploaded" })
|
||||||
async uploadVideoV2(
|
async uploadVideoV2(
|
||||||
@CurrentUser() expert: any,
|
@CurrentUser() expert: any,
|
||||||
@Param("requestId") requestId: string,
|
@Param("requestId") requestId: string,
|
||||||
@@ -383,17 +360,17 @@ export class ExpertInitiatedV2Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post("upload-voice/:requestId")
|
@Post("upload-voice/:requestId")
|
||||||
@ApiOperation({
|
// @ApiOperation({
|
||||||
summary: "[V2] Expert uploads voice for expert-initiated BlameRequest",
|
// summary: "[V2] Expert uploads voice for expert-initiated BlameRequest",
|
||||||
})
|
// })
|
||||||
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
// @ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
@ApiConsumes("multipart/form-data")
|
// @ApiConsumes("multipart/form-data")
|
||||||
@ApiBody({
|
// @ApiBody({
|
||||||
schema: {
|
// schema: {
|
||||||
type: "object",
|
// type: "object",
|
||||||
properties: { voice: { type: "string", format: "binary" } },
|
// properties: { voice: { type: "string", format: "binary" } },
|
||||||
},
|
// },
|
||||||
})
|
// })
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
FileInterceptor("voice", {
|
FileInterceptor("voice", {
|
||||||
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
||||||
@@ -408,7 +385,7 @@ export class ExpertInitiatedV2Controller {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ApiResponse({ status: 200, description: "Voice uploaded" })
|
// @ApiResponse({ status: 200, description: "Voice uploaded" })
|
||||||
async uploadVoiceV2(
|
async uploadVoiceV2(
|
||||||
@CurrentUser() expert: any,
|
@CurrentUser() expert: any,
|
||||||
@Param("requestId") requestId: string,
|
@Param("requestId") requestId: string,
|
||||||
@@ -423,12 +400,12 @@ export class ExpertInitiatedV2Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post("add-accident-fields/:requestId")
|
@Post("add-accident-fields/:requestId")
|
||||||
@ApiOperation({
|
// @ApiOperation({
|
||||||
summary: "[V2] Expert adds accident fields to expert-initiated BlameRequest",
|
// summary: "[V2] Expert adds accident fields to expert-initiated BlameRequest",
|
||||||
})
|
// })
|
||||||
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
// @ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
@ApiBody({ type: ExpertAccidentFieldsDto })
|
// @ApiBody({ type: ExpertAccidentFieldsDto })
|
||||||
@ApiResponse({ status: 200, description: "Accident fields added" })
|
// @ApiResponse({ status: 200, description: "Accident fields added" })
|
||||||
async addAccidentFieldsV2(
|
async addAccidentFieldsV2(
|
||||||
@CurrentUser() expert: any,
|
@CurrentUser() expert: any,
|
||||||
@Param("requestId") requestId: string,
|
@Param("requestId") requestId: string,
|
||||||
@@ -442,24 +419,24 @@ export class ExpertInitiatedV2Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post("upload-party-signature/:requestId")
|
@Post("upload-party-signature/:requestId")
|
||||||
@ApiOperation({
|
// @ApiOperation({
|
||||||
summary: "[V2] Expert uploads party signature (IN_PERSON)",
|
// summary: "[V2] Expert uploads party signature (IN_PERSON)",
|
||||||
description:
|
// description:
|
||||||
"For IN_PERSON only. Upload a party's signature collected on-site. CAR_BODY: use partyRole FIRST once. THIRD_PARTY: upload FIRST then SECOND. When all required parties have signed, blame case completes.",
|
// "For IN_PERSON only. Upload a party's signature collected on-site. CAR_BODY: use partyRole FIRST once. THIRD_PARTY: upload FIRST then SECOND. When all required parties have signed, blame case completes.",
|
||||||
})
|
// })
|
||||||
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
// @ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||||
@ApiConsumes("multipart/form-data")
|
// @ApiConsumes("multipart/form-data")
|
||||||
@ApiBody({
|
// @ApiBody({
|
||||||
schema: {
|
// schema: {
|
||||||
type: "object",
|
// type: "object",
|
||||||
required: ["partyRole", "sign"],
|
// required: ["partyRole", "sign"],
|
||||||
properties: {
|
// properties: {
|
||||||
partyRole: { type: "string", enum: ["FIRST", "SECOND"] },
|
// partyRole: { type: "string", enum: ["FIRST", "SECOND"] },
|
||||||
isAccept: { type: "boolean", default: true },
|
// isAccept: { type: "boolean", default: true },
|
||||||
sign: { type: "string", format: "binary" },
|
// sign: { type: "string", format: "binary" },
|
||||||
},
|
// },
|
||||||
},
|
// },
|
||||||
})
|
// })
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
FileInterceptor("sign", {
|
FileInterceptor("sign", {
|
||||||
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
||||||
@@ -473,7 +450,7 @@ export class ExpertInitiatedV2Controller {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ApiResponse({ status: 200, description: "Signature recorded" })
|
// @ApiResponse({ status: 200, description: "Signature recorded" })
|
||||||
async uploadPartySignatureV2(
|
async uploadPartySignatureV2(
|
||||||
@CurrentUser() expert: any,
|
@CurrentUser() expert: any,
|
||||||
@Param("requestId") requestId: string,
|
@Param("requestId") requestId: string,
|
||||||
@@ -496,13 +473,13 @@ export class ExpertInitiatedV2Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post("create-claim-from-blame/:blameRequestId")
|
@Post("create-claim-from-blame/:blameRequestId")
|
||||||
@ApiOperation({
|
// @ApiOperation({
|
||||||
summary: "[V2] Create claim from expert-initiated IN_PERSON blame",
|
// summary: "[V2] Create claim from expert-initiated IN_PERSON blame",
|
||||||
description:
|
// description:
|
||||||
"Field expert creates a claim on behalf of the damaged party. Blame must be COMPLETED (signatures collected). Then use claim v2 endpoints to fill parts, documents, and captures.",
|
// "Field expert creates a claim on behalf of the damaged party. Blame must be COMPLETED (signatures collected). Then use claim v2 endpoints to fill parts, documents, and captures.",
|
||||||
})
|
// })
|
||||||
@ApiParam({ name: "blameRequestId", description: "Completed blame request ID" })
|
// @ApiParam({ name: "blameRequestId", description: "Completed blame request ID" })
|
||||||
@ApiResponse({ status: 201, description: "Claim created" })
|
// @ApiResponse({ status: 201, description: "Claim created" })
|
||||||
async createClaimFromBlame(
|
async createClaimFromBlame(
|
||||||
@CurrentUser() expert: any,
|
@CurrentUser() expert: any,
|
||||||
@Param("blameRequestId") blameRequestId: string,
|
@Param("blameRequestId") blameRequestId: string,
|
||||||
@@ -514,12 +491,12 @@ export class ExpertInitiatedV2Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post("complete-claim-data/:claimRequestId")
|
@Post("complete-claim-data/:claimRequestId")
|
||||||
@ApiOperation({
|
// @ApiOperation({
|
||||||
summary: "Submit claim-needed data (expert-initiated IN_PERSON)",
|
// summary: "Submit claim-needed data (expert-initiated IN_PERSON)",
|
||||||
})
|
// })
|
||||||
@ApiParam({ name: "claimRequestId", description: "Claim file ID" })
|
// @ApiParam({ name: "claimRequestId", description: "Claim file ID" })
|
||||||
@ApiBody({ type: ExpertCompleteClaimDataDto })
|
// @ApiBody({ type: ExpertCompleteClaimDataDto })
|
||||||
@ApiResponse({ status: 200, description: "Claim data updated" })
|
// @ApiResponse({ status: 200, description: "Claim data updated" })
|
||||||
async completeClaimData(
|
async completeClaimData(
|
||||||
@CurrentUser() expert: any,
|
@CurrentUser() expert: any,
|
||||||
@Param("claimRequestId") claimRequestId: string,
|
@Param("claimRequestId") claimRequestId: string,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { extname } from "node:path";
|
|||||||
import { Body, Controller, Get, Param, Post, UploadedFile, UseGuards, UseInterceptors } from "@nestjs/common";
|
import { Body, Controller, Get, Param, Post, UploadedFile, UseGuards, UseInterceptors } from "@nestjs/common";
|
||||||
import { FileInterceptor } from "@nestjs/platform-express";
|
import { FileInterceptor } from "@nestjs/platform-express";
|
||||||
import { diskStorage } from "multer";
|
import { diskStorage } from "multer";
|
||||||
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiOperation, ApiParam, ApiTags } from "@nestjs/swagger";
|
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiOperation, ApiParam, ApiTags, ApiExcludeController } from "@nestjs/swagger";
|
||||||
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
||||||
import { RolesGuard } from "src/auth/guards/role.guard";
|
import { RolesGuard } from "src/auth/guards/role.guard";
|
||||||
import { CurrentUser } from "src/decorators/user.decorator";
|
import { CurrentUser } from "src/decorators/user.decorator";
|
||||||
@@ -16,7 +16,8 @@ import { VerifyPartyOtpsDto } from "./dto/verify-party-otps.dto";
|
|||||||
import { ExpertAccidentFieldsDto } from "./dto/expert-accident-fields.dto";
|
import { ExpertAccidentFieldsDto } from "./dto/expert-accident-fields.dto";
|
||||||
import { PartyRole } from "./entities/schema/partyRole.enum";
|
import { PartyRole } from "./entities/schema/partyRole.enum";
|
||||||
|
|
||||||
@ApiTags("registrar-initiated-blame (v1)")
|
@ApiExcludeController()
|
||||||
|
// @ApiTags("registrar-initiated-blame (v1)")
|
||||||
@Controller("registrar-initiated-blame")
|
@Controller("registrar-initiated-blame")
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
@UseGuards(LocalActorAuthGuard, RolesGuard)
|
||||||
@@ -25,8 +26,8 @@ export class RegistrarInitiatedController {
|
|||||||
constructor(private readonly requestManagementService: RequestManagementService) {}
|
constructor(private readonly requestManagementService: RequestManagementService) {}
|
||||||
|
|
||||||
@Post("create")
|
@Post("create")
|
||||||
@ApiOperation({ summary: "Registrar creates IN_PERSON blame file" })
|
// @ApiOperation({ summary: "Registrar creates IN_PERSON blame file" })
|
||||||
@ApiBody({ type: CreateRegistrarInitiatedFileDto })
|
// @ApiBody({ type: CreateRegistrarInitiatedFileDto })
|
||||||
create(@CurrentUser() registrar: any, @Body() dto: CreateRegistrarInitiatedFileDto) {
|
create(@CurrentUser() registrar: any, @Body() dto: CreateRegistrarInitiatedFileDto) {
|
||||||
return this.requestManagementService.createRegistrarInitiatedBlame(registrar, dto);
|
return this.requestManagementService.createRegistrarInitiatedBlame(registrar, dto);
|
||||||
}
|
}
|
||||||
@@ -42,8 +43,8 @@ export class RegistrarInitiatedController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post("send-party-otps/:requestId")
|
@Post("send-party-otps/:requestId")
|
||||||
@ApiParam({ name: "requestId" })
|
// @ApiParam({ name: "requestId" })
|
||||||
@ApiBody({ type: SendPartyOtpsDto })
|
// @ApiBody({ type: SendPartyOtpsDto })
|
||||||
sendPartyOtps(
|
sendPartyOtps(
|
||||||
@CurrentUser() registrar: any,
|
@CurrentUser() registrar: any,
|
||||||
@Param("requestId") requestId: string,
|
@Param("requestId") requestId: string,
|
||||||
@@ -53,8 +54,8 @@ export class RegistrarInitiatedController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post("verify-party-otps/:requestId")
|
@Post("verify-party-otps/:requestId")
|
||||||
@ApiParam({ name: "requestId" })
|
// @ApiParam({ name: "requestId" })
|
||||||
@ApiBody({ type: VerifyPartyOtpsDto })
|
// @ApiBody({ type: VerifyPartyOtpsDto })
|
||||||
verifyPartyOtps(
|
verifyPartyOtps(
|
||||||
@CurrentUser() registrar: any,
|
@CurrentUser() registrar: any,
|
||||||
@Param("requestId") requestId: string,
|
@Param("requestId") requestId: string,
|
||||||
@@ -73,7 +74,7 @@ export class RegistrarInitiatedController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post("upload-video/:requestId")
|
@Post("upload-video/:requestId")
|
||||||
@ApiConsumes("multipart/form-data")
|
// @ApiConsumes("multipart/form-data")
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
FileInterceptor("file", {
|
FileInterceptor("file", {
|
||||||
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
||||||
@@ -92,7 +93,7 @@ export class RegistrarInitiatedController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post("upload-voice/:requestId")
|
@Post("upload-voice/:requestId")
|
||||||
@ApiConsumes("multipart/form-data")
|
// @ApiConsumes("multipart/form-data")
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
FileInterceptor("voice", {
|
FileInterceptor("voice", {
|
||||||
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
||||||
@@ -111,7 +112,7 @@ export class RegistrarInitiatedController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post("add-accident-fields/:requestId")
|
@Post("add-accident-fields/:requestId")
|
||||||
@ApiBody({ type: ExpertAccidentFieldsDto })
|
// @ApiBody({ type: ExpertAccidentFieldsDto })
|
||||||
addAccidentFields(
|
addAccidentFields(
|
||||||
@CurrentUser() registrar: any,
|
@CurrentUser() registrar: any,
|
||||||
@Param("requestId") requestId: string,
|
@Param("requestId") requestId: string,
|
||||||
@@ -125,7 +126,7 @@ export class RegistrarInitiatedController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post("upload-party-signature/:requestId")
|
@Post("upload-party-signature/:requestId")
|
||||||
@ApiConsumes("multipart/form-data")
|
// @ApiConsumes("multipart/form-data")
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
FileInterceptor("sign", {
|
FileInterceptor("sign", {
|
||||||
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
limits: { fileSize: DEFAULT_MEDIA_MAX_BYTES },
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
ApiParam,
|
ApiParam,
|
||||||
ApiTags,
|
ApiTags,
|
||||||
ApiOperation,
|
ApiOperation,
|
||||||
|
ApiExcludeController,
|
||||||
} from "@nestjs/swagger";
|
} from "@nestjs/swagger";
|
||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
import { diskStorage } from "multer";
|
import { diskStorage } from "multer";
|
||||||
@@ -43,8 +44,9 @@ import {
|
|||||||
} from "./dto/create-request-management.dto";
|
} from "./dto/create-request-management.dto";
|
||||||
import { RequestManagementService } from "./request-management.service";
|
import { RequestManagementService } from "./request-management.service";
|
||||||
|
|
||||||
|
@ApiExcludeController()
|
||||||
@Controller("blame-request-management")
|
@Controller("blame-request-management")
|
||||||
@ApiTags("blame-request-management")
|
// @ApiTags("blame-request-management")
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@UseGuards(GlobalGuard, RolesGuard)
|
@UseGuards(GlobalGuard, RolesGuard)
|
||||||
@Roles(RoleEnum.USER)
|
@Roles(RoleEnum.USER)
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import { ExpertInitiatedV2Controller } from "./expert-initiated.v2.controller";
|
|||||||
import { ExpertInitiatedBlameMirrorController } from "./expert-initiated-blame.mirror.controller";
|
import { ExpertInitiatedBlameMirrorController } from "./expert-initiated-blame.mirror.controller";
|
||||||
import { RegistrarInitiatedController } from "./registrar-initiated.controller";
|
import { RegistrarInitiatedController } from "./registrar-initiated.controller";
|
||||||
import { RegistrarBlameMirrorController } from "./registrar-blame.mirror.controller";
|
import { RegistrarBlameMirrorController } from "./registrar-blame.mirror.controller";
|
||||||
|
import { ExpertInitiatedBlameV3MirrorController } from "./expert-initiated-blame-v3.mirror.controller";
|
||||||
import { InquiryRefreshController } from "./inquiry-refresh.controller";
|
import { InquiryRefreshController } from "./inquiry-refresh.controller";
|
||||||
import { InquiryRefreshService } from "./inquiry-refresh.service";
|
import { InquiryRefreshService } from "./inquiry-refresh.service";
|
||||||
|
|
||||||
@@ -77,6 +78,7 @@ import { InquiryRefreshService } from "./inquiry-refresh.service";
|
|||||||
ExpertInitiatedController,
|
ExpertInitiatedController,
|
||||||
ExpertInitiatedV2Controller,
|
ExpertInitiatedV2Controller,
|
||||||
ExpertInitiatedBlameMirrorController,
|
ExpertInitiatedBlameMirrorController,
|
||||||
|
ExpertInitiatedBlameV3MirrorController,
|
||||||
RegistrarInitiatedController,
|
RegistrarInitiatedController,
|
||||||
RegistrarBlameMirrorController,
|
RegistrarBlameMirrorController,
|
||||||
InquiryRefreshController,
|
InquiryRefreshController,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user