forked from Yara724/api
Fixed registrar and field expert
This commit is contained in:
@@ -23,7 +23,6 @@ import { UsersModule } from "./users/users.module";
|
||||
import { applyIranFaTimestampPlugin } from "./helpers/mongoose-fa-timestamps.plugin";
|
||||
import { CronModule } from "./utils/cron/cron.module";
|
||||
import { WorkflowStepManagementModule } from "./workflow-step-management/workflow-step-management.module";
|
||||
import { ExpertInitiatedModule } from "./expert-initiated/expert-initiated.module";
|
||||
import { DatabaseModule } from "./core/database/database.module";
|
||||
import { AppConfigModule } from "./core/config/config.module";
|
||||
|
||||
@@ -52,7 +51,6 @@ import { AppConfigModule } from "./core/config/config.module";
|
||||
ExpertInsurerModule,
|
||||
LookupsModule,
|
||||
WorkflowStepManagementModule,
|
||||
// ExpertInitiatedModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [
|
||||
|
||||
@@ -150,7 +150,7 @@ export class ActorAuthController {
|
||||
@ApiOperation({
|
||||
summary: "Actor login (returns access + refresh tokens)",
|
||||
description:
|
||||
'Authenticate any non-end-user actor (insurer/company, blame expert, damage expert, registrar, field expert, admin). Submit `role` as an array — e.g. `["damage_expert"]` — together with the actor\'s email/`username` and password. On success the response contains the JWT pair and the resolved profile.',
|
||||
'Authenticate any non-end-user actor (insurer/company, blame expert, damage expert, registrar, field expert, admin). Submit `role` as an array — e.g. `["damage_expert"]` — together with password and one of `username` / `email` / `nationalCode`. On success the response contains the JWT pair and the resolved profile.',
|
||||
})
|
||||
@ApiBody({
|
||||
type: LoginActorDto,
|
||||
@@ -193,11 +193,12 @@ export class ActorAuthController {
|
||||
},
|
||||
field_expert: {
|
||||
summary: "Field expert panel",
|
||||
description: "Sample credentials for a field-expert account.",
|
||||
description:
|
||||
"Login with email+password or nationalCode+password for seeded Parsian field experts.",
|
||||
value: {
|
||||
role: "field_expert",
|
||||
username: "fieldexpert@gmail.com",
|
||||
password: "123321",
|
||||
nationalCode: "0051967839",
|
||||
password: "Parsian@724",
|
||||
captchaId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
||||
captcha: "a7bx2",
|
||||
},
|
||||
|
||||
@@ -63,7 +63,7 @@ export class ActorAuthService {
|
||||
res = await this.expertDbService.findOne({
|
||||
_id: new Types.ObjectId(userId),
|
||||
});
|
||||
else res = await this.expertDbService.findOne({ email: username });
|
||||
else res = await this.findActorByLoginIdentifier(this.expertDbService, username);
|
||||
break;
|
||||
case RoleEnum.DAMAGE_EXPERT:
|
||||
if (username == null && userId)
|
||||
@@ -71,14 +71,18 @@ export class ActorAuthService {
|
||||
_id: new Types.ObjectId(userId),
|
||||
});
|
||||
else
|
||||
res = await this.damageExpertDbService.findOne({ email: username });
|
||||
res = await this.findActorByLoginIdentifier(
|
||||
this.damageExpertDbService,
|
||||
username,
|
||||
);
|
||||
break;
|
||||
case RoleEnum.FIELD_EXPERT:
|
||||
if (username == null && userId)
|
||||
res = await this.fieldExpertDbService.findOne({
|
||||
_id: new Types.ObjectId(userId),
|
||||
});
|
||||
else res = await this.fieldExpertDbService.findOne({ email: username });
|
||||
else
|
||||
res = await this.fieldExpertDbService.findByLoginIdentifier(username);
|
||||
break;
|
||||
case RoleEnum.REGISTRAR:
|
||||
if (username == null && userId)
|
||||
@@ -111,13 +115,27 @@ export class ActorAuthService {
|
||||
}
|
||||
|
||||
parseActorLoginUsername(body: Record<string, unknown>): string {
|
||||
const username = body?.username ?? body?.email;
|
||||
const username = body?.username ?? body?.email ?? body?.nationalCode;
|
||||
if (typeof username !== "string" || !username.trim()) {
|
||||
throw new BadRequestException("username (email) is required");
|
||||
throw new BadRequestException(
|
||||
"username, email, or nationalCode is required",
|
||||
);
|
||||
}
|
||||
return username.trim();
|
||||
}
|
||||
|
||||
private async findActorByLoginIdentifier(
|
||||
dbService: { findOne: (filter: any) => Promise<any> },
|
||||
identifier: string,
|
||||
) {
|
||||
const id = identifier.trim();
|
||||
const or: Record<string, string>[] = [{ email: id }, { username: id }];
|
||||
if (/^\d{10}$/.test(id)) {
|
||||
or.push({ nationalCode: id });
|
||||
}
|
||||
return dbService.findOne({ $or: or });
|
||||
}
|
||||
|
||||
issueActorTokens(actor: {
|
||||
_id: Types.ObjectId;
|
||||
username?: string;
|
||||
@@ -129,12 +147,13 @@ export class ActorAuthService {
|
||||
clientKey?: Types.ObjectId | string | null;
|
||||
}) {
|
||||
const payload = {
|
||||
username: actor.username || actor.email,
|
||||
username:
|
||||
actor.username || actor.email || (actor as any).nationalCode || null,
|
||||
sub: actor._id,
|
||||
fullName: `${actor.firstName || ""} ${actor.lastName || ""}`.trim(),
|
||||
role: actor.role || "User",
|
||||
userType: actor.userType || "UserType",
|
||||
clientKey: actor.clientKey ?? null,
|
||||
clientKey: actor.clientKey ? String(actor.clientKey) : null,
|
||||
};
|
||||
|
||||
const access_token = this.jwtService.sign(payload, {
|
||||
|
||||
@@ -1,13 +1,34 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsString, MaxLength } from "class-validator";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsOptional, IsString, MaxLength } from "class-validator";
|
||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
|
||||
export class LoginActorDto {
|
||||
@ApiProperty({ example: RoleEnum, type: "array", description: "LOGIN_DTO" })
|
||||
role: RoleEnum[];
|
||||
|
||||
@ApiProperty({})
|
||||
username: string;
|
||||
@ApiPropertyOptional({
|
||||
description:
|
||||
"Actor email or username. For field experts you may also send nationalCode instead.",
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
username?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: "Alias for username when logging in with email.",
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
email?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
example: "4311402422",
|
||||
description:
|
||||
"10-digit national ID. Alternative login identifier for actors (especially field experts without email).",
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
nationalCode?: string;
|
||||
|
||||
@ApiProperty({})
|
||||
password: string;
|
||||
|
||||
@@ -4169,6 +4169,50 @@ export class ClaimRequestManagementService {
|
||||
};
|
||||
}
|
||||
|
||||
private async assertActorCanViewClaimV2(
|
||||
claim: any,
|
||||
currentUserId: string,
|
||||
actor?: { sub: string; role?: string },
|
||||
): Promise<void> {
|
||||
const ownerId = claim.owner?.userId?.toString();
|
||||
if (ownerId && ownerId === currentUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (actor?.role === RoleEnum.FIELD_EXPERT) {
|
||||
if (claim.initiatedByFieldExpertId?.toString() === currentUserId) {
|
||||
return;
|
||||
}
|
||||
if (claim.blameRequestId) {
|
||||
const blame = await this.blameRequestDbService.findById(
|
||||
claim.blameRequestId.toString(),
|
||||
);
|
||||
if (
|
||||
blame?.expertInitiated &&
|
||||
blame.initiatedByFieldExpertId &&
|
||||
String(blame.initiatedByFieldExpertId) === currentUserId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (actor?.role === RoleEnum.REGISTRAR && claim.blameRequestId) {
|
||||
const blame = await this.blameRequestDbService.findById(
|
||||
claim.blameRequestId.toString(),
|
||||
);
|
||||
if (
|
||||
blame?.registrarInitiated &&
|
||||
blame.initiatedByRegistrarId &&
|
||||
String(blame.initiatedByRegistrarId) === currentUserId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ForbiddenException("You do not have access to this claim");
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve effective user id for claim operations.
|
||||
* For FIELD_EXPERT acting on expert-initiated IN_PERSON claim, returns the claim owner's id; otherwise returns currentUserId.
|
||||
@@ -6519,7 +6563,6 @@ export class ClaimRequestManagementService {
|
||||
.find(
|
||||
{
|
||||
expertInitiated: true,
|
||||
creationMethod: "IN_PERSON",
|
||||
initiatedByFieldExpertId: new Types.ObjectId(currentUserId),
|
||||
},
|
||||
{ select: "_id", lean: true },
|
||||
@@ -6532,6 +6575,7 @@ export class ClaimRequestManagementService {
|
||||
...(expertBlameIds.length
|
||||
? [{ blameRequestId: { $in: expertBlameIds } }]
|
||||
: []),
|
||||
{ initiatedByFieldExpertId: new Types.ObjectId(currentUserId) },
|
||||
],
|
||||
},
|
||||
{ lean: true },
|
||||
@@ -6621,17 +6665,7 @@ export class ClaimRequestManagementService {
|
||||
if (!claim) {
|
||||
throw new NotFoundException("Claim request not found");
|
||||
}
|
||||
const effectiveUserId = await this.resolveClaimEffectiveUserId(
|
||||
claim,
|
||||
currentUserId,
|
||||
actor?.role,
|
||||
);
|
||||
if (
|
||||
!claim.owner?.userId ||
|
||||
claim.owner.userId.toString() !== effectiveUserId
|
||||
) {
|
||||
throw new ForbiddenException("You do not have access to this claim");
|
||||
}
|
||||
await this.assertActorCanViewClaimV2(claim, currentUserId, actor);
|
||||
|
||||
const hasCapture = (data: any, key: string) =>
|
||||
data && (data instanceof Map ? data.get(key) : data[key]);
|
||||
@@ -6745,7 +6779,9 @@ export class ClaimRequestManagementService {
|
||||
s ? s.replace(/^(.{4})(.*)(.{4})$/, "IR$1************$3") : undefined;
|
||||
const maskNationalCode = (s?: string) =>
|
||||
s ? s.replace(/^(.{2})(.*)(.{2})$/, "$1******$3") : undefined;
|
||||
const isExpertViewer = actor?.role === RoleEnum.FIELD_EXPERT;
|
||||
const isExpertViewer =
|
||||
actor?.role === RoleEnum.FIELD_EXPERT ||
|
||||
actor?.role === RoleEnum.REGISTRAR;
|
||||
const ownerData = claim.owner
|
||||
? {
|
||||
userId: claim.owner.userId?.toString(),
|
||||
|
||||
@@ -52,7 +52,7 @@ import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
|
||||
@Controller("v2/claim-request-management")
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(GlobalGuard, RolesGuard)
|
||||
@Roles(RoleEnum.USER, RoleEnum.FIELD_EXPERT)
|
||||
@Roles(RoleEnum.USER, RoleEnum.FIELD_EXPERT, RoleEnum.REGISTRAR)
|
||||
export class ClaimRequestManagementV2Controller {
|
||||
constructor(
|
||||
private readonly claimRequestManagementService: ClaimRequestManagementService,
|
||||
@@ -63,7 +63,7 @@ export class ClaimRequestManagementV2Controller {
|
||||
@ApiOperation({
|
||||
summary: "Get My Claims (V2)",
|
||||
description:
|
||||
"Claims for the current user (or field-expert in-person files). Optional query: `search`, `sortBy` (publicId | createdAt | requestNo | status), `sortOrder`, `page`, `limit`. Without `page`/`limit`, returns the full filtered list.",
|
||||
"Claims for the current user, or claims from blame files initiated by the current FIELD_EXPERT / REGISTRAR (LINK and IN_PERSON). Optional query: `search`, `sortBy`, `sortOrder`, `page`, `limit`.",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
@@ -97,7 +97,7 @@ export class ClaimRequestManagementV2Controller {
|
||||
@ApiOperation({
|
||||
summary: "Get Claim Details (V2)",
|
||||
description:
|
||||
"Returns the claim snapshot for **USER** (owner) or **FIELD_EXPERT** when permitted. Owners get `ownerGuidance`: `{ phaseKey, headline, nextActions[] (method + pathTemplate), objectionAllowed }` mapped from `status` / `claimStatus` / workflow so the client can show the correct screen without duplicating orchestration logic. FIELD_EXPERT does not receive `ownerGuidance`. Core payload includes documents, captures, evaluation replies, optional expert resend, and masked bank info.",
|
||||
"Returns the claim snapshot for **USER** (owner), **FIELD_EXPERT**, or **REGISTRAR** when permitted. Initiating experts/registrars see unmasked money fields; owners get `ownerGuidance`.",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
|
||||
@@ -30,7 +30,6 @@ 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 { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
||||
import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
|
||||
import { ClaimRequestManagementService } from "./claim-request-management.service";
|
||||
import {
|
||||
@@ -93,19 +92,6 @@ export class ExpertInitiatedClaimMirrorController {
|
||||
);
|
||||
}
|
||||
|
||||
@Get("requests")
|
||||
@ApiOperation({ summary: "[Expert mirror] List my (in-person) claims" })
|
||||
async getMyClaims(
|
||||
@CurrentUser() expert: any,
|
||||
@Query() query: ListQueryV2Dto,
|
||||
) {
|
||||
return this.claimRequestManagementService.getMyClaimsV2(
|
||||
expert.sub,
|
||||
expert,
|
||||
query,
|
||||
);
|
||||
}
|
||||
|
||||
@Get("outer-parts-catalog")
|
||||
@ApiOperation({
|
||||
summary: "Get outer parts catalog (V2)",
|
||||
@@ -158,24 +144,6 @@ export class ExpertInitiatedClaimMirrorController {
|
||||
);
|
||||
}
|
||||
|
||||
@Get("request/:claimRequestId")
|
||||
@ApiParam({
|
||||
name: "claimRequestId",
|
||||
description: "The claim case ID (MongoDB ObjectId)",
|
||||
example: "507f1f77bcf86cd799439011",
|
||||
})
|
||||
@ApiOperation({ summary: "[Expert mirror] Get claim details" })
|
||||
async getClaimDetails(
|
||||
@Param("claimRequestId") claimRequestId: string,
|
||||
@CurrentUser() expert: any,
|
||||
) {
|
||||
return this.claimRequestManagementService.getClaimDetailsV2(
|
||||
claimRequestId,
|
||||
expert.sub,
|
||||
expert,
|
||||
);
|
||||
}
|
||||
|
||||
@Patch("select-outer-parts/:claimRequestId")
|
||||
@ApiOperation({
|
||||
summary: "Select Damaged Outer Car Parts (V2 - Step 2)",
|
||||
|
||||
@@ -30,7 +30,6 @@ 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 { ListQueryV2Dto } from "src/common/dto/list-query-v2.dto";
|
||||
import { ClaimVehicleTypeV2 } from "src/static/outer-car-parts-catalog";
|
||||
import { ClaimRequestManagementService } from "./claim-request-management.service";
|
||||
import {
|
||||
@@ -93,19 +92,6 @@ export class RegistrarClaimMirrorController {
|
||||
);
|
||||
}
|
||||
|
||||
@Get("requests")
|
||||
@ApiOperation({ summary: "[Registrar mirror] List my (in-person) claims" })
|
||||
async getMyClaims(
|
||||
@CurrentUser() registrar: any,
|
||||
@Query() query: ListQueryV2Dto,
|
||||
) {
|
||||
return this.claimRequestManagementService.getMyClaimsV2(
|
||||
registrar.sub,
|
||||
registrar,
|
||||
query,
|
||||
);
|
||||
}
|
||||
|
||||
@Get("outer-parts-catalog")
|
||||
@ApiOperation({
|
||||
summary: "Get outer parts catalog (V2)",
|
||||
@@ -158,24 +144,6 @@ export class RegistrarClaimMirrorController {
|
||||
);
|
||||
}
|
||||
|
||||
@Get("request/:claimRequestId")
|
||||
@ApiParam({
|
||||
name: "claimRequestId",
|
||||
description: "The claim case ID (MongoDB ObjectId)",
|
||||
example: "507f1f77bcf86cd799439011",
|
||||
})
|
||||
@ApiOperation({ summary: "[Registrar mirror] Get claim details" })
|
||||
async getClaimDetails(
|
||||
@Param("claimRequestId") claimRequestId: string,
|
||||
@CurrentUser() registrar: any,
|
||||
) {
|
||||
return this.claimRequestManagementService.getClaimDetailsV2(
|
||||
claimRequestId,
|
||||
registrar.sub,
|
||||
registrar,
|
||||
);
|
||||
}
|
||||
|
||||
@Patch("select-outer-parts/:claimRequestId")
|
||||
@ApiOperation({
|
||||
summary: "Select Damaged Outer Car Parts (V2 - Step 2)",
|
||||
|
||||
@@ -68,6 +68,7 @@ import { snapshotFromFieldExpert } from "src/helpers/expert-profile-snapshot";
|
||||
import { ExpertModel } from "src/users/entities/schema/expert.schema";
|
||||
import { SmsOrchestrationService } from "src/sms-orchestration/sms-orchestration.service";
|
||||
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
|
||||
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||
import { ExpertFileActivityDbService } from "src/users/entities/db-service/expert-file-activity.db.service";
|
||||
import {
|
||||
ExpertFileActivityType,
|
||||
@@ -243,7 +244,9 @@ export class ExpertBlameService {
|
||||
* Portfolio = file-activity (checked/handled), lock, decision, or expert-initiated file.
|
||||
*/
|
||||
async getStatusReportBucketsV2(actor: any): Promise<Record<string, number>> {
|
||||
requireActorClientKey(actor);
|
||||
if (actor.role !== RoleEnum.FIELD_EXPERT) {
|
||||
requireActorClientKey(actor);
|
||||
}
|
||||
const expertId = String(actor.sub);
|
||||
const expertOid = new Types.ObjectId(expertId);
|
||||
|
||||
@@ -300,6 +303,9 @@ export class ExpertBlameService {
|
||||
query: ListQueryV2Dto = {},
|
||||
): Promise<AllRequestDtoRsV2> {
|
||||
try {
|
||||
if (actor.role === RoleEnum.FIELD_EXPERT) {
|
||||
return this.getFieldExpertBlameListV2(actor, query);
|
||||
}
|
||||
requireActorClientKey(actor);
|
||||
const expertId = actor.sub;
|
||||
|
||||
@@ -429,6 +435,90 @@ export class ExpertBlameService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Blame review inbox for FIELD_EXPERT — expert-initiated DISAGREEMENT files only.
|
||||
* IN_PERSON flows are AGREED and handled outside this panel; completed blames appear in expert-claim.
|
||||
*/
|
||||
private async getFieldExpertBlameListV2(
|
||||
actor: { sub: string },
|
||||
query: ListQueryV2Dto = {},
|
||||
): Promise<AllRequestDtoRsV2> {
|
||||
const expertId = actor.sub;
|
||||
const expertOid = new Types.ObjectId(expertId);
|
||||
|
||||
const visibleCases = (await this.blameRequestDbService.find(
|
||||
{
|
||||
blameStatus: BlameStatus.DISAGREEMENT,
|
||||
expertInitiated: true,
|
||||
initiatedByFieldExpertId: expertOid,
|
||||
},
|
||||
{ lean: true },
|
||||
)) as Record<string, unknown>[];
|
||||
|
||||
const staleIds = new Set<string>();
|
||||
for (const doc of visibleCases) {
|
||||
const w = doc.workflow as Record<string, unknown> | undefined;
|
||||
if (!w?.locked) continue;
|
||||
if (!this.isBlameV2WorkflowLockCurrentlyEnforced(doc as any)) {
|
||||
staleIds.add(String(doc._id));
|
||||
}
|
||||
}
|
||||
await Promise.all(
|
||||
[...staleIds].map((id) =>
|
||||
this.expireBlameCaseWorkflowLockV2IfStale(id),
|
||||
),
|
||||
);
|
||||
for (const doc of visibleCases) {
|
||||
if (!staleIds.has(String(doc._id))) continue;
|
||||
const w = doc.workflow as Record<string, unknown>;
|
||||
if (w) {
|
||||
w.locked = false;
|
||||
delete w.lockedAt;
|
||||
delete w.expiredAt;
|
||||
delete w.lockedBy;
|
||||
}
|
||||
}
|
||||
|
||||
const paged = applyListQueryV2(
|
||||
visibleCases,
|
||||
{
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
private mapBlameRequestToListItemV2(
|
||||
doc: Record<string, unknown>,
|
||||
): AllRequestDtoV2 {
|
||||
@@ -1344,7 +1434,9 @@ export class ExpertBlameService {
|
||||
try {
|
||||
await this.expireBlameCaseWorkflowLockV2IfStale(requestId);
|
||||
|
||||
requireActorClientKey(actor);
|
||||
if (actor.role !== RoleEnum.FIELD_EXPERT) {
|
||||
requireActorClientKey(actor);
|
||||
}
|
||||
const actorId = actor.sub;
|
||||
const request = await this.blameRequestDbService.findById(requestId);
|
||||
|
||||
@@ -1584,7 +1676,9 @@ export class ExpertBlameService {
|
||||
): Promise<{ requestId: string; status: string }> {
|
||||
try {
|
||||
await this.expireBlameCaseWorkflowLockV2IfStale(requestId);
|
||||
requireActorClientKey(actor);
|
||||
if (actor.role !== RoleEnum.FIELD_EXPERT) {
|
||||
requireActorClientKey(actor);
|
||||
}
|
||||
const actorId = actor.sub;
|
||||
const request = await this.blameRequestDbService.findById(requestId);
|
||||
|
||||
|
||||
@@ -40,9 +40,12 @@ export class ExpertBlameV2Controller {
|
||||
|
||||
@Get()
|
||||
@ApiOperation({
|
||||
summary: "List blame cases for field expert (V2)",
|
||||
summary: "List blame cases for expert review (V2)",
|
||||
description:
|
||||
"Optional query: `search`, `sortBy` (publicId | createdAt | requestNo | status), `sortOrder`, `page`, `limit`. Without `page`/`limit`, returns the full filtered list.",
|
||||
"Damage experts (`expert`): tenant-scoped **DISAGREEMENT** queue (available, locked, or decided by you). " +
|
||||
"Field experts (`field_expert`): only **DISAGREEMENT** files they initiated that need expert review (e.g. LINK disputes). " +
|
||||
"IN_PERSON expert-initiated blames are usually `AGREED` and are managed via expert-initiated / request-management APIs; after completion, use expert-claim. " +
|
||||
"Optional query: `search`, `sortBy` (publicId | createdAt | requestNo | status), `sortOrder`, `page`, `limit`.",
|
||||
})
|
||||
async findAll(
|
||||
@CurrentUser() actor: any,
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
@@ -90,14 +89,6 @@ export class ExpertInitiatedBlameMirrorController {
|
||||
);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({
|
||||
summary: "[Expert mirror] List my expert-initiated blame files",
|
||||
})
|
||||
async list(@CurrentUser() expert: any) {
|
||||
return this.requestManagementService.getMyExpertInitiatedFilesV2(expert);
|
||||
}
|
||||
|
||||
@Post("send-link/:requestId")
|
||||
@ApiParam({ name: "requestId" })
|
||||
@ApiBody({ type: SendExpertInitiatedLinkV2Dto })
|
||||
@@ -420,14 +411,4 @@ export class ExpertInitiatedBlameMirrorController {
|
||||
fields,
|
||||
);
|
||||
}
|
||||
|
||||
@Get(":requestId")
|
||||
@ApiParam({ name: "requestId" })
|
||||
@ApiOperation({ summary: "[Expert mirror] Get one blame request" })
|
||||
async getOne(
|
||||
@Param("requestId") requestId: string,
|
||||
@CurrentUser() expert: any,
|
||||
) {
|
||||
return this.requestManagementService.getBlameRequestV2(requestId, expert);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
@@ -86,14 +85,6 @@ export class RegistrarBlameMirrorController {
|
||||
);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({
|
||||
summary: "[Registrar mirror] List my registrar-initiated blame files",
|
||||
})
|
||||
async list(@CurrentUser() registrar: any) {
|
||||
return this.requestManagementService.getMyExpertInitiatedFilesV2(registrar);
|
||||
}
|
||||
|
||||
@Post("send-party-otp/:requestId")
|
||||
@ApiParam({ name: "requestId" })
|
||||
@ApiBody({ type: SendPartyOtpDto })
|
||||
@@ -402,17 +393,4 @@ export class RegistrarBlameMirrorController {
|
||||
sign,
|
||||
);
|
||||
}
|
||||
|
||||
@Get(":requestId")
|
||||
@ApiParam({ name: "requestId" })
|
||||
@ApiOperation({ summary: "[Registrar mirror] Get one blame request" })
|
||||
async getOne(
|
||||
@Param("requestId") requestId: string,
|
||||
@CurrentUser() registrar: any,
|
||||
) {
|
||||
return this.requestManagementService.getBlameRequestV2(
|
||||
requestId,
|
||||
registrar,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5022,21 +5022,46 @@ export class RequestManagementService {
|
||||
? { "parties.person.phoneNumber": user.username }
|
||||
: null;
|
||||
|
||||
const filters = [userIdFilter, phoneFilter].filter(Boolean);
|
||||
if (filters.length === 0) {
|
||||
const orConditions: Record<string, unknown>[] = [];
|
||||
if (userIdFilter) orConditions.push(userIdFilter);
|
||||
if (phoneFilter) orConditions.push(phoneFilter);
|
||||
|
||||
if (user?.role === RoleEnum.FIELD_EXPERT && user?.sub) {
|
||||
orConditions.push({
|
||||
expertInitiated: true,
|
||||
initiatedByFieldExpertId: new Types.ObjectId(user.sub),
|
||||
});
|
||||
} else if (user?.role === RoleEnum.REGISTRAR && user?.sub) {
|
||||
orConditions.push({
|
||||
registrarInitiated: true,
|
||||
initiatedByRegistrarId: new Types.ObjectId(user.sub),
|
||||
});
|
||||
}
|
||||
|
||||
if (orConditions.length === 0) {
|
||||
return { list: [], total: 0 };
|
||||
}
|
||||
|
||||
const requests = await this.blameRequestDbService.find(
|
||||
filters.length === 1 ? (filters[0] as any) : ({ $or: filters } as any),
|
||||
orConditions.length === 1
|
||||
? (orConditions[0] as any)
|
||||
: ({ $or: orConditions } as any),
|
||||
{
|
||||
select:
|
||||
"publicId requestNo type status blameStatus createdAt updatedAt parties",
|
||||
"publicId requestNo type status blameStatus createdAt updatedAt parties expertInitiated registrarInitiated initiatedByFieldExpertId initiatedByRegistrarId creationMethod",
|
||||
},
|
||||
);
|
||||
|
||||
const enriched = requests.map((req: any) => {
|
||||
const party = req.parties.find(
|
||||
const isInitiator =
|
||||
(user?.role === RoleEnum.FIELD_EXPERT &&
|
||||
req.expertInitiated &&
|
||||
String(req.initiatedByFieldExpertId) === String(user.sub)) ||
|
||||
(user?.role === RoleEnum.REGISTRAR &&
|
||||
req.registrarInitiated &&
|
||||
String(req.initiatedByRegistrarId) === String(user.sub));
|
||||
|
||||
const party = req.parties?.find(
|
||||
(p: any) =>
|
||||
(p?.person?.userId &&
|
||||
String(p.person.userId) === String(user.sub)) ||
|
||||
@@ -5045,11 +5070,12 @@ export class RequestManagementService {
|
||||
|
||||
const obj = req.toObject();
|
||||
|
||||
delete obj.parties; // remove parties completely
|
||||
delete obj.parties;
|
||||
|
||||
return {
|
||||
...obj,
|
||||
userSide: party?.role ?? null,
|
||||
initiatedByMe: isInitiator,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -77,11 +77,11 @@ export class RequestManagementV2Controller {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@Roles(RoleEnum.USER, RoleEnum.FIELD_EXPERT)
|
||||
@Roles(RoleEnum.USER, RoleEnum.FIELD_EXPERT, RoleEnum.REGISTRAR)
|
||||
@ApiOperation({
|
||||
summary: "List my blame requests (V2)",
|
||||
description:
|
||||
"All blame files for the current user. Optional query: `search`, `sortBy` (publicId | createdAt | requestNo | status), `sortOrder`, `page`, `limit`. Without `page`/`limit`, returns the full filtered list.",
|
||||
"Party-owned blame files, or files initiated by the current FIELD_EXPERT / REGISTRAR. Optional query: `search`, `sortBy` (publicId | createdAt | requestNo | status), `sortOrder`, `page`, `limit`.",
|
||||
})
|
||||
async getAllBlameRequestsV2(
|
||||
@CurrentUser() user: any,
|
||||
@@ -91,16 +91,16 @@ export class RequestManagementV2Controller {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get one blame request by id. Allowed for the request owner (party) or the initiating field expert.
|
||||
* Get one blame request by id. Allowed for a party, or the initiating field expert / registrar.
|
||||
*/
|
||||
@Get(":requestId")
|
||||
@ApiOperation({
|
||||
summary: "Get one blame request (v2)",
|
||||
description:
|
||||
"Returns a minimal, user-safe payload: requestNo, publicId, type, status, blameStatus, workflow, parties (PII stripped), expert (with **expertName**), carBodyInsuranceDetail, plus **claimCreation** ({ hasClaim, shouldGuideToCreateClaim }). History and other internal fields are omitted.",
|
||||
"Returns a minimal payload for parties or the initiating FIELD_EXPERT / REGISTRAR: requestNo, publicId, type, status, blameStatus, workflow, parties (PII stripped for parties; full for initiator), expert, carBodyInsuranceDetail, plus **claimCreation**.",
|
||||
})
|
||||
@ApiParam({ name: "requestId", description: "Blame request ID" })
|
||||
@Roles(RoleEnum.USER, RoleEnum.FIELD_EXPERT)
|
||||
@Roles(RoleEnum.USER, RoleEnum.FIELD_EXPERT, RoleEnum.REGISTRAR)
|
||||
async getBlameRequestV2(
|
||||
@Param("requestId") requestId: string,
|
||||
@CurrentUser() user: any,
|
||||
|
||||
@@ -22,6 +22,20 @@ export class FieldExpertDbService {
|
||||
return await this.fieldExpertModel.findOne(filter);
|
||||
}
|
||||
|
||||
async findByLoginIdentifier(
|
||||
identifier: string,
|
||||
): Promise<FieldExpertModel | null> {
|
||||
const id = identifier.trim();
|
||||
const or: FilterQuery<FieldExpertModel>[] = [
|
||||
{ email: id },
|
||||
{ username: id },
|
||||
];
|
||||
if (/^\d{10}$/.test(id)) {
|
||||
or.push({ nationalCode: id });
|
||||
}
|
||||
return await this.fieldExpertModel.findOne({ $or: or });
|
||||
}
|
||||
|
||||
async findById(userId: string): Promise<FieldExpertModel | null> {
|
||||
return await this.fieldExpertModel.findOne({
|
||||
_id: new Types.ObjectId(userId),
|
||||
|
||||
@@ -16,17 +16,26 @@ export class FieldExpertModel {
|
||||
@Prop({ required: true })
|
||||
lastName: string;
|
||||
|
||||
@Prop({ type: "string", unique: true })
|
||||
email: string;
|
||||
@Prop({ type: String, unique: true, sparse: true, required: false })
|
||||
email?: string;
|
||||
|
||||
@Prop({ type: "string" })
|
||||
username: string;
|
||||
@Prop({ type: String })
|
||||
username?: string;
|
||||
|
||||
@Prop({ type: String, index: true, sparse: true })
|
||||
nationalCode?: string;
|
||||
|
||||
@Prop({ type: Types.ObjectId, index: true })
|
||||
clientKey?: Types.ObjectId;
|
||||
|
||||
@Prop({ type: Types.ObjectId, index: true })
|
||||
branchId?: Types.ObjectId;
|
||||
|
||||
@Prop({ required: true })
|
||||
password: string;
|
||||
|
||||
@Prop({ required: true })
|
||||
mobile: string;
|
||||
@Prop({ required: false })
|
||||
mobile?: string;
|
||||
|
||||
@Prop({ required: false })
|
||||
phone?: string;
|
||||
@@ -41,4 +50,16 @@ export class FieldExpertModel {
|
||||
}
|
||||
|
||||
export const FieldExpertDbSchema =
|
||||
SchemaFactory.createForClass(FieldExpertModel);
|
||||
SchemaFactory.createForClass(FieldExpertModel);
|
||||
|
||||
FieldExpertDbSchema.index(
|
||||
{ clientKey: 1, nationalCode: 1 },
|
||||
{ unique: true, sparse: true },
|
||||
);
|
||||
|
||||
FieldExpertDbSchema.pre("save", function (next) {
|
||||
if (!this.username) {
|
||||
this.username = this.email || this.nationalCode;
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user