forked from Yara724/api
Fix V4 FileMaker workflow re-entry
This commit is contained in:
219
src/request-management/file-maker-blame-v4-workflow.spec.ts
Normal file
219
src/request-management/file-maker-blame-v4-workflow.spec.ts
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
import { Types } from "mongoose";
|
||||||
|
import { BlameRequestType } from "src/Types&Enums/blame-request-management/blameRequestType.enum";
|
||||||
|
import { WorkflowStep } from "src/Types&Enums/blame-request-management/blameWorkflow-steps.enum";
|
||||||
|
import { RoleEnum } from "src/Types&Enums/role.enum";
|
||||||
|
import { CreationMethod } from "./entities/schema/request-management.schema";
|
||||||
|
import { PartyRole } from "./entities/schema/partyRole.enum";
|
||||||
|
import { RequestManagementService } from "./request-management.service";
|
||||||
|
|
||||||
|
describe("RequestManagementService V4 FileMaker workflow", () => {
|
||||||
|
it("persists FIRST_INITIAL_FORM after the first party OTP is verified", async () => {
|
||||||
|
const fileMakerId = new Types.ObjectId();
|
||||||
|
const userId = new Types.ObjectId();
|
||||||
|
const request = {
|
||||||
|
_id: new Types.ObjectId(),
|
||||||
|
publicId: "BL-V4-001",
|
||||||
|
type: BlameRequestType.THIRD_PARTY,
|
||||||
|
expertInitiated: true,
|
||||||
|
isMadeByFileMaker: true,
|
||||||
|
requiresFileMakerApproval: false,
|
||||||
|
initiatedByFieldExpertId: fileMakerId,
|
||||||
|
creationMethod: CreationMethod.IN_PERSON,
|
||||||
|
parties: [{ role: PartyRole.FIRST, person: {} }],
|
||||||
|
workflow: {
|
||||||
|
currentStep: WorkflowStep.CREATED,
|
||||||
|
nextStep: WorkflowStep.FIRST_BLAME_CONFESSION,
|
||||||
|
completedSteps: [WorkflowStep.CREATED],
|
||||||
|
},
|
||||||
|
history: [],
|
||||||
|
save: jest.fn().mockResolvedValue(undefined),
|
||||||
|
};
|
||||||
|
const service =
|
||||||
|
new (RequestManagementService as any)() as RequestManagementService;
|
||||||
|
(service as any).blameRequestDbService = {
|
||||||
|
findById: jest.fn().mockResolvedValue(request),
|
||||||
|
};
|
||||||
|
(service as any).userDbService = {
|
||||||
|
findOne: jest.fn().mockResolvedValue({
|
||||||
|
_id: userId,
|
||||||
|
otp: "hashed-otp",
|
||||||
|
otpExpire: Date.now() + 60_000,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
(service as any).hashService = {
|
||||||
|
compare: jest.fn().mockResolvedValue(true),
|
||||||
|
};
|
||||||
|
(service as any).claimCaseDbService = {
|
||||||
|
findOne: jest.fn().mockResolvedValue(null),
|
||||||
|
};
|
||||||
|
(service as any).workflowStepDbService = {
|
||||||
|
findByStepNumber: jest.fn().mockImplementation((stepNumber: number) => {
|
||||||
|
if (stepNumber === 2) {
|
||||||
|
return {
|
||||||
|
stepNumber: 2,
|
||||||
|
stepKey: WorkflowStep.FIRST_BLAME_CONFESSION,
|
||||||
|
nextPossibleSteps: [WorkflowStep.FIRST_VIDEO],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (stepNumber === 3) {
|
||||||
|
return {
|
||||||
|
stepNumber: 3,
|
||||||
|
stepKey: WorkflowStep.FIRST_VIDEO,
|
||||||
|
nextPossibleSteps: [WorkflowStep.FIRST_INITIAL_FORM],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
findByStepKey: jest.fn().mockResolvedValue(null),
|
||||||
|
};
|
||||||
|
|
||||||
|
await service.verifyPartyOtpV2(
|
||||||
|
{
|
||||||
|
sub: String(fileMakerId),
|
||||||
|
role: RoleEnum.FILE_MAKER,
|
||||||
|
firstName: "File",
|
||||||
|
lastName: "Maker",
|
||||||
|
},
|
||||||
|
String(request._id),
|
||||||
|
{
|
||||||
|
phoneNumber: "09121234567",
|
||||||
|
otp: "12345",
|
||||||
|
partyRole: "FIRST",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(request.workflow.currentStep).toBe(WorkflowStep.FIRST_INITIAL_FORM);
|
||||||
|
expect(request.workflow.currentStep).not.toBe(WorkflowStep.FIRST_VIDEO);
|
||||||
|
expect(request.workflow.nextStep).toBe(WorkflowStep.FIRST_VOICE);
|
||||||
|
expect(request.workflow.completedSteps).not.toContain(
|
||||||
|
WorkflowStep.FIRST_VIDEO,
|
||||||
|
);
|
||||||
|
expect(request.save).toHaveBeenCalled();
|
||||||
|
|
||||||
|
const reopened = await service.getMyFileMakerFileDetail(
|
||||||
|
{ sub: String(fileMakerId), role: RoleEnum.FILE_MAKER },
|
||||||
|
String(request._id),
|
||||||
|
);
|
||||||
|
expect(reopened.workflow.currentStep).toBe(WorkflowStep.FIRST_INITIAL_FORM);
|
||||||
|
expect(reopened.workflow.currentStep).not.toBe(WorkflowStep.FIRST_VIDEO);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps FIRST_VIDEO out of the FileMaker car-body workflow", async () => {
|
||||||
|
const fileMakerId = new Types.ObjectId();
|
||||||
|
const request = {
|
||||||
|
_id: new Types.ObjectId(),
|
||||||
|
publicId: "BL-V4-002",
|
||||||
|
type: BlameRequestType.CAR_BODY,
|
||||||
|
expertInitiated: true,
|
||||||
|
isMadeByFileMaker: true,
|
||||||
|
requiresFileMakerApproval: false,
|
||||||
|
initiatedByFieldExpertId: fileMakerId,
|
||||||
|
creationMethod: CreationMethod.IN_PERSON,
|
||||||
|
parties: [{ role: PartyRole.FIRST, person: {} }],
|
||||||
|
workflow: {
|
||||||
|
currentStep: WorkflowStep.CREATED,
|
||||||
|
nextStep: WorkflowStep.CAR_BODY_ACCIDENT_TYPE,
|
||||||
|
completedSteps: [WorkflowStep.CREATED],
|
||||||
|
},
|
||||||
|
history: [],
|
||||||
|
save: jest.fn().mockResolvedValue(undefined),
|
||||||
|
};
|
||||||
|
const service =
|
||||||
|
new (RequestManagementService as any)() as RequestManagementService;
|
||||||
|
(service as any).blameRequestDbService = {
|
||||||
|
findById: jest.fn().mockResolvedValue(request),
|
||||||
|
};
|
||||||
|
(service as any).userDbService = {
|
||||||
|
findOne: jest.fn().mockResolvedValue({
|
||||||
|
_id: new Types.ObjectId(),
|
||||||
|
otp: "hashed-otp",
|
||||||
|
otpExpire: Date.now() + 60_000,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
(service as any).hashService = {
|
||||||
|
compare: jest.fn().mockResolvedValue(true),
|
||||||
|
};
|
||||||
|
(service as any).workflowStepDbService = {
|
||||||
|
findByStepNumber: jest.fn().mockResolvedValue(null),
|
||||||
|
findByStepKey: jest.fn().mockResolvedValue({
|
||||||
|
stepNumber: 2,
|
||||||
|
stepKey: WorkflowStep.CAR_BODY_ACCIDENT_TYPE,
|
||||||
|
nextPossibleSteps: [WorkflowStep.FIRST_VIDEO],
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const actor = {
|
||||||
|
sub: String(fileMakerId),
|
||||||
|
role: RoleEnum.FILE_MAKER,
|
||||||
|
firstName: "File",
|
||||||
|
lastName: "Maker",
|
||||||
|
};
|
||||||
|
|
||||||
|
await service.verifyPartyOtpV2(actor, String(request._id), {
|
||||||
|
phoneNumber: "09121234567",
|
||||||
|
otp: "12345",
|
||||||
|
partyRole: "FIRST",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(request.workflow.currentStep).toBe(
|
||||||
|
WorkflowStep.CAR_BODY_ACCIDENT_TYPE,
|
||||||
|
);
|
||||||
|
expect(request.workflow.nextStep).toBe(WorkflowStep.FIRST_INITIAL_FORM);
|
||||||
|
|
||||||
|
await service.carBodyAccidentTypeFormV3(
|
||||||
|
String(request._id),
|
||||||
|
{ car: true, object: false },
|
||||||
|
actor,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(request.workflow.currentStep).toBe(WorkflowStep.FIRST_INITIAL_FORM);
|
||||||
|
expect(request.workflow.nextStep).toBe(WorkflowStep.FIRST_VOICE);
|
||||||
|
expect(request.workflow.completedSteps).not.toContain(
|
||||||
|
WorkflowStep.FIRST_VIDEO,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("repairs legacy stuck workflow values when a V4 file is reopened", async () => {
|
||||||
|
const fileMakerId = new Types.ObjectId();
|
||||||
|
const request = {
|
||||||
|
_id: new Types.ObjectId(),
|
||||||
|
publicId: "BL-V4-LEGACY",
|
||||||
|
type: BlameRequestType.THIRD_PARTY,
|
||||||
|
isMadeByFileMaker: true,
|
||||||
|
initiatedByFieldExpertId: fileMakerId,
|
||||||
|
workflow: {
|
||||||
|
currentStep: WorkflowStep.FIRST_VIDEO,
|
||||||
|
nextStep: WorkflowStep.FIRST_INITIAL_FORM,
|
||||||
|
completedSteps: [
|
||||||
|
WorkflowStep.CREATED,
|
||||||
|
WorkflowStep.FIRST_BLAME_CONFESSION,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
parties: [
|
||||||
|
{ role: PartyRole.FIRST, person: { userId: new Types.ObjectId() } },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const service =
|
||||||
|
new (RequestManagementService as any)() as RequestManagementService;
|
||||||
|
(service as any).blameRequestDbService = {
|
||||||
|
findById: jest.fn().mockResolvedValue(request),
|
||||||
|
};
|
||||||
|
(service as any).claimCaseDbService = {
|
||||||
|
findOne: jest.fn().mockResolvedValue(null),
|
||||||
|
};
|
||||||
|
|
||||||
|
const reopened = await service.getMyFileMakerFileDetail(
|
||||||
|
{ sub: String(fileMakerId), role: RoleEnum.FILE_MAKER },
|
||||||
|
String(request._id),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(reopened.workflow).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
currentStep: WorkflowStep.FIRST_INITIAL_FORM,
|
||||||
|
nextStep: WorkflowStep.FIRST_VOICE,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(reopened.workflow.completedSteps).not.toContain(
|
||||||
|
WorkflowStep.FIRST_VIDEO,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -6228,8 +6228,9 @@ export class RequestManagementService {
|
|||||||
// the workflow is still at CREATED, advance past any intro step to the first
|
// the workflow is still at CREATED, advance past any intro step to the first
|
||||||
// real data-entry step.
|
// real data-entry step.
|
||||||
//
|
//
|
||||||
// THIRD_PARTY: skip FIRST_BLAME_CONFESSION (first party is always guilty) and
|
// THIRD_PARTY: skip FIRST_BLAME_CONFESSION (first party is always guilty).
|
||||||
// land on FIRST_VIDEO.
|
// V4/V5 FileMaker files have no video step and land directly on the
|
||||||
|
// initial inquiry form; other in-person flows land on FIRST_VIDEO.
|
||||||
// CAR_BODY: no confession exists; advance from CREATED to CAR_BODY_ACCIDENT_TYPE
|
// CAR_BODY: no confession exists; advance from CREATED to CAR_BODY_ACCIDENT_TYPE
|
||||||
// so the expert can fill the car-body form before video.
|
// so the expert can fill the car-body form before video.
|
||||||
if (
|
if (
|
||||||
@@ -6247,9 +6248,10 @@ export class RequestManagementService {
|
|||||||
const carBodyStepDoc = await this.getWorkflowStep({
|
const carBodyStepDoc = await this.getWorkflowStep({
|
||||||
stepKey: WorkflowStep.CAR_BODY_ACCIDENT_TYPE,
|
stepKey: WorkflowStep.CAR_BODY_ACCIDENT_TYPE,
|
||||||
});
|
});
|
||||||
const afterCarBody =
|
const afterCarBody = (req as any).isMadeByFileMaker
|
||||||
(carBodyStepDoc?.nextPossibleSteps?.[0] as WorkflowStep) ??
|
? WorkflowStep.FIRST_INITIAL_FORM
|
||||||
WorkflowStep.FIRST_VIDEO;
|
: ((carBodyStepDoc?.nextPossibleSteps?.[0] as WorkflowStep) ??
|
||||||
|
WorkflowStep.FIRST_VIDEO);
|
||||||
|
|
||||||
req.workflow.completedSteps = completed;
|
req.workflow.completedSteps = completed;
|
||||||
req.workflow.currentStep = WorkflowStep.CAR_BODY_ACCIDENT_TYPE;
|
req.workflow.currentStep = WorkflowStep.CAR_BODY_ACCIDENT_TYPE;
|
||||||
@@ -6269,16 +6271,24 @@ export class RequestManagementService {
|
|||||||
// THIRD_PARTY: skip confession — first party is always guilty in IN_PERSON
|
// THIRD_PARTY: skip confession — first party is always guilty in IN_PERSON
|
||||||
const step2 = await this.getWorkflowStep({ stepNumber: 2 }); // FIRST_BLAME_CONFESSION
|
const step2 = await this.getWorkflowStep({ stepNumber: 2 }); // FIRST_BLAME_CONFESSION
|
||||||
const step2Key = step2.stepKey as WorkflowStep;
|
const step2Key = step2.stepKey as WorkflowStep;
|
||||||
|
const isFileMakerFlow = !!(req as any).isMadeByFileMaker;
|
||||||
|
let advancedTo: WorkflowStep;
|
||||||
|
|
||||||
|
if (isFileMakerFlow) {
|
||||||
|
advancedTo = WorkflowStep.FIRST_INITIAL_FORM;
|
||||||
|
req.workflow.currentStep = advancedTo;
|
||||||
|
req.workflow.nextStep = WorkflowStep.FIRST_VOICE;
|
||||||
|
} else {
|
||||||
const step3 = await this.getWorkflowStep({ stepNumber: 3 }); // FIRST_VIDEO
|
const step3 = await this.getWorkflowStep({ stepNumber: 3 }); // FIRST_VIDEO
|
||||||
const step3Key = step3.stepKey as WorkflowStep;
|
advancedTo = step3.stepKey as WorkflowStep;
|
||||||
const nextAfterVideo =
|
req.workflow.currentStep = advancedTo;
|
||||||
|
req.workflow.nextStep =
|
||||||
(step3.nextPossibleSteps?.[0] as WorkflowStep) ??
|
(step3.nextPossibleSteps?.[0] as WorkflowStep) ??
|
||||||
WorkflowStep.FIRST_INITIAL_FORM;
|
WorkflowStep.FIRST_INITIAL_FORM;
|
||||||
|
}
|
||||||
|
|
||||||
if (!completed.includes(step2Key)) completed.push(step2Key);
|
if (!completed.includes(step2Key)) completed.push(step2Key);
|
||||||
req.workflow.completedSteps = completed;
|
req.workflow.completedSteps = completed;
|
||||||
req.workflow.currentStep = step3Key;
|
|
||||||
req.workflow.nextStep = nextAfterVideo;
|
|
||||||
|
|
||||||
// Auto-guilt: first party is always guilty in expert-initiated IN_PERSON
|
// Auto-guilt: first party is always guilty in expert-initiated IN_PERSON
|
||||||
const fIdx = this.getPartyIndex(req, PartyRole.FIRST);
|
const fIdx = this.getPartyIndex(req, PartyRole.FIRST);
|
||||||
@@ -6303,7 +6313,7 @@ export class RequestManagementService {
|
|||||||
reason:
|
reason:
|
||||||
"IN_PERSON expert-initiated: first party is always guilty; confession auto-resolved",
|
"IN_PERSON expert-initiated: first party is always guilty; confession auto-resolved",
|
||||||
stepKey: step2Key,
|
stepKey: step2Key,
|
||||||
advancedTo: step3Key,
|
advancedTo,
|
||||||
},
|
},
|
||||||
} as any);
|
} as any);
|
||||||
}
|
}
|
||||||
@@ -11750,8 +11760,13 @@ export class RequestManagementService {
|
|||||||
firstParty.carBodyFirstForm.object = body.object;
|
firstParty.carBodyFirstForm.object = body.object;
|
||||||
|
|
||||||
this.pushWorkflowSteps(req, [WorkflowStep.CAR_BODY_ACCIDENT_TYPE]);
|
this.pushWorkflowSteps(req, [WorkflowStep.CAR_BODY_ACCIDENT_TYPE]);
|
||||||
|
if ((req as any).isMadeByFileMaker) {
|
||||||
|
req.workflow.currentStep = WorkflowStep.FIRST_INITIAL_FORM;
|
||||||
|
req.workflow.nextStep = WorkflowStep.FIRST_VOICE;
|
||||||
|
} else {
|
||||||
req.workflow.currentStep = WorkflowStep.CAR_BODY_ACCIDENT_TYPE;
|
req.workflow.currentStep = WorkflowStep.CAR_BODY_ACCIDENT_TYPE;
|
||||||
req.workflow.nextStep = WorkflowStep.FIRST_INITIAL_FORM;
|
req.workflow.nextStep = WorkflowStep.FIRST_INITIAL_FORM;
|
||||||
|
}
|
||||||
|
|
||||||
if (!Array.isArray(req.history)) req.history = [];
|
if (!Array.isArray(req.history)) req.history = [];
|
||||||
req.history.push({
|
req.history.push({
|
||||||
@@ -12712,6 +12727,51 @@ export class RequestManagementService {
|
|||||||
*/
|
*/
|
||||||
// ─── FileMaker file list / detail (V4 + V5) ────────────────────────────────
|
// ─── FileMaker file list / detail (V4 + V5) ────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Older V4/V5 records may have FIRST_VIDEO persisted by the shared in-person
|
||||||
|
* OTP transition. FileMaker flows never exposed that step, so repair the API
|
||||||
|
* projection until the first inquiry submission naturally persists the next
|
||||||
|
* valid workflow state.
|
||||||
|
*/
|
||||||
|
private fileMakerWorkflowProjection(file: any): any {
|
||||||
|
const rawWorkflow = file?.workflow ?? {};
|
||||||
|
const workflow =
|
||||||
|
typeof rawWorkflow.toObject === "function"
|
||||||
|
? rawWorkflow.toObject({ versionKey: false })
|
||||||
|
: { ...rawWorkflow };
|
||||||
|
const completedSteps = Array.isArray(workflow.completedSteps)
|
||||||
|
? workflow.completedSteps.filter(
|
||||||
|
(step: WorkflowStep) => step !== WorkflowStep.FIRST_VIDEO,
|
||||||
|
)
|
||||||
|
: workflow.completedSteps;
|
||||||
|
|
||||||
|
if (
|
||||||
|
workflow.currentStep === WorkflowStep.FIRST_VIDEO &&
|
||||||
|
!(completedSteps ?? []).includes(WorkflowStep.FIRST_INITIAL_FORM)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...workflow,
|
||||||
|
currentStep: WorkflowStep.FIRST_INITIAL_FORM,
|
||||||
|
nextStep: WorkflowStep.FIRST_VOICE,
|
||||||
|
completedSteps,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
file?.type === BlameRequestType.CAR_BODY &&
|
||||||
|
workflow.currentStep === WorkflowStep.CAR_BODY_ACCIDENT_TYPE &&
|
||||||
|
workflow.nextStep === WorkflowStep.FIRST_VIDEO
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...workflow,
|
||||||
|
nextStep: WorkflowStep.FIRST_INITIAL_FORM,
|
||||||
|
completedSteps,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...workflow, completedSteps };
|
||||||
|
}
|
||||||
|
|
||||||
async getMyFileMakerFiles(fileMaker: any): Promise<any[]> {
|
async getMyFileMakerFiles(fileMaker: any): Promise<any[]> {
|
||||||
if (fileMaker?.role !== RoleEnum.FILE_MAKER) {
|
if (fileMaker?.role !== RoleEnum.FILE_MAKER) {
|
||||||
throw new ForbiddenException("Only FileMakers can use this endpoint.");
|
throw new ForbiddenException("Only FileMakers can use this endpoint.");
|
||||||
@@ -12721,7 +12781,9 @@ export class RequestManagementService {
|
|||||||
isMadeByFileMaker: true,
|
isMadeByFileMaker: true,
|
||||||
initiatedByFieldExpertId: makerId,
|
initiatedByFieldExpertId: makerId,
|
||||||
});
|
});
|
||||||
return (files || []).map((f: any) => ({
|
return (files || []).map((f: any) => {
|
||||||
|
const workflow = this.fileMakerWorkflowProjection(f);
|
||||||
|
return {
|
||||||
_id: f._id,
|
_id: f._id,
|
||||||
publicId: f.publicId,
|
publicId: f.publicId,
|
||||||
requestNo: f.requestNo,
|
requestNo: f.requestNo,
|
||||||
@@ -12729,14 +12791,15 @@ export class RequestManagementService {
|
|||||||
status: f.status,
|
status: f.status,
|
||||||
blameStatus: f.blameStatus,
|
blameStatus: f.blameStatus,
|
||||||
workflow: {
|
workflow: {
|
||||||
currentStep: f.workflow?.currentStep,
|
currentStep: workflow.currentStep,
|
||||||
nextStep: f.workflow?.nextStep,
|
nextStep: workflow.nextStep,
|
||||||
completedSteps: f.workflow?.completedSteps,
|
completedSteps: workflow.completedSteps,
|
||||||
},
|
},
|
||||||
requiresFileMakerApproval: f.requiresFileMakerApproval,
|
requiresFileMakerApproval: f.requiresFileMakerApproval,
|
||||||
createdAt: f.createdAt,
|
createdAt: f.createdAt,
|
||||||
updatedAt: f.updatedAt,
|
updatedAt: f.updatedAt,
|
||||||
}));
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getMyFileMakerFileDetail(
|
async getMyFileMakerFileDetail(
|
||||||
@@ -12777,7 +12840,7 @@ export class RequestManagementService {
|
|||||||
type: plain.type,
|
type: plain.type,
|
||||||
status: plain.status,
|
status: plain.status,
|
||||||
blameStatus: plain.blameStatus,
|
blameStatus: plain.blameStatus,
|
||||||
workflow: plain.workflow,
|
workflow: this.fileMakerWorkflowProjection(plain),
|
||||||
requiresFileMakerApproval: plain.requiresFileMakerApproval,
|
requiresFileMakerApproval: plain.requiresFileMakerApproval,
|
||||||
parties: (plain.parties ?? []).map((p: any) => ({
|
parties: (plain.parties ?? []).map((p: any) => ({
|
||||||
role: p.role,
|
role: p.role,
|
||||||
|
|||||||
Reference in New Issue
Block a user