Skip to content

Instantly share code, notes, and snippets.

@MohammedEsafi
Created July 17, 2024 14:54
Show Gist options
  • Save MohammedEsafi/6e51d3070b15c96fbcadc2c6c2a420ec to your computer and use it in GitHub Desktop.
Save MohammedEsafi/6e51d3070b15c96fbcadc2c6c2a420ec to your computer and use it in GitHub Desktop.
async bindInvestment({ investmentId, portalUser }) {
const investment = await PmInvestmentOrderModel.findById(investmentId);
if (!investment) {
throw new NotFound('The investment not found.');
}
if (PmInvestmentOrderStatusRev.CANCELED.includes(investment.status)) {
throw new BadRequest("You can't bind the canceled investment.");
}
if (PmInvestmentOrderStatusRev.BINDING.includes(investment.status)) {
throw new BadRequest('The investment already been binded.');
}
const clientAccount = await this.getClientAccount(investment.client);
const investmentFees = investment.managementFees;
const reservedAmount = investment.amount + investmentFees;
if (clientAccount.balance < reservedAmount) {
throw new BadRequest('Insufficient account balance.');
}
await mongoose.connection.transaction(async session => {
try {
await this.externalBindInvestment({
accountId: clientAccount.id,
userId: clientAccount.user_id,
requestId: investment.requestId,
managementFees: investmentFees,
amount: investment.amount
});
await Promise.all([
investment.update(
{
status: confirmedByInternal,
isReserved: true,
$push: {
statusHistory: {
amount: investment.amount,
status: confirmedByInternal,
createdBy: portalUser
}
}
},
{ session }
)
]);
// DO: sending email to the client regarding investment bind action.
{
const clientInfo = await clientData(investment.client);
sendInvestmentBindEmail({
client: {
name: getUsername(clientInfo.name),
email: clientInfo.email.value
},
smartfolio: investment.smartfolio,
className: investment.class,
investment: {
amount: investment.amount,
fees: investmentFees,
reservedAmount: reservedAmount,
status: investment.status,
createdAt: investment.createdAt,
bindingDate: new Date()
},
// Todo: refactot gettting factsheets
buttons: this.#getSmartfolioFactsheets(
investment.smartfolio,
investment.smartfolioId,
investment.tickers
)
})
.then(() => {
console.log(
'Investment confirmation notification emails was sent.'
);
})
.catch(error => {
console.error(
'Sending investment confirmation notification emails encountered an error.',
error
);
});
}
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
await session.endSession();
}
});
return PmInvestmentOrderModel.findById(investmentId);
}
async cancelInvestment({ investmentId, portalUser, attachments, comment }) {
const investment = await PmInvestmentOrderModel.findById(investmentId);
if (!investment) {
throw new NotFound('The investment not found.');
}
if (PmInvestmentOrderStatusRev.CANCELED.includes(investment.status)) {
throw new BadRequest('The investment already been cancelled.');
}
if (PmInvestmentOrderStatusRev.COMMITTED.includes(investment.status)) {
throw new BadRequest("You can't cancel the committed investment.");
}
await mongoose.connection.transaction(async session => {
try {
const clientAccount = await this.getClientAccount(investment.client);
const reservedAmount = investment.amount + investment.managementFees;
const isAmountInsufficient =
clientAccount.balance < reservedAmount &&
!PmInvestmentOrderStatusRev.BINDING.includes(investment.status);
if (PmInvestmentOrderStatusRev.BINDING.includes(investment.status)) {
await this.externalCancelInvestment({
requestId: investment.requestId,
userId: clientAccount.user_id
});
}
const attachmentsId = ObjectId();
await Promise.all([
investment.update(
{
status: rejectedByInternal,
isReserved: false,
rejectionReason: comment,
rejectionAttachments: attachmentsId,
$push: {
statusHistory: {
amount: investment.amount,
status: rejectedByInternal,
createdBy: portalUser
}
}
},
{ session }
),
new PmAttachmentGroupModel({
_id: attachmentsId,
smartfolio: investment.smartfolio,
createdBy: portalUser,
title: `Canceling an investment ${new Date().toISOString()}`,
description: `The investment with Id \`${investmentId}\` canceled cause to: ${comment}`,
attachments
}).save({ session })
]);
// DO: sending email to the client regarding investment cancellation action.
{
const sender = isAmountInsufficient
? sendInvestmentCancellationDueToInsufficientAmountEmail
: sendInvestmentCancellationEmail;
const clientInfo = await clientData(investment.client);
sender({
client: {
name: getUsername(clientInfo.name),
email: clientInfo.email.value
},
smartfolio: investment.smartfolio,
className: investment.class,
investment: {
amount: investment.amount,
status: 'CANCELED',
createdAt: investment.createdAt,
cancellationDate: new Date()
}
})
.then(() => {
console.log(
'Investment cancellation notification emails was sent.'
);
})
.catch(error => {
console.error(
'Sending investment cancellation notification emails encountered an error.',
error
);
});
}
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
await session.endSession();
}
});
return PmInvestmentOrderModel.findById(investmentId);
}
async createPrivateSmartfolioCommitment({ smartfolio, createdBy }) {
const commitment = await this.getCommitmentBySmartfolio({ smartfolio });
if (
commitment &&
commitment.status === PRIVATE_MARKET_COMMITMENT_STATUS.PENDING
) {
throw new BadRequest('The commitment is already created.');
}
if (
commitment &&
commitment.status === PRIVATE_MARKET_COMMITMENT_STATUS.CONFIRMED
) {
throw new BadRequest('The commitment is already confirmed.');
}
const investments = await this.getBindedInvestments({ smartfolio });
if (investments.length === 0) {
throw new BadRequest('There is no investments to commit.');
}
const smartfolioInfo = await this.getPrivateSmartfolioInfo({ smartfolio });
const commitmentDocument = await PmCommitmentModel.create({
class: smartfolioInfo.asset_class,
smartfolio,
statusHistory: [
{
createdBy: createdBy
}
]
});
// DO: sending all related emails regarding commitment creation action.
{
const commitmentCreatorInfo = await getUserInfo(createdBy.reference);
sendCommitCreationEmail({
commitmentCreatorName: getUsername(commitmentCreatorInfo.name),
smartfolio
})
.then(() => {
console.log('Commit creation notification emails was sent.');
})
.catch(error => {
console.error(
'Sending commit creation notification emails encountered an error.',
error
);
});
}
return commitmentDocument;
}
async confirmPrivateSmartfolioCommitment({ smartfolio, confirmedBy }) {
const pmCommitment = await this.getCommitmentBySmartfolio({ smartfolio });
if (
pmCommitment &&
pmCommitment.status === PRIVATE_MARKET_COMMITMENT_STATUS.CONFIRMED
) {
throw new BadRequest('The commitment is already confirmed.');
}
if (
!pmCommitment ||
pmCommitment.status === PRIVATE_MARKET_COMMITMENT_STATUS.REJECTED
) {
throw new NotFound(
"The commitment doesn't exist or rejected, create a new one."
);
}
if (
pmCommitment.statusHistory.at(0).createdBy.reference ===
confirmedBy.reference
) {
throw new Forbidden(
'The admin who created the commitment can not confirm it as well.'
);
}
const investments = await this.getBindedInvestments({ smartfolio });
if (investments.length === 0) {
throw new BadRequest('There is no investments to commit.');
}
await mongoose.connection.transaction(async session => {
let initialAmount = 0;
const transactions = [];
const accountsUpdate = [];
investments.forEach(async investment => {
const investmentFees = investment.managementFees;
accountsUpdate.push({
requestId: investment.requestId,
userId: investment.userId
});
transactions.push({
clientId: investment.client,
amount: -investment.amount,
type: 'INVEST',
details: 'PRIVATE MARKET INVESTMENT'
});
initialAmount += investment.amount;
investment.remainingCapitalCallAmount = investment.amount;
investment.investmentNAV = investment.amount;
investment.pmCommitmentId = pmCommitment._id;
investment.status = approvedByInternal;
if (!investment.isCommissionTaken) {
investment.isCommissionTaken = true;
investment.commission = investmentFees;
}
investment.statusHistory.push({
status: investment.status,
amount: investment.amount,
createdBy: confirmedBy
});
});
const normalizedInitialAmount = initialAmount;
try {
await Promise.all([
PmTransactionModel.insertMany(transactions, { session }),
PmCommitmentNavHistoryModel.create(
[
{
pmCommitmentId: pmCommitment._id,
nav: normalizedInitialAmount,
date: new Date(),
createdBy: confirmedBy,
comment: 'Initial NAV (Automatically initialized)'
}
],
{ session }
),
pmCommitment.updateOne(
{
initialAmount: normalizedInitialAmount,
remainingCapitalCallAmount: normalizedInitialAmount,
latestNav: normalizedInitialAmount,
status: PRIVATE_MARKET_COMMITMENT_STATUS.CONFIRMED,
$push: {
statusHistory: [
{
createdBy: confirmedBy,
status: PRIVATE_MARKET_COMMITMENT_STATUS.CONFIRMED
}
]
}
},
{ session }
),
...investments.map(async investment => investment.save({ session }))
]);
await Promise.allSettled(
accountsUpdate.map(this.externalCommitInvestment)
);
// DO: sending all related emails regarding commitment confirmation action.
{
const [commitCreatorInfo, commitConfirmerInfo] = await Promise.all([
getUserInfo(pmCommitment.statusHistory.at(0).createdBy.reference),
getUserInfo(confirmedBy.reference)
]);
const clientsMap = await this.#getClientsInfoMap(
investments.map(({ client }) => client).join(',')
);
sendCommitConfirmationEmail({
smartfolio,
className: investments[0].class,
commitment: {
creatorName: getUsername(commitCreatorInfo.name),
confirmerName: getUsername(commitConfirmerInfo.name)
},
investments: investments.map(investment => {
const clientInfo = clientsMap.get(investment.client.toString());
return {
client: {
name: getUsername(clientInfo.name),
email: clientInfo.email.value
},
committedAmount: investment.amount,
fees: investment.commission,
createdAt: investment.createdAt,
bindingDate: investment.statusHistory.findLast(({ status }) =>
PmInvestmentOrderStatusRev.BINDING.includes(status)
).createdAt,
commitmentDate: new Date()
};
})
})
.then(() => {
console.log('Commit confirmation notification emails was sent.');
})
.catch(error => {
console.error(
'Sending commit confirmation notification emails encountered an error.',
error
);
});
}
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
await session.endSession();
}
});
return PmCommitmentModel.findById(pmCommitment._id);
}
async rejectPrivateSmartfolioCommitment({ smartfolio, rejectedBy }) {
const commitment = await this.getCommitmentBySmartfolio({ smartfolio });
if (!commitment) {
throw new NotFound("The commitment doesn't exist");
}
if (
commitment.statusHistory.at(0).createdBy.reference ===
rejectedBy.reference
) {
throw new Forbidden(
'The admin who created the commitment can not reject it as well.'
);
}
if (commitment.status !== PRIVATE_MARKET_COMMITMENT_STATUS.PENDING) {
throw new BadRequest(`The commitment is on ${commitment.status} status.`);
}
commitment.status = PRIVATE_MARKET_COMMITMENT_STATUS.REJECTED;
commitment.statusHistory.push({
status: PRIVATE_MARKET_COMMITMENT_STATUS.REJECTED,
createdBy: rejectedBy
});
await commitment.save();
// DO: sending all related emails regarding commitment rejection action.
{
const [commitCreatorInfo, commitRejecterInfo] = await Promise.all([
getUserInfo(commitment.statusHistory.at(0).createdBy.reference),
getUserInfo(rejectedBy.reference)
]);
sendCommitRejectionEmail({
commitment: {
creatorName: getUsername(commitCreatorInfo.name),
rejecterName: getUsername(commitRejecterInfo.name)
},
smartfolio
})
.then(() => {
console.log('Commit rejection notification emails was sent.');
})
.catch(error => {
console.error(
'Sending commit rejection notification emails encountered an error.',
error
);
});
}
}
async createPrivateMarketCapitalCall(
{ smartfolio, privateEquity, amount, attachments, portalUser: createdBy },
_session
) {
const commitment = await this.getCommitmentBySmartfolio({
smartfolio,
status: PRIVATE_MARKET_COMMITMENT_STATUS.CONFIRMED
});
if (!commitment) {
throw new BadRequest("The commitment doesn't exist");
}
if (commitment.remainingCapitalCallAmount < amount) {
throw new BadRequest('insufficient amount for this smartfolio');
}
const investmentsOrders = await PmInvestmentOrderModel.find({
pmCommitmentId: ObjectId(commitment._id)
});
if (investmentsOrders.length === 0) {
return null;
}
return useMongooseSession(async session => {
const [capitalCallDocument] = await Promise.all([
new PmSmartfolioCapitalCallModel({
pmCommitmentId: commitment._id,
smartfolio,
amount: amount,
privateEquity,
createdBy,
attachments
}).save({ session }),
new PmAttachmentGroupModel({
smartfolio,
createdBy,
title: `Payment creation ${new Date().toISOString()}`,
description: `Private Equity: ${privateEquity}`,
attachments
}).save({ session })
]);
// DO: sending all related emails regarding payment creation action.
{
const paymentCreatorInfo = await getUserInfo(
capitalCallDocument.createdBy.reference
);
sendPaymentCreationEmail({
payment: {
id: capitalCallDocument._id,
creatorName: getUsername(paymentCreatorInfo.name)
},
smartfolio: capitalCallDocument.smartfolio,
privateEquity: capitalCallDocument.privateEquity
})
.then(() => {
console.log('Payment creation notification emails was sent.');
})
.catch(error => {
console.error(
'Sending payment creation notification emails encountered an error.',
error
);
});
}
return capitalCallDocument;
}, _session);
}
async confirmPrivateMarketCapitalCall({
capitalCallId,
comment,
attachments,
portalUser: confirmedBy
}) {
const capitalCall = await PmSmartfolioCapitalCallModel.findOne({
_id: capitalCallId
});
if (!capitalCall) {
throw new NotFound('No payment with this id');
}
if (capitalCall.status !== PRIVATE_EQUITY_CAPITAL_CALL_STATUS.PENDING) {
throw new Forbidden(
'Can not confirm a rejected or already confirmed payment.'
);
}
if (capitalCall.createdBy.reference === confirmedBy.reference) {
throw new Forbidden(
'The admin who created the payment can not confirm it as well.'
);
}
const commitment = await PmCommitmentModel.findOne({
_id: capitalCall.pmCommitmentId
});
const investments = await PmInvestmentOrderModel.aggregate([
{
$match: {
pmCommitmentId: commitment._id
}
},
{
$group: {
_id: '$accountId',
investments: {
$push: '$$ROOT'
}
}
}
]);
const { amount: capitalCallAmount } = capitalCall;
const transactions = [];
const updates = {
accounts: [],
investments: []
};
const clientsCapitalCall = new Map();
await mongoose.connection.transaction(async session => {
try {
const capitalCallPercentage =
capitalCallAmount / commitment.initialAmount;
let totalInvCapitallCall = 0;
const investmentsCapitalCalls = investments.reduce(
(accumulator, accountInvestments) => {
const investmentsUpdate = accountInvestments.investments.map(
investment => {
const capitalCallPortion =
capitalCallPercentage * investment.amount;
totalInvCapitallCall += capitalCallPortion;
if (
investment.remainingCapitalCallAmount < capitalCallPortion
) {
console.error(
'There is not enough money left for an investment.',
{
investmentId: investment._id,
clientId: investment.client,
investmentAmount: investment.amount,
currentPaymentAmount: capitalCallPortion,
remainingCapitalCallAmount:
investment.remainingCapitalCallAmount
}
);
throw new BadRequest(
'Insufficient remaining investment amount.'
);
}
updates.investments.push({
updateOne: {
filter: {
_id: investment._id
},
update: {
$inc: { remainingCapitalCallAmount: -capitalCallPortion }
}
}
});
transactions.push({
clientId: investment.client,
amount: -capitalCallPortion,
type: 'INVEST',
details: 'PRIVATE MARKET PAYMENT'
});
updates.accounts.push({
accountId: investment.accountId,
userId: investment.userId,
amount: capitalCallPortion,
requestId: investment.requestId
});
clientsCapitalCall.set(
`${investment.client.toString()}_${investment._id.toString()}`,
{
capitalCallAmount: capitalCallPortion,
remainingCapitalCallAmount:
investment.remainingCapitalCallAmount - capitalCallPortion
}
);
return {
investmentId: investment._id,
amount: capitalCallPortion
};
}
);
return [...accumulator, ...investmentsUpdate];
},
[]
);
await Promise.all(updates.accounts.map(this.externalCapitalCall));
await Promise.all([
new PmAttachmentGroupModel({
smartfolio: capitalCall.smartfolio,
createdBy: confirmedBy,
title: `Payment confirmation ${new Date().toISOString()}`,
description: `Private Equity: ${capitalCall.privateEquity}`,
attachments
}).save({ session }),
PmInvestmentOrderModel.bulkWrite(updates.investments, {
ordered: false,
session
}),
new C99ArchiveModel({
amount: totalInvCapitallCall,
description: `This transaction pertains to the '${capitalCall.privateEquity}' private equity capital-call identified by ID '${capitalCall._id}'.`
}).save({ session }),
commitment.updateOne(
{ $inc: { remainingCapitalCallAmount: -capitalCallAmount } },
{ session }
),
capitalCall.updateOne(
{
status: PRIVATE_EQUITY_CAPITAL_CALL_STATUS.CONFIRMED,
confirmedBy,
comment,
$push: {
attachments
}
},
{ session }
),
PmInvestmentCapitalCallModel.insertMany(investmentsCapitalCalls, {
session
})
]);
// DO: sending all related emails regarding payment confirmation action.
{
const [clientsMap, investmentsCapitalCallsMap] = await Promise.all([
this.#getClientsInfoMap(
investments
.map(
accountInvestments => accountInvestments.investments[0].client
)
.join(',')
),
this.#getInvestmentsCapitalCallsMap()
]);
const recipients = investments.reduce(
(accumulator, accountInvestments) => [
...accumulator,
...accountInvestments.investments.map(
({ client, _id, amount }) => {
const clientDoc = clientsMap.get(client.toString());
const {
capitalCallAmount,
remainingCapitalCallAmount
} = clientsCapitalCall.get(
`${client.toString()}_${_id.toString()}`
);
const capitalCallsList = [
...(investmentsCapitalCallsMap.get(_id.toString()) ?? []),
{
amount: capitalCallAmount,
createdAt: new Date()
}
];
const totalPaidAmount = capitalCallsList.reduce(
(accumulator, currentValue) =>
accumulator + currentValue.amount,
0
);
return {
commitedAmount: amount,
totalPaidAmount,
capitalCallsList,
remainingCapitalCallAmount: remainingCapitalCallAmount,
client: {
name: getUsername(clientDoc.name),
email: clientDoc.email.value
}
};
}
)
],
[]
);
const [paymentCreatorInfo, paymentConfirmerInfo] = await Promise.all([
getUserInfo(capitalCall.createdBy.reference),
getUserInfo(confirmedBy.reference)
]);
sendPaymentConfirmationEmail({
payment: {
id: capitalCall._id,
creatorName: getUsername(paymentCreatorInfo.name),
confirmerName: getUsername(paymentConfirmerInfo.name)
},
recipients,
smartfolio: capitalCall.smartfolio,
className: investments[0].investments[0].class,
privateEquity: capitalCall.privateEquity
})
.then(() => {
console.log('Payment confirmation notification emails was sent.');
})
.catch(error => {
console.error(
'Sending payment confirmation notification emails encountered an error.',
error
);
});
}
await session.commitTransaction();
} catch (error) {
console.error(error);
await session.abortTransaction();
throw error;
} finally {
await session.endSession();
}
});
return PmSmartfolioCapitalCallModel.findById(capitalCallId);
}
async rejectPrivatemarketCapitalCall({
capitalCallId,
comment,
portalUser: rejectedBy
}) {
const capitalCall = await PmSmartfolioCapitalCallModel.findOne({
_id: capitalCallId
});
if (!capitalCall) {
throw new NotFound('No payment with this Id');
}
if (capitalCall.status !== PRIVATE_EQUITY_CAPITAL_CALL_STATUS.PENDING) {
throw new Forbidden(
'Can not reject a confirmed or already rejected payment.'
);
}
if (capitalCall.createdBy.reference === rejectedBy.reference) {
throw new Forbidden(
'The admin who created the payment can not confirm it as well.'
);
}
await capitalCall.updateOne({
status: PRIVATE_EQUITY_CAPITAL_CALL_STATUS.REJECTED,
rejectedBy,
comment
});
// DO: sending all related emails regarding payment rejection action.
{
const [paymentCreatorInfo, paymentRejecterInfo] = await Promise.all([
getUserInfo(capitalCall.createdBy.reference),
getUserInfo(rejectedBy.reference)
]);
sendPaymentRejectionEmail({
payment: {
id: capitalCall._id,
creatorName: getUsername(paymentCreatorInfo.name),
rejecterName: getUsername(paymentRejecterInfo.name)
},
smartfolio: capitalCall.smartfolio,
privateEquity: capitalCall.privateEquity
})
.then(() => {
console.log('Payment rejection notification emails was sent.');
})
.catch(error => {
console.error(
'Sending payment rejection notification emails encountered an error.',
error
);
});
}
}
async createCashBack(
{
createdBy,
amount,
nav,
type,
privateEquity,
smartfolio,
date,
currency,
attachments
},
_session
) {
const commitment = await this.getCommitmentBySmartfolio({
smartfolio,
status: PRIVATE_MARKET_COMMITMENT_STATUS.CONFIRMED
});
if (!commitment) {
throw new BadRequest("The commitment doesn't exist");
}
const { _id: pmCommitmentId } = commitment;
const pmInvestmentOrders = await PmInvestmentOrderModel.find({
pmCommitmentId: ObjectId(pmCommitmentId)
});
if (pmInvestmentOrders.length === 0) {
throw new NotFound('There is no investment with this smartfolio.');
}
return useMongooseSession(async session => {
const [cashBackDocument] = await Promise.all([
new PmSmartfolioCashBackModel({
pmCommitmentId,
amount,
nav,
type: ObjectId(type),
privateEquity,
smartfolio,
attachments,
date,
currency,
createdBy
}).save({ session }),
new PmAttachmentGroupModel({
smartfolio,
createdBy,
title: `Receipt creation ${new Date().toISOString()}`,
description: `Private Equity: ${privateEquity}`,
attachments
}).save({ session })
]);
// DO: sending all related emails regarding receipt creation action.
{
const receiptCreatorInfo = await getUserInfo(
cashBackDocument.createdBy.reference
);
sendReceiptCreationEmail({
receipt: {
id: cashBackDocument._id,
creatorName: getUsername(receiptCreatorInfo.name)
},
smartfolio: cashBackDocument.smartfolio,
privateEquity: cashBackDocument.privateEquity
})
.then(() => {
console.log('Receipt creation notification emails was sent.');
})
.catch(error => {
console.error(
'Sending receipt creation notification emails encountered an error.',
error
);
});
}
return cashBackDocument;
}, _session);
}
async createCashBack(
{
createdBy,
amount,
nav,
type,
privateEquity,
smartfolio,
date,
currency,
attachments
},
_session
) {
const commitment = await this.getCommitmentBySmartfolio({
smartfolio,
status: PRIVATE_MARKET_COMMITMENT_STATUS.CONFIRMED
});
if (!commitment) {
throw new BadRequest("The commitment doesn't exist");
}
const { _id: pmCommitmentId } = commitment;
const pmInvestmentOrders = await PmInvestmentOrderModel.find({
pmCommitmentId: ObjectId(pmCommitmentId)
});
if (pmInvestmentOrders.length === 0) {
throw new NotFound('There is no investment with this smartfolio.');
}
return useMongooseSession(async session => {
const [cashBackDocument] = await Promise.all([
new PmSmartfolioCashBackModel({
pmCommitmentId,
amount,
nav,
type: ObjectId(type),
privateEquity,
smartfolio,
attachments,
date,
currency,
createdBy
}).save({ session }),
new PmAttachmentGroupModel({
smartfolio,
createdBy,
title: `Receipt creation ${new Date().toISOString()}`,
description: `Private Equity: ${privateEquity}`,
attachments
}).save({ session })
]);
// DO: sending all related emails regarding receipt creation action.
{
const receiptCreatorInfo = await getUserInfo(
cashBackDocument.createdBy.reference
);
sendReceiptCreationEmail({
receipt: {
id: cashBackDocument._id,
creatorName: getUsername(receiptCreatorInfo.name)
},
smartfolio: cashBackDocument.smartfolio,
privateEquity: cashBackDocument.privateEquity
})
.then(() => {
console.log('Receipt creation notification emails was sent.');
})
.catch(error => {
console.error(
'Sending receipt creation notification emails encountered an error.',
error
);
});
}
return cashBackDocument;
}, _session);
}
async rejectCashBack({ comment, cashBackId, rejectedBy }) {
const cashBack = await PmSmartfolioCashBackModel.findOneAndUpdate(
{ _id: cashBackId },
{
status: PRIVATE_EQUITY_CASH_BACK_STATUS.REJECTED,
comment,
rejectedBy
}
);
// DO: sending all related emails regarding receipt rejection action.
{
const [receiptCreatorInfo, receiptRejecterInfo] = await Promise.all([
getUserInfo(cashBack.createdBy.reference),
getUserInfo(rejectedBy.reference)
]);
sendReceiptRejectionEmail({
receipt: {
id: cashBack._id,
creatorName: getUsername(receiptCreatorInfo.name),
rejecterName: getUsername(receiptRejecterInfo.name)
},
smartfolio: cashBack.smartfolio,
privateEquity: cashBack.privateEquity
})
.then(() => {
console.log('Receipt rejection notification emails was sent.');
})
.catch(error => {
console.error(
'Sending receipt rejection notification emails encountered an error.',
error
);
});
}
return cashBack;
}
async confirmCashBack(
{ comment, cashBackId, confirmedBy, attachments },
_session
) {
return useMongooseSession(async session => {
const cashBack = await PmSmartfolioCashBackModel.findOne({
_id: cashBackId
}).populate('type');
if (!cashBack) {
throw new NotFound('No receipt found with this id');
}
if (cashBack.status !== PRIVATE_EQUITY_CASH_BACK_STATUS.PENDING) {
throw new BadRequest(
'Can not confirm a rejected or already confirmed receipt'
);
}
if (cashBack.createdBy.reference === confirmedBy.reference) {
throw new Forbidden(
'The admin who created the receipt can not confirm it as well.'
);
}
const commitment = await this.getCommitmentBySmartfolio({
smartfolio: cashBack.smartfolio,
status: PRIVATE_MARKET_COMMITMENT_STATUS.CONFIRMED
});
if (!commitment) {
throw new BadRequest("The commitment doesn't exist");
}
const { initialAmount: pmCommitmentAmount } = commitment;
const pmInvestmentOrders = await PmInvestmentOrderModel.find({
pmCommitmentId: ObjectId(cashBack.pmCommitmentId)
});
if (pmInvestmentOrders.length === 0) {
throw new NotFound('No order for this smartfolio.');
}
const proportion = cashBack.amount / pmCommitmentAmount;
const transactions = [];
let transferredAmount = 0;
const emailsData = [];
const investmentCashBack = await Promise.all(
pmInvestmentOrders.map(async order => {
const portionAmount = order.amount * proportion;
const { data: accountData } = await this.externalCashBack({
accountId: order.accountId,
userId: order.userId,
amount: portionAmount,
requestId: order.requestId
});
emailsData.push({
clientId: order.client.toString(),
commitedAmount: order.amount,
previousBalance: accountData.balance - portionAmount,
receivedAmount: portionAmount,
currentBalance: accountData.balance
});
transferredAmount += portionAmount;
transactions.push({
clientId: order.client,
amount: portionAmount,
type: cashBack.type.typeName.toUpperCase(),
details: 'PRIVATE MARKET RECEIVE'
});
return {
investmentId: order._id,
amount: portionAmount,
type: cashBack.type._id
};
})
);
let editNAVPromise;
if (cashBack.nav) {
editNAVPromise = this.editPmCommitmentNav({
smartfolio: cashBack.smartfolio,
nav: cashBack.nav,
date: new Date(),
attachments: [...cashBack.attachments, ...attachments],
comment: 'The NAV has been adjusted due to receipt creation.',
createdBy: cashBack.createdBy
});
}
await Promise.all([
editNAVPromise,
PmTransactionModel.insertMany(transactions, { session }),
PmInvestmentCashBackModel.insertMany(investmentCashBack, {
session
}),
new C99ArchiveModel({
amount: -transferredAmount,
description: `This transaction pertains to the '${cashBack.privateEquity}' private equity cash-back identified by ID '${cashBack._id}'.`
}).save({ session }),
cashBack.updateOne(
{
transferredAmount,
confirmedBy,
comment,
status: PRIVATE_EQUITY_CASH_BACK_STATUS.CONFIRMED,
$push: {
attachments
}
},
{ session }
),
new PmAttachmentGroupModel({
smartfolio: cashBack.smartfolio,
createdBy: confirmedBy,
title: `Receipt confirmation ${new Date().toISOString()}`,
description: `Private Equity: ${cashBack.privateEquity}`,
attachments
}).save({ session })
]);
// DO: sending all related emails regarding receipt confirmation action.
{
const [
receiptCreatorInfo,
receiptConfirmerInfo,
clientsMap
] = await Promise.all([
getUserInfo(cashBack.createdBy.reference),
getUserInfo(confirmedBy.reference),
this.#getClientsInfoMap(
pmInvestmentOrders.map(({ client }) => client).join(',')
)
]);
const recipients = emailsData.map(({ clientId, ...rest }) => {
const clientInfo = clientsMap.get(clientId);
return {
client: {
name: getUsername(clientInfo.name),
email: clientInfo.email.value
},
...rest
};
});
sendReceiptConfirmationEmail({
receipt: {
id: cashBack._id,
creatorName: getUsername(receiptCreatorInfo.name),
confirmerName: getUsername(receiptConfirmerInfo.name),
type: cashBack.type.typeName
},
smartfolio: cashBack.smartfolio,
privateEquity: cashBack.privateEquity,
className: pmInvestmentOrders[0].class,
recipients
})
.then(() => {
console.log('Receipt rejection notification emails was sent.');
})
.catch(error => {
console.error(
'Sending receipt rejection notification emails encountered an error.',
error
);
});
}
return PmSmartfolioCashBackModel.findOne({ _id: cashBackId });
}, _session);
}
async editPmCommitmentNav(
{ smartfolio, nav, date, attachments, comment, createdBy },
_session
) {
const pmCommitment = await this.getCommitmentBySmartfolio({
smartfolio,
status: PRIVATE_MARKET_COMMITMENT_STATUS.CONFIRMED
});
if (!pmCommitment) {
throw new BadRequest("The commitment doesn't exist");
}
return useMongooseSession(async session => {
const navDocument = await new PmCommitmentNavHistoryModel({
pmCommitmentId: pmCommitment._id,
nav: nav,
date: date ?? new Date(),
createdBy: createdBy,
...(comment && { comment }),
...(attachments && { attachments })
}).save({ session });
const cachedData = {
NAV: pmCommitment.latestNav,
committedAmount: pmCommitment.initialAmount
};
pmCommitment.latestNav = nav;
await pmCommitment.save({ session });
const investments = await PmInvestmentOrderModel.find({
pmCommitmentId: pmCommitment._id
});
const investmentsPreviousNAVMap = new Map();
await Promise.all([
...investments.map(async inv => {
const investmentPercentage = inv.amount / pmCommitment.initialAmount;
investmentsPreviousNAVMap.set(inv._id.toString(), inv.investmentNAV);
inv.investmentNAV = roundToTwo(investmentPercentage * nav);
return inv.save({ session });
}),
new PmAttachmentGroupModel({
smartfolio,
createdBy: createdBy,
title: `${smartfolio} NAV modification ${new Date().toISOString()}`,
description: comment,
attachments
}).save({ session })
]);
// DO: sending all related emails regarding edit NAV action.
{
const editorInfo = await getUserInfo(createdBy.reference);
const clientsMap = await this.#getClientsInfoMap(
investments.map(({ client }) => client).join(',')
);
sendEditNavEmail({
smartfolio,
className: investments[0].class,
editorName: getUsername(editorInfo.name),
committedAmount: cachedData.committedAmount,
previousNAV: cachedData.NAV,
newNAV: nav,
currentPerformance: calculatePerformance(
nav,
cachedData.committedAmount
),
investments: investments.map(
({ _id, amount, investmentNAV, client }) => {
const clientInfo = clientsMap.get(client.toString());
return {
client: {
name: getUsername(clientInfo.name),
email: clientInfo.email.value
},
committedAmount: amount,
previousNAV: investmentsPreviousNAVMap.get(_id.toString()),
newNAV: investmentNAV,
currentPerformance: calculatePerformance(investmentNAV, amount)
};
}
)
})
.then(() => {
console.log('Edit NAV notification emails was sent.');
})
.catch(error => {
console.error(
'Sending edit NAV notification emails encountered an error.',
error
);
});
}
return navDocument;
}, _session);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment