import {
BadRequestException,
CACHE_MANAGER,
Inject,
Injectable,
ServiceUnavailableException,
UnauthorizedException,
} from "@nestjs/common";
import { OrderRequest } from "./entities/orderRequest.entity";
import { InjectModel } from "@nestjs/mongoose";
import { LeanDocument, Model } from "mongoose";
import { OrderRequestDocument } from "./schemas/orderRequest.schema";
import { RequestOrderDto } from "./dtos/create-orderRequest.dto";
import { FilterRequestOrderDto } from "./dtos/filter-orderRequest.dto";
import { ApolloQueryResult } from "@apollo/client";
import { decodeBytes32String, formatEther, formatUnits } from "ethers";
import {
EndUserRoles,
IGetDealerIdAndStatusByOrderIdQuery,
IGetOrderDetailsQuery,
IOrder,
RequestStatus,
} from "src/common/interfaces";
import {
apolloClient,
getDealerIdAndStatusByOrderIdQuery,
getOrderDetailsQuery,
} from "src/common/provider";
import { UserService } from "src/user/user.service";
import { EmailService } from "src/common/provider/mail/email.service";
import {
OrderApprovedTemplate,
OrderRejectedTemplate,
OrderArchivedTemplate,
} from "src/common/provider/mail/templates";
import { BLOCKCHAIN_EXPLORER } from "src/common/constants";
import { getFundName } from "src/common/utils";
import { Cache } from "cache-manager";
@Injectable()
export class OrderRequestService {
@Inject(UserService)
private readonly userService: UserService;
@Inject(EmailService)
private readonly emailService: EmailService;
@Inject(CACHE_MANAGER)
private readonly cacheService: Cache;
constructor(
@InjectModel(OrderRequest.name)
private readonly orderRequestModel: Model<OrderRequestDocument>,
) {}
async create(requestOrderDto: RequestOrderDto) {
return this.orderRequestModel.create(requestOrderDto);
}
async update(
id: string,
updateRequestOrderDto: FilterRequestOrderDto & { investorId?: string },
): Promise<OrderRequestDocument> {
const [orderRequest, subgraphOrderbook, cachedOrder]: [
OrderRequestDocument,
ApolloQueryResult<IGetOrderDetailsQuery>,
Partial<IOrder>,
] = await Promise.all([
this.orderRequestModel
.findByIdAndUpdate(id, updateRequestOrderDto, {
new: true,
})
.exec(),
apolloClient.query({
query: getOrderDetailsQuery,
variables: {
orderBook: updateRequestOrderDto.orderId.split("-")[0],
orderId: updateRequestOrderDto.orderId,
},
}),
this.cacheService.get(updateRequestOrderDto.orderId),
]);
if (subgraphOrderbook.errors) {
console.error(subgraphOrderbook.errors[0]);
throw new ServiceUnavailableException(subgraphOrderbook.errors);
}
const order =
subgraphOrderbook.data.order &&
subgraphOrderbook.data.order.confirmedTxHash
? subgraphOrderbook.data.order
: cachedOrder;
const investor = await this.userService.findOne(
decodeBytes32String(order.onBehalf.id),
);
const fundId = decodeBytes32String(
subgraphOrderbook.data.orderBook.fund.id,
);
if (
!investor.subscribedFunds.find((fund) => fund._id.toString() === fundId)
) {
throw new BadRequestException(
"Investor is not subscribed to this fund anymore",
);
}
if (!subgraphOrderbook.data.order) {
return orderRequest;
}
let emailTemplate = OrderApprovedTemplate({
orderType: order.type,
txHashUrl: `${BLOCKCHAIN_EXPLORER}/tx/${order.confirmedTxHash}`,
fundName: getFundName(investor, subgraphOrderbook.data.orderBook.fund.id),
instrumentName:
subgraphOrderbook.data.orderBook.instrument.securityToken.name,
txTime: new Date().toUTCString(),
amount:
order.type === "Redemption"
? `${formatEther(order.amount)} ${
subgraphOrderbook.data.orderBook.instrument.securityToken.symbol
}`
: `${formatUnits(order.amount, 6)} USD`,
investorName: investor.name,
});
if (orderRequest.status === RequestStatus.rejected) {
emailTemplate = OrderRejectedTemplate({
orderType: order.type,
txHashUrl: `${BLOCKCHAIN_EXPLORER}/tx/${order.confirmedTxHash}`,
fundName: getFundName(
investor,
subgraphOrderbook.data.orderBook.fund.id,
),
instrumentName:
subgraphOrderbook.data.orderBook.instrument.securityToken.name,
txTime: new Date().toUTCString(),
amount:
order.type === "Redemption"
? `${formatEther(order.amount)} ${
subgraphOrderbook.data.orderBook.instrument.securityToken.symbol
}`
: `${formatUnits(order.amount, 6)} USD`,
optionalComment: orderRequest.optionalComment || "No comment provided",
});
}
if (orderRequest.status === RequestStatus.archived) {
emailTemplate = OrderArchivedTemplate({
orderType: order.type,
txHashUrl: `${BLOCKCHAIN_EXPLORER}/tx/${order.confirmedTxHash}`,
fundName: getFundName(
investor,
subgraphOrderbook.data.orderBook.fund.id,
),
instrumentName:
subgraphOrderbook.data.orderBook.instrument.securityToken.name,
txTime: new Date().toUTCString(),
amount:
order.type === "Redemption"
? `${formatEther(order.amount)} ${
subgraphOrderbook.data.orderBook.instrument.securityToken.symbol
}`
: `${formatUnits(order.amount, 6)} USD`,
});
}
await this.emailService.sendEmail({
from: "yehia@nethermind.io",
to: [investor.email],
subject: `Order has been ${orderRequest.status}`,
html: emailTemplate,
});
return orderRequest;
}
async findRequestOrderById(id: string) {
return this.orderRequestModel.findById(id).lean().exec();
}
async findRequestOrderbyOrderId(id: string, dealerEmail?: string) {
if (dealerEmail) {
await this.checkIfUserIsAllowedToAccessOrder(dealerEmail, id);
}
return this.orderRequestModel.findOne({ orderId: id }).lean().exec();
}
async checkIfUserIsAllowedToAccessOrder(email: string, orderId: string) {
const user = await this.userService.findUserByProperty({
email: email,
});
const dealerIdSubgraph: ApolloQueryResult<IGetDealerIdAndStatusByOrderIdQuery> =
await apolloClient.query({
query: getDealerIdAndStatusByOrderIdQuery,
variables: {
orderId,
},
});
if (!dealerIdSubgraph.data.order) {
throw new BadRequestException(
`The order with id ${orderId} does not exist`,
);
}
if (
user.endUserRole === EndUserRoles.dealer &&
decodeBytes32String(dealerIdSubgraph.data.order.dealer.id) !==
user._id.toString()
) {
throw new UnauthorizedException(
"The dealer is not assigned to this order",
);
}
if (
user.endUserRole === EndUserRoles.investor &&
decodeBytes32String(dealerIdSubgraph.data.order.onBehalf.id) !==
user._id.toString()
) {
throw new UnauthorizedException(
"The investor is not related to this order",
);
}
}
async getRequestedOrders(
filterRequestOrderDto: FilterRequestOrderDto,
colName?: string,
valuesIn?: string[],
): Promise<LeanDocument<OrderRequestDocument>[]> {
const { limit, skip, orderDirection, ...restOfFilter } =
filterRequestOrderDto ?? {};
return await this.orderRequestModel
.find({
[colName]: { $in: valuesIn },
...restOfFilter,
})
.limit(limit)
.skip(skip * limit)
.sort({ created: orderDirection })
.lean()
.exec();
}
}