forked from Yara724/api
Added Field Expert flows, and deactivated inquiry
This commit is contained in:
@@ -65,6 +65,8 @@ import { WorkflowStepModel } from "src/workflow-step-management/entities/schema/
|
||||
import { BlameStatus } from "src/Types&Enums/blame-request-management/blameStatus.enum";
|
||||
import { CaseStatus } from "src/Types&Enums/blame-request-management/caseStatus.enum";
|
||||
import { UploadContext } from "./entities/schema/blame-voice.schema";
|
||||
import { HashService } from "src/utils/hash/hash.service";
|
||||
import { UserAuthService } from "src/auth/auth-services/user.auth.service";
|
||||
|
||||
@Injectable()
|
||||
export class RequestManagementService {
|
||||
@@ -78,6 +80,14 @@ export class RequestManagementService {
|
||||
);
|
||||
}
|
||||
|
||||
/** Convert plate (string or { ir, leftDigits, centerAlphabet, centerDigits }) to string for vehicle.plateId */
|
||||
private plateToPlateIdString(plate: any): string {
|
||||
if (plate == null) return "";
|
||||
if (typeof plate === "string") return plate;
|
||||
const p = plate as { ir?: number; leftDigits?: number; centerAlphabet?: string; centerDigits?: number };
|
||||
return [p.ir, p.leftDigits, p.centerAlphabet, p.centerDigits].filter((x) => x != null).join("-");
|
||||
}
|
||||
|
||||
private getPartyIndex(req: any, role: PartyRole): number {
|
||||
return Array.isArray(req.parties)
|
||||
? req.parties.findIndex((p) => p?.role === role)
|
||||
@@ -292,6 +302,8 @@ export class RequestManagementService {
|
||||
private readonly claimRequestManagementDbService: ClaimRequestManagementDbService,
|
||||
private readonly publicIdService: PublicIdService,
|
||||
private readonly workflowStepDbService: WorkflowStepDbService,
|
||||
private readonly hashService: HashService,
|
||||
private readonly userAuthService: UserAuthService,
|
||||
) {}
|
||||
|
||||
async createRequestV2(user: any, type: BlameRequestType) {
|
||||
@@ -738,12 +750,12 @@ export class RequestManagementService {
|
||||
let inquiryRaw: any;
|
||||
let inquiryMapped: any;
|
||||
try {
|
||||
const inquiry = await this.sandHubService.getTejaratBlockInquiry({
|
||||
plate: body.plate,
|
||||
nationalCodeOfInsurer: body.nationalCodeOfInsurer,
|
||||
});
|
||||
inquiryRaw = inquiry.raw;
|
||||
inquiryMapped = inquiry.mapped;
|
||||
// const inquiry = await this.sandHubService.getTejaratBlockInquiry({
|
||||
// plate: body.plate,
|
||||
// nationalCodeOfInsurer: body.nationalCodeOfInsurer,
|
||||
// });
|
||||
// inquiryRaw = inquiry.raw;
|
||||
// inquiryMapped = inquiry.mapped;
|
||||
this.logger.log(
|
||||
`[TEJARAT] block inquiry raw for request=${req._id}: ${JSON.stringify(inquiryRaw)}`,
|
||||
);
|
||||
@@ -808,7 +820,10 @@ export class RequestManagementService {
|
||||
|
||||
if (!party.vehicle) party.vehicle = {} as any;
|
||||
party.vehicle.isNew = body.isNewCar;
|
||||
party.vehicle.plateId = body.plateId;
|
||||
party.vehicle.plateId =
|
||||
typeof body.plateId === "string"
|
||||
? body.plateId
|
||||
: this.plateToPlateIdString(body.plate);
|
||||
party.vehicle.name = inquiryMapped?.MapTypNam;
|
||||
party.vehicle.type = `${inquiryMapped?.UsageField} / ${inquiryMapped?.MapUsageName || "-"}`;
|
||||
party.vehicle.inquiry = {
|
||||
@@ -1140,9 +1155,53 @@ export class RequestManagementService {
|
||||
);
|
||||
}
|
||||
|
||||
// Check if second party already exists
|
||||
// Check if second party already exists (e.g. expert-initiated LINK: expert already added both parties)
|
||||
const secondPartyIdx = this.getPartyIndex(req, PartyRole.SECOND);
|
||||
if (secondPartyIdx !== -1) {
|
||||
if (
|
||||
req.expertInitiated &&
|
||||
req.creationMethod === CreationMethod.LINK
|
||||
) {
|
||||
// Just advance workflow; second party already in parties. Optionally resend SMS.
|
||||
const secondPartyPhone =
|
||||
(req.parties[secondPartyIdx]?.person as any)?.phoneNumber || phoneNumber;
|
||||
req.status = CaseStatus.WAITING_FOR_SECOND_PARTY;
|
||||
await this.advanceWorkflowToNext(req, stepKey);
|
||||
if (!Array.isArray(req.history)) req.history = [];
|
||||
req.history.push({
|
||||
type: "SECOND_PARTY_INVITED",
|
||||
actor: {
|
||||
actorId: Types.ObjectId.isValid(user?.sub)
|
||||
? new Types.ObjectId(user.sub)
|
||||
: undefined,
|
||||
actorName: user?.fullName,
|
||||
actorType: "user",
|
||||
},
|
||||
metadata: { secondPartyPhone, expertInitiatedLink: true },
|
||||
} as any);
|
||||
await (req as any).save();
|
||||
const url = `${process.env.URL}/${frontendRoute}?token=${requestId}`;
|
||||
try {
|
||||
await this.smsManagerService.verifyLookUp({
|
||||
token: url,
|
||||
template: "yara724-invite-link",
|
||||
receptor: secondPartyPhone,
|
||||
});
|
||||
this.logger.log(
|
||||
`[SMS] Invitation resent to ${secondPartyPhone} for expert-initiated request ${req.publicId}`,
|
||||
);
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
`[SMS] Failed to send invitation to ${secondPartyPhone}: ${err?.message || err}`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
requestId: req._id,
|
||||
publicId: req.publicId,
|
||||
workflow: req.workflow,
|
||||
invitationUrl: url,
|
||||
};
|
||||
}
|
||||
throw new ConflictException("Second party already added to this request");
|
||||
}
|
||||
|
||||
@@ -1602,47 +1661,48 @@ export class RequestManagementService {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Activate
|
||||
// --- Proceed with existing logic if the check passes ---
|
||||
// 1) Main third-party/block inquiry (existing behavior)
|
||||
const sanHubResponse = await this.sandHubService.getSandHubResponse({
|
||||
plate: body.plate,
|
||||
nationalCodeOfInsurer: body.nationalCodeOfInsurer,
|
||||
});
|
||||
// const sanHubResponse = await this.sandHubService.getSandHubResponse({
|
||||
// plate: body.plate,
|
||||
// nationalCodeOfInsurer: body.nationalCodeOfInsurer,
|
||||
// });
|
||||
|
||||
if (sanHubResponse.Error) {
|
||||
throw new HttpException(
|
||||
sanHubResponse.Error.Message,
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
// if (sanHubResponse.Error) {
|
||||
// throw new HttpException(
|
||||
// sanHubResponse.Error.Message,
|
||||
// HttpStatus.BAD_REQUEST,
|
||||
// );
|
||||
// }
|
||||
|
||||
// 2) Personal inquiry (new shared SandHub API)
|
||||
// This is a best-effort call; if it fails we log and continue.
|
||||
try {
|
||||
const personalInquiry = await this.sandHubService.getPersonalInquiry(
|
||||
body.nationalCodeOfInsurer,
|
||||
body.insurerBirthday,
|
||||
);
|
||||
this.logger.log(
|
||||
`[SANDHUB] Personal inquiry succeeded for nationalCode=${body.nationalCodeOfInsurer}: ${JSON.stringify(
|
||||
personalInquiry,
|
||||
)}`,
|
||||
);
|
||||
// NOTE: We are not persisting this data yet; once the exact fields
|
||||
// are finalized, we can map and store them on the request document.
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
`[SANDHUB] Personal inquiry failed for nationalCode=${body.nationalCodeOfInsurer}: ${err?.message || err}`,
|
||||
);
|
||||
}
|
||||
// try {
|
||||
// const personalInquiry = await this.sandHubService.getPersonalInquiry(
|
||||
// body.nationalCodeOfInsurer,
|
||||
// body.insurerBirthday,
|
||||
// );
|
||||
// this.logger.log(
|
||||
// `[SANDHUB] Personal inquiry succeeded for nationalCode=${body.nationalCodeOfInsurer}: ${JSON.stringify(
|
||||
// personalInquiry,
|
||||
// )}`,
|
||||
// );
|
||||
// // NOTE: We are not persisting this data yet; once the exact fields
|
||||
// // are finalized, we can map and store them on the request document.
|
||||
// } catch (err) {
|
||||
// this.logger.error(
|
||||
// `[SANDHUB] Personal inquiry failed for nationalCode=${body.nationalCodeOfInsurer}: ${err?.message || err}`,
|
||||
// );
|
||||
// }
|
||||
|
||||
return await this.addPlate(
|
||||
sanHubResponse,
|
||||
request,
|
||||
user,
|
||||
body,
|
||||
isFirstParty ? "firstParty" : "secondParty",
|
||||
);
|
||||
// return await this.addPlate(
|
||||
// sanHubResponse,
|
||||
// request,
|
||||
// user,
|
||||
// body,
|
||||
// isFirstParty ? "firstParty" : "secondParty",
|
||||
// );
|
||||
}
|
||||
|
||||
async carBodyFormStep(
|
||||
@@ -3463,6 +3523,212 @@ export class RequestManagementService {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Send blame link to first party (and second party for THIRD_PARTY) for expert-initiated LINK method.
|
||||
* SMS delivery is mocked for now; replace with real SMS provider later. First party opens the link and fills the form via normal v2 flow.
|
||||
*/
|
||||
async sendLinkV2(
|
||||
expert: any,
|
||||
requestId: string,
|
||||
): Promise<{
|
||||
sent: boolean;
|
||||
linkUrl: string;
|
||||
sentTo: { role: string; phoneNumber: string }[];
|
||||
}> {
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Request not found");
|
||||
if (!req.expertInitiated || req.creationMethod !== CreationMethod.LINK) {
|
||||
throw new BadRequestException(
|
||||
"This endpoint is only for expert-initiated LINK blame files. Create the file with creationMethod LINK first.",
|
||||
);
|
||||
}
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
|
||||
const baseUrl =
|
||||
process.env.FRONTEND_BASE_URL || process.env.APP_URL || "";
|
||||
const linkUrl = baseUrl
|
||||
? `${baseUrl.replace(/\/$/, "")}/blame/${requestId}`
|
||||
: "";
|
||||
|
||||
const sentTo: { role: string; phoneNumber: string }[] = [];
|
||||
const firstIdx = this.getPartyIndex(req, PartyRole.FIRST);
|
||||
if (firstIdx !== -1 && req.parties[firstIdx]?.person?.phoneNumber) {
|
||||
const phone = req.parties[firstIdx].person.phoneNumber;
|
||||
this.logger.log(
|
||||
`[MOCK SMS] Would send link to first party ${phone}: ${linkUrl}`,
|
||||
);
|
||||
sentTo.push({ role: PartyRole.FIRST, phoneNumber: phone });
|
||||
}
|
||||
if (req.type === BlameRequestType.THIRD_PARTY) {
|
||||
const secondIdx = this.getPartyIndex(req, PartyRole.SECOND);
|
||||
if (secondIdx !== -1 && req.parties[secondIdx]?.person?.phoneNumber) {
|
||||
const phone = req.parties[secondIdx].person.phoneNumber;
|
||||
this.logger.log(
|
||||
`[MOCK SMS] Would send link to second party ${phone}: ${linkUrl}`,
|
||||
);
|
||||
sentTo.push({ role: PartyRole.SECOND, phoneNumber: phone });
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray((req as any).history)) (req as any).history = [];
|
||||
(req as any).history.push({
|
||||
type: "LINK_SENT",
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(expert.sub),
|
||||
actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(),
|
||||
actorType: "field_expert",
|
||||
},
|
||||
metadata: { linkUrl, sentTo },
|
||||
});
|
||||
await (req as any).save();
|
||||
|
||||
return { sent: true, linkUrl, sentTo };
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Send OTP via SMS to one or two parties for expert-initiated IN_PERSON blame.
|
||||
* Uses the same flow as /user/send-otp (same template, expiry). After this, parties receive SMS; expert collects OTPs and calls verify-party-otps.
|
||||
*/
|
||||
async sendPartyOtpsV2(
|
||||
expert: any,
|
||||
requestId: string,
|
||||
dto: { firstPartyPhoneNumber: string; secondPartyPhoneNumber?: string },
|
||||
): Promise<{ sent: boolean; message: string }> {
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Request not found");
|
||||
if (!req.expertInitiated || req.creationMethod !== CreationMethod.IN_PERSON) {
|
||||
throw new BadRequestException(
|
||||
"This endpoint is only for expert-initiated IN_PERSON blame files.",
|
||||
);
|
||||
}
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
if (req.type === BlameRequestType.THIRD_PARTY && !dto.secondPartyPhoneNumber) {
|
||||
throw new BadRequestException(
|
||||
"Second party phone number is required for THIRD_PARTY. Provide secondPartyPhoneNumber to send OTP to both parties.",
|
||||
);
|
||||
}
|
||||
const sent: string[] = [];
|
||||
try {
|
||||
await this.userAuthService.sendOtpRequest(dto.firstPartyPhoneNumber);
|
||||
sent.push(dto.firstPartyPhoneNumber);
|
||||
} catch (e: any) {
|
||||
if (e?.message?.includes("Wait for expiry")) {
|
||||
throw new BadRequestException(
|
||||
`First party (${dto.firstPartyPhoneNumber}): OTP was recently sent. Wait for the expiry time before requesting again.`,
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
if (dto.secondPartyPhoneNumber) {
|
||||
try {
|
||||
await this.userAuthService.sendOtpRequest(dto.secondPartyPhoneNumber);
|
||||
sent.push(dto.secondPartyPhoneNumber);
|
||||
} catch (e: any) {
|
||||
if (e?.message?.includes("Wait for expiry")) {
|
||||
throw new BadRequestException(
|
||||
`Second party (${dto.secondPartyPhoneNumber}): OTP was recently sent. Wait for the expiry time before requesting again.`,
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return {
|
||||
sent: true,
|
||||
message:
|
||||
sent.length === 2
|
||||
? `OTP sent to both parties (${sent.join(", ")}). Have them tell you the code, then call verify-party-otps.`
|
||||
: `OTP sent to ${sent[0]}. Have the party tell you the code, then call verify-party-otps.`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Verify one or two party OTPs for expert-initiated IN_PERSON blame.
|
||||
* Call this after send-party-otps (or after parties requested OTP via /user/send-otp). On success, parties are linked to their user ids so the expert can proceed with complete-blame-data.
|
||||
*/
|
||||
async verifyPartyOtpsV2(
|
||||
expert: any,
|
||||
requestId: string,
|
||||
dto: {
|
||||
firstPartyPhoneNumber: string;
|
||||
firstPartyOtp: string;
|
||||
secondPartyPhoneNumber?: string;
|
||||
secondPartyOtp?: string;
|
||||
},
|
||||
): Promise<{ verified: boolean; message: string }> {
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Request not found");
|
||||
if (!req.expertInitiated || req.creationMethod !== CreationMethod.IN_PERSON) {
|
||||
throw new BadRequestException(
|
||||
"This endpoint is only for expert-initiated IN_PERSON blame files.",
|
||||
);
|
||||
}
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
|
||||
const now = Date.now();
|
||||
const verifyOne = async (phone: string, otp: string): Promise<Types.ObjectId> => {
|
||||
const user = await this.userDbService.findOne({
|
||||
$or: [{ username: phone }, { mobile: phone }],
|
||||
});
|
||||
if (!user) throw new BadRequestException(`User not found for phone: ${phone}`);
|
||||
const u = user as any;
|
||||
if (u.otp == null) throw new BadRequestException(`No OTP requested for ${phone}. User must call /user/send-otp first.`);
|
||||
if (u.otpExpire < now) throw new BadRequestException(`OTP expired for ${phone}. User must request a new OTP.`);
|
||||
const valid = await this.hashService.compare(otp, u.otp);
|
||||
if (!valid) throw new BadRequestException(`Invalid OTP for ${phone}`);
|
||||
return u._id;
|
||||
};
|
||||
|
||||
const firstUserId = await verifyOne(dto.firstPartyPhoneNumber, dto.firstPartyOtp);
|
||||
if (!Array.isArray(req.parties)) req.parties = [];
|
||||
const firstIdx = req.parties.findIndex((p: any) => p?.role === PartyRole.FIRST);
|
||||
if (firstIdx === -1) throw new BadRequestException("First party not found on request");
|
||||
if (!req.parties[firstIdx].person) req.parties[firstIdx].person = {} as any;
|
||||
req.parties[firstIdx].person.userId = firstUserId;
|
||||
req.parties[firstIdx].person.phoneNumber = dto.firstPartyPhoneNumber;
|
||||
|
||||
if (req.type === BlameRequestType.THIRD_PARTY && dto.secondPartyPhoneNumber && dto.secondPartyOtp) {
|
||||
const secondUserId = await verifyOne(dto.secondPartyPhoneNumber, dto.secondPartyOtp);
|
||||
let secondIdx = req.parties.findIndex((p: any) => p?.role === PartyRole.SECOND);
|
||||
if (secondIdx === -1) {
|
||||
req.parties.push({
|
||||
role: PartyRole.SECOND,
|
||||
person: {
|
||||
userId: secondUserId,
|
||||
phoneNumber: dto.secondPartyPhoneNumber,
|
||||
},
|
||||
} as any);
|
||||
} else {
|
||||
if (!req.parties[secondIdx].person) req.parties[secondIdx].person = {} as any;
|
||||
req.parties[secondIdx].person.userId = secondUserId;
|
||||
req.parties[secondIdx].person.phoneNumber = dto.secondPartyPhoneNumber;
|
||||
}
|
||||
} else if (req.type === BlameRequestType.THIRD_PARTY && (dto.secondPartyPhoneNumber || dto.secondPartyOtp)) {
|
||||
throw new BadRequestException("For THIRD_PARTY both secondPartyPhoneNumber and secondPartyOtp are required.");
|
||||
}
|
||||
|
||||
if (!Array.isArray(req.history)) req.history = [];
|
||||
req.history.push({
|
||||
type: "PARTY_OTPS_VERIFIED",
|
||||
actor: {
|
||||
actorId: new Types.ObjectId(expert.sub),
|
||||
actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(),
|
||||
actorType: "field_expert",
|
||||
},
|
||||
metadata: {
|
||||
firstPartyVerified: true,
|
||||
secondPartyVerified: req.type === BlameRequestType.THIRD_PARTY && !!dto.secondPartyPhoneNumber,
|
||||
},
|
||||
} as any);
|
||||
await (req as any).save();
|
||||
|
||||
return {
|
||||
verified: true,
|
||||
message: req.type === BlameRequestType.THIRD_PARTY && dto.secondPartyPhoneNumber
|
||||
? "Both parties verified. You can proceed to fill the blame form."
|
||||
: "First party verified. You can proceed to fill the blame form.",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* List all expert-initiated blame files for the current field expert (their panel).
|
||||
* Only files where initiatedBy === current user are returned.
|
||||
@@ -3684,7 +3950,7 @@ export class RequestManagementService {
|
||||
driverBirthday: firstPartyPlate.driverBirthday,
|
||||
},
|
||||
vehicle: {
|
||||
plateId: firstPartyPlate.plate,
|
||||
plateId: this.plateToPlateIdString(firstPartyPlate.plate) || (firstPartyPlate as any).plateId,
|
||||
name: sandHubReport?.MapTypNam || sandHubReport?.CarName,
|
||||
model: sandHubReport?.MapTypNam,
|
||||
type: `${sandHubReport?.UsageField || ""} / ${sandHubReport?.MapUsageName || "-"}`,
|
||||
@@ -3723,7 +3989,7 @@ export class RequestManagementService {
|
||||
|
||||
req.parties = [firstParty];
|
||||
req.workflow = {
|
||||
currentStep: WorkflowStep.COMPLETED,
|
||||
currentStep: WorkflowStep.WAITING_FOR_SIGNATURES as any,
|
||||
nextStep: undefined,
|
||||
completedSteps: [
|
||||
WorkflowStep.CREATED,
|
||||
@@ -3736,7 +4002,7 @@ export class RequestManagementService {
|
||||
].filter((s) => s),
|
||||
locked: false,
|
||||
};
|
||||
req.status = CaseStatus.COMPLETED;
|
||||
req.status = CaseStatus.WAITING_FOR_SIGNATURES;
|
||||
req.blameStatus = BlameStatus.AGREED;
|
||||
(req as any).filledBy = FilledBy.EXPERT;
|
||||
|
||||
@@ -3827,7 +4093,7 @@ export class RequestManagementService {
|
||||
driverBirthday: plateDto.driverBirthday,
|
||||
},
|
||||
vehicle: {
|
||||
plateId: plateDto.plate,
|
||||
plateId: this.plateToPlateIdString(plateDto.plate) || (plateDto as any).plateId,
|
||||
name: sandHubReport?.MapTypNam || sandHubReport?.CarName,
|
||||
model: sandHubReport?.MapTypNam,
|
||||
type: `${sandHubReport?.UsageField || ""} / ${sandHubReport?.MapUsageName || "-"}`,
|
||||
@@ -3867,8 +4133,14 @@ export class RequestManagementService {
|
||||
);
|
||||
|
||||
req.parties = [firstParty, secondParty];
|
||||
const guiltyPartyId =
|
||||
formData.guiltyPartyPhoneNumber === formData.firstPartyPhoneNumber
|
||||
? String(firstPartyUserId)
|
||||
: String(secondPartyUserId);
|
||||
if (!req.expert) req.expert = {} as any;
|
||||
req.expert.decision = { guiltyPartyId: new Types.ObjectId(guiltyPartyId) } as any;
|
||||
req.workflow = {
|
||||
currentStep: WorkflowStep.COMPLETED,
|
||||
currentStep: WorkflowStep.WAITING_FOR_SIGNATURES as any,
|
||||
nextStep: undefined,
|
||||
completedSteps: [
|
||||
WorkflowStep.CREATED,
|
||||
@@ -3885,7 +4157,7 @@ export class RequestManagementService {
|
||||
].filter((s) => s),
|
||||
locked: false,
|
||||
};
|
||||
req.status = CaseStatus.COMPLETED;
|
||||
req.status = CaseStatus.WAITING_FOR_SIGNATURES;
|
||||
req.blameStatus = BlameStatus.AGREED;
|
||||
(req as any).filledBy = FilledBy.EXPERT;
|
||||
|
||||
@@ -4006,6 +4278,116 @@ export class RequestManagementService {
|
||||
return { requestId: req._id };
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Expert uploads a party's signature for expert-initiated IN_PERSON blame.
|
||||
* CAR_BODY: upload FIRST only. THIRD_PARTY: upload FIRST then SECOND.
|
||||
* When all required parties have signed, status becomes COMPLETED.
|
||||
*/
|
||||
async expertUploadPartySignatureV2(
|
||||
expert: any,
|
||||
requestId: string,
|
||||
partyRole: PartyRole,
|
||||
isAccept: boolean,
|
||||
signFile: Express.Multer.File,
|
||||
): Promise<UserSignatureResponseDto> {
|
||||
if (!signFile) {
|
||||
throw new BadRequestException("A signature file is required");
|
||||
}
|
||||
const req = await this.blameRequestDbService.findById(requestId);
|
||||
if (!req) throw new NotFoundException("Request not found");
|
||||
if (!req.expertInitiated || req.creationMethod !== CreationMethod.IN_PERSON) {
|
||||
throw new BadRequestException(
|
||||
"This endpoint is only for expert-initiated IN_PERSON blame files.",
|
||||
);
|
||||
}
|
||||
this.verifyExpertAccessForBlameV2(req, expert);
|
||||
if (req.status !== CaseStatus.WAITING_FOR_SIGNATURES) {
|
||||
throw new BadRequestException(
|
||||
"Request is not waiting for signatures. Current status: " + req.status,
|
||||
);
|
||||
}
|
||||
const partyIndex = this.getPartyIndex(req, partyRole);
|
||||
if (partyIndex === -1) {
|
||||
throw new BadRequestException(`Party ${partyRole} not found on this request`);
|
||||
}
|
||||
if (req.type === BlameRequestType.CAR_BODY && partyRole === PartyRole.SECOND) {
|
||||
throw new BadRequestException("CAR_BODY has only first party; use partyRole FIRST.");
|
||||
}
|
||||
const party = req.parties[partyIndex];
|
||||
if (party.confirmation) {
|
||||
throw new BadRequestException(
|
||||
`Party ${partyRole} has already signed.`,
|
||||
);
|
||||
}
|
||||
const partyUserId = party.person?.userId ? String(party.person.userId) : undefined;
|
||||
const signDoc = await this.userSign.create({
|
||||
fileName: signFile.filename,
|
||||
userId: partyUserId || expert.sub,
|
||||
path: signFile.path,
|
||||
requestId: new Types.ObjectId(requestId),
|
||||
});
|
||||
const signatureData = {
|
||||
fileId: String((signDoc as any)._id),
|
||||
fileName: signFile.filename,
|
||||
fileUrl: signFile.path,
|
||||
};
|
||||
const updatePayload: any = {
|
||||
$set: {
|
||||
[`parties.${partyIndex}.confirmation`]: {
|
||||
partyRole: party.role,
|
||||
accepted: isAccept,
|
||||
signature: signatureData,
|
||||
},
|
||||
},
|
||||
};
|
||||
await this.blameRequestDbService.findByIdAndUpdate(requestId, updatePayload);
|
||||
const updatedRequest = await this.blameRequestDbService.findById(requestId);
|
||||
const requiredCount = req.type === BlameRequestType.CAR_BODY ? 1 : 2;
|
||||
const signedParties = (updatedRequest.parties || []).filter(
|
||||
(p) => p.confirmation != null && p.confirmation !== undefined,
|
||||
);
|
||||
let finalStatus = CaseStatus.WAITING_FOR_SIGNATURES;
|
||||
let message = "Signature recorded successfully.";
|
||||
if (signedParties.length >= requiredCount) {
|
||||
const allAccepted = updatedRequest.parties
|
||||
.slice(0, requiredCount)
|
||||
.every((p) => p.confirmation?.accepted === true);
|
||||
if (allAccepted) {
|
||||
finalStatus = CaseStatus.COMPLETED;
|
||||
message = "All parties have signed. Blame case completed.";
|
||||
await this.blameRequestDbService.findByIdAndUpdate(requestId, {
|
||||
$set: {
|
||||
status: CaseStatus.COMPLETED,
|
||||
"workflow.currentStep": WorkflowStep.COMPLETED as any,
|
||||
"workflow.nextStep": null,
|
||||
},
|
||||
$push: {
|
||||
"workflow.completedSteps": WorkflowStep.WAITING_FOR_SIGNATURES as any,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
finalStatus = CaseStatus.CANCELLED;
|
||||
message = "One or both parties rejected. Case requires in-person resolution.";
|
||||
await this.blameRequestDbService.findByIdAndUpdate(requestId, {
|
||||
$set: {
|
||||
status: CaseStatus.CANCELLED,
|
||||
"workflow.currentStep": WorkflowStep.COMPLETED as any,
|
||||
"workflow.nextStep": null,
|
||||
},
|
||||
$push: {
|
||||
"workflow.completedSteps": WorkflowStep.WAITING_FOR_SIGNATURES as any,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
requestId: String(req._id),
|
||||
accepted: isAccept,
|
||||
status: finalStatus,
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* V2: Expert adds accident fields to expert-initiated BlameRequest.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user