"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.LoanStatementService = void 0;
const dayjs_1 = __importDefault(require("dayjs"));
const utc_1 = __importDefault(require("dayjs/plugin/utc"));
const timezone_1 = __importDefault(require("dayjs/plugin/timezone"));
const decimal_js_1 = __importDefault(require("decimal.js"));
const loan_types_enum_1 = require("../enums/loan-types.enum");
const common_helper_1 = require("../helpers/common.helper");
const loan_charge_type_enum_1 = require("../enums/loan-charge-type.enum");
const LoanStatementTransaction_model_1 = require("../models/LoanStatementTransaction.model");
const LoanCharges_model_1 = require("../models/LoanCharges.model");
const LoanProducts_model_1 = require("../models/LoanProducts.model");
const Borrower_model_1 = require("../models/Borrower.model");
const LoanPayment_model_1 = require("../models/LoanPayment.model");
const date_helper_1 = require("../helpers/date.helper");
const TermLoanCalculated_model_1 = require("../models/TermLoanCalculated.model");
const TermLoan_model_1 = require("../models/TermLoan.model");
const loan_statement_db_1 = require("../db/loan-statement.db");
const BBCDate_model_1 = require("../models/BBCDate.model");
const financial_indexes_service_1 = require("./financial-indexes.service");
dayjs_1.default.extend(utc_1.default);
dayjs_1.default.extend(timezone_1.default);
class LoanStatementService {
    financialIndexesService;
    loanChargesService;
    loanPaymentsService;
    loanStatementBalanceService;
    loanStatementStatusService;
    loanTransactionsService;
    termLoanService;
    uploadsService;
    getLoanStatementEffectsService;
    constructor(financialIndexesService, loanChargesService, loanPaymentsService, loanStatementBalanceService, loanStatementStatusService, loanTransactionsService, termLoanService, uploadsService, getLoanStatementEffectsService) {
        this.financialIndexesService = financialIndexesService;
        this.loanChargesService = loanChargesService;
        this.loanPaymentsService = loanPaymentsService;
        this.loanStatementBalanceService = loanStatementBalanceService;
        this.loanStatementStatusService = loanStatementStatusService;
        this.loanTransactionsService = loanTransactionsService;
        this.termLoanService = termLoanService;
        this.uploadsService = uploadsService;
        this.getLoanStatementEffectsService = getLoanStatementEffectsService;
    }
    getGroupedData(statements) {
        const groups = {
            [loan_charge_type_enum_1.ELoanChargeType.INTEREST_FEE]: 0,
            [loan_charge_type_enum_1.ELoanChargeType.ADMIN_FEE]: 0,
            [loan_charge_type_enum_1.ELoanChargeType.UNUSED_LINE_FEE]: 0,
            [loan_charge_type_enum_1.ELoanChargeType.WIRE_FEE]: 0,
            [loan_charge_type_enum_1.ELoanChargeType.ANNUAL_LINE_FEE]: 0,
        };
        const otherStatements = [];
        let totalAmount = 0;
        const groupedData = statements.reduce((acc, record) => {
            totalAmount = new decimal_js_1.default(totalAmount).add(record.amount).toNumber();
            if (record.chargeType === loan_charge_type_enum_1.ELoanChargeType.OTHER || record.chargeType === loan_charge_type_enum_1.ELoanChargeType.RECOVERABLE) {
                otherStatements.push(record);
                return acc;
            }
            return {
                ...acc,
                [record.chargeType]: new decimal_js_1.default(acc[record.chargeType]).add(record.amount).toNumber(),
            };
        }, groups);
        return { groupedData, otherStatements };
    }
    async getTransactionById(transactionId) {
        return LoanStatementTransaction_model_1.LoanStatementTransactionModel.findById(transactionId).lean();
    }
    async getFirstStatementTransaction(productId) {
        const loanCharges = await this.loanChargesService.getLoanChargeForProduct(productId);
        const chargesIds = loanCharges.map((charge) => charge._id);
        const firstStatementTransactions = await LoanStatementTransaction_model_1.LoanStatementTransactionModel.aggregate([
            {
                $match: {
                    'chargeId': { '$in': chargesIds },
                },
            }, {
                $sort: {
                    'date': 1,
                    'createdAt': 1,
                    'order': 1,
                },
            }, {
                $limit: 1,
            },
        ]);
        if (!firstStatementTransactions.length) {
            return null;
        }
        return firstStatementTransactions[0];
    }
    async getBorrowerWithPeriods(borrowerId, period, splitPeriods = false) {
        return (0, loan_statement_db_1.getBorrowerWithPeriods)(borrowerId, period, splitPeriods);
    }
    async getPeriods(selectedPeriod, productId) {
        return (0, loan_statement_db_1.getPeriods)(selectedPeriod, productId);
    }
    getDays(period) {
        const days = [];
        for (let day = (0, dayjs_1.default)(period.start); day.isBefore(period.end); day = day.add(1, 'day')) {
            days.push(day.format('YYYY-MM-DD'));
        }
        return days;
    }
    async calculateBorrowerStatement(borrowerId, selectedPeriod) {
        const products = await this.loanChargesService.getLoanProducts(borrowerId);
        for (const product of products) {
            const periods = await this.getPeriods(selectedPeriod, product._id.toString());
            if (selectedPeriod === 'ENTIRE_LOAN') {
                const charges = await this.loanChargesService.getLoanChargeForProduct(product._id.toString());
                await this.cleanProductAllStatements(charges.map((charge) => charge._id.toString()));
            }
            await this.calculateProductStatement(product._id.toString(), periods);
        }
    }
    async recalculateAllProducts() {
        const borrowers = await Borrower_model_1.BorrowerModel.find({ active: true });
        for (const borrower of borrowers) {
            const products = await this.loanChargesService.getLoanProducts(borrower._id.toString());
            await Promise.all(products.map(async (product) => {
                await this.loanTransactionsService.recalculateProduct(product._id.toString());
            }));
        }
    }
    async calculateAllBorrowers() {
        const statementDate = this.loanStatementStatusService.getStatementDateFromStringPeriod('CURRENT_MONTH');
        const date = this.loanStatementStatusService.getDateFromStatementDate(statementDate);
        const periods = this.loanStatementStatusService.getPeriodsFromStatementDate(statementDate);
        const borrowers = await Borrower_model_1.BorrowerModel.find({ active: true });
        for (const borrower of borrowers) {
            const products = await this.loanChargesService.getLoanProducts(borrower._id.toString());
            await Promise.all(products.map(async (product) => {
                const validationError = await this.loanStatementStatusService.validateDate(product._id.toString(), date);
                if (!validationError) {
                    try {
                        console.log('calculating borrower/product', borrower.name, product.code);
                        await this.calculateProductStatement(product._id.toString(), periods);
                    }
                    catch (e) {
                        console.error(e);
                    }
                }
            }));
        }
    }
    async calculateProductStatement(productId, periods) {
        await Promise.all(periods.map(async (period) => {
            const days = this.getDays(period);
            for (const day of days) {
                await this.calculateStatement(productId, period, dayjs_1.default.utc(day).toDate());
            }
            const statementDate = this.loanStatementStatusService.getStatementDateFromPeriod(period);
            await this.loanStatementStatusService.updateStatementStatus(productId, statementDate, true);
        }));
        const startDate = periods[0].start.startOf('day').toDate();
        await this.loanStatementBalanceService.calculateProductBalance(productId, startDate);
    }
    async useFloatingBalance(productId) {
        const product = await this.loanChargesService.getLoanProductById(productId);
        return product.type === loan_types_enum_1.ELoanTypes.REVOLVER;
    }
    async calculateStatement(productId, period, statementDate) {
        if (!statementDate) {
            statementDate = dayjs_1.default.utc().startOf('day').toDate();
        }
        const primeRate = await this.financialIndexesService.getFinancialIndexValue(financial_indexes_service_1.EFinancialIndex.PRIME_RATE, statementDate);
        if (!primeRate) {
            return;
        }
        await this.cleanProductStatement(productId, statementDate);
        const balanceDate = dayjs_1.default.utc(statementDate).add(1, 'day').subtract(1, 'second').toDate();
        const balance = await this.loanChargesService.getLoanProductBalance(productId, balanceDate);
        const loanStatementEffectsService = this.getLoanStatementEffectsService();
        await loanStatementEffectsService.saveMonthData(productId, statementDate);
        const charges = await this.loanChargesService.getLoanChargeForProduct(productId);
        const product = await this.loanChargesService.getLoanProductById(productId);
        // If product is already deactivated for this statement date (inclusive) — skip everything early
        if (product.deactivationDate && (0, dayjs_1.default)(statementDate).isSameOrAfter((0, dayjs_1.default)(product.deactivationDate))) {
            return;
        }
        const daysPerYear = 360;
        const dayInMonth = (0, dayjs_1.default)(period.end).diff(period.start, 'days') + 1;
        let orderIndex = 0;
        for (const charge of charges) {
            if (charge.applyFrom.getTime() > statementDate.getTime()) {
                continue;
            }
            const statementDateFormat = (0, dayjs_1.default)(statementDate).format('YYYY-MM-DD');
            let calculateFee = false;
            switch (charge.frequency) {
                case loan_types_enum_1.EChargeFrequencies.DISBURSEMENT: {
                    calculateFee = true;
                    break;
                }
                case loan_types_enum_1.EChargeFrequencies.DAILY: {
                    calculateFee = true;
                    break;
                }
                case loan_types_enum_1.EChargeFrequencies.WEEKLY: {
                    if ((0, dayjs_1.default)(charge.applyFrom).day() === (0, dayjs_1.default)(statementDate).day()) {
                        calculateFee = true;
                    }
                    break;
                }
                case loan_types_enum_1.EChargeFrequencies.MONTHLY: {
                    const lastDayOfMonthFormat = (0, dayjs_1.default)(statementDate).endOf('month').format('YYYY-MM-DD');
                    if (lastDayOfMonthFormat === statementDateFormat) {
                        calculateFee = true;
                    }
                    break;
                }
                case loan_types_enum_1.EChargeFrequencies.ANNUAL: {
                    const chargeDate = (0, dayjs_1.default)(charge.applyFrom).year(statementDate.getFullYear()).format('YYYY-MM-DD');
                    if (chargeDate === statementDateFormat) {
                        calculateFee = true;
                    }
                    break;
                }
                case loan_types_enum_1.EChargeFrequencies.ONE_TIME: {
                    const chargeDate = (0, dayjs_1.default)(charge.applyFrom).format('YYYY-MM-DD');
                    if (chargeDate === statementDateFormat) {
                        calculateFee = true;
                    }
                    break;
                }
            }
            const useFloating = await this.useFloatingBalance(productId);
            const getBalance = () => {
                return Math.max(0, useFloating ? balance.floatedBalance : balance.balance);
            };
            if (calculateFee) {
                let fee = 0;
                let calculatedFee = 0;
                let percentFee = charge.percent;
                if (charge.chargeType === loan_charge_type_enum_1.ELoanChargeType.INTEREST_FEE) {
                    percentFee = new decimal_js_1.default(primeRate).add(percentFee).toDP(4).toNumber();
                    const minPercent = product.minPercent ?? 0;
                    const maxPercent = (product.maxPercent ?? 0) === 0 ? 1 : product.maxPercent;
                    percentFee = Math.min(Math.max(percentFee, minPercent), maxPercent);
                }
                const dailyPercent = new decimal_js_1.default(percentFee).div(daysPerYear).toNumber();
                switch (charge.calculationBasis) {
                    case loan_types_enum_1.EChargeCalculationBasis.DAILY_BALANCE:
                        if ((0, dayjs_1.default)(statementDate).isAfter((0, dayjs_1.default)(charge.applyFrom)) || (0, dayjs_1.default)(statementDate).isSame((0, dayjs_1.default)(charge.applyFrom))) {
                            fee = new decimal_js_1.default(getBalance()).mul(dailyPercent).toDP(2).toNumber();
                        }
                        break;
                    case loan_types_enum_1.EChargeCalculationBasis.AVERAGE_MONTHLY_BALANCE:
                        if ((0, dayjs_1.default)(statementDate).isAfter((0, dayjs_1.default)(charge.applyFrom)) || (0, dayjs_1.default)(statementDate).isSame((0, dayjs_1.default)(charge.applyFrom))) {
                            fee = new decimal_js_1.default(getBalance()).mul(percentFee).div(dayInMonth).toDP(2).toNumber();
                        }
                        if (!percentFee && charge.minimumAmount) {
                            fee = new decimal_js_1.default(charge.minimumAmount).div(dayInMonth).toDP(2).toNumber();
                        }
                        break;
                    case loan_types_enum_1.EChargeCalculationBasis.UNUSED_AMOUNT:
                        if ((0, dayjs_1.default)(statementDate).isAfter((0, dayjs_1.default)(charge.applyFrom)) || (0, dayjs_1.default)(statementDate).isSame((0, dayjs_1.default)(charge.applyFrom))) {
                            calculatedFee = new decimal_js_1.default(product.commitment).sub(getBalance()).mul(charge.percent).div(daysPerYear).toDP(2).toNumber();
                            fee = Math.max(calculatedFee, 0);
                        }
                        break;
                    case loan_types_enum_1.EChargeCalculationBasis.COMMITMENT:
                        fee = new decimal_js_1.default(product.commitment).mul(charge.percent).toDP(2).toNumber();
                        break;
                    case loan_types_enum_1.EChargeCalculationBasis.DISBURSEMENT_AMOUNT:
                        if ((0, dayjs_1.default)(statementDate).isAfter((0, dayjs_1.default)(charge.applyFrom)) || (0, dayjs_1.default)(statementDate).isSame((0, dayjs_1.default)(charge.applyFrom))) {
                            fee = await this.calculateDisbursementFee(product._id.toString(), charge, statementDate);
                        }
                        break;
                    case loan_types_enum_1.EChargeCalculationBasis.FIXED:
                        if ((0, dayjs_1.default)(statementDate).isAfter((0, dayjs_1.default)(charge.applyFrom)) || (0, dayjs_1.default)(statementDate).isSame((0, dayjs_1.default)(charge.applyFrom))) {
                            calculatedFee = new decimal_js_1.default(getBalance()).mul(dailyPercent).toDP(2).toNumber();
                            fee = Math.max(calculatedFee, charge.minimumAmount);
                        }
                        break;
                }
                const checkMinAmount = (fee) => {
                    if (charge.minimumAmount > 0) {
                        if (charge.chargeType === loan_charge_type_enum_1.ELoanChargeType.WIRE_FEE) {
                            return fee ? Math.max(charge.minimumAmount, fee) : 0;
                        }
                        if (charge.frequency === loan_types_enum_1.EChargeFrequencies.DAILY && charge.calculationBasis === loan_types_enum_1.EChargeCalculationBasis.AVERAGE_MONTHLY_BALANCE) {
                            return fee;
                        }
                        return fee ? Math.max(charge.minimumAmount, fee) : charge.minimumAmount;
                    }
                    return fee;
                };
                fee = checkMinAmount(fee);
                if (fee !== null && fee > 0) {
                    if ((0, dayjs_1.default)(statementDate).isSameOrAfter((0, dayjs_1.default)(charge.applyFrom))) {
                        const newStatement = {
                            order: orderIndex,
                            date: statementDate,
                            chargeId: charge._id,
                            amount: fee,
                            amountPaid: 0,
                            isSystem: true,
                            memo: 'System Generated',
                            balance: 0,
                        };
                        const newStatementDoc = await LoanStatementTransaction_model_1.LoanStatementTransactionModel.create(newStatement);
                        await newStatementDoc.save();
                        orderIndex = orderIndex + 1;
                    }
                }
            }
        }
        await this.reorderStatementForDate(productId, statementDate);
    }
    async reorderStatementForDate(productId, date) {
        const charges = await this.loanChargesService.getLoanChargeForProduct(productId);
        const lastTransactions = await LoanStatementTransaction_model_1.LoanStatementTransactionModel.aggregate([
            {
                $match: {
                    'chargeId': { $in: charges.map((charge) => charge._id) },
                    'date': { $eq: date },
                    'isSystem': false,
                },
            }, {
                $sort: {
                    'order': 1,
                },
            },
        ]);
        if (lastTransactions.length > 0) {
            let lastOrder = await this.getLastTransactionOrder(lastTransactions[0]);
            for (const transaction of lastTransactions) {
                lastOrder = lastOrder + 1;
                await LoanStatementTransaction_model_1.LoanStatementTransactionModel.findByIdAndUpdate(transaction._id, { order: lastOrder });
            }
        }
    }
    async getBalancesDaily(productId, period) {
        const days = this.getDays({ start: (0, dayjs_1.default)(period.start).utcOffset(0), end: (0, dayjs_1.default)(period.end).utcOffset(0) });
        return Promise.all(days.map(async (day) => {
            const balance = await this.loanChargesService.getLoanProductBalance(productId, dayjs_1.default.utc(day).toDate());
            return { day, ...balance };
        }));
    }
    async calculateDisbursementFee(productId, charge, day) {
        const totalDoc = await this.loanTransactionsService.getDisbursementTransactions(productId, day);
        if (totalDoc === 0) {
            return 0;
        }
        return new decimal_js_1.default(totalDoc).mul(charge.minimumAmount).toDP(2).toNumber();
    }
    async cleanProductStatement(productId, statementDate) {
        const charges = await this.loanChargesService.getLoanChargeForProduct(productId);
        await LoanStatementTransaction_model_1.LoanStatementTransactionModel.deleteMany({
            chargeId: { $in: charges.map((charge) => charge._id) },
            date: statementDate,
            isSystem: true,
        });
    }
    async cleanProductAllStatements(chargesId) {
        await LoanStatementTransaction_model_1.LoanStatementTransactionModel.deleteMany({ chargeId: { $in: chargesId }, isSystem: true });
    }
    async getProductStatementPdfData(productId, start, end) {
        const product = await LoanProducts_model_1.LoanProduct.findById(productId).lean();
        const borrower = await Borrower_model_1.BorrowerModel.findById(product.borrowerId).lean();
        const charges = await this.loanChargesService.getLoanChargeForProduct(productId);
        const periodStart = (0, dayjs_1.default)(start);
        const periodEnd = (0, dayjs_1.default)(end);
        const statements = await LoanStatementTransaction_model_1.LoanStatementTransactionModel.aggregate([
            {
                $match: {
                    $and: [
                        { 'chargeId': { $in: charges.map((charge) => charge._id) } },
                        { 'date': { $gte: periodStart.toDate() } },
                        { 'date': { $lte: periodEnd.toDate() } },
                    ],
                },
            }, {
                $sort: {
                    'date': 1,
                    'order': 1,
                },
            }, {
                $lookup: {
                    from: 'loan_charges',
                    localField: 'chargeId',
                    foreignField: '_id',
                    as: 'charge',
                },
            }, {
                $unwind: {
                    'path': '$charge',
                },
            }, {
                $addFields: {
                    'chargeType': '$charge.chargeType',
                    'chargeName': '$charge.name',
                },
            }, {
                $unset: ['_id', 'productId', 'chargeId', 'charge', 'createdAt', 'updatedAt', '__v'],
            },
        ]);
        const totalAmount = 0;
        const { groupedData, otherStatements } = this.getGroupedData(statements);
        const filter = {
            borrowerId: borrower._id.toString(),
            productId: product._id.toString(),
            periodEnd: periodEnd.toDate(),
            periodStart: periodStart.toDate(),
        };
        const transactions = await this.loanTransactionsService.getLoanTransactions(filter, null, true);
        return {
            groupedData,
            otherStatements,
            totalAmount,
            transactions,
        };
    }
    async getProductShortStatementRepresentation(productId, start, end) {
        const product = await LoanProducts_model_1.LoanProduct.findById(productId).lean();
        const borrower = await Borrower_model_1.BorrowerModel.findById(product.borrowerId).lean();
        const charges = await this.loanChargesService.getLoanChargeForProduct(productId);
        const periodStart = (0, dayjs_1.default)(start);
        const periodEnd = (0, dayjs_1.default)(end);
        const statements = await LoanStatementTransaction_model_1.LoanStatementTransactionModel.aggregate([
            {
                $match: {
                    $and: [
                        { 'chargeId': { $in: charges.map((charge) => charge._id) } },
                        { 'date': { $gte: periodStart.toDate() } },
                        { 'date': { $lte: periodEnd.toDate() } },
                    ],
                },
            }, {
                $sort: {
                    'date': 1,
                    'order': 1,
                },
            }, {
                $lookup: {
                    from: 'loan_charges',
                    localField: 'chargeId',
                    foreignField: '_id',
                    as: 'charge',
                },
            }, {
                $unwind: {
                    'path': '$charge',
                },
            }, {
                $addFields: {
                    'chargeType': '$charge.chargeType',
                },
            }, {
                $unset: ['_id', 'productId', 'chargeId', 'charge', 'createdAt', 'updatedAt', '__v'],
            },
        ]);
        const totalAmount = 0;
        const { groupedData, otherStatements } = this.getGroupedData(statements);
        const chargeMap = {
            [loan_charge_type_enum_1.ELoanChargeType.INTEREST_FEE]: 'INTEREST',
            [loan_charge_type_enum_1.ELoanChargeType.ADMIN_FEE]: 'ADMIN FEE',
            [loan_charge_type_enum_1.ELoanChargeType.UNUSED_LINE_FEE]: 'UNUSED',
            [loan_charge_type_enum_1.ELoanChargeType.WIRE_FEE]: 'WIRE FEES',
            [loan_charge_type_enum_1.ELoanChargeType.ANNUAL_LINE_FEE]: 'ANNUAL LINE',
            [loan_charge_type_enum_1.ELoanChargeType.OTHER]: 'OTHER',
            [loan_charge_type_enum_1.ELoanChargeType.RECOVERABLE]: 'RECOVERABLE',
        };
        const preparedStatement = Object.entries(groupedData).map(([chargeType, amount]) => {
            return [chargeMap[chargeType], null, amount];
        });
        otherStatements.forEach((record) => {
            preparedStatement.push([chargeMap[record.chargeType], record.memo, record.amount]);
        });
        const preparedStatementHeader = [
            [`STATEMENT: ${borrower.name}`],
            ['PERIOD FROM', periodStart.format('MM/DD/YYYY'), 'PERIOD TO', (0, dayjs_1.default)(end.slice(0, 10)).format('MM/DD/YYYY')],
            [null],
            ['THIS PERIOD STATEMENT', null, 'AMOUNT'],
        ];
        const preparedStatementTotal = [
            [null],
            [null, 'TOTAL', totalAmount],
            [null],
            [null],
        ];
        const preparedTransactionsHeader = [
            [`LOAN TRANSACTIONS: ${product.name}`],
            ['Date', 'Type', 'Amount', 'Reference', 'Bank', 'Balance'],
        ];
        const filter = {
            borrowerId: borrower._id.toString(),
            productId: product._id.toString(),
            periodEnd: periodEnd.toDate(),
            periodStart: periodStart.toDate(),
        };
        const transactions = await this.loanTransactionsService.getLoanTransactions(filter, null, true);
        const preparedTransactions = transactions.map((transaction) => {
            return [
                (0, dayjs_1.default)(transaction.date).format('MM/DD/YYYY'),
                transaction.transactionType,
                transaction.amount,
                transaction.reference,
                transaction.bank?.name ?? '',
                transaction.balance,
            ];
        });
        return [
            ...preparedStatementHeader,
            ...preparedStatement,
            ...preparedStatementTotal,
            ...preparedTransactionsHeader,
            ...preparedTransactions,
        ];
    }
    async createStatementTransaction(transaction) {
        const pureTransactions = (0, common_helper_1.createFilteredObject)(transaction, LoanStatementTransaction_model_1.LOAN_STATEMENT_TRANSACTION_FIELDS);
        const lastOrder = await this.getLastTransactionOrder(transaction);
        await this.saveLoanTransaction({ ...pureTransactions, isSystem: false, amountPaid: 0, order: lastOrder + 1 });
    }
    async updateStatementTransaction(transaction) {
        const foundTransaction = await LoanStatementTransaction_model_1.LoanStatementTransactionModel.findById(transaction._id).lean();
        if (!foundTransaction) {
            return;
        }
        const pureTransactions = foundTransaction.isSystem
            ? (0, common_helper_1.createFilteredObject)({
                ...foundTransaction,
                memo: transaction.memo,
            }, LoanStatementTransaction_model_1.LOAN_STATEMENT_TRANSACTION_FIELDS)
            : (0, common_helper_1.createFilteredObject)(transaction, LoanStatementTransaction_model_1.LOAN_STATEMENT_TRANSACTION_FIELDS);
        await this.saveLoanTransaction(pureTransactions);
    }
    async saveLoanTransaction(transaction) {
        if (!transaction._id) {
            delete transaction._id;
            const updatedStatementTransaction = new LoanStatementTransaction_model_1.LoanStatementTransactionModel(transaction);
            await updatedStatementTransaction.save();
        }
        else {
            await LoanStatementTransaction_model_1.LoanStatementTransactionModel.findByIdAndUpdate(transaction._id, transaction, { new: true });
        }
        const charge = await LoanCharges_model_1.LoanCharge.findById(transaction.chargeId);
        await this.loanStatementBalanceService.calculateProductBalance(charge.productId.toString(), new Date(transaction.date));
        const statementDate = this.loanStatementStatusService.getStatementDateForDate(new Date(transaction.date));
        await this.loanStatementStatusService.updateStatementStatus(charge.productId.toString(), statementDate, false);
    }
    async deleteStatementTransaction(transactionId) {
        const foundTransaction = await LoanStatementTransaction_model_1.LoanStatementTransactionModel.findById(transactionId).lean();
        if (!foundTransaction) {
            return;
        }
        const charge = await LoanCharges_model_1.LoanCharge.findById(foundTransaction.chargeId);
        await LoanStatementTransaction_model_1.LoanStatementTransactionModel.findByIdAndDelete(transactionId);
        await this.loanStatementBalanceService.calculateProductBalance(charge.productId.toString(), new Date(foundTransaction.date));
        const statementDate = this.loanStatementStatusService.getStatementDateForDate(new Date(foundTransaction.date));
        await this.loanStatementStatusService.updateStatementStatus(charge.productId.toString(), statementDate, false);
    }
    async addBalancesToStatements(transactionDocs) {
        return await Promise.all(transactionDocs.map(async (item) => {
            const charge = await LoanCharges_model_1.LoanCharge.findById(item.chargeId).lean();
            const balanceDate = dayjs_1.default.utc(item.date).add(1, 'day').subtract(1, 'second').toDate();
            const balances = await this.loanChargesService.getLoanProductBalance(charge.productId.toString(), balanceDate);
            return { ...item, floatedBalance: balances.floatedBalance };
        }));
    }
    async getLastTransactionOrder(transaction, onlySystem = false) {
        const charge = await LoanCharges_model_1.LoanCharge.findById(transaction.chargeId);
        if (!charge) {
            return null;
        }
        const charges = await this.loanChargesService.getLoanChargeForProduct(charge.productId.toString());
        const getOnlySystem = () => onlySystem ? { isSystem: true } : {};
        const lastTransactions = await LoanStatementTransaction_model_1.LoanStatementTransactionModel.aggregate([
            {
                $match: {
                    'chargeId': { $in: charges.map((charge) => charge._id) },
                    '_id': { $ne: transaction._id },
                    'date': { $eq: new Date(transaction.date) },
                    ...getOnlySystem(),
                },
            }, {
                $sort: {
                    'date': -1,
                    'order': -1,
                },
            }, {
                $limit: 1,
            },
        ]);
        if (!lastTransactions || lastTransactions.length === 0) {
            return 0;
        }
        return lastTransactions[0].order;
    }
    async getPreviousTransaction(transaction) {
        const charge = await LoanCharges_model_1.LoanCharge.findById(transaction.chargeId);
        if (!charge) {
            return null;
        }
        const charges = await this.loanChargesService.getLoanChargeForProduct(charge.productId.toString());
        const lastTransactions = await LoanStatementTransaction_model_1.LoanStatementTransactionModel.aggregate([
            {
                $match: {
                    'chargeId': { $in: charges.map((charge) => charge._id) },
                    '_id': { $ne: transaction._id },
                    $or: [
                        {
                            $and: [
                                { 'date': { $eq: transaction.date } },
                                { 'order': { $lt: transaction.order } },
                            ],
                        },
                        {
                            'date': { $lt: transaction.date },
                        },
                    ],
                },
            }, {
                $sort: {
                    'date': -1,
                    'order': -1,
                },
            }, {
                $limit: 1,
            },
        ]);
        if (!lastTransactions || lastTransactions.length === 0) {
            return null;
        }
        return lastTransactions[0];
    }
    async getTransactionsFromDate(productId, startDate) {
        const charges = await this.loanChargesService.getLoanChargeForProduct(productId);
        return LoanStatementTransaction_model_1.LoanStatementTransactionModel.aggregate([
            {
                $match: {
                    'chargeId': { $in: charges.map((charge) => charge._id) },
                    'date': { $gte: startDate },
                },
            }, {
                $sort: {
                    'date': 1,
                    'order': 1,
                },
            },
        ]);
    }
    async getTransactionsForPeriod(productId, period) {
        const charges = await this.loanChargesService.getLoanChargeForProduct(productId);
        return LoanStatementTransaction_model_1.LoanStatementTransactionModel.aggregate([
            {
                $match: {
                    $and: [
                        { 'chargeId': { $in: charges.map((charge) => charge._id) } },
                        { 'date': { $gte: period.start } },
                        { 'date': { $lt: period.end } },
                    ],
                },
            }, {
                $sort: {
                    'date': 1,
                    'order': 1,
                },
            },
        ]);
    }
    async recalculateAllBalances(borrowerIds, userId) {
        for (const borrowerId of borrowerIds) {
            const allProducts = await LoanProducts_model_1.LoanProduct.find({ borrowerId }).lean();
            for (const product of allProducts) {
                const firstTransaction = await this.loanTransactionsService.getFirstTransaction(product._id.toString());
                if (firstTransaction) {
                    try {
                        await this.loanTransactionsService.updateLoanTransaction(firstTransaction);
                    }
                    catch (e) {
                        console.error(e);
                    }
                }
                const firstStatementTransaction = await this.getFirstStatementTransaction(product._id.toString());
                if (firstStatementTransaction) {
                    try {
                        await this.updateStatementTransaction(firstStatementTransaction);
                    }
                    catch (e) {
                        console.error(e);
                    }
                }
                const allPayments = await LoanPayment_model_1.LoanPaymentModel.find({ productId: product._id.toString() }).lean();
                for (const payment of allPayments) {
                    await this.loanPaymentsService.updateLoanPayment(payment, userId);
                }
            }
        }
    }
    async recalculateAllStatements(selectedPeriod, borrowerIds) {
        for (const borrowerId of borrowerIds) {
            try {
                await this.calculateBorrowerStatement(borrowerId, selectedPeriod);
            }
            catch (e) {
                console.error('recalculateAllStatements - ERROR, borrowerId', borrowerId, e);
            }
        }
    }
    async recalculateAllTermLoans(borrowerIds) {
        for (const borrowerId of borrowerIds) {
            try {
                const products = await this.loanChargesService.getLoanProducts(borrowerId);
                const termProducts = products.filter((product) => product.type === loan_types_enum_1.ELoanTypes.TERM);
                await Promise.all(termProducts.map(async (termProduct) => {
                    await this.termLoanService.calculateTermLoan(termProduct._id.toString(), true);
                }));
            }
            catch (e) {
                console.error('recalculateAllTermLoans - ERROR, borrowerId', borrowerId, e);
            }
        }
    }
    async getStatementBorrowersTransactions(start, end, selectedPeriod, selectedBorrowerIds, ledgerType) {
        return (0, loan_statement_db_1.getStatementBorrowersTransactions)(start, end, selectedPeriod, selectedBorrowerIds, ledgerType);
    }
    async getBorrowerComplianceData(borrowerId, date) {
        const end = dayjs_1.default.utc(date).subtract(1, 'month').endOf('month');
        const start = end.startOf('month');
        const products = await this.loanChargesService.getLoanProducts(borrowerId);
        const data = {
            productTotals: products.reduce((acc, product) => ({ ...acc, [product._id.toString()]: 0 }), {}),
            total: 0,
            totalPaid: 0,
            dueDate: null,
            revolverBalance: 0,
            termBalance: 0,
        };
        await Promise.all(products.map(async (product) => {
            const lastTransaction = await this.loanTransactionsService.getLastTransactionForDate(product._id.toString(), end.toDate());
            if (lastTransaction) {
                switch (product.type) {
                    case loan_types_enum_1.ELoanTypes.REVOLVER: {
                        data.revolverBalance = lastTransaction.balance ?? 0;
                        break;
                    }
                    case loan_types_enum_1.ELoanTypes.TERM: {
                        data.termBalance = lastTransaction.balance ?? 0;
                        break;
                    }
                }
            }
            const lastStatementTransaction = await this.loanStatementBalanceService.getLastStatementTransactionForDate(product._id.toString(), end.toDate());
            const transactions = await this.getTransactionsForPeriod(product._id.toString(), {
                start: start.toDate(),
                end: end.toDate(),
            });
            data.totalPaid = transactions.reduce((acc, t) => new decimal_js_1.default(acc).add(t.amountPaid).toNumber(), 0);
            if (lastStatementTransaction) {
                data.productTotals[product._id.toString()] = lastStatementTransaction.balance ?? 0;
            }
            if (product.type === loan_types_enum_1.ELoanTypes.TERM) {
                const termLoan = await this.termLoanService.getTermLoan(product._id.toString(), true);
                if (termLoan) {
                    const lastDate = termLoan.calculated.slice().reverse().find((c) => (0, dayjs_1.default)(c.relevantStatement).isBefore(date));
                    if (lastDate) {
                        const calculatedResultsForDate = termLoan.calculated.filter((c) => c.relevantStatement === lastDate.relevantStatement);
                        const totalPrincipal = calculatedResultsForDate.reduce((acc, c) => new decimal_js_1.default(acc).add(c.monthlyPrincipal).toNumber(), 0);
                        const totalPrincipalPaid = calculatedResultsForDate.reduce((totalAcc, c) => {
                            const totalMonthPaid = c.payments
                                ? c.payments.reduce((acc, p) => new decimal_js_1.default(acc).add(p.amount).toNumber(), 0)
                                : 0;
                            return new decimal_js_1.default(totalAcc).add(totalMonthPaid).toNumber();
                        }, 0);
                        data.productTotals[product._id.toString()] = new decimal_js_1.default(data.productTotals[product._id.toString()]).add(totalPrincipal).sub(totalPrincipalPaid).toNumber();
                    }
                }
            }
        }));
        data.total = Object.values(data.productTotals).reduce((acc, productDue) => new decimal_js_1.default(acc).add(productDue).toNumber(), 0);
        return data;
    }
    async getOutstandingStatements(borrowerIds, selectedDate, fullMonth) {
        const results = await Promise.all(borrowerIds.map(async (borrowerId) => {
            const borrower = await Borrower_model_1.BorrowerModel.findById(borrowerId).lean();
            const products = await this.loanChargesService.getLoanProducts(borrowerId);
            return await Promise.all(products
                .filter(product => !(0, dayjs_1.default)(product.deactivationDate).isBefore((0, dayjs_1.default)(selectedDate)))
                .map(async (product) => {
                if ((0, dayjs_1.default)(product.deactivationDate).isBefore((0, dayjs_1.default)(selectedDate))) {
                    return null;
                }
                const relevantStatementEndMonth = fullMonth
                    ? (0, dayjs_1.default)(selectedDate).utcOffset(0).startOf('month').subtract(1, 'seconds')
                    : (0, dayjs_1.default)(selectedDate).utcOffset(0).endOf('month');
                const relevantStatementEnd = fullMonth
                    ? (0, dayjs_1.default)(selectedDate).utcOffset(0).startOf('month').subtract(1, 'seconds')
                    : (0, dayjs_1.default)(selectedDate).utcOffset(0).endOf('day');
                const relevantStatementStart = (0, dayjs_1.default)(selectedDate).utcOffset(0).startOf('month').subtract(1, 'seconds').startOf('month');
                const paymentFilterEnd = (0, dayjs_1.default)(selectedDate).utcOffset(0).endOf('day');
                const paymentFilterStart = relevantStatementStart;
                const lastTransaction = await this.loanTransactionsService.getLastTransactionForDate(product._id.toString(), (0, dayjs_1.default)(selectedDate).endOf('day').toDate());
                let actualBalance = 0;
                if (lastTransaction) {
                    actualBalance = new decimal_js_1.default(lastTransaction.balance).toDP(2).toNumber();
                }
                const statementFilter = {
                    productIds: [product._id.toString()],
                    start: relevantStatementStart.toDate(),
                    end: relevantStatementEnd.toDate(),
                };
                const paymentFilter = {
                    productIds: [product._id.toString()],
                    start: paymentFilterStart.toDate(),
                    end: paymentFilterEnd.toDate(),
                };
                const statementTransactions = await this.loanChargesService.getLoanStatementsForForProductWithCharges(statementFilter);
                const statementPayments = await this.loanPaymentsService.getLoanPayments(paymentFilter, null);
                const paidStatementIds = statementPayments.reduce((acc, s) => [...acc, ...s.paid.map((p) => p.statementId.toString())], []);
                const fees = {
                    [loan_charge_type_enum_1.ELoanChargeType.INTEREST_FEE]: 0,
                    [loan_charge_type_enum_1.ELoanChargeType.ADMIN_FEE]: 0,
                    [loan_charge_type_enum_1.ELoanChargeType.UNUSED_LINE_FEE]: 0,
                    [loan_charge_type_enum_1.ELoanChargeType.WIRE_FEE]: 0,
                    [loan_charge_type_enum_1.ELoanChargeType.OTHER]: 0,
                    [loan_charge_type_enum_1.ELoanChargeType.RECOVERABLE]: 0,
                    [loan_charge_type_enum_1.ELoanChargeType.ANNUAL_LINE_FEE]: 0,
                };
                const sumResults = statementTransactions.reduce((acc, t) => {
                    const amountPaid = paidStatementIds.includes(t._id.toString()) ? t.amountPaid : 0;
                    return {
                        ...acc,
                        [t.charge.chargeType]: new decimal_js_1.default(acc[t.charge.chargeType] ?? 0).add(t.amount ?? 0).sub(amountPaid ?? 0).toNumber(),
                    };
                }, fees);
                let principal = 0;
                let principalExpectedDate = null;
                if (product.type === loan_types_enum_1.ELoanTypes.TERM) {
                    const termLoan = await TermLoan_model_1.TermLoanModel.findOne({ productId: product._id.toString(), actual: true });
                    if (termLoan) {
                        const calculatedTermLoan = await TermLoanCalculated_model_1.TermLoanCalculatedModel.findOne({
                            termLoanId: termLoan._id.toString(),
                            relevantStatement: relevantStatementEndMonth.format('YYYY-MM-DD'),
                        });
                        if (calculatedTermLoan) {
                            if (calculatedTermLoan.payments && calculatedTermLoan.payments.length > 0) {
                                const totalPayment = calculatedTermLoan.payments.reduce((acc, p) => new decimal_js_1.default(acc ?? 0).add(p.amount ?? 0).toNumber(), 0);
                                principal = new decimal_js_1.default(calculatedTermLoan.monthlyPrincipal).sub(totalPayment).toDP(2).toNumber();
                            }
                            else {
                                principal = calculatedTermLoan.monthlyPrincipal;
                                principalExpectedDate = calculatedTermLoan.paymentDueDate;
                            }
                        }
                    }
                }
                return {
                    borrowerCode: borrower.code,
                    ...product,
                    actualBalance,
                    ...sumResults,
                    principal,
                    principalExpectedDate,
                };
            }));
        }));
        return results
            .flat()
            .sort((a, b) => a.code.toUpperCase().localeCompare(b.code.toUpperCase()));
    }
    async getOutstandingStatementsExcel(borrowerIds, selectedDate, fullMonth) {
        const results = await this.getOutstandingStatements(borrowerIds, selectedDate, fullMonth);
        const header = [
            ['Date', (0, dayjs_1.default)(selectedDate).format(date_helper_1.defaultDateFormat)],
            ['', '', 'Outstanding statement'],
            ['client ID', 'product ID', 'product name', 'balance', 'interest', 'admin', 'unused', 'wire & other', 'recoverable expenses', 'principal'],
        ];
        const onlyData = results.map((res) => {
            return [
                res.borrowerCode,
                res.code,
                res.name,
                res.actualBalance,
                res[loan_charge_type_enum_1.ELoanChargeType.INTEREST_FEE],
                res[loan_charge_type_enum_1.ELoanChargeType.ADMIN_FEE],
                res[loan_charge_type_enum_1.ELoanChargeType.UNUSED_LINE_FEE],
                new decimal_js_1.default(res[loan_charge_type_enum_1.ELoanChargeType.WIRE_FEE]).add(res[loan_charge_type_enum_1.ELoanChargeType.OTHER]).add(res[loan_charge_type_enum_1.ELoanChargeType.ANNUAL_LINE_FEE]).toNumber(),
                res[loan_charge_type_enum_1.ELoanChargeType.RECOVERABLE],
                res.principal,
            ];
        });
        return await this.uploadsService.convertDataToFile([{ data: [...header, ...onlyData] }]);
    }
    async getBorrowerProductTotals(borrowerId) {
        return await this.getBorrowerComplianceData(borrowerId, new Date());
    }
    async getProductsTotals(productIds) {
        const totals = {};
        await Promise.all(productIds.map(async (productId) => {
            const product = await LoanProducts_model_1.LoanProduct.findById(productId);
            if (product) {
                const borrowerData = await this.getBorrowerComplianceData(product.borrowerId.toString(), new Date());
                Object.entries(borrowerData.productTotals).forEach(([id, value]) => {
                    if (productIds.includes(id)) {
                        totals[id] = value;
                    }
                });
            }
        }));
        return totals;
    }
    getAccruedStatement(lastStatements) {
        const totals = lastStatements.reduce((acc, s) => {
            return {
                amountPaid: new decimal_js_1.default(acc.amountPaid).add(s.amountPaid).toNumber(),
                amount: new decimal_js_1.default(acc.amount).add(s.amount).toNumber(),
            };
        }, { amountPaid: 0, amount: 0 });
        return new decimal_js_1.default(totals.amount).sub(totals.amountPaid).toNumber();
    }
    async calculateAccruedStatementForBBC(bbcDateId) {
        const bbcDate = await BBCDate_model_1.BBCDateModel.findById(bbcDateId);
        if (!bbcDate) {
            return 0;
        }
        const charges = await this.loanChargesService.getLoanCharges(bbcDate.borrowerId.toString());
        const chargesId = charges.map((c) => c._id);
        const lastStatements = await LoanStatementTransaction_model_1.LoanStatementTransactionModel.aggregate([
            {
                $match: {
                    'date': { $lt: bbcDate.bbcDate },
                    'chargeId': { $in: chargesId },
                    $expr: { $gt: ['$amount', '$amountPaid'] },
                },
            }, {
                $project: {
                    _id: 0,
                    amount: 1,
                    amountPaid: 1,
                },
            },
        ]);
        return this.getAccruedStatement(lastStatements);
    }
    async calculateAccruedStatementForBorrower(borrowerId) {
        const charges = await this.loanChargesService.getLoanCharges(borrowerId);
        const chargesId = charges.map((c) => c._id);
        const lastStatements = await LoanStatementTransaction_model_1.LoanStatementTransactionModel.aggregate([
            {
                $match: {
                    'date': { $lt: new Date() },
                    'chargeId': { $in: chargesId },
                    $expr: { $gt: ['$amount', '$amountPaid'] },
                },
            }, {
                $project: {
                    _id: 0,
                    amount: 1,
                    amountPaid: 1,
                },
            },
        ]);
        return this.getAccruedStatement(lastStatements);
    }
}
exports.LoanStatementService = LoanStatementService;
