File

src/user/fundAdmin.service.ts

Index

Properties
Methods

Constructor

constructor(accessRequestModel: Model, userModel: Model<UserDocument>)
Parameters :
Name Type Optional
accessRequestModel Model<AccessRequestDocument> No
userModel Model<UserDocument> No

Methods

Async aggregateQuery
aggregateQuery(requestAccessByStatusDto: RequestAccessByStatusDto, match?: IMatchQuery)
Parameters :
Name Type Optional
requestAccessByStatusDto RequestAccessByStatusDto No
match IMatchQuery Yes
Returns : unknown
Async create
create(createAccessRequests: Omit[])
Parameters :
Name Type Optional
createAccessRequests Omit<AccessRequest, status>[] No
Returns : Promise<AccessRequestDocument[]>
Async deleteAccessRequestForUser
deleteAccessRequestForUser(userId: ObjectId | LeanDocument<UserDocument>)
Parameters :
Name Type Optional
userId ObjectId | LeanDocument<UserDocument> No
Returns : Promise<void>
Async getRequestDetails
getRequestDetails(id: string)
Parameters :
Name Type Optional
id string No
Returns : Promise<AccessRequestDetails>
Async getRequestsByFundsId
getRequestsByFundsId(userRole: EndUserRoles, fundIds: string[], userIds: string[], requestAccessByStatusDto: RequestAccessByStatusDto)
Parameters :
Name Type Optional
userRole EndUserRoles No
fundIds string[] No
userIds string[] No
requestAccessByStatusDto RequestAccessByStatusDto No
Async setInvestorAccessForInstrument
setInvestorAccessForInstrument(userJwtPayload: UserJwtPayload, setInvestorAccessDto: SetInvestorAccessDto)
Parameters :
Name Type Optional
userJwtPayload UserJwtPayload No
setInvestorAccessDto SetInvestorAccessDto No
Returns : unknown
Async updateRequest
updateRequest(id: string, updateRequestStatus: IUpdateAccessRequest)
Parameters :
Name Type Optional
id string No
updateRequestStatus IUpdateAccessRequest No
Returns : Promise<AccessRequestDocument>

Properties

Private Readonly emailService
Type : EmailService
Decorators :
@Inject(EmailService)
Private Readonly internalCustodialService
Type : InternalCustodialService
Decorators :
@Inject(InternalCustodialService)
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();
  }
}

results matching ""

    No results matching ""