Created
July 17, 2024 14:54
-
-
Save MohammedEsafi/6e51d3070b15c96fbcadc2c6c2a420ec to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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