"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.LoanTransactionsService = void 0;
const lodash_1 = __importDefault(require("lodash"));
const mongoose_1 = __importDefault(require("mongoose"));
const dayjs_1 = __importDefault(require("dayjs"));
const decimal_js_1 = __importDefault(require("decimal.js"));
const date_helper_1 = require("../helpers/date.helper");
const common_helper_1 = require("../helpers/common.helper");
const BBCDate_model_1 = require("../models/BBCDate.model");
const loan_types_enum_1 = require("../enums/loan-types.enum");
const LoanTransaction_model_1 = require("../models/LoanTransaction.model");
const collaterals_db_1 = require("../db/collaterals.db");
const PostponedTransactions_model_1 = require("../models/PostponedTransactions.model");
const LoanProducts_model_1 = require("../models/LoanProducts.model");
const Borrower_model_1 = require("../models/Borrower.model");
const BorrowerCompliance_model_1 = require("../models/BorrowerCompliance.model");
const LoanStatementTransaction_model_1 = require("../models/LoanStatementTransaction.model");
const LoanPayment_model_1 = require("../models/LoanPayment.model");
const loan_products_db_1 = require("../db/loan-products.db");
const loan_transactions_db_1 = require("../db/loan-transactions.db");
const LoanTransactionFile_model_1 = require("../models/LoanTransactionFile.model");
const event_map_1 = require("../microservice/event-map");
const queue_map_1 = require("../microservice/queue-map");
class LoanTransactionsService {
    attachedFilesService;
    banksService;
    borrowersDB;
    calendarService;
    loanChargesService;
    getLoanPaymentsService;
    getLoanStatementService;
    loanStatementStatusService;
    getNodemailerService;
    lockService;
    termLoanService;
    uploadsService;
    postponedTransactionsService;
    mainQueueService;
    config;
    constructor(config, attachedFilesService, banksService, borrowersDB, calendarService, loanChargesService, getLoanPaymentsService, getLoanStatementService, loanStatementStatusService, getNodemailerService, lockService, termLoanService, uploadsService, postponedTransactionsService, mainQueueService) {
        this.attachedFilesService = attachedFilesService;
        this.banksService = banksService;
        this.borrowersDB = borrowersDB;
        this.calendarService = calendarService;
        this.loanChargesService = loanChargesService;
        this.getLoanPaymentsService = getLoanPaymentsService;
        this.getLoanStatementService = getLoanStatementService;
        this.loanStatementStatusService = loanStatementStatusService;
        this.getNodemailerService = getNodemailerService;
        this.lockService = lockService;
        this.termLoanService = termLoanService;
        this.uploadsService = uploadsService;
        this.postponedTransactionsService = postponedTransactionsService;
        this.mainQueueService = mainQueueService;
        this.config = config;
    }
    async getTransactionsById(transactionId) {
        return LoanTransaction_model_1.LoanTransaction.findById(transactionId).lean();
    }
    async getLoanTransactionsInternal(productIds, transactionsFilter, paginatorOptions, withBanks, isReverse = false, isMinified = false) {
        const optionalFilters = [];
        if (transactionsFilter.amount) {
            optionalFilters.push({ amount: transactionsFilter.amount });
        }
        if (transactionsFilter.reference) {
            optionalFilters.push({
                reference: {
                    $regex: transactionsFilter.reference,
                    $options: 'i',
                },
            });
        }
        const pipeline = [
            {
                $match: {
                    date: {
                        $gte: transactionsFilter.periodStart,
                        $lte: transactionsFilter.periodEnd,
                    },
                    productId: { $in: productIds },
                    ...Object.assign({}, ...optionalFilters),
                },
            },
            {
                $sort: {
                    date: isReverse ? -1 : 1,
                    order: isReverse ? -1 : 1,
                    createdAt: isReverse ? -1 : 1,
                },
            },
            ...(0, collaterals_db_1.ITEMS_PAGINATION)(paginatorOptions),
        ];
        if (withBanks) {
            pipeline.push({
                $lookup: {
                    from: 'banks',
                    localField: 'bankId',
                    foreignField: '_id',
                    as: 'bank',
                },
            }, {
                $unwind: {
                    path: '$bank',
                    preserveNullAndEmptyArrays: true,
                },
            });
        }
        pipeline.push({
            $unset: loan_products_db_1.fieldsToUnset,
        });
        if (isMinified) {
            pipeline.push({
                $project: {
                    _id: 1,
                    date: 1,
                    amount: 1,
                    reference: 1,
                    balance: 1,
                    transactionType: 1,
                },
            });
        }
        return LoanTransaction_model_1.LoanTransaction.aggregate(pipeline);
    }
    async getLoanTransactions(transactionsFilter, paginatorOptions, withBanks, isReverse = false, isMinified = false) {
        const borrowerProducts = await this.loanChargesService.getLoanProducts(transactionsFilter.borrowerId);
        const productIds = borrowerProducts
            .filter((product) => transactionsFilter.productId
            ? product._id.toString() === transactionsFilter.productId
            : true)
            .map((product) => product._id);
        return this.getLoanTransactionsInternal(productIds, transactionsFilter, paginatorOptions, withBanks, isReverse, isMinified);
    }
    async getLoanTransactionsForProducts(transactionsFilter, paginatorOptions, withBanks, isReverse = false, isMinified = false) {
        return this.getLoanTransactionsInternal(transactionsFilter.productIds.map((id) => new mongoose_1.default.Types.ObjectId(id)), transactionsFilter, paginatorOptions, withBanks, isReverse, isMinified);
    }
    async getLoanTransactionsForFilter(periodStart, periodEnd, productId) {
        return LoanTransaction_model_1.LoanTransaction.aggregate([
            {
                $match: {
                    $and: [
                        { 'date': { $gte: periodStart } },
                        { 'date': { $lte: periodEnd } },
                        { 'productId': new mongoose_1.default.Types.ObjectId(productId) },
                    ],
                },
            }, {
                $sort: {
                    'date': -1,
                    'order': -1,
                    'createdAt': -1,
                },
            }, {
                $unset: loan_products_db_1.fieldsToUnset,
            },
        ]);
    }
    async getLoanTransactionsForProduct(productId) {
        return LoanTransaction_model_1.LoanTransaction.aggregate([
            {
                $match: {
                    $and: [
                        { 'productId': new mongoose_1.default.Types.ObjectId(productId) },
                    ],
                },
            }, {
                $sort: {
                    'date': -1,
                    'order': -1,
                    'createdAt': -1,
                },
            }, {
                $unset: loan_products_db_1.fieldsToUnset,
            },
        ]);
    }
    async getLoanTransactionFiles() {
        const transactionFiles = await LoanTransactionFile_model_1.LoanTransactionFileModel.aggregate([
            {
                $sort: {
                    'effectiveDate': -1,
                    'createdAt': 1,
                },
            }, {
                $unset: [...loan_products_db_1.fieldsToUnset.filter((field) => field !== 'createdAt')],
            },
        ]);
        return await Promise.all(transactionFiles.map(async (file) => {
            const effectiveDate = (0, dayjs_1.default)(file.effectiveDate).format('YYYY-MM-DD');
            const attachedFiles = await this.attachedFilesService.getAttachedFilesByEffectiveDate(effectiveDate);
            return { ...file, attachedFileIds: attachedFiles.map((attachedFile) => attachedFile._id.toString()) };
        }));
    }
    async getDisbursementTransactions(productId, date) {
        const startOfDay = dayjs_1.default.utc(date).startOf('day').toDate();
        const endOfDay = dayjs_1.default.utc(date).endOf('day').toDate();
        return LoanTransaction_model_1.LoanTransaction.countDocuments({
            date: { $gte: startOfDay, $lte: endOfDay },
            productId: new mongoose_1.default.Types.ObjectId(productId),
            transactionType: LoanTransaction_model_1.ELoanTransactionTypes.DISBURSEMENT,
            bankId: { $ne: null },
        });
    }
    async getTotalLoanTransactions(transactionsFilter) {
        const borrowerProducts = await this.loanChargesService.getLoanProducts(transactionsFilter.borrowerId);
        const productIds = borrowerProducts
            .filter((product) => transactionsFilter.productId ? product._id.toString() === transactionsFilter.productId : true)
            .map((product) => product._id);
        return LoanTransaction_model_1.LoanTransaction.countDocuments({
            $and: [
                { 'date': { $gte: transactionsFilter.periodStart } },
                { 'date': { $lte: transactionsFilter.periodEnd } },
                { 'productId': { $in: productIds } },
            ],
        });
    }
    async uploadLoanTransactions(uploadedItems, uploadOption, userId, shift = 0) {
        const orderedItems = uploadedItems.map((item, index) => ({
            ...item,
            order: index + shift,
        }));
        const transactionsToRecalculate = [];
        const statementToSave = [];
        await Promise.all(orderedItems.map(async (item) => {
            const foundBank = await this.banksService.findBankByNumber(item.bankAccountNumber);
            const productId = await this.loanChargesService.getLoanProductIdByCode(item.ledgerAccountCode);
            if (productId) {
                const itemToSave = {
                    ...(lodash_1.default.pick(item, LoanTransaction_model_1.LOAN_TRANSACTION_FIELDS)),
                    date: (0, date_helper_1.calculateTimeZone)(item.date),
                    bankId: new mongoose_1.default.Types.ObjectId(String(foundBank._id)),
                    productId: new mongoose_1.default.Types.ObjectId(productId),
                    balance: 0,
                    floatedBalance: 0,
                    effectiveDate: await this.calculateEffectiveDate(item),
                    userId,
                };
                const newLoanTransaction = new LoanTransaction_model_1.LoanTransaction({
                    order: item.order,
                    ...itemToSave,
                });
                await newLoanTransaction.save();
                transactionsToRecalculate.push({
                    productId: newLoanTransaction.productId.toString(),
                    date: newLoanTransaction.date,
                    transactionId: newLoanTransaction._id.toString(),
                    order: newLoanTransaction.order,
                });
            }
            const chargeId = await this.loanChargesService.getLoanChargeIdByCode(item.ledgerAccountCode);
            if (chargeId) {
                const itemToSave = {
                    ...(lodash_1.default.pick(item, LoanStatementTransaction_model_1.LOAN_STATEMENT_TRANSACTION_FIELDS)),
                    date: (0, date_helper_1.calculateTimeZone)(item.date),
                    chargeId: new mongoose_1.default.Types.ObjectId(chargeId),
                    memo: item.reference,
                    isSystem: false,
                    amountPaid: 0,
                };
                const loanStatementService = this.getLoanStatementService();
                const lastOrder = await loanStatementService.getLastTransactionOrder(itemToSave);
                itemToSave.order = lastOrder + 1;
                const newLoanStatement = new LoanStatementTransaction_model_1.LoanStatementTransactionModel(itemToSave);
                await newLoanStatement.save();
                statementToSave.push(newLoanStatement._id.toString());
            }
        }));
        const newLoanTransactionFile = new LoanTransactionFile_model_1.LoanTransactionFileModel({
            userUploaded: '',
            transactionsId: transactionsToRecalculate.map((transaction) => new mongoose_1.default.Types.ObjectId(transaction.transactionId)),
            statementIds: statementToSave.map((statement) => new mongoose_1.default.Types.ObjectId(statement)),
            fileName: uploadOption.fileName,
            sheetName: uploadOption.sheetName,
            effectiveDate: uploadOption.effectiveDate,
        });
        await newLoanTransactionFile.save();
        const earliestTransactionIds = new Map();
        transactionsToRecalculate.forEach((transaction) => {
            const { productId, transactionId, date } = transaction;
            if (earliestTransactionIds.has(productId)) {
                if (date < earliestTransactionIds.get(productId).date) {
                    earliestTransactionIds.set(productId, { transactionId, date });
                }
            }
            else {
                earliestTransactionIds.set(productId, { transactionId, date });
            }
        });
        for (const [productId, values] of earliestTransactionIds) {
            await this.recalculateBalanceQueued(productId, values.transactionId);
        }
    }
    async normalizeLoanTransaction(transaction, userId) {
        transaction.effectiveDate = await this.calculateEffectiveDate(transaction);
        transaction.date = new Date(transaction.date);
        switch (LoanTransaction_model_1.ELoanTransactionTypes[transaction.transactionType]) {
            case LoanTransaction_model_1.ELoanTransactionTypes.DISBURSEMENT:
                transaction.amount = new decimal_js_1.default(transaction.amount).abs().toNumber();
                break;
            case LoanTransaction_model_1.ELoanTransactionTypes.COLLECTION:
                transaction.amount = new decimal_js_1.default(transaction.amount).abs().neg().toNumber();
                break;
        }
        transaction.userId = userId;
        return transaction;
    }
    async createLoanTransaction(transaction) {
        const pureTransactions = (0, common_helper_1.createFilteredObject)(transaction, LoanTransaction_model_1.LOAN_TRANSACTION_FIELDS);
        const product = await this.loanChargesService.getLoanProductById(pureTransactions.productId.toString());
        if (product) {
            const bank = await this.banksService.findBankByAccountCode(product.code);
            if (bank) {
                pureTransactions.bankId = bank._id;
            }
        }
        pureTransactions.customerId = '';
        return await this.saveLoanTransaction(pureTransactions);
    }
    async updateLoanTransaction(transaction) {
        const pureTransactions = (0, common_helper_1.createFilteredObject)(transaction, LoanTransaction_model_1.LOAN_TRANSACTION_FIELDS);
        await this.saveLoanTransaction(pureTransactions);
    }
    async saveLoanTransaction(transaction) {
        let startId;
        let updatedTransaction;
        if (!transaction._id) {
            delete transaction._id;
            const newTransaction = new LoanTransaction_model_1.LoanTransaction(transaction);
            startId = newTransaction._id.toString();
            const saved = await newTransaction.save();
            updatedTransaction = saved.toObject();
        }
        else {
            startId = transaction._id.toString();
            const updated = await LoanTransaction_model_1.LoanTransaction.findByIdAndUpdate(transaction._id, transaction, { new: true }).lean();
            if (!updated) {
                throw new Error(`LoanTransaction not found for update: ${transaction._id.toString()}`);
            }
            updatedTransaction = updated;
        }
        if (startId) {
            this.recalculateBalanceQueued(transaction.productId.toString(), startId).catch(console.error);
        }
        const statementDate = this.loanStatementStatusService.getStatementDateForDate(new Date(transaction.date));
        this.loanStatementStatusService.updateStatementStatus(transaction.productId.toString(), statementDate, false).catch(console.error);
        this.termLoanService.setTermLoanExpected(transaction.productId.toString()).catch(console.error);
        return updatedTransaction;
    }
    async recalculateProduct(productId) {
        const firstTransactions = await LoanTransaction_model_1.LoanTransaction.aggregate([
            {
                '$match': {
                    'productId': new mongoose_1.default.Types.ObjectId(productId),
                },
            }, {
                '$sort': {
                    'date': 1,
                },
            }, {
                '$limit': 1,
            },
        ]);
        if (firstTransactions.length > 0) {
            await this.recalculateBalanceQueued(firstTransactions[0].productId.toString(), firstTransactions[0]._id.toString());
        }
    }
    async recalculateBalanceLegacy(transactionId) {
        const changedTransaction = await LoanTransaction_model_1.LoanTransaction.findById(transactionId);
        if (!changedTransaction) {
            console.error(`no transactions with id ${transactionId}`);
            return;
        }
        const recalcBalance = async () => {
            await LoanProducts_model_1.LoanProduct.findByIdAndUpdate(changedTransaction.productId, { isBalanceActual: false });
            const previousTransaction = await LoanTransaction_model_1.LoanTransaction.aggregate([
                {
                    $match: {
                        'productId': new mongoose_1.default.Types.ObjectId(changedTransaction.productId.toString()),
                        'date': { $lt: new Date(changedTransaction.date) },
                    },
                }, {
                    $sort: {
                        'date': -1,
                        'order': -1,
                        'createdAt': -1,
                    },
                }, {
                    $limit: 1,
                },
            ]);
            let currentBalance = previousTransaction.length
                ? previousTransaction[0].balance ?? 0
                : 0;
            const recalculatedTransactions = await LoanTransaction_model_1.LoanTransaction.aggregate([
                {
                    $match: {
                        'productId': new mongoose_1.default.Types.ObjectId(changedTransaction.productId.toString()),
                        'date': { $gte: new Date(changedTransaction.date) },
                    },
                }, {
                    $sort: {
                        'date': 1,
                        'order': 1,
                        'createdAt': 1,
                    },
                },
            ]);
            for (const transaction of recalculatedTransactions) {
                currentBalance = new decimal_js_1.default(currentBalance).add(transaction.amount).toNumber();
                await LoanTransaction_model_1.LoanTransaction.findByIdAndUpdate(transaction._id, { balance: currentBalance });
            }
            await LoanProducts_model_1.LoanProduct.findByIdAndUpdate(changedTransaction.productId, { isBalanceActual: true });
        };
        const recalcFloatedBalance = async () => {
            await LoanProducts_model_1.LoanProduct.findByIdAndUpdate(changedTransaction.productId, { isFloatedBalanceActual: false });
            const previousEqualBalancesTransaction = await LoanTransaction_model_1.LoanTransaction.aggregate([
                {
                    $match: {
                        'productId': new mongoose_1.default.Types.ObjectId(changedTransaction.productId.toString()),
                        'date': { $lt: new Date(changedTransaction.date) },
                        $expr: { $eq: ['$balance', '$floatedBalance'] },
                    },
                }, {
                    $sort: {
                        'date': -1,
                        'order': -1,
                        'createdAt': -1,
                    },
                }, {
                    $limit: 1,
                },
            ]);
            let currentFloatedBalance = 0;
            if (previousEqualBalancesTransaction.length) {
                const previousTransaction = await LoanTransaction_model_1.LoanTransaction.aggregate([
                    {
                        $match: {
                            'productId': new mongoose_1.default.Types.ObjectId(previousEqualBalancesTransaction[0].productId.toString()),
                            'date': { $lt: new Date(previousEqualBalancesTransaction[0].date) },
                        },
                    }, {
                        $sort: {
                            'date': -1,
                            'order': -1,
                            'createdAt': -1,
                        },
                    }, {
                        $limit: 1,
                    },
                ]);
                if (previousTransaction.length) {
                    currentFloatedBalance = previousTransaction[0].floatedBalance;
                }
            }
            const initTransaction = previousEqualBalancesTransaction.length ? previousEqualBalancesTransaction[0] : changedTransaction;
            const recalculatedTransactions = await LoanTransaction_model_1.LoanTransaction.aggregate([
                {
                    $match: {
                        'productId': new mongoose_1.default.Types.ObjectId(initTransaction.productId.toString()),
                        'date': { $gte: new Date(initTransaction.date) },
                    },
                }, {
                    $sort: {
                        'date': 1,
                        'order': 1,
                        'createdAt': 1,
                    },
                },
            ]);
            for (const transaction of recalculatedTransactions) {
                const totalPostponed = await (0, loan_products_db_1.getPostponedTransactions)(transaction.date, transaction.productId.toString(), true);
                switch (transaction.transactionType) {
                    case LoanTransaction_model_1.ELoanTransactionTypes.COLLECTION: {
                        await this.postponedTransactionsService.cleanRecalculated(transaction._id.toString());
                        await this.postponedTransactionsService.createPostponedTransaction(transaction);
                        currentFloatedBalance = new decimal_js_1.default(currentFloatedBalance).add(totalPostponed).toNumber();
                        break;
                    }
                    default: {
                        currentFloatedBalance = new decimal_js_1.default(currentFloatedBalance).add(transaction.amount).add(totalPostponed).toNumber();
                    }
                }
                await LoanTransaction_model_1.LoanTransaction.findByIdAndUpdate(transaction._id, { floatedBalance: currentFloatedBalance });
            }
            await LoanProducts_model_1.LoanProduct.findByIdAndUpdate(changedTransaction.productId, { isFloatedBalanceActual: true });
        };
        await recalcBalance();
        await recalcFloatedBalance();
    }
    async recalculateBalanceQueued(productId, transactionId) {
        await this.mainQueueService.sendTaskToQueue({
            data: { productId, transactionId },
            pattern: event_map_1.EventMap.recalc.PRODUCT_BALANCE,
        }, queue_map_1.QueueMap.main);
    }
    async recalculateBalance(transactionId) {
        const startTransaction = await LoanTransaction_model_1.LoanTransaction.findById(transactionId).lean();
        if (!startTransaction) {
            console.error(`Transaction not found: ${transactionId}`);
            return;
        }
        const earliestTransaction = await LoanTransaction_model_1.LoanTransaction.aggregate([
            {
                $match: {
                    productId: startTransaction.productId,
                    date: { $lte: new Date(startTransaction.date) },
                },
            },
            { $sort: { date: 1, order: 1, createdAt: 1 } },
            { $limit: 1 },
        ]);
        const fromDate = earliestTransaction.length
            ? earliestTransaction[0].date
            : startTransaction.date;
        const allTransactions = await LoanTransaction_model_1.LoanTransaction.aggregate([
            {
                $match: {
                    productId: startTransaction.productId,
                    date: { $gte: fromDate },
                },
            },
            { $sort: { date: 1, order: 1, createdAt: 1 } },
        ]);
        let balance = 0;
        let floatedBalance = 0;
        const prevBeforeStart = await LoanTransaction_model_1.LoanTransaction.aggregate([
            {
                $match: {
                    productId: startTransaction.productId,
                    date: { $lt: fromDate },
                },
            },
            { $sort: { date: -1, order: -1, createdAt: -1 } },
            { $limit: 1 },
        ]);
        if (prevBeforeStart.length) {
            balance = prevBeforeStart[0].balance || 0;
            floatedBalance = prevBeforeStart[0].floatedBalance || 0;
        }
        for (const tr of allTransactions) {
            // regular balance recalculation
            balance = new decimal_js_1.default(balance).add(tr.amount).toNumber();
            // floatedBalance recalculation
            const totalPostponed = await (0, loan_products_db_1.getPostponedTransactions)(tr.date, tr.productId.toString(), true);
            switch (tr.transactionType) {
                case LoanTransaction_model_1.ELoanTransactionTypes.COLLECTION: {
                    await this.postponedTransactionsService.cleanRecalculated(tr._id.toString());
                    await this.postponedTransactionsService.createPostponedTransaction(tr);
                    floatedBalance = new decimal_js_1.default(floatedBalance).add(totalPostponed).toNumber();
                    break;
                }
                default: {
                    floatedBalance = new decimal_js_1.default(floatedBalance).add(tr.amount).add(totalPostponed).toNumber();
                }
            }
            await LoanTransaction_model_1.LoanTransaction.updateOne({ _id: tr._id }, { balance, floatedBalance });
        }
        await LoanProducts_model_1.LoanProduct.updateOne({ _id: startTransaction.productId }, { isBalanceActual: true, isFloatedBalanceActual: true });
    }
    async deleteLoanTransaction(transactionId, updateLoanPayment, userId = null) {
        const currentTransaction = await LoanTransaction_model_1.LoanTransaction.findById(transactionId).lean();
        if (!currentTransaction) {
            console.error(`no transactions with id ${transactionId}`);
            return;
        }
        const nextTransaction = await LoanTransaction_model_1.LoanTransaction.aggregate([
            {
                $match: {
                    'productId': new mongoose_1.default.Types.ObjectId(currentTransaction.productId.toString()),
                    'date': { $gte: new Date(currentTransaction.date) },
                    '_id': { $ne: new mongoose_1.default.Types.ObjectId(transactionId) },
                },
            }, {
                $sort: {
                    'date': 1,
                    'order': 1,
                    'createdAt': 1,
                },
            }, {
                $limit: 1,
            },
        ]);
        await LoanTransaction_model_1.LoanTransaction.findByIdAndDeleteWithUserId(transactionId, userId);
        await PostponedTransactions_model_1.PostponedTransaction.findOneAndDelete({ transactionId });
        if (currentTransaction.loanPaymentId && updateLoanPayment) {
            const payment = await LoanPayment_model_1.LoanPaymentModel.findById(currentTransaction.loanPaymentId).lean();
            if (payment) {
                const loanPaymentsService = this.getLoanPaymentsService();
                await loanPaymentsService.updateLoanPayment(payment, userId);
            }
        }
        if (nextTransaction.length) {
            this.recalculateBalanceQueued(nextTransaction[0].productId.toString(), nextTransaction[0]._id.toString()).catch(console.error);
        }
        const statementDate = this.loanStatementStatusService.getStatementDateForDate(new Date(currentTransaction.date));
        this.loanStatementStatusService.updateStatementStatus(currentTransaction.productId.toString(), statementDate, false).catch(console.error);
        this.termLoanService.setTermLoanExpected(currentTransaction.productId.toString()).catch(console.error);
    }
    async deleteLoanTransactionFile(transactionFileId, userId) {
        const foundFile = await LoanTransactionFile_model_1.LoanTransactionFileModel.findById(transactionFileId).lean();
        if (!foundFile) {
            return null;
        }
        await Promise.all(foundFile.transactionsId.map(async (transactionId) => {
            await this.deleteLoanTransaction(transactionId.toString(), true, userId);
        }));
        await Promise.all(foundFile.statementIds.map(async (statementId) => {
            await LoanStatementTransaction_model_1.LoanStatementTransactionModel.findByIdAndDelete(statementId);
        }));
        await LoanTransactionFile_model_1.LoanTransactionFileModel.findByIdAndDelete(transactionFileId);
    }
    async getFirstTransaction(productId, excludeAdjustments = false) {
        const excludeFilter = excludeAdjustments
            ? {
                'transactionType': {
                    $nin: ['ADJUSTMENT'],
                },
            }
            : {};
        const lastTransaction = await LoanTransaction_model_1.LoanTransaction.aggregate([
            {
                $match: {
                    'productId': new mongoose_1.default.Types.ObjectId(productId),
                    ...excludeFilter,
                },
            }, {
                $sort: {
                    'date': 1,
                    'order': 1,
                    'createdAt': 1,
                },
            }, {
                $limit: 1,
            },
        ]);
        if (!lastTransaction.length) {
            return null;
        }
        return lastTransaction[0];
    }
    async getLastTransactionForDate(productId, date = new Date(), excludeAdjustments = false) {
        return (0, loan_transactions_db_1.getLastTransactionForDate)(productId, date, excludeAdjustments);
    }
    async getLastDisbursementTransactionForDate(productId, startDate, endDate) {
        return LoanTransaction_model_1.LoanTransaction.aggregate([
            {
                $match: {
                    $and: [
                        { 'date': { $gte: startDate } },
                        { 'date': { $lte: endDate } },
                        { 'productId': new mongoose_1.default.Types.ObjectId(productId) },
                        { 'transactionType': { $ne: LoanTransaction_model_1.ELoanTransactionTypes.COLLECTION } },
                    ],
                },
            }, {
                $sort: {
                    'date': -1,
                    'order': -1,
                    'createdAt': -1,
                },
            },
        ]);
    }
    async getPaymentsForPeriod(productId, startDate, endDate) {
        return LoanTransaction_model_1.LoanTransaction.aggregate([
            {
                $match: {
                    $and: [
                        { 'date': { $gte: startDate } },
                        { 'date': { $lte: endDate } },
                        { 'productId': new mongoose_1.default.Types.ObjectId(productId) },
                        { 'transactionType': LoanTransaction_model_1.ELoanTransactionTypes.COLLECTION },
                    ],
                },
            }, {
                $sort: {
                    'date': -1,
                    'order': -1,
                    'createdAt': -1,
                },
            },
        ]);
    }
    async getLoanBalanceForBBCDateId(BBCDateId) {
        const bbcDate = await BBCDate_model_1.BBCDateModel.findById(BBCDateId);
        const loanProducts = await LoanProducts_model_1.LoanProduct.aggregate([
            {
                '$match': {
                    'borrowerId': new mongoose_1.default.Types.ObjectId(bbcDate.borrowerId),
                },
            },
        ]);
        const balances = {
            [loan_types_enum_1.ELoanTypes.TERM]: 0,
            [loan_types_enum_1.ELoanTypes.REVOLVER]: 0,
        };
        await Promise.all(loanProducts
            .filter((product) => (0, date_helper_1.isProductActive)(product.startDate, product.deactivationDate, bbcDate.bbcDate))
            .map(async (product) => {
            const lastTransaction = await this.getLastTransactionForDate(product._id.toString(), (0, dayjs_1.default)(bbcDate.bbcDate).utcOffset(0).endOf('day').toDate());
            if (lastTransaction) {
                balances[loan_types_enum_1.ELoanTypes[product.type]] = new decimal_js_1.default(balances[loan_types_enum_1.ELoanTypes[product.type]]).add(lastTransaction.balance).toNumber();
            }
        }));
        return balances;
    }
    async getLoanBalanceForBBCDateIdPerProduct(BBCDateId, productType) {
        const bbcDate = await BBCDate_model_1.BBCDateModel.findById(BBCDateId);
        const productFilter = productType ? { 'type': productType } : {};
        const loanProducts = await LoanProducts_model_1.LoanProduct.aggregate([
            {
                '$match': {
                    'borrowerId': new mongoose_1.default.Types.ObjectId(bbcDate.borrowerId),
                    ...productFilter,
                },
            },
        ]);
        const balances = {};
        await Promise.all(loanProducts.map(async (product) => {
            const lastTransaction = await this.getLastTransactionForDate(product._id.toString(), (0, dayjs_1.default)(bbcDate.bbcDate).utcOffset(0).endOf('day').toDate());
            balances[product._id.toString()] = { balance: lastTransaction.balance, product };
        }, {}));
        return balances;
    }
    async getLoanBalanceForBorrowerId(borrowerId) {
        const products = await this.loanChargesService.getLoanProducts(borrowerId);
        const extraInfo = {
            [loan_types_enum_1.ELoanTypes.REVOLVER]: {
                balance: 0,
                lastActivityDate: null,
            },
            [loan_types_enum_1.ELoanTypes.TERM]: {
                balance: 0,
                lastActivityDate: null,
            },
            accruedStatement: {
                balance: 0,
                lastActivityDate: null,
            },
            effectiveLoanBalance: {
                balance: 0,
                lastActivityDate: null,
            },
        };
        if (products.length === 0) {
            return extraInfo;
        }
        for (const product of products) {
            try {
                const lastTransaction = await this.getLastTransactionForDate(product._id.toString());
                if (lastTransaction) {
                    extraInfo[product.type].balance = new decimal_js_1.default(extraInfo[product.type].balance).add(lastTransaction.balance).toNumber();
                }
                const latRegularTransaction = await this.getLastTransactionForDate(product._id.toString(), new Date(), true);
                if (latRegularTransaction) {
                    if (!extraInfo[product.type].lastActivityDate || latRegularTransaction.date > extraInfo[product.type].lastActivityDate) {
                        extraInfo[product.type].lastActivityDate = latRegularTransaction.date;
                    }
                }
            }
            catch (error) {
                console.error('Error:', error);
            }
        }
        const loanStatementService = this.getLoanStatementService();
        extraInfo.accruedStatement.balance = await loanStatementService.calculateAccruedStatementForBorrower(borrowerId);
        extraInfo.effectiveLoanBalance.balance = new decimal_js_1.default(extraInfo[loan_types_enum_1.ELoanTypes.REVOLVER].balance).add(extraInfo.accruedStatement.balance).toNumber();
        return extraInfo;
    }
    async getUnsentTransactionForDate(borrowerId, date) {
        const products = await this.loanChargesService.getBorrowersLoanProducts(borrowerId);
        return LoanTransaction_model_1.LoanTransaction.aggregate([
            {
                $match: {
                    'productId': { $in: products.map((product) => product._id) },
                    'isSent': { $in: [false, null] },
                    'date': { $gte: date },
                },
            }, {
                $lookup: {
                    from: 'loan_products',
                    localField: 'productId',
                    foreignField: '_id',
                    as: 'product',
                },
            }, {
                $unwind: {
                    path: '$product',
                },
            },
        ]);
    }
    async getTransactionReport(transactionIds, borrowerId, effectiveDate) {
        const transactionIdsObjects = transactionIds.map((t) => new mongoose_1.default.Types.ObjectId(t));
        const uploadedTransactions = await LoanTransaction_model_1.LoanTransaction.aggregate([
            {
                $match: {
                    '_id': {
                        $in: transactionIdsObjects,
                    },
                },
            }, {
                $lookup: {
                    from: 'loan_products',
                    localField: 'productId',
                    foreignField: '_id',
                    as: 'product',
                },
            }, {
                $unwind: {
                    path: '$product',
                },
            }, {
                $match: {
                    'product.borrowerId': new mongoose_1.default.Types.ObjectId(borrowerId),
                },
            },
        ]);
        const products = await this.loanChargesService.getBorrowersLoanProducts(borrowerId);
        const backDatedTransactions = await LoanTransaction_model_1.LoanTransaction.aggregate([
            {
                $match: {
                    '_id': { $nin: transactionIdsObjects },
                    'productId': { $in: products.map((product) => product._id) },
                    'isSent': { $in: [false, null] },
                    'date': { $lte: effectiveDate },
                },
            }, {
                $lookup: {
                    from: 'loan_products',
                    localField: 'productId',
                    foreignField: '_id',
                    as: 'product',
                },
            }, {
                $unwind: {
                    path: '$product',
                },
            },
        ]);
        const productGroups = products
            .reduce((acc, product) => {
            acc[product._id.toString()] = [];
            return acc;
        }, {});
        const headerRow = [
            'date',
            'amount',
            'reference',
            'transaction type',
        ];
        const backDatedTransactionsTitle = [[''], ['Backdated transactions']];
        const transactionIdsToMark = [...uploadedTransactions, ...backDatedTransactions].map((tr) => tr._id.toString());
        const mappedTransactions = uploadedTransactions
            .filter((t) => !!t)
            .reduce((acc, t) => {
            const reportRow = [
                t.date,
                t.amount,
                t.reference,
                t.transactionType,
            ];
            acc[t.productId.toString()] = [...acc[t.productId.toString()], reportRow];
            return acc;
        }, lodash_1.default.cloneDeep(productGroups));
        const mappedBackDatedTransactions = backDatedTransactions
            .filter((t) => !!t)
            .reduce((acc, t) => {
            const reportRow = [
                t.date,
                t.amount,
                t.reference,
                t.transactionType,
            ];
            acc[t.productId.toString()] = [...acc[t.productId.toString()], reportRow];
            return acc;
        }, lodash_1.default.cloneDeep(productGroups));
        const transactions = products
            .filter((product) => {
            const uploaded = mappedTransactions[product._id.toString()];
            const backdated = mappedBackDatedTransactions[product._id.toString()];
            return uploaded.length > 0 || backdated.length > 0;
        })
            .map((product) => {
            const uploaded = mappedTransactions[product._id.toString()];
            const backdated = mappedBackDatedTransactions[product._id.toString()];
            return {
                [product.name]: [
                    headerRow,
                    ...uploaded,
                    ...backDatedTransactionsTitle,
                    headerRow,
                    ...backdated,
                ],
            };
        });
        return { transactionIdsToMark, transactions };
    }
    async getBorrowerIdsForFile(transactionFileId) {
        const transactionFile = await LoanTransactionFile_model_1.LoanTransactionFileModel.findById(transactionFileId).lean();
        if (!transactionFile) {
            console.log('unknown file id', transactionFileId);
            return;
        }
        const productIdsSet = new Set();
        await Promise.all(transactionFile.transactionsId.map(async (transactionId) => {
            const transaction = await LoanTransaction_model_1.LoanTransaction.findById(transactionId);
            if (transaction) {
                productIdsSet.add(transaction.productId.toString());
            }
        }));
        const productIds = Array.from(productIdsSet.values());
        const borrowerIdsSet = new Map();
        await Promise.all(productIds.map(async (productId) => {
            const product = await LoanProducts_model_1.LoanProduct.findById(productId);
            if (product) {
                const foundBorrower = await BorrowerCompliance_model_1.BorrowerCompliance.findOne({ borrower: product.borrowerId.toString() });
                const isActive = foundBorrower.isEmailingActive && foundBorrower.isDailyTransactionsEmailingActive;
                borrowerIdsSet.set(product.borrowerId.toString(), isActive);
            }
        }));
        return Object.fromEntries(borrowerIdsSet);
    }
    async getEmailBorrowers(transactionFileId) {
        return await this.getBorrowerIdsForFile(transactionFileId);
    }
    async sendReport(transactionFileId, selectedBorrowerIds) {
        const transactionFile = await LoanTransactionFile_model_1.LoanTransactionFileModel.findById(transactionFileId).lean();
        if (!transactionFile) {
            console.log('unknown file id', transactionFileId);
            return;
        }
        const borrowerIds = await this.getBorrowerIdsForFile(transactionFileId);
        const transactionIds = transactionFile.transactionsId.map((id) => id.toString());
        await Promise.all(Object.entries(borrowerIds).map(async ([borrowerId, active]) => {
            if (!selectedBorrowerIds.includes(borrowerId)) {
                return;
            }
            if (!active) {
                return;
            }
            const borrower = await Borrower_model_1.BorrowerModel.findById(borrowerId).lean();
            if (!borrower) {
                console.log('unknown borrower', borrowerId);
                return;
            }
            const foundBorrower = await BorrowerCompliance_model_1.BorrowerCompliance.findOne({ borrower: borrowerId });
            if (!foundBorrower) {
                return;
            }
            const addresses = [...foundBorrower.mainEmails, ...foundBorrower.financialEmails].reduce((acc, email) => email.isActive ? [...acc, email.email] : acc, []);
            await this.sendBorrowerReport(borrowerId, borrower.code, addresses, transactionIds, new Date(transactionFile.effectiveDate));
        }));
    }
    async sendAllReport(effectiveDate) {
        const borrowers = await this.borrowersDB.getActiveBorrowers();
        await Promise.all(borrowers.map(async (borrower) => {
            if (!borrower.active) {
                return;
            }
            const foundBorrower = await BorrowerCompliance_model_1.BorrowerCompliance.findOne({ borrower: borrower._id.toString() });
            if (!foundBorrower) {
                return;
            }
            const addresses = [...foundBorrower.mainEmails, ...foundBorrower.financialEmails].reduce((acc, email) => email.isActive ? [...acc, email.email] : acc, []);
            const dailyTransactions = await this.getUnsentTransactionForDate(borrower._id.toString(), effectiveDate);
            const transactionIds = dailyTransactions.map((tr) => tr._id.toString());
            await this.sendBorrowerReport(borrower._id.toString(), borrower.code, addresses, transactionIds, effectiveDate);
        }));
    }
    async sendBorrowerReport(borrowerId, borrowerCode, addresses, transactionIds, effectiveDate) {
        const title = `${borrowerCode} ACTIVITY REPORT: ${(0, dayjs_1.default)(effectiveDate).format('MM-DD-YYYY')}`;
        const fileName = title;
        const subject = title;
        const { transactions, transactionIdsToMark, } = await this.getTransactionReport(transactionIds, borrowerId, effectiveDate);
        if (transactionIdsToMark.length === 0) {
            return;
        }
        const attachedFile = await this.uploadsService.convertDataToFile(transactions);
        const attachment = { filename: `${fileName}.xlsx`, content: attachedFile };
        const attachedPDFs = await this.attachedFilesService.getAttachedRealFilesByTransactionFileId(borrowerId, dayjs_1.default.utc(effectiveDate).format('YYYY-MM-DD'));
        let text = 'Attached is your daily activity report. Please log into the portal to view your account balance.';
        if (attachedPDFs.length > 0) {
            text += ' Uploaded PDF files are available for downloading.';
        }
        const email = {
            addresses: [...addresses, this.config.portfolioEmail],
            subject,
            text,
        };
        const nodemailerService = this.getNodemailerService();
        await nodemailerService.sendLoanUploadReport(email, [attachment], borrowerId);
        await Promise.all(transactionIdsToMark.map(async (id) => {
            await this.switchTransactionSentStatus(id, true);
        }));
    }
    async calculateEffectiveDate(transaction) {
        if (transaction.transactionType !== LoanTransaction_model_1.ELoanTransactionTypes.COLLECTION) {
            return new Date(transaction.date);
        }
        const nextDate = await this.calendarService.getNextWorkingDay((0, dayjs_1.default)(transaction.date).format('YYYY-MM-DD'));
        return new Date(nextDate);
    }
    async switchTransactionSentStatus(transactionId, isSent) {
        if (isSent) {
            await LoanTransaction_model_1.LoanTransaction.findByIdAndUpdate(transactionId, { isSent });
        }
        else {
            const transaction = await LoanTransaction_model_1.LoanTransaction.findById(transactionId).lean();
            if (transaction) {
                await LoanTransaction_model_1.LoanTransaction.findByIdAndUpdate(transactionId, { isSent: !(transaction.isSent ?? true) });
            }
        }
    }
    async checkLockStatus(transaction) {
        const product = await this.loanChargesService.getLoanProductById(transaction.productId.toString());
        if (product) {
            return await this.lockService.isDateLocked(new Date(transaction.date), product.borrowerId.toString());
        }
        return false;
    }
    ;
}
exports.LoanTransactionsService = LoanTransactionsService;
