"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.LoanPaymentsService = exports.paymentOrder = void 0;
const mongoose_1 = __importDefault(require("mongoose"));
const dayjs_1 = __importDefault(require("dayjs"));
const decimal_js_1 = __importDefault(require("decimal.js"));
const joi_1 = __importDefault(require("joi"));
const common_helper_1 = require("../helpers/common.helper");
const loan_charge_type_enum_1 = require("../enums/loan-charge-type.enum");
const loan_types_enum_1 = require("../enums/loan-types.enum");
const LoanStatementTransaction_model_1 = require("../models/LoanStatementTransaction.model");
const collaterals_db_1 = require("../db/collaterals.db");
const LoanTransaction_model_1 = require("../models/LoanTransaction.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");
exports.paymentOrder = [
    loan_charge_type_enum_1.ELoanChargeType.RECOVERABLE,
    loan_charge_type_enum_1.ELoanChargeType.INTEREST_FEE,
    loan_charge_type_enum_1.ELoanChargeType.ADMIN_FEE,
    loan_charge_type_enum_1.ELoanChargeType.UNUSED_LINE_FEE,
    loan_charge_type_enum_1.ELoanChargeType.WIRE_FEE,
    loan_charge_type_enum_1.ELoanChargeType.OTHER,
    loan_charge_type_enum_1.ELoanChargeType.ANNUAL_LINE_FEE,
];
const CALCULATION_VAR = 'BULK_PAYMENT';
class LoanPaymentsService {
    redisClient;
    borrowersDB;
    financialComplianceService;
    loanChargesService;
    loanProductsService;
    loanStatementBalanceService;
    getLoanStatementService;
    loanStatementStatusService;
    loanTransactionsService;
    lockService;
    termLoanService;
    usersService;
    constructor(redisClient, borrowersDB, financialComplianceService, loanChargesService, loanProductsService, loanStatementBalanceService, getLoanStatementService, loanStatementStatusService, loanTransactionsService, lockService, termLoanService, usersService) {
        this.redisClient = redisClient;
        this.borrowersDB = borrowersDB;
        this.financialComplianceService = financialComplianceService;
        this.loanChargesService = loanChargesService;
        this.loanProductsService = loanProductsService;
        this.loanStatementBalanceService = loanStatementBalanceService;
        this.getLoanStatementService = getLoanStatementService;
        this.loanStatementStatusService = loanStatementStatusService;
        this.loanTransactionsService = loanTransactionsService;
        this.lockService = lockService;
        this.termLoanService = termLoanService;
        this.usersService = usersService;
    }
    async getLoanPayment(paymentId) {
        return LoanPayment_model_1.LoanPaymentModel.findById(paymentId).lean();
    }
    async getLoanPayments(filter, paginatorOptions) {
        return LoanPayment_model_1.LoanPaymentModel.aggregate([
            {
                $match: {
                    $and: [
                        { 'productId': { $in: filter.productIds.map((id) => new mongoose_1.default.Types.ObjectId(id)) } },
                        { 'date': { $gte: filter.start } },
                        { 'date': { $lte: (0, dayjs_1.default)(filter.end).utcOffset(0).endOf('day').toDate() } },
                    ],
                },
            }, {
                $sort: {
                    'date': 1,
                },
            },
            ...(0, collaterals_db_1.ITEMS_PAGINATION)(paginatorOptions),
        ]);
    }
    async getTotalLoanPaymentsForProduct(filter) {
        return LoanPayment_model_1.LoanPaymentModel.countDocuments({
            $and: [
                { 'productId': { $in: filter.productIds } },
                { 'date': { $gte: filter.start } },
                { 'date': { $lte: filter.end } },
            ],
        });
    }
    async saveLoanPayment(payment, userId) {
        if (!payment._id) {
            delete payment._id;
            const newPayment = new LoanPayment_model_1.LoanPaymentModel(payment);
            await newPayment.save();
            await this.calculatePaymentProportions(newPayment, newPayment.productId.toString(), userId);
        }
        else {
            await LoanPayment_model_1.LoanPaymentModel.findByIdAndUpdate(payment._id, payment);
            await this.calculatePaymentProportions(payment, payment.productId.toString(), userId);
        }
    }
    async createLoanPayment(payment, userId) {
        const purePayment = (0, common_helper_1.createFilteredObject)(payment, LoanPayment_model_1.LOAN_PAYMENT_FIELDS);
        await this.saveLoanPayment(purePayment, userId);
    }
    async updateLoanPayment(payment, userId) {
        const foundPayment = await LoanPayment_model_1.LoanPaymentModel.findById(payment._id).lean();
        if (!foundPayment) {
            return;
        }
        await this.loanStatementBalanceService.cleanDecreasePayment(foundPayment, userId);
        const purePayment = (0, common_helper_1.createFilteredObject)(payment, LoanPayment_model_1.LOAN_PAYMENT_FIELDS);
        await this.saveLoanPayment(purePayment, userId);
    }
    async recalculateAfterPayment(productId, userId) {
        console.log('started recalculating product', productId);
        await this.recalculateProduct(productId, userId);
        await this.loanTransactionsService.recalculateProduct(productId);
    }
    async deleteLoanPayment(paymentId, userId) {
        const foundPayment = await LoanPayment_model_1.LoanPaymentModel.findById(paymentId).lean();
        if (!foundPayment) {
            return;
        }
        await this.loanStatementBalanceService.cleanDecreasePayment(foundPayment, userId);
        await LoanPayment_model_1.LoanPaymentModel.findByIdAndDelete(paymentId);
        const statementDate = this.loanStatementStatusService.getStatementDateForDate(new Date(foundPayment.date));
        await this.loanStatementStatusService.updateStatementStatus(foundPayment.productId.toString(), statementDate, false);
        await this.termLoanService.setTermLoanExpected(foundPayment.productId.toString());
    }
    async calculatePaymentProportions(payment, productId, userId) {
        const product = await LoanProducts_model_1.LoanProduct.findById(productId).lean();
        const settlementCode = payment.settlementCode;
        const borrower = await Borrower_model_1.BorrowerModel.findById(product.borrowerId).lean();
        const loanCharges = await this.loanChargesService.getLoanChargeForProduct(productId);
        const chargesIds = loanCharges.map((charge) => charge._id);
        const productPaymentOrder = !payment.paymentOrder || payment.paymentOrder.length === 0
            ? exports.paymentOrder
            : payment.paymentOrder.map((p) => loanCharges.find((charge) => charge._id.toString() === p).chargeType);
        const paymentOrderQuery = productPaymentOrder.map((charge, index) => {
            return {
                case: {
                    $eq: ['$charge.chargeType', charge],
                },
                then: index,
            };
        });
        const unpaidStatements = await LoanStatementTransaction_model_1.LoanStatementTransactionModel.aggregate([
            {
                $match: {
                    'chargeId': { '$in': chargesIds },
                    'date': { $lte: (0, dayjs_1.default)(new Date(payment.date)).utcOffset(0).startOf('month').subtract(1, 'second').toDate() },
                    $expr: { $lt: ['$amountPaid', '$amount'] },
                },
            }, {
                $lookup: {
                    from: 'loan_charges',
                    localField: 'chargeId',
                    foreignField: '_id',
                    as: 'charge',
                },
            }, {
                $unwind: {
                    path: '$charge',
                },
            }, {
                $addFields: {
                    customSortValue: {
                        $switch: {
                            branches: paymentOrderQuery,
                            default: exports.paymentOrder.length,
                        },
                    },
                },
            }, {
                $sort: {
                    customSortValue: 1,
                    date: 1,
                    order: 1,
                },
            },
        ]);
        let availableAmount = payment.amount;
        const paidArray = [];
        let firstPaidStatement = null;
        for (const statement of unpaidStatements) {
            const foundStatement = await LoanStatementTransaction_model_1.LoanStatementTransactionModel.findById(statement._id);
            if (!foundStatement) {
                return;
            }
            const prefPaidAmount = new decimal_js_1.default(statement.amount).sub(foundStatement.amountPaid).toNumber();
            const amountPaid = prefPaidAmount > availableAmount ? availableAmount : prefPaidAmount;
            availableAmount = new decimal_js_1.default(availableAmount).sub(amountPaid).toNumber();
            paidArray.push({ statementId: statement._id, amount: amountPaid });
            await LoanStatementTransaction_model_1.LoanStatementTransactionModel.findByIdAndUpdate(statement._id, {
                amountPaid: new decimal_js_1.default(foundStatement.amountPaid).add(amountPaid).toNumber(),
                settlementCode,
            });
            if (!firstPaidStatement ||
                (statement.date.getTime() < firstPaidStatement.date.getTime()) ||
                (firstPaidStatement.date.getTime() === statement.date.getTime() && firstPaidStatement.order > statement.order)) {
                firstPaidStatement = statement;
            }
            if (availableAmount === 0) {
                break;
            }
        }
        const loanTransactions = [];
        if (availableAmount > 0) {
            const transaction = {
                amount: -availableAmount,
                date: payment.date,
                description: `${borrower.code} - STATEMENT PAID`,
                effectiveDate: null,
                loanPaymentId: payment._id,
                order: 0,
                productId: new mongoose_1.default.Types.ObjectId(productId),
                reference: `${borrower.code} - STATEMENT PAID`,
                transactionType: LoanTransaction_model_1.ELoanTransactionTypes.COLLECTION,
                userId,
            };
            transaction.effectiveDate = await this.loanTransactionsService.calculateEffectiveDate(transaction);
            const newTransaction = await this.loanTransactionsService.createLoanTransaction(transaction);
            loanTransactions.push({ transactionId: newTransaction._id, amount: newTransaction.amount });
        }
        await LoanPayment_model_1.LoanPaymentModel.findByIdAndUpdate(payment._id, { paid: paidArray, loanTransactions }, { new: true });
        if (firstPaidStatement) {
            await this.loanStatementBalanceService.calculateProductBalance(productId, firstPaidStatement.date);
        }
        await this.updateComplianceBorrowerAmountReceived(borrower._id.toString());
        const statementDate = this.loanStatementStatusService.getStatementDateForDate(new Date(payment.date));
        await this.loanStatementStatusService.updateStatementStatus(productId, statementDate, false);
        await this.termLoanService.setTermLoanExpected(productId, true);
    }
    async updateComplianceBorrowerAmountReceived(borrowerId) {
        const allProducts = await this.loanChargesService.getLoanProducts(borrowerId);
        const productIds = allProducts.map((p) => p._id.toString());
        const startDate = (0, dayjs_1.default)().startOf('month').toDate();
        const endDate = (0, dayjs_1.default)().endOf('month').toDate();
        const payments = await this.getPaymentsForPeriod(productIds, startDate, endDate);
        const totalPayments = payments.reduce((acc, p) => new decimal_js_1.default(acc).add(p.amount ?? 0).toNumber(), 0);
        const complianceBorrower = await this.financialComplianceService.getFinancialComplianceBorrower(borrowerId);
        if (!complianceBorrower) {
            return;
        }
        await this.financialComplianceService.updateBorrower(complianceBorrower._id.toString(), {
            ...complianceBorrower,
            amountReceived: totalPayments,
        });
    }
    async recalculateProduct(productId, userId) {
        const loanCharges = await this.loanChargesService.getLoanChargeForProduct(productId);
        const product = await this.loanChargesService.getLoanProductById(productId);
        const chargesIds = loanCharges.map((charge) => charge._id);
        await LoanStatementTransaction_model_1.LoanStatementTransactionModel.updateMany({ 'chargeId': { $in: chargesIds } }, { amountPaid: 0 });
        const firstStatements = await LoanStatementTransaction_model_1.LoanStatementTransactionModel.aggregate([
            {
                $match: {
                    'chargeId': { $in: chargesIds },
                },
            }, {
                $sort: {
                    'date': 1,
                    'order': 1,
                },
            }, {
                $limit: 1,
            },
        ]);
        if (firstStatements.length > 0) {
            const loanStatementService = this.getLoanStatementService();
            await loanStatementService.updateStatementTransaction(firstStatements[0]);
        }
        const payments = await LoanPayment_model_1.LoanPaymentModel.aggregate([
            {
                $match: { 'productId': new mongoose_1.default.Types.ObjectId(productId) },
            },
            {
                $sort: {
                    'date': 1,
                    'createdAt': 1,
                },
            },
        ]);
        for (const [index, payment] of payments.entries()) {
            try {
                console.log(`updating payment (for ${product.name}) ${index + 1}/${payments.length}`, (0, dayjs_1.default)(payment.date).format('DD-MM-YYYY'), payment.amount);
                await LoanTransaction_model_1.LoanTransaction.deleteMany({
                    _id: { $in: payment.loanTransactions.map(t => t.transactionId) }
                });
                await LoanPayment_model_1.LoanPaymentModel.findByIdAndUpdate(payment._id, { paid: [], loanTransactions: [] });
                await this.updateLoanPayment(payment, userId);
            }
            catch (e) {
                console.error({ productId, payments });
                console.error(e);
            }
            finally {
                console.log(`finished updating payment (for ${product.name}) ${index + 1}/${payments.length}`);
            }
        }
    }
    async getPaymentsForPeriod(productIds, startDate, endDate) {
        return LoanPayment_model_1.LoanPaymentModel.aggregate([
            {
                $match: {
                    $and: [
                        { 'productId': { $in: productIds.map((id) => new mongoose_1.default.Types.ObjectId(id)) } },
                        { 'date': { $gte: startDate } },
                        { 'date': { $lte: endDate } },
                    ],
                },
            }, {
                $sort: {
                    'date': 1,
                    'createdAt': 1,
                },
            },
        ]);
    }
    async proceedCombinedPayment(combinedPayment, userId) {
        try {
            if (combinedPayment.addToRevolver) {
                const isLockedRes = await Promise.all([...combinedPayment.products, ...combinedPayment.linkedProducts].map(async (productGroup) => {
                    const product = await LoanProducts_model_1.LoanProduct.findById(productGroup.productId).lean();
                    if (product) {
                        return await this.lockService.isDateLocked(new Date(combinedPayment.date), product.borrowerId.toString());
                    }
                    return false;
                }));
                if (isLockedRes.some((isLocked) => !!isLocked)) {
                    return { success: false, message: 'Period is signed and locked' };
                }
            }
            const loanStatementService = this.getLoanStatementService();
            const data = await loanStatementService.getBorrowerComplianceData(combinedPayment.borrowerId, new Date());
            let totalSumToCover = combinedPayment.payment;
            const products = await this.loanChargesService.getLoanProducts(combinedPayment.borrowerId);
            const revolverProduct = products.find((product) => product.type === loan_types_enum_1.ELoanTypes.REVOLVER && (0, date_helper_1.isProductActive)(product.startDate, product.deactivationDate, combinedPayment.date));
            for (const p of combinedPayment.products) {
                await this.loanProductsService.savePaymentOrder(p.productId, p.paymentOrder);
                const desiredProductSum = data.productTotals[p.productId];
                const product = await LoanProducts_model_1.LoanProduct.findById(p.productId).lean();
                const sumForProduct = desiredProductSum > totalSumToCover ? totalSumToCover : desiredProductSum;
                if (sumForProduct > 0) {
                    const newPayment = {
                        _id: null,
                        productId: new mongoose_1.default.Types.ObjectId(p.productId),
                        amount: sumForProduct,
                        date: new Date(combinedPayment.date),
                        settlementCode: p.settlementCode,
                        paymentOrder: p.paymentOrder,
                    };
                    await this.createLoanPayment(newPayment, userId);
                    if (combinedPayment.addToRevolver && revolverProduct) {
                        const newTransactions = {
                            amount: sumForProduct,
                            bankId: null,
                            date: combinedPayment.date,
                            description: `${product.type} STATEMENT ADDED TO LOAN`,
                            effectiveDate: null,
                            productId: revolverProduct._id,
                            reference: `${product.type} STATEMENT ADDED TO LOAN`,
                            transactionType: LoanTransaction_model_1.ELoanTransactionTypes.DISBURSEMENT,
                            userId,
                        };
                        newTransactions.effectiveDate = await this.loanTransactionsService.calculateEffectiveDate(newTransactions);
                        await this.loanTransactionsService.createLoanTransaction(newTransactions);
                    }
                }
                totalSumToCover = new decimal_js_1.default(totalSumToCover).sub(sumForProduct).toNumber();
            }
            if (totalSumToCover > 0 && revolverProduct && !combinedPayment.addToRevolver) {
                const newTransactions = {
                    amount: -totalSumToCover,
                    bankId: null,
                    date: combinedPayment.date,
                    description: `OVERPAYMENT OF STATEMENT`,
                    effectiveDate: null,
                    productId: revolverProduct._id,
                    reference: `OVERPAYMENT OF STATEMENT`,
                    transactionType: LoanTransaction_model_1.ELoanTransactionTypes.COLLECTION,
                    userId,
                };
                newTransactions.effectiveDate = await this.loanTransactionsService.calculateEffectiveDate(newTransactions);
                await this.loanTransactionsService.createLoanTransaction(newTransactions);
            }
            await Promise.all(combinedPayment.linkedProducts.map(async (p) => {
                const newTransactions = {
                    amount: -p.payableSum,
                    bankId: null,
                    date: combinedPayment.date,
                    description: `EXTRA PAYMENT`,
                    effectiveDate: null,
                    productId: new mongoose_1.default.Types.ObjectId(p.productId),
                    reference: `EXTRA PAYMENT REF`,
                    transactionType: LoanTransaction_model_1.ELoanTransactionTypes.COLLECTION,
                    userId,
                };
                newTransactions.effectiveDate = await this.loanTransactionsService.calculateEffectiveDate(newTransactions);
                await this.loanTransactionsService.createLoanTransaction(newTransactions);
            }));
            return { success: true, message: null };
        }
        catch (e) {
            console.error(e);
        }
    }
    async getExpectedPayments(userId) {
        const user = await this.usersService.getUserById(userId);
        if (!user) {
            return [];
        }
        const borrowers = await this.borrowersDB.getAllowedBorrowers(user);
        const isCalculated = await this.redisClient.get(CALCULATION_VAR);
        const borrowersData = await Promise.all(borrowers
            .filter((borrower) => borrower.active)
            .map(async (borrower) => {
            const loanStatementService = this.getLoanStatementService();
            const dueAmounts = await loanStatementService.getBorrowerProductTotals(borrower._id.toString());
            return {
                borrowerId: borrower._id.toString(),
                borrowerTitle: borrower.name,
                totalDue: dueAmounts.total,
                productTotals: dueAmounts.productTotals,
                revolverBalance: dueAmounts.revolverBalance,
                termBalance: dueAmounts.termBalance,
            };
        }));
        return { borrowers: borrowersData, isCalculated: !!isCalculated };
    }
    async payExpectedPayments(userId, borrowers, selectedDate) {
        const arrayOfStringsSchema = joi_1.default.array().items(joi_1.default.string()).required();
        const borrowerIds = Object.keys(borrowers);
        const { error } = arrayOfStringsSchema.validate(borrowerIds, { abortEarly: false });
        if (error) {
            return { message: 'Incorrect list of borrowers IDs' };
        }
        await this.redisClient.set(CALCULATION_VAR, 1);
        const user = await this.usersService.getUserById(userId);
        if (!user) {
            return { message: 'Cannot find user with this id' };
        }
        const allowedBorrowers = await this.borrowersDB.getAllowedBorrowers(user);
        const paymentPromises = allowedBorrowers.map(async (borrower) => {
            if (borrowerIds.includes(borrower._id.toString())) {
                try {
                    const borrowerProducts = await this.loanChargesService.getLoanProducts(borrower._id.toString());
                    const revolverProduct = borrowerProducts.find((product) => product.type === loan_types_enum_1.ELoanTypes.REVOLVER && (0, date_helper_1.isProductActive)(product.startDate, product.deactivationDate, selectedDate));
                    const loanStatementService = this.getLoanStatementService();
                    const dueAmounts = await loanStatementService.getBorrowerProductTotals(borrower._id.toString());
                    if (dueAmounts.total === 0) {
                        console.log('no payment for this borrower');
                        return;
                    }
                    const products = await Promise.all(Object.entries(dueAmounts.productTotals).map(async ([productId, amount]) => {
                        const product = await LoanProducts_model_1.LoanProduct.findById(productId);
                        if (product) {
                            return {
                                productId: productId,
                                payableSum: amount,
                                settlementCode: borrowers[borrower._id.toString()] ? (revolverProduct?.code ?? product.code) : product.settlementCode,
                                paymentOrder: null,
                            };
                        }
                    }));
                    const combinedPayment = {
                        addToRevolver: borrowers[borrower._id.toString()],
                        borrowerId: borrower._id.toString(),
                        date: selectedDate,
                        linkedProducts: [],
                        payment: dueAmounts.total,
                        products: products.filter((product) => !!product),
                    };
                    await this.proceedCombinedPayment(combinedPayment, userId);
                    console.log(`Success on payment ${borrower.title}`);
                }
                catch (err) {
                    console.error(`Error on payment ${borrower.title}:`, err);
                }
            }
        });
        const processPayments = async (paymentPromises) => {
            try {
                await Promise.allSettled(paymentPromises);
                console.log('All payments processed.');
                await this.redisClient.del(CALCULATION_VAR);
            }
            catch (err) {
                console.error('Error during payment processing:', err);
            }
        };
        processPayments(paymentPromises).catch(console.error);
        return { message: 'Payments are being processed in the background.' };
    }
}
exports.LoanPaymentsService = LoanPaymentsService;
