forked from Yara724/api
Merge branch 'main' of git.ittalie.com:s.yahyaee/yara724-api
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||
import { Schema as MongooseSchema, Types } from "mongoose";
|
||||
import {
|
||||
ExpertProfileSnapshot,
|
||||
ExpertProfileSnapshotSchema,
|
||||
} from "src/request-management/entities/schema/expert-profile-snapshot.schema";
|
||||
import { FactorStatus } from "src/Types&Enums/claim-request-management/factor-status.enum";
|
||||
import { UserReplyEnum } from "src/Types&Enums/claim-request-management/userReply.enum";
|
||||
|
||||
@@ -8,8 +12,12 @@ export class ClaimPartPricing {
|
||||
@Prop({ type: String })
|
||||
partId: string;
|
||||
|
||||
@Prop({ type: String })
|
||||
carPartDamage?: string;
|
||||
/**
|
||||
* Legacy string or structured `{ part, side }` from expert reply DTOs (V1/V2).
|
||||
* Must be Mixed — objects cannot cast to String.
|
||||
*/
|
||||
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||
carPartDamage?: string | { part?: string; side?: string };
|
||||
|
||||
@Prop({ type: String })
|
||||
typeOfDamage?: string;
|
||||
@@ -23,8 +31,9 @@ export class ClaimPartPricing {
|
||||
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||
totalPayment?: string | number;
|
||||
|
||||
/** V1/V2: string/number legacy, or `{ option, price?, branchId? }` after expert normalization. */
|
||||
@Prop({ type: MongooseSchema.Types.Mixed })
|
||||
daghi?: string | number;
|
||||
daghi?: string | number | Record<string, unknown>;
|
||||
|
||||
@Prop({ type: Boolean, default: false })
|
||||
factorNeeded?: boolean;
|
||||
@@ -83,6 +92,10 @@ export class ClaimExpertReply {
|
||||
|
||||
@Prop({ type: ClaimUserCommentSchema })
|
||||
userComment?: ClaimUserComment;
|
||||
|
||||
/** Damage expert profile at reply time (from `damage-expert` collection). */
|
||||
@Prop({ type: ExpertProfileSnapshotSchema })
|
||||
expertProfileSnapshot?: ExpertProfileSnapshot;
|
||||
}
|
||||
export const ClaimExpertReplySchema =
|
||||
SchemaFactory.createForClass(ClaimExpertReply);
|
||||
@@ -160,6 +173,10 @@ export class ClaimResendRequest {
|
||||
/** Set when the owner has satisfied every requested document/part (or acknowledged description-only resend). */
|
||||
@Prop({ type: Date })
|
||||
fulfilledAt?: Date;
|
||||
|
||||
/** Damage expert profile when resend was requested (`damage-expert` collection). */
|
||||
@Prop({ type: ExpertProfileSnapshotSchema })
|
||||
expertProfileSnapshot?: ExpertProfileSnapshot;
|
||||
}
|
||||
export const ClaimResendRequestSchema =
|
||||
SchemaFactory.createForClass(ClaimResendRequest);
|
||||
@@ -239,6 +256,14 @@ export class ClaimEvaluation {
|
||||
|
||||
@Prop({ type: ClaimPriceDropSchema })
|
||||
priceDrop?: ClaimPriceDrop;
|
||||
|
||||
/** Damage expert profile at last factor validation (`damage-expert` collection). */
|
||||
@Prop({ type: ExpertProfileSnapshotSchema })
|
||||
factorValidationExpertProfileSnapshot?: ExpertProfileSnapshot;
|
||||
|
||||
/** Damage expert profile when in-person visit was requested (`damage-expert` collection). */
|
||||
@Prop({ type: ExpertProfileSnapshotSchema })
|
||||
inPersonVisitExpertProfileSnapshot?: ExpertProfileSnapshot;
|
||||
}
|
||||
export const ClaimEvaluationSchema =
|
||||
SchemaFactory.createForClass(ClaimEvaluation);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||
import { Types } from "mongoose";
|
||||
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
|
||||
import {
|
||||
ExpertProfileSnapshot,
|
||||
ExpertProfileSnapshotSchema,
|
||||
} from "src/request-management/entities/schema/expert-profile-snapshot.schema";
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ClaimActorLock {
|
||||
@@ -12,6 +16,10 @@ export class ClaimActorLock {
|
||||
|
||||
@Prop({ type: String, default: "damage_expert" })
|
||||
actorRole: "damage_expert" | "expert" | "admin";
|
||||
|
||||
/** Damage expert profile when lock was taken (`damage-expert` collection). */
|
||||
@Prop({ type: ExpertProfileSnapshotSchema })
|
||||
expertProfileSnapshot?: ExpertProfileSnapshot;
|
||||
}
|
||||
export const ClaimActorLockSchema = SchemaFactory.createForClass(ClaimActorLock);
|
||||
|
||||
|
||||
@@ -122,6 +122,10 @@ export class SubmitReply {
|
||||
|
||||
@Prop()
|
||||
userComment?: UserComment;
|
||||
|
||||
/** Damage expert profile at reply time (`damage-expert` collection). */
|
||||
@Prop({ type: Object })
|
||||
expertProfileSnapshot?: Record<string, string | undefined>;
|
||||
}
|
||||
|
||||
export class ActorLockDetails {
|
||||
@@ -130,6 +134,10 @@ export class ActorLockDetails {
|
||||
|
||||
@Prop({ type: Types.ObjectId })
|
||||
actorId?: Types.ObjectId;
|
||||
|
||||
/** Damage expert profile when lock was taken (`damage-expert` collection). */
|
||||
@Prop({ type: Object })
|
||||
expertProfileSnapshot?: Record<string, string | undefined>;
|
||||
}
|
||||
|
||||
export class ResendCarPartsDto {
|
||||
@@ -149,6 +157,10 @@ export class ClaimSubmitResend {
|
||||
|
||||
@Prop({ required: true })
|
||||
resendCarParts: ResendCarPartsDto[];
|
||||
|
||||
/** Damage expert profile when resend was requested (`damage-expert` collection). */
|
||||
@Prop({ type: Object })
|
||||
expertProfileSnapshot?: Record<string, string | undefined>;
|
||||
}
|
||||
|
||||
export class UserResendDocuments {
|
||||
@@ -360,6 +372,14 @@ export class ClaimRequestManagementModel {
|
||||
@Prop({ type: UserClaimRating, required: false })
|
||||
userRating?: UserClaimRating;
|
||||
|
||||
/** Latest damage expert profile when validating repair factors (V1). */
|
||||
@Prop({ type: Object })
|
||||
factorValidationExpertProfileSnapshot?: Record<string, string | undefined>;
|
||||
|
||||
/** Damage expert profile when in-person visit was requested (V1). */
|
||||
@Prop({ type: Object })
|
||||
damageExpertInPersonVisitProfileSnapshot?: Record<string, string | undefined>;
|
||||
|
||||
@Prop({ type: String, required: false })
|
||||
visitLocation?: string;
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@ import {
|
||||
blameCaseAccessibleToExpert,
|
||||
requireActorClientKey,
|
||||
} from "src/helpers/tenant-scope";
|
||||
import {
|
||||
blameCaseStatusToReportBucket,
|
||||
initialBlameExpertReportBuckets,
|
||||
} from "src/helpers/expert-panel-status-report";
|
||||
import { RequestManagementDbService } from "src/request-management/entities/db-service/request-management.db.service";
|
||||
import { BlameRequestDbService } from "src/request-management/entities/db-service/blame-request.db.service";
|
||||
import {
|
||||
@@ -39,6 +43,8 @@ import { ExpertDbService } from "src/users/entities/db-service/expert.db.service
|
||||
import { BlameDocumentDbService } from "src/request-management/entities/db-service/blame-document.db.service";
|
||||
import { UserSignDbService } from "src/request-management/entities/db-service/sign.db.service";
|
||||
import { RequestManagementService } from "src/request-management/request-management.service";
|
||||
import { snapshotFromFieldExpert } from "src/helpers/expert-profile-snapshot";
|
||||
import { ExpertModel } from "src/users/entities/schema/expert.schema";
|
||||
|
||||
interface CheckedRequestEntry {
|
||||
CheckedRequest?: {
|
||||
@@ -81,6 +87,14 @@ export class ExpertBlameService {
|
||||
private readonly requestManagementService: RequestManagementService,
|
||||
) { }
|
||||
|
||||
/** Load immutable profile fields from `expert` for the acting field expert. */
|
||||
private async snapshotFieldExpert(actorId: string) {
|
||||
const expertDoc = await this.expertDbService.findOne({
|
||||
_id: new Types.ObjectId(actorId),
|
||||
});
|
||||
return snapshotFromFieldExpert(expertDoc as ExpertModel);
|
||||
}
|
||||
|
||||
async findAll(actor: any): Promise<AllRequestDtoRs> {
|
||||
// 1. Fetch all potentially relevant requests from the database.
|
||||
// Exclude CAR_BODY type requests as they are automatically handled and don't need expert review
|
||||
@@ -195,6 +209,38 @@ export class ExpertBlameService {
|
||||
* 2. Requests where current expert made the decision
|
||||
* Does NOT show requests decided by other experts.
|
||||
*/
|
||||
/**
|
||||
* V2: Count blame cases for this expert’s tenant, grouped for dashboard:
|
||||
* `IN_PROGRESS` = OPEN + WAITING_FOR_SECOND_PARTY; other {@link CaseStatus} keys unchanged.
|
||||
*/
|
||||
async getStatusReportBucketsV2(actor: any): Promise<Record<string, number>> {
|
||||
requireActorClientKey(actor);
|
||||
const rows = await this.blameRequestDbService.find({}, { lean: true });
|
||||
const buckets = initialBlameExpertReportBuckets();
|
||||
for (const doc of rows as Record<string, unknown>[]) {
|
||||
if (!this.blameDocIncludedInExpertTenantReport(doc, actor)) continue;
|
||||
const st = String(doc.status ?? "");
|
||||
const key = blameCaseStatusToReportBucket(st);
|
||||
buckets.all++;
|
||||
buckets[key] = (buckets[key] ?? 0) + 1;
|
||||
}
|
||||
return buckets;
|
||||
}
|
||||
|
||||
/** Tenant + expert-initiated visibility (same as list, without queue filters). */
|
||||
private blameDocIncludedInExpertTenantReport(
|
||||
doc: Record<string, unknown>,
|
||||
actor: { sub: string; clientKey?: string },
|
||||
): boolean {
|
||||
if (!blameCaseAccessibleToExpert(doc, actor)) {
|
||||
return false;
|
||||
}
|
||||
if (doc.expertInitiated && doc.initiatedByFieldExpertId) {
|
||||
return String(doc.initiatedByFieldExpertId) === String(actor.sub);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async findAllV2(actor: any): Promise<AllRequestDtoRsV2> {
|
||||
try {
|
||||
requireActorClientKey(actor);
|
||||
@@ -767,6 +813,7 @@ export class ExpertBlameService {
|
||||
}
|
||||
|
||||
const fifteenMinutes = new Date(Date.now() + 15 * 60 * 1000);
|
||||
const lockSnapshot = await this.snapshotFieldExpert(actorDetail.sub);
|
||||
|
||||
const updateResult = await this.requestManagementDbService.findOneAndUpdate(
|
||||
{
|
||||
@@ -782,6 +829,7 @@ export class ExpertBlameService {
|
||||
actorLocked: {
|
||||
fullName: actorDetail.fullName,
|
||||
actorId: new Types.ObjectId(actorDetail.sub),
|
||||
...(lockSnapshot && { expertProfileSnapshot: lockSnapshot }),
|
||||
},
|
||||
},
|
||||
$push: {
|
||||
@@ -872,6 +920,8 @@ export class ExpertBlameService {
|
||||
}
|
||||
}
|
||||
|
||||
const lockSnapshot = await this.snapshotFieldExpert(actorDetail.sub);
|
||||
|
||||
// Lock the request (either unlocked or expired lock)
|
||||
const updateResult = await this.blameRequestDbService.findByIdAndUpdate(
|
||||
requestId,
|
||||
@@ -883,6 +933,7 @@ export class ExpertBlameService {
|
||||
actorId: new Types.ObjectId(actorDetail.sub),
|
||||
actorName: actorDetail.fullName || "Unknown Expert",
|
||||
actorRole: "expert",
|
||||
...(lockSnapshot && { expertProfileSnapshot: lockSnapshot }),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1004,6 +1055,7 @@ export class ExpertBlameService {
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const resendSnapshot = await this.snapshotFieldExpert(actorId);
|
||||
|
||||
const partyResendRequests = resendDto.parties.map((party) => {
|
||||
const uid = String(party.partyId);
|
||||
@@ -1026,6 +1078,7 @@ export class ExpertBlameService {
|
||||
parties: partyResendRequests,
|
||||
requestedAt: now,
|
||||
requestedByExpertId: new Types.ObjectId(actorId),
|
||||
...(resendSnapshot && { expertProfileSnapshot: resendSnapshot }),
|
||||
},
|
||||
status: CaseStatus.WAITING_FOR_DOCUMENT_RESEND,
|
||||
},
|
||||
@@ -1151,12 +1204,15 @@ export class ExpertBlameService {
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const expertProfileSnapshot = await this.snapshotFieldExpert(actorId);
|
||||
|
||||
// Build expert decision with fields
|
||||
const decisionPayload: any = {
|
||||
guiltyPartyId: new Types.ObjectId(String(reply.guiltyUserId)),
|
||||
description: reply.description,
|
||||
decidedAt: now,
|
||||
decidedByExpertId: new Types.ObjectId(actorId),
|
||||
...(expertProfileSnapshot && { expertProfileSnapshot }),
|
||||
fields: {
|
||||
accidentWay: {
|
||||
id: reply.fields.accidentWay.id,
|
||||
@@ -1279,6 +1335,7 @@ export class ExpertBlameService {
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const inPersonSnapshot = await this.snapshotFieldExpert(actorId);
|
||||
|
||||
// Build expert decision with fields
|
||||
const decisionPayload: any = {
|
||||
@@ -1286,6 +1343,7 @@ export class ExpertBlameService {
|
||||
description: "حضوری مراجعه شود.",
|
||||
decidedAt: now,
|
||||
decidedByExpertId: new Types.ObjectId(actorId),
|
||||
...(inPersonSnapshot && { expertProfileSnapshot: inPersonSnapshot }),
|
||||
fields: {
|
||||
accidentWay: null,
|
||||
accidentReason: null,
|
||||
@@ -1445,6 +1503,8 @@ export class ExpertBlameService {
|
||||
);
|
||||
}
|
||||
|
||||
const expertProfileSnapshot = await this.snapshotFieldExpert(userId);
|
||||
|
||||
const newReplyObject = {
|
||||
description: reply.description,
|
||||
submitTime: new Date(),
|
||||
@@ -1466,6 +1526,7 @@ export class ExpertBlameService {
|
||||
},
|
||||
firstPartyComment: request.expertSubmitReply?.firstPartyComment || null,
|
||||
secondPartyComment: request.expertSubmitReply?.secondPartyComment || null,
|
||||
...(expertProfileSnapshot && { expertProfileSnapshot }),
|
||||
};
|
||||
|
||||
const updatePayload: any = {
|
||||
@@ -1522,6 +1583,7 @@ export class ExpertBlameService {
|
||||
}
|
||||
|
||||
const partyType = req.route.path.split("/")[4];
|
||||
const resendSnapshot = await this.snapshotFieldExpert(userId);
|
||||
|
||||
switch (partyType) {
|
||||
case "first": {
|
||||
@@ -1542,6 +1604,9 @@ export class ExpertBlameService {
|
||||
"expertResendReply.firstParty.firstPartyId": firstPartyId,
|
||||
"expertResendReply.firstParty.firstPartyDescription":
|
||||
firstPartyDescription,
|
||||
...(resendSnapshot && {
|
||||
expertResendByExpertProfileSnapshot: resendSnapshot,
|
||||
}),
|
||||
$push: {
|
||||
actorsChecker: {
|
||||
[ReqBlameStatus.UserPending]: request.actorLocked,
|
||||
@@ -1579,6 +1644,9 @@ export class ExpertBlameService {
|
||||
"expertResendReply.secondParty.secondPartyId": secondPartyId,
|
||||
"expertResendReply.secondParty.secondPartyDescription":
|
||||
secondPartyDescription,
|
||||
...(resendSnapshot && {
|
||||
expertResendByExpertProfileSnapshot: resendSnapshot,
|
||||
}),
|
||||
$push: {
|
||||
actorsChecker: {
|
||||
[ReqBlameStatus.UserPending]: request.actorLocked,
|
||||
@@ -1658,10 +1726,15 @@ export class ExpertBlameService {
|
||||
throw new NotFoundException("Blame not found");
|
||||
}
|
||||
|
||||
const visitSnapshot = await this.snapshotFieldExpert(actorDetail.sub);
|
||||
|
||||
const updated = await this.requestManagementDbService.findAndUpdate(
|
||||
{ _id: new Types.ObjectId(requestId) },
|
||||
{
|
||||
blameStatus: ReqBlameStatus.InPersonVisit,
|
||||
...(visitSnapshot && {
|
||||
fieldExpertInPersonVisitProfileSnapshot: visitSnapshot,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
Put,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import { ApiBearerAuth, ApiBody, ApiParam, ApiTags } from "@nestjs/swagger";
|
||||
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiTags } from "@nestjs/swagger";
|
||||
import { LocalActorAuthGuard } from "src/auth/guards/actor-local.guard";
|
||||
import { RolesGuard } from "src/auth/guards/role.guard";
|
||||
import { Roles } from "src/decorators/roles.decorator";
|
||||
@@ -38,6 +38,23 @@ export class ExpertBlameV2Controller {
|
||||
}
|
||||
}
|
||||
|
||||
@Get("report/status-counts")
|
||||
@ApiOperation({
|
||||
summary: "Count blame cases by grouped status bucket (tenant)",
|
||||
description:
|
||||
"IN_PROGRESS groups OPEN and WAITING_FOR_SECOND_PARTY. WAITING_FOR_EXPERT, WAITING_FOR_DOCUMENT_RESEND, WAITING_FOR_SIGNATURES, and terminal statuses are counted separately. Expert-initiated files count only for the initiating expert.",
|
||||
})
|
||||
async getStatusReportBucketsV2(@CurrentUser() actor: any) {
|
||||
try {
|
||||
return await this.expertBlameService.getStatusReportBucketsV2(actor);
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
throw new InternalServerErrorException(
|
||||
error instanceof Error ? error.message : "Failed to load blame status report",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Get(":id")
|
||||
@ApiParam({ name: "id", description: "Blame case request id" })
|
||||
async findOne(@Param("id") id: string, @CurrentUser() actor: any) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { Type } from 'class-transformer';
|
||||
import { ClaimRequiredDocumentType } from 'src/Types&Enums/claim-request-management/required-document-type.enum';
|
||||
import { DamagedPartItem } from 'src/claim-request-management/dto/capture-requirements-v2.dto';
|
||||
import { DaghiOption } from 'src/Types&Enums/claim-request-management/daghi-option.enum';
|
||||
|
||||
export class CarPartDamageV2Dto {
|
||||
@ApiProperty({ example: 'left' })
|
||||
@@ -22,6 +23,31 @@ export class CarPartDamageV2Dto {
|
||||
part: string;
|
||||
}
|
||||
|
||||
/** Same shape as V1 {@link PartsList} `daghi` (damage expert reply). */
|
||||
export class DaghiDetailsV2Dto {
|
||||
@ApiProperty({
|
||||
enum: DaghiOption,
|
||||
description:
|
||||
'Daghi option: ارزش لوازم بازیافتی, تحویل داغی, فاقد ارزش, با احتساب داغی',
|
||||
})
|
||||
@IsEnum(DaghiOption)
|
||||
option: DaghiOption;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: `Required when option is '${DaghiOption.RECYCLED_PARTS_VALUE}'`,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
price?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: `Required when option is '${DaghiOption.DELIVER_DAMAGED_PART}' (Mongo ObjectId string)`,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
branchId?: string;
|
||||
}
|
||||
|
||||
export class PartPricingV2Dto {
|
||||
@ApiProperty({ example: 'part-001' })
|
||||
@IsString()
|
||||
@@ -49,10 +75,10 @@ export class PartPricingV2Dto {
|
||||
@IsString()
|
||||
totalPayment: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '500000' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
daghi?: string;
|
||||
@ApiProperty({ type: DaghiDetailsV2Dto })
|
||||
@ValidateNested()
|
||||
@Type(() => DaghiDetailsV2Dto)
|
||||
daghi: DaghiDetailsV2Dto;
|
||||
|
||||
@ApiProperty({ example: false })
|
||||
@IsBoolean()
|
||||
|
||||
@@ -39,6 +39,10 @@ import {
|
||||
claimCaseTouchesClient,
|
||||
requireActorClientKey,
|
||||
} from "src/helpers/tenant-scope";
|
||||
import {
|
||||
claimCaseStatusToReportBucket,
|
||||
initialClaimExpertReportBuckets,
|
||||
} from "src/helpers/expert-panel-status-report";
|
||||
import { ClaimWorkflowStep } from "src/Types&Enums/claim-request-management/claim-workflow-steps.enum";
|
||||
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||
import { PartyRole } from "src/request-management/entities/schema/partyRole.enum";
|
||||
@@ -59,6 +63,8 @@ import {
|
||||
enrichBlamePartiesForAgreementView,
|
||||
} from "src/helpers/blame-party-agreement-decision";
|
||||
import { resendRequestHasPayload } from "src/helpers/claim-expert-resend";
|
||||
import { snapshotFromDamageExpert } from "src/helpers/expert-profile-snapshot";
|
||||
import { DamageExpertModel } from "src/users/entities/schema/damage-expert.schema";
|
||||
|
||||
@Injectable()
|
||||
export class ExpertClaimService {
|
||||
@@ -183,6 +189,12 @@ export class ExpertClaimService {
|
||||
private readonly branchDbService: BranchDbService,
|
||||
) { }
|
||||
|
||||
/** Load immutable profile fields from `damage-expert` for the acting expert. */
|
||||
private async snapshotDamageExpert(actorId: string) {
|
||||
const doc = await this.damageExpertDbService.findById(actorId);
|
||||
return snapshotFromDamageExpert(doc as DamageExpertModel);
|
||||
}
|
||||
|
||||
/** Map blame party `Vehicle` (name/model/type) to expert panel shape. */
|
||||
private expertVehicleFromPartyVehicle(v: any): {
|
||||
carName?: string;
|
||||
@@ -474,6 +486,7 @@ export class ExpertClaimService {
|
||||
}
|
||||
|
||||
const fifteenMinutes = new Date(Date.now() + 15 * 60 * 1000);
|
||||
const lockSnapshot = await this.snapshotDamageExpert(actorDetail.sub);
|
||||
|
||||
await this.claimRequestManagementDbService.findOneAndUpdate(
|
||||
new Types.ObjectId(requestId),
|
||||
@@ -486,6 +499,7 @@ export class ExpertClaimService {
|
||||
actorLocked: {
|
||||
actorId: new Types.ObjectId(actorDetail.sub),
|
||||
fullName: actorDetail.fullName,
|
||||
...(lockSnapshot && { expertProfileSnapshot: lockSnapshot }),
|
||||
},
|
||||
},
|
||||
$push: {
|
||||
@@ -535,6 +549,48 @@ export class ExpertClaimService {
|
||||
);
|
||||
}
|
||||
|
||||
/** Same rules as V1 `submitReplyRequest`: daghi option + conditional price / branchId; `branchId` stored as ObjectId. */
|
||||
private validateAndNormalizeDaghiForExpertReplyV2(
|
||||
parts: import("./dto/expert-claim-v2.dto").PartPricingV2Dto[],
|
||||
) {
|
||||
for (const part of parts) {
|
||||
if (!part.daghi || !part.daghi.option) {
|
||||
throw new BadRequestException(
|
||||
`Daghi option is required for part ${part.partId}`,
|
||||
);
|
||||
}
|
||||
if (part.daghi.option === DaghiOption.RECYCLED_PARTS_VALUE) {
|
||||
if (!part.daghi.price) {
|
||||
throw new BadRequestException(
|
||||
`Price is required for part ${part.partId} when option is '${DaghiOption.RECYCLED_PARTS_VALUE}'`,
|
||||
);
|
||||
}
|
||||
} else if (part.daghi.option === DaghiOption.DELIVER_DAMAGED_PART) {
|
||||
if (!part.daghi.branchId) {
|
||||
throw new BadRequestException(
|
||||
`Branch ID is required for part ${part.partId} when option is '${DaghiOption.DELIVER_DAMAGED_PART}'`,
|
||||
);
|
||||
}
|
||||
if (!Types.ObjectId.isValid(part.daghi.branchId)) {
|
||||
throw new BadRequestException(
|
||||
`Invalid branch ID format for part ${part.partId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parts.map((part) => ({
|
||||
...part,
|
||||
daghi: {
|
||||
option: part.daghi.option,
|
||||
...(part.daghi.price && { price: part.daghi.price }),
|
||||
...(part.daghi.branchId && {
|
||||
branchId: new Types.ObjectId(part.daghi.branchId),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
private findClosestCar(input: string, cars: { carName: string }[]) {
|
||||
if (!input || !cars || cars.length === 0) {
|
||||
this.logger.debug(
|
||||
@@ -1298,6 +1354,8 @@ export class ExpertClaimService {
|
||||
},
|
||||
}));
|
||||
|
||||
const expertProfileSnapshot = await this.snapshotDamageExpert(userId.sub);
|
||||
|
||||
const replyPayload = {
|
||||
description: reply.description,
|
||||
parts: processedParts,
|
||||
@@ -1306,6 +1364,7 @@ export class ExpertClaimService {
|
||||
actorName: userId.fullName,
|
||||
actorId: userId.sub,
|
||||
},
|
||||
...(expertProfileSnapshot && { expertProfileSnapshot }),
|
||||
};
|
||||
|
||||
const updatePayload = {
|
||||
@@ -1378,6 +1437,8 @@ export class ExpertClaimService {
|
||||
if (request.damageExpertReplyFinal)
|
||||
throw new ForbiddenException("request already has final reply");
|
||||
|
||||
const resendSnapshot = await this.snapshotDamageExpert(userId);
|
||||
|
||||
await this.claimRequestManagementDbService.findAndUpdate(requestId, {
|
||||
lockFile: false,
|
||||
claimStatus: ReqClaimStatus.WaitingForUserToResend,
|
||||
@@ -1385,6 +1446,7 @@ export class ExpertClaimService {
|
||||
resendDescription: body.resendDescription,
|
||||
resendDocuments: body.resendDocuments,
|
||||
resendCarParts: body.resendCarParts,
|
||||
...(resendSnapshot && { expertProfileSnapshot: resendSnapshot }),
|
||||
},
|
||||
$push: {
|
||||
actorsChecker: {
|
||||
@@ -1668,6 +1730,16 @@ export class ExpertClaimService {
|
||||
);
|
||||
}
|
||||
|
||||
const factorSnap = await this.snapshotDamageExpert(expertId);
|
||||
if (factorSnap && decisions.length > 0) {
|
||||
await this.claimRequestManagementDbService.findOneAndUpdate(
|
||||
{ _id: new Types.ObjectId(claimId) },
|
||||
{
|
||||
$set: { factorValidationExpertProfileSnapshot: factorSnap },
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const updatedClaim =
|
||||
await this.claimRequestManagementDbService.findOne(claimId);
|
||||
const updatedReply =
|
||||
@@ -1748,6 +1820,8 @@ export class ExpertClaimService {
|
||||
|
||||
assertClaimCaseForTenant(claim, actor);
|
||||
|
||||
const factorValidationSnapshot = await this.snapshotDamageExpert(actor.sub);
|
||||
|
||||
if (
|
||||
claim.status !== ClaimCaseStatus.WAITING_FOR_INSURER_APPROVAL ||
|
||||
claim.claimStatus !== ClaimStatus.UNDER_REVIEW ||
|
||||
@@ -1828,6 +1902,11 @@ export class ExpertClaimService {
|
||||
throw new BadRequestException("No valid factor decisions to apply.");
|
||||
}
|
||||
|
||||
if (factorValidationSnapshot) {
|
||||
$set["evaluation.factorValidationExpertProfileSnapshot"] =
|
||||
factorValidationSnapshot;
|
||||
}
|
||||
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
$set,
|
||||
});
|
||||
@@ -1941,10 +2020,15 @@ export class ExpertClaimService {
|
||||
throw new NotFoundException("Blame not found");
|
||||
}
|
||||
|
||||
const visitSnapshot = await this.snapshotDamageExpert(actorDetail.sub);
|
||||
|
||||
const updated = await this.claimRequestManagementDbService.findAndUpdate(
|
||||
requestId,
|
||||
{
|
||||
claimStatus: ReqClaimStatus.InPersonVisit,
|
||||
...(visitSnapshot && {
|
||||
damageExpertInPersonVisitProfileSnapshot: visitSnapshot,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -2013,6 +2097,8 @@ export class ExpertClaimService {
|
||||
return { claimRequestId, locked: true, message: 'Already locked by you' };
|
||||
}
|
||||
|
||||
const lockSnapshot = await this.snapshotDamageExpert(actor.sub);
|
||||
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
status: ClaimCaseStatus.EXPERT_REVIEWING,
|
||||
claimStatus: ClaimStatus.UNDER_REVIEW,
|
||||
@@ -2022,6 +2108,7 @@ export class ExpertClaimService {
|
||||
actorId: new Types.ObjectId(actor.sub),
|
||||
actorName: actor.fullName,
|
||||
actorRole: 'damage_expert',
|
||||
...(lockSnapshot && { expertProfileSnapshot: lockSnapshot }),
|
||||
},
|
||||
'workflow.currentStep': ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT,
|
||||
$push: {
|
||||
@@ -2095,6 +2182,8 @@ export class ExpertClaimService {
|
||||
);
|
||||
}
|
||||
|
||||
const resendSnapshot = await this.snapshotDamageExpert(actor.sub);
|
||||
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
$set: {
|
||||
status: ClaimCaseStatus.WAITING_FOR_USER_RESEND,
|
||||
@@ -2106,6 +2195,7 @@ export class ExpertClaimService {
|
||||
resendDescription: desc || undefined,
|
||||
resendDocuments: docs,
|
||||
resendCarParts: parts,
|
||||
...(resendSnapshot && { expertProfileSnapshot: resendSnapshot }),
|
||||
},
|
||||
},
|
||||
$unset: {
|
||||
@@ -2148,6 +2238,7 @@ export class ExpertClaimService {
|
||||
* - Must be locked by this expert (workflow.lockedBy.actorId === actor.sub)
|
||||
* - Must be in EXPERT_REVIEWING status
|
||||
* - Total payment across all parts must not exceed 30,000,000
|
||||
* - Each part must include `daghi` (option + conditional price/branchId) like V1
|
||||
*
|
||||
* On success:
|
||||
* - Stores reply in evaluation.damageExpertReply
|
||||
@@ -2201,6 +2292,11 @@ export class ExpertClaimService {
|
||||
});
|
||||
}
|
||||
|
||||
const processedParts =
|
||||
reply.parts?.length > 0
|
||||
? this.validateAndNormalizeDaghiForExpertReplyV2(reply.parts)
|
||||
: [];
|
||||
|
||||
const needsFactorUpload = reply.parts?.some((p) => p.factorNeeded === true) ?? false;
|
||||
|
||||
const objectionSubmitted = !!claim.evaluation?.objection?.submittedAt;
|
||||
@@ -2220,14 +2316,17 @@ export class ExpertClaimService {
|
||||
? ClaimWorkflowStep.EXPERT_FINAL_REPLY
|
||||
: ClaimWorkflowStep.EXPERT_DAMAGE_ASSESSMENT;
|
||||
|
||||
const expertProfileSnapshot = await this.snapshotDamageExpert(actor.sub);
|
||||
|
||||
const replyPayload = {
|
||||
description: reply.description,
|
||||
parts: reply.parts,
|
||||
parts: processedParts,
|
||||
submittedAt: new Date(),
|
||||
actorDetail: {
|
||||
actorId: actor.sub,
|
||||
actorName: actor.fullName,
|
||||
},
|
||||
...(expertProfileSnapshot && { expertProfileSnapshot }),
|
||||
};
|
||||
|
||||
const nextStep = needsFactorUpload
|
||||
@@ -2262,7 +2361,7 @@ export class ExpertClaimService {
|
||||
timestamp: new Date(),
|
||||
metadata: {
|
||||
factorNeeded: needsFactorUpload,
|
||||
partsCount: reply.parts?.length ?? 0,
|
||||
partsCount: processedParts.length,
|
||||
isFinalReplyAfterObjection,
|
||||
replyField,
|
||||
},
|
||||
@@ -2325,10 +2424,15 @@ export class ExpertClaimService {
|
||||
throw new ForbiddenException('This claim is locked by another expert');
|
||||
}
|
||||
|
||||
const visitSnapshot = await this.snapshotDamageExpert(actor.sub);
|
||||
|
||||
await this.claimCaseDbService.findByIdAndUpdate(claimRequestId, {
|
||||
claimStatus: ClaimStatus.NEEDS_REVISION,
|
||||
'workflow.locked': false,
|
||||
...(note ? { 'evaluation.visitLocation': note } : {}),
|
||||
...(visitSnapshot && {
|
||||
'evaluation.inPersonVisitExpertProfileSnapshot': visitSnapshot,
|
||||
}),
|
||||
$push: {
|
||||
history: {
|
||||
event: 'IN_PERSON_VISIT_REQUESTED',
|
||||
@@ -2348,6 +2452,25 @@ export class ExpertClaimService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Count claim cases for this damage expert’s tenant, grouped for dashboard:
|
||||
* `IN_PROGRESS` = user flow before submission complete (CREATED … CAPTURING_PART_DAMAGES);
|
||||
* other {@link ClaimCaseStatus} keys unchanged.
|
||||
*/
|
||||
async getStatusReportBucketsV2(actor: any): Promise<Record<string, number>> {
|
||||
const clientKey = requireActorClientKey(actor);
|
||||
const rows = await this.claimCaseDbService.find({}, { lean: true });
|
||||
const buckets = initialClaimExpertReportBuckets();
|
||||
for (const doc of rows as Record<string, unknown>[]) {
|
||||
if (!claimCaseTouchesClient(doc, clientKey)) continue;
|
||||
const st = String(doc.status ?? "");
|
||||
const key = claimCaseStatusToReportBucket(st);
|
||||
buckets.all++;
|
||||
buckets[key] = (buckets[key] ?? 0) + 1;
|
||||
}
|
||||
return buckets;
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Get claim list for damage expert
|
||||
*
|
||||
@@ -2815,6 +2938,8 @@ export class ExpertClaimService {
|
||||
throw new ForbiddenException("This claim is locked by another expert");
|
||||
}
|
||||
|
||||
const damagedPartsEditSnapshot = await this.snapshotDamageExpert(actor.sub);
|
||||
|
||||
const previous = Array.isArray((claim as any).damage?.selectedParts)
|
||||
? [...(claim as any).damage.selectedParts]
|
||||
: [];
|
||||
@@ -2848,6 +2973,9 @@ export class ExpertClaimService {
|
||||
metadata: {
|
||||
previousSelectedParts: previous,
|
||||
selectedParts,
|
||||
...(damagedPartsEditSnapshot && {
|
||||
expertProfileSnapshot: damagedPartsEditSnapshot,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -34,6 +34,16 @@ class InPersonVisitV2Dto {
|
||||
export class ExpertClaimV2Controller {
|
||||
constructor(private readonly expertClaimService: ExpertClaimService) { }
|
||||
|
||||
@Get("report/status-counts")
|
||||
@ApiOperation({
|
||||
summary: "Count claim cases by grouped status bucket (tenant)",
|
||||
description:
|
||||
"IN_PROGRESS groups user-phase statuses before submission is complete (CREATED through CAPTURING_PART_DAMAGES). Other keys match ClaimCaseStatus. Scoped to the insurer in the JWT.",
|
||||
})
|
||||
async getStatusReportBucketsV2(@CurrentUser() actor: any) {
|
||||
return await this.expertClaimService.getStatusReportBucketsV2(actor);
|
||||
}
|
||||
|
||||
@Get("requests")
|
||||
@ApiOperation({
|
||||
summary: "List available claim requests for damage expert",
|
||||
@@ -76,7 +86,7 @@ export class ExpertClaimV2Controller {
|
||||
@ApiOperation({
|
||||
summary: "Submit expert damage assessment reply",
|
||||
description:
|
||||
"Claim must be locked by this expert (status EXPERT_REVIEWING). Submitting unlocks the claim. If any part has factorNeeded=true the claim moves to EXPERT_COST_EVALUATION; otherwise moves to INSURER_REVIEW. Total payment cap is 30,000,000.",
|
||||
"Claim must be locked by this expert (status EXPERT_REVIEWING). Submitting unlocks the claim. Each part requires `daghi` with the same rules as V1 (DaghiOption, price for recycled value, branchId for deliver damaged part). If any part has factorNeeded=true the claim moves to EXPERT_COST_EVALUATION; otherwise moves to INSURER_REVIEW. Total payment cap is 30,000,000.",
|
||||
})
|
||||
@ApiParam({ name: "claimRequestId" })
|
||||
@ApiBody({ type: SubmitExpertReplyV2Dto })
|
||||
|
||||
49
src/helpers/expert-panel-status-report.ts
Normal file
49
src/helpers/expert-panel-status-report.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
|
||||
import { ClaimCaseStatus } from "src/Types&Enums/claim-request-management/claim-case-status.enum";
|
||||
|
||||
/** Blame: early workflow before expert queue / resend / signatures / terminals. */
|
||||
const BLAME_IN_PROGRESS_STATUSES = new Set<string>([
|
||||
CaseStatus.OPEN,
|
||||
CaseStatus.WAITING_FOR_SECOND_PARTY,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Claim: user flow before submission is complete (analog to `ClaimWorkflowStep.USER_SUBMISSION_COMPLETE`).
|
||||
*/
|
||||
const CLAIM_IN_PROGRESS_STATUSES = new Set<string>([
|
||||
ClaimCaseStatus.CREATED,
|
||||
ClaimCaseStatus.SELECTING_OUTER_PARTS,
|
||||
ClaimCaseStatus.SELECTING_OTHER_PARTS,
|
||||
ClaimCaseStatus.UPLOADING_REQUIRED_DOCUMENTS,
|
||||
ClaimCaseStatus.CAPTURING_PART_DAMAGES,
|
||||
]);
|
||||
|
||||
export function blameCaseStatusToReportBucket(status: string): string {
|
||||
if (BLAME_IN_PROGRESS_STATUSES.has(status)) return "IN_PROGRESS";
|
||||
return status;
|
||||
}
|
||||
|
||||
export function claimCaseStatusToReportBucket(status: string): string {
|
||||
if (CLAIM_IN_PROGRESS_STATUSES.has(status)) return "IN_PROGRESS";
|
||||
return status;
|
||||
}
|
||||
|
||||
/** Keys returned for blame V2 expert report (IN_PROGRESS + each native post-early status). */
|
||||
export function initialBlameExpertReportBuckets(): Record<string, number> {
|
||||
const out: Record<string, number> = { all: 0, IN_PROGRESS: 0 };
|
||||
for (const s of Object.values(CaseStatus)) {
|
||||
if (BLAME_IN_PROGRESS_STATUSES.has(s)) continue;
|
||||
out[s] = 0;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Keys returned for claim V2 expert report. */
|
||||
export function initialClaimExpertReportBuckets(): Record<string, number> {
|
||||
const out: Record<string, number> = { all: 0, IN_PROGRESS: 0 };
|
||||
for (const s of Object.values(ClaimCaseStatus)) {
|
||||
if (CLAIM_IN_PROGRESS_STATUSES.has(s)) continue;
|
||||
out[s] = 0;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
29
src/helpers/expert-profile-snapshot.ts
Normal file
29
src/helpers/expert-profile-snapshot.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { ExpertModel } from "src/users/entities/schema/expert.schema";
|
||||
import { DamageExpertModel } from "src/users/entities/schema/damage-expert.schema";
|
||||
import { ExpertProfileSnapshot } from "src/request-management/entities/schema/expert-profile-snapshot.schema";
|
||||
|
||||
export function snapshotFromFieldExpert(
|
||||
doc: ExpertModel | null | undefined,
|
||||
): ExpertProfileSnapshot | undefined {
|
||||
if (!doc) return undefined;
|
||||
return {
|
||||
firstName: doc.firstName ?? undefined,
|
||||
lastName: doc.lastName ?? undefined,
|
||||
email: doc.email ?? undefined,
|
||||
insuActivityCo: doc.insuActivityCo ?? undefined,
|
||||
mobile: (doc.mobile ?? doc.phone) ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function snapshotFromDamageExpert(
|
||||
doc: DamageExpertModel | null | undefined,
|
||||
): ExpertProfileSnapshot | undefined {
|
||||
if (!doc) return undefined;
|
||||
return {
|
||||
firstName: doc.firstName ?? undefined,
|
||||
lastName: doc.lastName ?? undefined,
|
||||
email: doc.email ?? undefined,
|
||||
insuActivityCo: doc.insuActivityCo ?? undefined,
|
||||
mobile: doc.mobile ?? undefined,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||
|
||||
/**
|
||||
* Immutable snapshot of the expert at reply time (field expert or damage expert).
|
||||
* Stored on blame `expert.decision` and claim `evaluation.damageExpertReply*`.
|
||||
*/
|
||||
@Schema({ _id: false })
|
||||
export class ExpertProfileSnapshot {
|
||||
@Prop()
|
||||
firstName?: string;
|
||||
|
||||
@Prop()
|
||||
lastName?: string;
|
||||
|
||||
@Prop()
|
||||
email?: string;
|
||||
|
||||
@Prop()
|
||||
insuActivityCo?: string;
|
||||
|
||||
@Prop()
|
||||
mobile?: string;
|
||||
}
|
||||
|
||||
export const ExpertProfileSnapshotSchema =
|
||||
SchemaFactory.createForClass(ExpertProfileSnapshot);
|
||||
@@ -1,6 +1,10 @@
|
||||
//! NEW
|
||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
ExpertProfileSnapshot,
|
||||
ExpertProfileSnapshotSchema,
|
||||
} from "./expert-profile-snapshot.schema";
|
||||
|
||||
|
||||
@Schema({ _id: false })
|
||||
@@ -52,6 +56,10 @@ export class ExpertDecision {
|
||||
accidentReason: { id: string; label: string; fanavaran: number };
|
||||
accidentType: { id: string; label: string };
|
||||
};
|
||||
|
||||
/** Field expert profile at decision time (from `expert` collection). */
|
||||
@Prop({ type: ExpertProfileSnapshotSchema })
|
||||
expertProfileSnapshot?: ExpertProfileSnapshot;
|
||||
}
|
||||
export const ExpertDecisionSchema = SchemaFactory.createForClass(ExpertDecision);
|
||||
|
||||
@@ -108,6 +116,10 @@ export class ExpertResend {
|
||||
@Prop({ type: Types.ObjectId })
|
||||
requestedByExpertId?: Types.ObjectId;
|
||||
|
||||
/** Field expert profile when resend was requested (`expert` collection). */
|
||||
@Prop({ type: ExpertProfileSnapshotSchema })
|
||||
expertProfileSnapshot?: ExpertProfileSnapshot;
|
||||
|
||||
/**
|
||||
* @deprecated Unused for resend UI; clients use per-party requestedItems + inputKind.
|
||||
*/
|
||||
|
||||
@@ -249,6 +249,10 @@ export class SubmitReply {
|
||||
|
||||
@Prop()
|
||||
systemResponse: string;
|
||||
|
||||
/** Field expert profile at reply time (`expert` collection). */
|
||||
@Prop({ type: Object })
|
||||
expertProfileSnapshot?: Record<string, string | undefined>;
|
||||
}
|
||||
|
||||
export class ExpertResendParties {
|
||||
@@ -271,6 +275,10 @@ export class ActorLockDetails {
|
||||
|
||||
@Prop({ type: Types.ObjectId })
|
||||
actorId?: Types.ObjectId;
|
||||
|
||||
/** Field expert profile when lock was taken (`expert` collection). */
|
||||
@Prop({ type: Object })
|
||||
expertProfileSnapshot?: Record<string, string | undefined>;
|
||||
}
|
||||
|
||||
export enum CreationMethod {
|
||||
@@ -450,6 +458,14 @@ export class RequestManagementModel {
|
||||
@Prop({ type: Boolean, default: false })
|
||||
isHandledStatsUpdated?: boolean;
|
||||
|
||||
/** Latest field expert profile when expert requested party resend (V1 `sendAgainRequest`). */
|
||||
@Prop({ type: Object })
|
||||
expertResendByExpertProfileSnapshot?: Record<string, string | undefined>;
|
||||
|
||||
/** Field expert profile when expert marked in-person visit (V1 `inPersonVisit`). */
|
||||
@Prop({ type: Object })
|
||||
fieldExpertInPersonVisitProfileSnapshot?: Record<string, string | undefined>;
|
||||
|
||||
// Expert-initiated file tracking
|
||||
@Prop({ type: Boolean, default: false })
|
||||
expertInitiated?: boolean;
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
|
||||
import { Types } from "mongoose";
|
||||
import { WorkflowStep } from "src/Types&Enums/blame-request-management/blameWorkflow-steps.enum";
|
||||
import {
|
||||
ExpertProfileSnapshot,
|
||||
ExpertProfileSnapshotSchema,
|
||||
} from "./expert-profile-snapshot.schema";
|
||||
|
||||
@Schema({ _id: false })
|
||||
export class ActorLock {
|
||||
@@ -13,6 +17,10 @@ export class ActorLock {
|
||||
|
||||
@Prop({ type: String, default: "expert" })
|
||||
actorRole: "expert" | "admin";
|
||||
|
||||
/** Field expert profile when lock was taken (`expert` collection). */
|
||||
@Prop({ type: ExpertProfileSnapshotSchema })
|
||||
expertProfileSnapshot?: ExpertProfileSnapshot;
|
||||
}
|
||||
export const ActorLockSchema = SchemaFactory.createForClass(ActorLock);
|
||||
|
||||
|
||||
@@ -1154,13 +1154,42 @@ export class RequestManagementService {
|
||||
(party.statement as any).lightCondition = body.lightCondition;
|
||||
}
|
||||
|
||||
// If second party finished description, we’re ready for expert flow.
|
||||
const isSecondThirdParty =
|
||||
stepKey === WorkflowStep.SECOND_DESCRIPTION &&
|
||||
req.type === BlameRequestType.THIRD_PARTY;
|
||||
let closedByMutualAgreement = false;
|
||||
|
||||
// THIRD_PARTY + AGREED: guilt/damage already implied by first-party confession — no field expert needed.
|
||||
if (isSecondThirdParty && req.blameStatus === BlameStatus.AGREED) {
|
||||
const built = buildMutualAgreementExpertDecision(
|
||||
(req as any).toObject
|
||||
? (req as any).toObject()
|
||||
: { ...req },
|
||||
);
|
||||
if (built) {
|
||||
if (!(req as any).expert) (req as any).expert = {};
|
||||
(req as any).expert.decision = built as any;
|
||||
const completed = Array.isArray(req.workflow.completedSteps)
|
||||
? req.workflow.completedSteps
|
||||
: [];
|
||||
if (!completed.includes(stepKey)) completed.push(stepKey);
|
||||
// Same as post–field-expert reply: guilt phase is satisfied without an expert; parties must still sign.
|
||||
if (!completed.includes(WorkflowStep.WAITING_FOR_GUILT_DECISION)) {
|
||||
completed.push(WorkflowStep.WAITING_FOR_GUILT_DECISION);
|
||||
}
|
||||
req.workflow.completedSteps = completed;
|
||||
req.workflow.currentStep = WorkflowStep.WAITING_FOR_SIGNATURES;
|
||||
req.workflow.nextStep = undefined;
|
||||
req.status = CaseStatus.WAITING_FOR_SIGNATURES;
|
||||
closedByMutualAgreement = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!closedByMutualAgreement) {
|
||||
if (stepKey === WorkflowStep.SECOND_DESCRIPTION) {
|
||||
req.status = CaseStatus.WAITING_FOR_EXPERT;
|
||||
}
|
||||
|
||||
await this.advanceWorkflowToNext(req, stepKey);
|
||||
|
||||
if (
|
||||
stepKey === WorkflowStep.SECOND_DESCRIPTION &&
|
||||
req.type === BlameRequestType.THIRD_PARTY &&
|
||||
@@ -1176,6 +1205,7 @@ export class RequestManagementService {
|
||||
(req as any).expert.decision = built as any;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(req.history)) req.history = [];
|
||||
req.history.push({
|
||||
@@ -1187,7 +1217,16 @@ export class RequestManagementService {
|
||||
actorName: user?.fullName,
|
||||
actorType: "user",
|
||||
},
|
||||
metadata: { role },
|
||||
metadata: {
|
||||
role,
|
||||
...(closedByMutualAgreement
|
||||
? {
|
||||
mutualAgreementWithoutExpert: true,
|
||||
closedWithoutExpert: true,
|
||||
awaitingSignatures: true,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
} as any);
|
||||
|
||||
await (req as any).save();
|
||||
|
||||
Reference in New Issue
Block a user