1
0
forked from Yara724/api

Merge pull request 'Added registrar + fixed sign bug in car-body flow' (#10) from s.yahyaee/yara724-api:main into main

Reviewed-on: Yara724/api#10
This commit is contained in:
2026-03-26 16:37:33 +03:30
18 changed files with 706 additions and 31 deletions

View File

@@ -103,7 +103,7 @@ export class RequestManagementService {
}
private async advanceWorkflowToNext(req: any, submittedStepKey: WorkflowStep) {
// CAR_BODY: after first-party description there is no second party or expert; go to COMPLETED
// CAR_BODY: after first-party description, move to signature step.
if (
req.type === BlameRequestType.CAR_BODY &&
submittedStepKey === WorkflowStep.FIRST_DESCRIPTION
@@ -113,11 +113,11 @@ export class RequestManagementService {
: [];
if (!completed.includes(submittedStepKey)) completed.push(submittedStepKey);
req.workflow.completedSteps = completed;
req.workflow.currentStep = WorkflowStep.COMPLETED;
req.workflow.currentStep = WorkflowStep.WAITING_FOR_SIGNATURES;
req.workflow.nextStep = undefined;
req.status = CaseStatus.COMPLETED;
req.status = CaseStatus.WAITING_FOR_SIGNATURES;
this.logger.debug(
"[WORKFLOW] CAR_BODY: advanced to COMPLETED after FIRST_DESCRIPTION",
"[WORKFLOW] CAR_BODY: advanced to WAITING_FOR_SIGNATURES after FIRST_DESCRIPTION",
);
return;
}
@@ -3232,16 +3232,86 @@ export class RequestManagementService {
* Verify the current user (field expert) can access this BlameRequest (v2).
*/
private verifyExpertAccessForBlameV2(req: any, expert: any): void {
if (!req?.expertInitiated || !req?.initiatedByFieldExpertId) {
throw new ForbiddenException(
"This file is not expert-initiated or has no initiating expert",
if (req?.expertInitiated && req?.initiatedByFieldExpertId) {
if (String(req.initiatedByFieldExpertId) !== String(expert?.sub)) {
throw new ForbiddenException(
"You can only access files that you have initiated",
);
}
return;
}
if (req?.registrarInitiated && req?.initiatedByRegistrarId) {
if (String(req.initiatedByRegistrarId) !== String(expert?.sub)) {
throw new ForbiddenException(
"You can only access files that you have initiated",
);
}
return;
}
throw new ForbiddenException(
"This file is not initiator-scoped or has no initiating actor",
);
}
async createRegistrarInitiatedBlame(
registrar: any,
dto: { type: "THIRD_PARTY" | "CAR_BODY" },
): Promise<{ requestId: string; publicId: string }> {
const registrarId = new Types.ObjectId(registrar.sub);
const type =
dto.type === "CAR_BODY"
? BlameRequestType.CAR_BODY
: BlameRequestType.THIRD_PARTY;
const firstStep = await this.getWorkflowStep({ stepNumber: 1 });
if (!firstStep?.stepKey) {
throw new InternalServerErrorException(
"Workflow stepNumber=1 not configured in step manager",
);
}
if (String(req.initiatedByFieldExpertId) !== String(expert?.sub)) {
throw new ForbiddenException(
"You can only access files that you have initiated",
const nextStepFromManager = firstStep.nextPossibleSteps?.[0];
const nextStep =
type === BlameRequestType.CAR_BODY
? (WorkflowStep.CAR_BODY_ACCIDENT_TYPE as WorkflowStep)
: (nextStepFromManager as WorkflowStep);
if (!nextStep) {
throw new InternalServerErrorException(
"Workflow stepNumber=1 has no nextPossibleSteps configured",
);
}
const publicId = await this.publicIdService.generateRequestPublicId();
const parties: any[] = [{ role: PartyRole.FIRST, person: {} }];
if (type === BlameRequestType.THIRD_PARTY) {
parties.push({ role: PartyRole.SECOND, person: {} });
}
const created = await this.blameRequestDbService.create({
publicId,
requestNo: publicId,
type,
parties,
workflow: {
currentStep: firstStep.stepKey as WorkflowStep,
nextStep,
completedSteps: [firstStep.stepKey as WorkflowStep],
locked: false,
},
history: [],
registrarInitiated: true,
initiatedByRegistrarId: registrarId,
creationMethod: CreationMethod.IN_PERSON,
filledBy: FilledBy.REGISTRAR,
} as any);
if (!Array.isArray((created as any).history)) (created as any).history = [];
(created as any).history.push({
type: "FILE_CREATED_BY_REGISTRAR",
actor: {
actorId: registrarId,
actorName: `${registrar.firstName || ""} ${registrar.lastName || ""}`.trim(),
actorType: "registrar",
},
metadata: { creationMethod: CreationMethod.IN_PERSON, type: dto.type },
});
await (created as any).save();
return { requestId: String((created as any)._id), publicId: (created as any).publicId };
}
/**
@@ -3594,7 +3664,10 @@ export class RequestManagementService {
): 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) {
if (
(!req.expertInitiated && !req.registrarInitiated) ||
req.creationMethod !== CreationMethod.IN_PERSON
) {
throw new BadRequestException(
"This endpoint is only for expert-initiated IN_PERSON blame files.",
);
@@ -3655,7 +3728,10 @@ export class RequestManagementService {
): 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) {
if (
(!req.expertInitiated && !req.registrarInitiated) ||
req.creationMethod !== CreationMethod.IN_PERSON
) {
throw new BadRequestException(
"This endpoint is only for expert-initiated IN_PERSON blame files.",
);
@@ -3710,7 +3786,7 @@ export class RequestManagementService {
actor: {
actorId: new Types.ObjectId(expert.sub),
actorName: `${expert.firstName || ""} ${expert.lastName || ""}`.trim(),
actorType: "field_expert",
actorType: expert?.role === RoleEnum.REGISTRAR ? "registrar" : "field_expert",
},
metadata: {
firstPartyVerified: true,
@@ -3757,8 +3833,10 @@ export class RequestManagementService {
async getMyExpertInitiatedFilesV2(expert: any): Promise<any[]> {
const expertId = new Types.ObjectId(expert.sub);
const files = await this.blameRequestDbService.find({
expertInitiated: true,
initiatedByFieldExpertId: expertId,
$or: [
{ expertInitiated: true, initiatedByFieldExpertId: expertId },
{ registrarInitiated: true, initiatedByRegistrarId: expertId },
],
});
return (files || []).map((f: any) => ({
_id: f._id,
@@ -3807,16 +3885,20 @@ export class RequestManagementService {
req.expertInitiated &&
req.initiatedByFieldExpertId &&
String(req.initiatedByFieldExpertId) === String(user?.sub);
const isRegistrarOwner =
req.registrarInitiated &&
req.initiatedByRegistrarId &&
String(req.initiatedByRegistrarId) === String(user?.sub);
const isParty = Array.isArray(req.parties) && req.parties.some((p: any) => {
const pid = p?.person?.userId ? String(p.person.userId) : null;
const phone = p?.person?.phoneNumber;
return (pid && pid === String(user?.sub)) || (phone && phone === user?.username);
});
if (!isFieldExpertOwner && !isParty) {
if (!isFieldExpertOwner && !isRegistrarOwner && !isParty) {
throw new ForbiddenException("You do not have access to this request");
}
// Optionally bind userId to party when user opens via LINK (phone match, no userId yet)
if (!isFieldExpertOwner && user?.sub && Types.ObjectId.isValid(user.sub)) {
if (!isFieldExpertOwner && !isRegistrarOwner && user?.sub && Types.ObjectId.isValid(user.sub)) {
let updated = false;
for (const p of req.parties || []) {
if (
@@ -3879,7 +3961,10 @@ export class RequestManagementService {
if (!req) {
throw new NotFoundException("Request not found");
}
if (!req.expertInitiated || req.creationMethod !== CreationMethod.IN_PERSON) {
if (
(!req.expertInitiated && !req.registrarInitiated) ||
req.creationMethod !== CreationMethod.IN_PERSON
) {
throw new BadRequestException(
"This endpoint is only for expert-initiated IN_PERSON BlameRequest files.",
);
@@ -3907,7 +3992,10 @@ export class RequestManagementService {
): Promise<any> {
const req = await this.blameRequestDbService.findById(requestId);
if (!req) throw new NotFoundException("Request not found");
if (!req.expertInitiated || req.creationMethod !== CreationMethod.IN_PERSON) {
if (
(!req.expertInitiated && !req.registrarInitiated) ||
req.creationMethod !== CreationMethod.IN_PERSON
) {
throw new BadRequestException("Only expert-initiated IN_PERSON CAR_BODY files.");
}
if (req.type !== BlameRequestType.CAR_BODY) {
@@ -4210,7 +4298,7 @@ export class RequestManagementService {
const req = await this.blameRequestDbService.findById(requestId);
if (!req) throw new NotFoundException("Request not found");
if (!req.expertInitiated) {
if (!req.expertInitiated && !req.registrarInitiated) {
throw new BadRequestException("This endpoint is only for expert-initiated files.");
}
this.verifyExpertAccessForBlameV2(req, expert);