src/user/fundAdmin.service.ts
Properties |
|
Methods |
|
constructor(accessRequestModel: Model
|
|||||||||
|
Defined in src/user/fundAdmin.service.ts:57
|
|||||||||
|
Parameters :
|
| Async aggregateQuery | |||||||||
aggregateQuery(requestAccessByStatusDto: RequestAccessByStatusDto, match?: IMatchQuery)
|
|||||||||
|
Defined in src/user/fundAdmin.service.ts:327
|
|||||||||
|
Parameters :
Returns :
unknown
|
| Async create | ||||||
create(createAccessRequests: Omit
|
||||||
|
Defined in src/user/fundAdmin.service.ts:63
|
||||||
|
Parameters :
Returns :
Promise<AccessRequestDocument[]>
|
| Async deleteAccessRequestForUser | ||||||
deleteAccessRequestForUser(userId: ObjectId | LeanDocument<UserDocument>)
|
||||||
|
Defined in src/user/fundAdmin.service.ts:111
|
||||||
|
Parameters :
Returns :
Promise<void>
|
| Async getRequestDetails | ||||||
getRequestDetails(id: string)
|
||||||
|
Defined in src/user/fundAdmin.service.ts:157
|
||||||
|
Parameters :
Returns :
Promise<AccessRequestDetails>
|
| Async getRequestsByFundsId | |||||||||||||||
getRequestsByFundsId(userRole: EndUserRoles, fundIds: string[], userIds: string[], requestAccessByStatusDto: RequestAccessByStatusDto)
|
|||||||||||||||
|
Defined in src/user/fundAdmin.service.ts:117
|
|||||||||||||||
|
Parameters :
Returns :
Promise<IGetRequestsByFundsId[]>
|
| Async setInvestorAccessForInstrument | |||||||||
setInvestorAccessForInstrument(userJwtPayload: UserJwtPayload, setInvestorAccessDto: SetInvestorAccessDto)
|
|||||||||
|
Defined in src/user/fundAdmin.service.ts:249
|
|||||||||
|
Parameters :
Returns :
unknown
|
| Async updateRequest | |||||||||
updateRequest(id: string, updateRequestStatus: IUpdateAccessRequest)
|
|||||||||
|
Defined in src/user/fundAdmin.service.ts:173
|
|||||||||
|
Parameters :
Returns :
Promise<AccessRequestDocument>
|
| Private Readonly emailService |
Type : EmailService
|
Decorators :
@Inject(EmailService)
|
|
Defined in src/user/fundAdmin.service.ts:57
|
| Private Readonly internalCustodialService |
Type : InternalCustodialService
|
Decorators :
@Inject(InternalCustodialService)
|
|
Defined in src/user/fundAdmin.service.ts:54
|
import { LeanDocument, Model, ObjectId, PipelineStage, Types } from "mongoose";
import {
BadRequestException,
Inject,
Injectable,
ServiceUnavailableException,
} from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import {
AccessRequest,
AccessRequestDocument,
} from "src/user/schemas/access-request.schema";
import {
ContractName,
EndUserRoles,
Instrument,
RequestStatus,
TransactionNames,
UserJwtPayload,
} from "src/common/interfaces";
import { User, UserDocument } from "./schemas/user.schema";
import type { RequestAccessByStatusDto } from "./dto/requestAccess-status.dto";
import type { AccessRequestDetails } from "./entities/access-request.entity";
import {
IGetRequestsByFundsId,
IMatchQuery,
IUpdateAccessRequest,
} from "./user.interface";
import { Fund, FundDocument } from "src/security/schemas/fund.schema";
import { encodeBytes32String } from "ethers";
import {
apolloClient,
getRoleAndContractAddressByNameOrAddress,
transactionSubmitter,
} from "src/common/provider";
import { SetInvestorAccessDto } from "./dto/set-investor-access.dto";
import { InternalCustodialService } from "src/shared/custodial/internalCustodial.service";
import { getUserWithUnSignedUrl } from "src/common/utils";
import { EmailService } from "src/common/provider/mail/email.service";
import {
AllowInvestorAccessTemplate,
UserIsBlacklistedTemplate,
} from "src/common/provider/mail/templates";
import { RejectInstrumentAccessTemplate } from "src/common/provider/mail/templates/rejectInstrumentAccess";
import { ApolloQueryResult } from "@apollo/client";
import { getInstrumentByIdQuery } from "src/common/provider/thegraph/queries/instrument.qgl";
import { AllowInstrumentAccessTemplate } from "src/common/provider/mail/templates/allowInstrumentAccess";
@Injectable()
export class FundAdminService {
@Inject(InternalCustodialService)
private readonly internalCustodialService: InternalCustodialService;
@Inject(EmailService)
private readonly emailService: EmailService;
constructor(
@InjectModel(AccessRequest.name)
private readonly accessRequestModel: Model<AccessRequestDocument>,
@InjectModel(User.name) private readonly userModel: Model<UserDocument>,
) {}
async create(
createAccessRequests: Omit<AccessRequest, "status">[],
): Promise<AccessRequestDocument[]> {
try {
// Extract userIds and fundIds from createAccessRequests
const userIds = createAccessRequests.map(({ userId }) => userId);
const fundIds = createAccessRequests.map(({ fundId }) => fundId);
// Query the database to find approved access requests for the specified users and funds
const previousRequest = await this.accessRequestModel
.findOne({
userId: { $in: userIds },
fundId: { $in: fundIds },
})
.sort({ created: -1 })
.exec();
if (
previousRequest &&
previousRequest.status === RequestStatus.accepted
) {
// If any existing requests are approved, throw a BadRequestException
throw new BadRequestException(
"User have already been approved for this fund",
);
}
if (
previousRequest &&
previousRequest.status === RequestStatus.underReview
) {
// If any existing requests still under review, throw a BadRequestException
throw new BadRequestException("User request still under review");
}
return this.accessRequestModel.create(
createAccessRequests.map((accessRequest) => {
return { ...accessRequest, status: RequestStatus.underReview };
}),
);
} catch (error) {
if (error.status === 400) {
throw new BadRequestException(error);
}
throw new ServiceUnavailableException(error);
}
}
async deleteAccessRequestForUser(
userId: ObjectId | LeanDocument<UserDocument>,
): Promise<void> {
await this.accessRequestModel.deleteMany({ userId }).exec();
}
async getRequestsByFundsId(
userRole: EndUserRoles,
fundIds: string[],
userIds: string[],
requestAccessByStatusDto: RequestAccessByStatusDto,
): Promise<IGetRequestsByFundsId[]> {
if (!userIds.length && userRole === EndUserRoles.dealer) {
return [];
}
if (!fundIds.length && userRole === EndUserRoles.fundAdmin) {
return [];
}
const matchQuery =
userIds.length > 0
? {
fundId: {
$in: fundIds.map((fundId) =>
Types.ObjectId.createFromHexString(fundId),
),
},
userId: {
$in: userIds.map((userId) =>
Types.ObjectId.createFromHexString(userId),
),
},
}
: {
fundId: {
$in: fundIds.map((fundId) =>
Types.ObjectId.createFromHexString(fundId),
),
},
};
return this.aggregateQuery(
requestAccessByStatusDto,
matchQuery as unknown as IMatchQuery,
);
}
async getRequestDetails(id: string): Promise<AccessRequestDetails> {
const requestDetails = await this.accessRequestModel
.findById(id)
.populate(["fundId", "userId"])
.lean()
.exec();
const userSignedUrl = await getUserWithUnSignedUrl(
requestDetails.userId as User,
);
return {
_id: id,
status: requestDetails.status,
fundId: requestDetails.fundId,
userId: userSignedUrl as UserDocument,
};
}
async updateRequest(
id: string,
updateRequestStatus: IUpdateAccessRequest,
): Promise<AccessRequestDocument> {
try {
const { currentRequest, optionalComment, status } = updateRequestStatus;
const updateAction =
updateRequestStatus.status === RequestStatus.accepted
? {
$addToSet: {
subscribedFunds: (currentRequest.fundId as Fund)._id,
},
}
: {
$pull: {
subscribedFunds: (currentRequest.fundId as Fund)._id,
},
};
const [, newRequest] = await Promise.all([
this.userModel
.findByIdAndUpdate((currentRequest.userId as UserDocument)._id, {
...updateAction,
})
.exec(),
this.accessRequestModel.create({
status,
optionalComment,
userId: (currentRequest.userId as UserDocument)._id,
fundId: (currentRequest.fundId as FundDocument)._id,
}),
]);
const investor: LeanDocument<UserDocument> =
currentRequest.userId as LeanDocument<UserDocument>;
const fund: LeanDocument<FundDocument> =
currentRequest.fundId as LeanDocument<FundDocument>;
const emailObject =
updateRequestStatus.status === RequestStatus.accepted
? {
subject: `Access request for the ${fund.title} fund for user ${investor.name} was approved`,
html: AllowInvestorAccessTemplate({
investor: {
name: investor.name,
email: investor.email,
},
fundName: fund.title,
}),
}
: {
subject: `${investor.name} was denied access to ${fund.title} fund`,
html: UserIsBlacklistedTemplate({
type: EndUserRoles.investor,
email: investor.email,
userName: investor.name,
fundName: fund.title,
}),
};
if (
updateRequestStatus.status === RequestStatus.accepted ||
updateRequestStatus.status === RequestStatus.rejected
) {
await this.emailService.sendEmail({
from: "yehia@nethermind.io",
to: [investor.email, investor.onboardByEmail],
...emailObject,
});
}
return newRequest;
} catch (error) {
throw new ServiceUnavailableException(error);
}
}
async setInvestorAccessForInstrument(
userJwtPayload: UserJwtPayload,
setInvestorAccessDto: SetInvestorAccessDto,
) {
const { instrumentId, investorId, isAllowed } = setInvestorAccessDto;
let signerKey = process.env.RELAYER_KEY;
let user: LeanDocument<UserDocument>;
if (userJwtPayload.role !== "admin") {
[user] = await this.userModel
.find({
email: userJwtPayload.email,
})
.lean()
.exec();
signerKey = (
await this.internalCustodialService.findOne(user.wallets[0].address)
).privateKey;
}
const { contractAddress, role } =
await getRoleAndContractAddressByNameOrAddress({
contractName: ContractName.InstrumentRegistry,
transactionName: TransactionNames.allowInvestor,
userAddress:
userJwtPayload.role !== "admin"
? user.wallets[0].address
: process.env.ADMIN_WALLET_ADDRESS,
});
const txResponse = await transactionSubmitter({
signerKey,
contractAddress,
contractName: ContractName.InstrumentRegistry,
transactionName: TransactionNames.allowInvestor,
args: [
encodeBytes32String(instrumentId),
encodeBytes32String(investorId),
isAllowed,
role,
],
});
let subject;
let htmlTemplate;
const [investor, instrumentGraph]: [
LeanDocument<UserDocument>,
ApolloQueryResult<{ instrument: Instrument }>,
] = await Promise.all([
this.userModel.findOne({
_id: investorId,
}),
apolloClient.query({
query: getInstrumentByIdQuery,
variables: { instrumentId: encodeBytes32String(instrumentId) },
}),
]);
const instrument = instrumentGraph.data.instrument;
const instrumentName = instrument.securityToken.name;
if (setInvestorAccessDto.isAllowed) {
subject = `Access to instrument ${instrumentName} is approved for user ${investor.name}`;
htmlTemplate = AllowInstrumentAccessTemplate({
instrumentName: instrumentName,
investor: investor,
});
} else {
subject = `Access to instrument ${instrumentName} is denied for user ${investor.name}`;
htmlTemplate = RejectInstrumentAccessTemplate({
instrumentName: instrumentName,
investor: investor,
});
}
await this.emailService.sendEmail({
from: "yehia@nethermind.io",
to: [investor.email],
subject,
html: htmlTemplate,
});
return txResponse;
}
async aggregateQuery(
requestAccessByStatusDto: RequestAccessByStatusDto,
match?: IMatchQuery,
) {
const { limit, skip, orderDirection, status, ...restOfFilter } =
requestAccessByStatusDto ?? {};
const pipelineStage: PipelineStage[] = [
{
$match: { ...match, ...restOfFilter },
},
{
$lookup: {
from: "funds", // The name of the "funds" collection
localField: "fundId",
foreignField: "_id",
as: "fund",
},
},
{
$lookup: {
from: "users", // The name of the "users" collection
localField: "userId",
foreignField: "_id",
as: "user",
},
},
{
$unwind: "$user",
},
{
$unwind: "$fund",
},
{
$group: {
_id: {
userId: "$userId",
fundId: "$fundId",
},
user: { $first: "$user" },
fund: { $first: "$fund" },
accessRequests: { $push: "$$ROOT" },
},
},
{
$sort: {
_id: orderDirection === "asc" ? 1 : -1,
},
},
{
$skip: skip * limit || 0, // aggregate doesn't support NaN on skip and limit
},
{
$limit: limit || 10000, // aggregate doesn't support NaN on skip and limit
},
{
$project: {
_id: { $arrayElemAt: ["$accessRequests._id", -1] },
currentStatus: { $arrayElemAt: ["$accessRequests.status", -1] },
accessRequests: {
$map: {
input: "$accessRequests",
as: "ar",
in: {
_id: "$$ar._id",
status: "$$ar.status",
optionalComment: "$$ar.optionalComment",
created: "$$ar.created",
updated: "$$ar.updated",
},
},
},
user: 1, // Include the user object at the top level
fund: 1, // Include the fund object at the top level
},
},
];
if (status) {
pipelineStage.push({
$match: { currentStatus: status },
});
}
return this.accessRequestModel.aggregate(pipelineStage).exec();
}
}