1
0
forked from Yara724/api

Fix v3 mirror flow

This commit is contained in:
SepehrYahyaee
2026-06-22 17:30:58 +03:30
parent 83e82ea68e
commit db569db9d1
15 changed files with 1004 additions and 248 deletions

View File

@@ -117,6 +117,8 @@ export class AllRequestDtoV2 {
lockTime: string | null;
type: string;
blameStatus: string;
/** Calculated blame + linked claim lifecycle status */
unifiedFileStatus?: string;
partiesInitialForms: { firstParty: string; secondParty: string };
partiesVehicles: { firstPartyVehicle: string; secondPartyVehicle: string };
}

View File

@@ -7,6 +7,7 @@ import { ExpertBlameV2Controller } from "./expert-blame.v2.controller";
import { ExpertBlameService } from "./expert-blame.service";
import { RequestManagementModule } from "src/request-management/request-management.module";
import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.module";
import { ClaimRequestManagementModule } from "src/claim-request-management/claim-request-management.module";
@Module({
imports: [
@@ -15,6 +16,7 @@ import { SmsOrchestrationModule } from "src/sms-orchestration/sms-orchestration.
RequestManagementModule,
PlatesModule,
SmsOrchestrationModule,
ClaimRequestManagementModule,
],
controllers: [ExpertBlameController, ExpertBlameV2Controller],
providers: [ExpertBlameService],

View File

@@ -23,7 +23,13 @@ import {
expertPortfolioFileIdsFromActivityEvents,
objectIdsFromStringSet,
} from "src/helpers/expert-portfolio";
import {
countUnifiedFileStatuses,
getUnifiedFileStatusCatalog,
resolveUnifiedFileStatus,
} from "src/helpers/unified-file-status";
import { applyListQueryV2 } 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 {
ExpertFileAssignResultDto,
@@ -115,8 +121,109 @@ export class ExpertBlameService {
private readonly requestManagementService: RequestManagementService,
private readonly smsOrchestrationService: SmsOrchestrationService,
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) {
const rows = await this.collectBlamePortfolioDocs(actor);
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. */
private async snapshotFieldExpert(actorId: string) {
const expertDoc = await this.expertDbService.findOne({
@@ -386,44 +493,8 @@ export class ExpertBlameService {
}
}
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,
});
const pagedResult = await this.paginateBlameListV2(visibleCases, query);
return pagedResult;
} catch (error) {
if (error instanceof HttpException) throw error;
this.logger.error(
@@ -480,8 +551,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,
claimByPublicId,
query.unifiedStatus,
);
const paged = applyListQueryV2(
filtered,
{
publicId: (doc) =>
String((doc as { publicId?: string }).publicId ?? ""),
@@ -492,23 +582,42 @@ export class ExpertBlameService {
(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,
});
},
searchExtras: (doc) => {
const d = doc as {
_id?: unknown;
blameStatus?: string;
type?: string;
publicId?: string;
status?: string;
};
return [String(d._id ?? ""), d.blameStatus, d.type].filter(
Boolean,
) as string[];
const publicId = String(d.publicId ?? "");
const claim = claimByPublicId.get(publicId);
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,
);
const items: AllRequestDtoV2[] = paged.list.map((doc) =>
this.mapBlameRequestToListItemV2(doc),
this.mapBlameRequestToListItemV2(doc, claimByPublicId),
);
return new AllRequestDtoRsV2({
@@ -522,6 +631,7 @@ export class ExpertBlameService {
private mapBlameRequestToListItemV2(
doc: Record<string, unknown>,
claimByPublicId?: Map<string, { status?: string }>,
): AllRequestDtoV2 {
const createdAt = doc.createdAt
? new Date(doc.createdAt as string)
@@ -552,10 +662,17 @@ export class ExpertBlameService {
const firstParty = parties.find((p) => p.role === "FIRST");
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 {
requestId: String(doc._id),
status: String(doc.status ?? ""),
unifiedFileStatus,
userComment: null,
requestCode: String(doc.publicId ?? ""),
date,

View File

@@ -44,7 +44,7 @@ export class ExpertBlameV2Controller {
description:
"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. " +
"Optional query: `search`, `sortBy` (publicId | createdAt | requestNo | status), `sortOrder`, `page`, `limit`.",
"Optional query: `search`, `sortBy` (publicId | createdAt | requestNo | status), `sortOrder`, `page`, `limit`, `unifiedStatus` (calculated blame+claim status).",
})
async findAll(
@CurrentUser() actor: any,
@@ -60,11 +60,31 @@ 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` query param.",
})
async getUnifiedFileStatusReportV2(@CurrentUser() actor: any) {
try {
return await this.expertBlameService.getUnifiedFileStatusReportV2(actor);
} 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")
@ApiOperation({
summary: "Count blame cases by grouped status bucket (this field expert)",
deprecated: true,
description:
"Counts only files in the acting experts 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) {
try {