"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BankUploadedTransactionsService = void 0;
const mongoose_1 = __importDefault(require("mongoose"));
const decimal_js_1 = require("decimal.js");
const lodash_1 = __importDefault(require("lodash"));
const dayjs_1 = __importDefault(require("dayjs"));
const LoanTransaction_model_1 = require("../models/LoanTransaction.model");
const Banks_model_1 = require("../models/Banks.model");
const UploadedBankTransaction_model_1 = require("../models/UploadedBankTransaction.model");
const main_helper_1 = require("../helpers/main.helper");
const CashAllocationReference_model_1 = require("../models/CashAllocationReference.model");
const CashAllocationProduct_model_1 = require("../models/CashAllocationProduct.model");
const AllocatedBankTransaction_model_1 = require("../models/AllocatedBankTransaction.model");
const AllocatedData_model_1 = require("../models/AllocatedData.model");
const date_formats_constants_1 = require("../constants/date-formats.constants");
const bank_reference_helper_1 = require("../helpers/bank-reference.helper");
const useExternalUploader = false; // TODO keep only external upload
const createCashAllocationReferencesOnUpload = false;
class BankUploadedTransactionsService {
    cashAllocationService;
    loanStatementStatusService;
    getLoanTransactionsService;
    constructor(cashAllocationService, loanStatementStatusService, getLoanTransactionsService) {
        this.cashAllocationService = cashAllocationService;
        this.loanStatementStatusService = loanStatementStatusService;
        this.getLoanTransactionsService = getLoanTransactionsService;
    }
    async uploadTransactions(transactions) {
        if (useExternalUploader) {
            const taskId = (0, main_helper_1.getUUID)();
            // await this.queueService.sendTaskToQueue({
            //   data: { transactions, taskId },
            //   pattern: 'bank_uploaded_transactions#CREATE',
            // }, 'goat_upload');
            return taskId;
        }
        else {
            const mappedTransactions = transactions.reduce((acc, transaction) => {
                const normalizedTransaction = {
                    ...transaction,
                    reference: transaction.reference.trim(),
                    isConverted: false,
                };
                return [...acc, normalizedTransaction];
            }, []);
            await Promise.all(mappedTransactions.map(async (transaction) => {
                const newTransactions = new UploadedBankTransaction_model_1.UploadedBankTransaction(transaction);
                await newTransactions.save();
            }));
            if (createCashAllocationReferencesOnUpload) {
                await this.cashAllocationService.createCashAllocation(mappedTransactions);
            }
            return null;
        }
    }
    async getAllocatedBankTransactions(startDate, endDate) {
        return AllocatedBankTransaction_model_1.AllocatedBankTransaction.aggregate([
            {
                $match: {
                    $and: [
                        { 'date': { $gte: startDate } },
                        { 'date': { $lte: endDate } },
                    ],
                },
            },
            {
                $sort: { order: 1, date: 1 },
            },
        ]);
    }
    async deleteAllocatedBankTransaction(transactionIds) {
        await AllocatedBankTransaction_model_1.AllocatedBankTransaction.deleteMany({ uploadedBankTransactionId: { $in: transactionIds.map((id) => new mongoose_1.default.Types.ObjectId(id)) } });
    }
    async getMappedUploadedBankTransactions(startDate, endDate) {
        const bankTransactions = await this.getUploadedBankTransactions(startDate, endDate);
        const cashAllocationReferences = await CashAllocationReference_model_1.CashAllocationReference.find().lean();
        const cashAllocationProducts = await CashAllocationProduct_model_1.CashAllocationProduct.find().lean();
        const cashAllocationProductsMap = new Map();
        cashAllocationProducts.forEach((product) => {
            cashAllocationProductsMap.set(product._id.toString(), product);
        });
        const findMatchingCashAllocationReference = (bankTransaction, searchInDetails) => {
            return cashAllocationReferences.find((cashAllocationReference) => {
                const referenceToSearch = cashAllocationReference.reference.toLowerCase();
                const fieldsToSearch = [bankTransaction.reference];
                if (searchInDetails) {
                    fieldsToSearch.push(bankTransaction.detail1, bankTransaction.detail2, bankTransaction.detail3);
                }
                return fieldsToSearch
                    .filter((field) => !!field)
                    .some((field) => field.toLowerCase().includes(referenceToSearch));
            });
        };
        const transactionHasMatch = (bankTransaction) => {
            if (bankTransaction.splitTransactions.length > 0) {
                const totalSplit = bankTransaction.splitTransactions.reduce((acc, split) => new decimal_js_1.Decimal(split.amount).add(acc).toNumber(), 0);
                if (totalSplit === bankTransaction.amount) {
                    return true;
                }
            }
            const foundCashAllocationReference = findMatchingCashAllocationReference(bankTransaction, true);
            if (!foundCashAllocationReference || !foundCashAllocationReference.cashAllocationProductId) {
                return false;
            }
            return !!cashAllocationProductsMap.get(foundCashAllocationReference.cashAllocationProductId.toString());
        };
        const transactionCanSplit = (bankTransaction) => {
            const foundCashAllocationReference = findMatchingCashAllocationReference(bankTransaction, true);
            if (!foundCashAllocationReference || !foundCashAllocationReference.cashAllocationProductId) {
                return false;
            }
            const foundProduct = cashAllocationProductsMap.get(foundCashAllocationReference.cashAllocationProductId.toString());
            return foundProduct.isCommonSplit;
        };
        return bankTransactions.map((bankTransaction) => {
            return {
                ...bankTransaction,
                hasMatch: transactionHasMatch(bankTransaction),
                canSplit: transactionCanSplit(bankTransaction),
            };
        });
    }
    async getUploadedBankTransactions(startDate, endDate) {
        return UploadedBankTransaction_model_1.UploadedBankTransaction.aggregate([
            {
                $match: {
                    $and: [
                        { 'date': { $gte: startDate } },
                        { 'date': { $lte: endDate } },
                    ],
                },
            },
            {
                $sort: { date: 1 },
            },
        ]);
    }
    async getAllocatedTransactions(date) {
        const startDate = (0, dayjs_1.default)(date).utcOffset(0).startOf('day');
        const endDate = startDate.endOf('day');
        return AllocatedBankTransaction_model_1.AllocatedBankTransaction.aggregate([
            {
                $match: {
                    $and: [
                        { 'date': { $gte: startDate.toDate() } },
                        { 'date': { $lte: endDate.toDate() } },
                    ],
                },
            },
            {
                $sort: { date: 1, order: 1 },
            },
        ]);
    }
    async getPreAllocatedTransactions(date) {
        const startDate = (0, dayjs_1.default)(date).utcOffset(0).startOf('day');
        const endDate = startDate.endOf('day');
        const pureUploadedBankTransactions = await this.getMappedUploadedBankTransactions(startDate.toDate(), endDate.toDate());
        const { references, products } = await this.cashAllocationService.getAllCashAllocations();
        const populateAllocatedBankTransaction = (allocatedBankTransaction, cashAllocation) => {
            const updatedAllocatedBankTransaction = lodash_1.default.cloneDeep(allocatedBankTransaction);
            if (cashAllocation) {
                const keysToUpdate = ['class', 'location', 'DRMemo', 'CRMemo'];
                keysToUpdate.forEach((key) => {
                    updatedAllocatedBankTransaction[key] = cashAllocation[key];
                });
                updatedAllocatedBankTransaction.cashAllocationProductId = cashAllocation._id;
            }
            return updatedAllocatedBankTransaction;
        };
        let order = 0;
        const uploadedBankTransactions = pureUploadedBankTransactions
            .reduce((acc, tr) => {
            if (tr.splitTransactions.length > 0) {
                const restTransaction = {
                    ...tr,
                    order,
                    splitTransactionId: null,
                    splitMemo: '',
                };
                order = order + 1;
                const splitTransactions = tr.splitTransactions.map((splitTr) => {
                    const foundRef = references.find((i) => i._id.toString() === splitTr.referenceId.toString());
                    const transaction = {
                        _id: tr._id,
                        order,
                        accountNumber: tr.accountNumber,
                        amount: splitTr.amount,
                        date: tr.date,
                        detail1: '',
                        detail2: '',
                        detail3: '',
                        hasMatch: true,
                        canSplit: false,
                        isConverted: tr.isConverted,
                        reference: foundRef ? foundRef.reference : tr.reference,
                        splitTransactions: [],
                        transactionType: tr.transactionType,
                        splitTransactionId: splitTr._id,
                        splitMemo: splitTr.memo,
                    };
                    restTransaction.amount = new decimal_js_1.Decimal(restTransaction.amount).sub(transaction.amount).toNumber();
                    return transaction;
                });
                if (restTransaction.amount !== 0) {
                    return [...acc, ...splitTransactions, restTransaction];
                }
                return [...acc, ...splitTransactions];
            }
            order = order + 1;
            return [...acc, { ...tr, splitTransactionId: null, splitMemo: '', order }];
        }, []);
        const allocatedBankTransactions = uploadedBankTransactions
            .reduce((acc, tr) => {
            const allocatedBankTransaction = {
                order: tr.order,
                uploadedBankTransactionId: tr._id,
                accountNumber: tr.accountNumber,
                amount: tr.amount,
                date: tr.date,
                reference: tr.reference,
                cashAllocationReferenceId: null,
                cashAllocationProductId: null,
                accounts: null,
                class: '',
                location: '',
                memo: tr.splitMemo,
                splitTransactionId: tr.splitTransactionId,
            };
            const foundCashAllocationReferences = references
                .filter((reference) => tr.reference.toLowerCase().includes(reference.reference.toLowerCase()));
            if (!foundCashAllocationReferences.length) {
                return acc;
            }
            const useOnlyOneReference = true;
            const reducedFoundCashAllocationReferences = useOnlyOneReference ? foundCashAllocationReferences.slice(0, 1) : foundCashAllocationReferences;
            return [
                ...acc,
                ...reducedFoundCashAllocationReferences.map((foundCashAllocationReference) => {
                    const foundCashAllocationProduct = products
                        .find((product) => product._id.toString() === foundCashAllocationReference.cashAllocationProductId.toString());
                    if (!foundCashAllocationProduct) {
                        return null;
                    }
                    return foundCashAllocationProduct
                        ? populateAllocatedBankTransaction({
                            ...allocatedBankTransaction,
                            cashAllocationReferenceId: foundCashAllocationReference._id,
                            accounts: foundCashAllocationProduct.accounts,
                            memo: (tr.splitMemo && tr.splitMemo.trim() !== '')
                                ? tr.splitMemo
                                : (tr.amount > 0 ? foundCashAllocationProduct.DRMemo : foundCashAllocationProduct.CRMemo),
                        }, foundCashAllocationProduct)
                        : allocatedBankTransaction;
                })
            ];
        }, [])
            .filter((tr) => !!tr);
        return { allocatedBankTransactions, uploadedBankTransactions };
    }
    async resetAllocatedBankTransactions(date) {
        const { allocatedBankTransactions } = await this.getPreAllocatedTransactions(date);
        const uniqueTransactionsIds = new Set(allocatedBankTransactions.map((tr) => tr.uploadedBankTransactionId.toString()));
        await this.deleteAllocatedBankTransaction(Array.from(uniqueTransactionsIds));
        await Promise.all(allocatedBankTransactions.map(async (transaction) => {
            const newTransaction = new AllocatedBankTransaction_model_1.AllocatedBankTransaction(transaction);
            await newTransaction.save();
        }));
        await this.updateLockStatus(date, false);
    }
    async createAllocatedBankTransactions(date) {
        const cashAllocations = await this.getAllocatedTransactions(date);
        const allocatedData = await AllocatedData_model_1.AllocatedData.findOne({ date: (0, dayjs_1.default)(date).format(date_formats_constants_1.MAIN_DATE_FORMAT) });
        if (cashAllocations.length > 0 && (allocatedData && allocatedData.isLocked)) {
            return;
        }
        await this.resetAllocatedBankTransactions(date);
    }
    async convertBankTransactions(startDate, userId) {
        const allocatedTransactions = await this.getAllocatedTransactions(startDate);
        const transactionIds = allocatedTransactions.map((transaction) => transaction.uploadedBankTransactionId.toString());
        const transactions = await UploadedBankTransaction_model_1.UploadedBankTransaction.find({ _id: { $in: transactionIds } });
        const cashAllocations = await this.cashAllocationService.getAllCashAllocations();
        const banks = await Banks_model_1.Bank.find().lean();
        const loanTransactionsMap = transactions.reduce((acc, transaction) => {
            return {
                ...acc,
                [transaction._id.toString()]: [],
            };
        }, {});
        const productMap = {};
        const productIds = new Set();
        await Promise.all(allocatedTransactions.map(async (transactionToConvert) => {
            const transaction = transactions.find((tr) => tr._id.toString() === transactionToConvert.uploadedBankTransactionId.toString());
            if (!transaction || transaction.isConverted) {
                return;
            }
            const splitTransaction = transactionToConvert.splitTransactionId
                ? transaction.splitTransactions.find((tr) => tr._id.toString() === transactionToConvert.splitTransactionId.toString())
                : null;
            const foundCashAllocationProduct = cashAllocations.products.find((c) => c._id.toString() === transactionToConvert.cashAllocationProductId.toString());
            if (!foundCashAllocationProduct || !foundCashAllocationProduct.productId) {
                return;
            }
            const foundBank = banks.find((b) => b.bankAccountNumber === transaction.accountNumber);
            const getLoanTransaction = () => ({
                amount: transactionToConvert.amount,
                bankId: foundBank ? new mongoose_1.default.Types.ObjectId(String(foundBank._id)) : null,
                customerId: null,
                date: new Date(transaction.date),
                description: '',
                effectiveDate: null,
                productId: foundCashAllocationProduct.productId,
                reference: splitTransaction ? (0, bank_reference_helper_1.convertBankReferenceToTransactionReference)(splitTransaction.memo) : (0, bank_reference_helper_1.convertBankReferenceToTransactionReference)(transactionToConvert.reference),
                referenceRaw: splitTransaction ? (0, bank_reference_helper_1.convertBankReferenceToTransactionReference)(splitTransaction.memo) : transactionToConvert.reference,
                transactionType: transaction.amount > 0 ? LoanTransaction_model_1.ELoanTransactionTypes.COLLECTION : LoanTransaction_model_1.ELoanTransactionTypes.DISBURSEMENT,
                balance: 0,
                floatedBalance: 0,
            });
            const loanTransactionsService = this.getLoanTransactionsService();
            const normalizedTransaction = await loanTransactionsService.normalizeLoanTransaction(getLoanTransaction(), userId);
            const createdLoanTransaction = await loanTransactionsService.createLoanTransaction(normalizedTransaction);
            loanTransactionsMap[String(transaction._id)] = [...loanTransactionsMap[String(transaction._id)], new mongoose_1.default.Types.ObjectId(String(createdLoanTransaction._id))];
            productIds.add(String(foundCashAllocationProduct.productId));
            if (!productMap[createdLoanTransaction.productId.toString()]) {
                productMap[createdLoanTransaction.productId.toString()] = createdLoanTransaction.date.toISOString();
            }
            else {
                const foundDate = new Date(productMap[createdLoanTransaction.productId.toString()]);
                if (foundDate > createdLoanTransaction.date) {
                    productMap[createdLoanTransaction.productId.toString()] = createdLoanTransaction.date.toISOString();
                }
            }
        }));
        await Promise.all(transactions.map(async (transaction) => {
            await transaction.updateOne({
                isConverted: true,
                loanTransactionIds: loanTransactionsMap[String(transaction._id)],
            });
        }));
        await this.recalculateProducts(Array.from(productIds), productMap);
        return transactions.map((transaction) => transaction._id.toString());
    }
    async recalculateProducts(productIds, productMap) {
        // TODO rework in something better
        setTimeout(async () => {
            await Promise.all(productIds.map(async (productId) => {
                const loanTransactionsService = this.getLoanTransactionsService();
                await loanTransactionsService.recalculateProduct(productId);
            }));
        }, 2000);
        await this.loanStatementStatusService.recalculateStatementQueued(productMap);
    }
    async preSplitBankTransactions(transactionId, splitTransaction) {
        const mappedSplitTransactions = splitTransaction.map((transaction) => {
            const { _id, ...transactionRest } = transaction;
            return _id.toString().includes('new_') ? transactionRest : transaction;
        });
        await UploadedBankTransaction_model_1.UploadedBankTransaction.updateOne({ _id: new mongoose_1.default.Types.ObjectId(transactionId) }, { splitTransactions: mappedSplitTransactions });
    }
    async deleteBankTransaction(transactionIds) {
        await UploadedBankTransaction_model_1.UploadedBankTransaction.deleteMany({
            _id: { $in: transactionIds },
            isConverted: { $ne: true },
        });
    }
    async revertAllBankTransactions(startDate, endDate, userId) {
        const transactions = await this.getUploadedBankTransactions(startDate, endDate);
        const transactionIds = transactions.map((t) => t._id.toString());
        await this.revertBankTransactions(transactionIds, userId);
    }
    async revertBankTransactions(transactionIds, userId) {
        const foundTransaction = await UploadedBankTransaction_model_1.UploadedBankTransaction.find({ _id: { $in: transactionIds }, isConverted: true });
        const productIds = new Set();
        const productMap = {};
        await Promise.all(foundTransaction.map(async (transaction) => {
            try {
                await Promise.all(transaction.loanTransactionIds.map(async (loanTransactionId) => {
                    const loanTransaction = await LoanTransaction_model_1.LoanTransaction.findById(loanTransactionId).lean().exec();
                    if (!productMap[loanTransaction.productId.toString()]) {
                        productMap[loanTransaction.productId.toString()] = loanTransaction.date.toISOString();
                    }
                    else {
                        const foundDate = new Date(productMap[loanTransaction.productId.toString()]);
                        if (foundDate > loanTransaction.date) {
                            productMap[loanTransaction.productId.toString()] = loanTransaction.date.toISOString();
                        }
                    }
                    const loanTransactionsService = this.getLoanTransactionsService();
                    const isLocked = await loanTransactionsService.checkLockStatus(loanTransaction);
                    if (isLocked) {
                        throw new Error('period is locked');
                    }
                    const validationError = await this.loanStatementStatusService.validateDate(loanTransaction.productId.toString(), new Date(transaction.date));
                    if (validationError) {
                        throw new Error('validation error');
                    }
                    await loanTransactionsService.deleteLoanTransaction(loanTransactionId.toString(), false, userId);
                    productIds.add(loanTransaction.productId.toString());
                }));
                transaction.loanTransactionIds = [];
                transaction.isConverted = false;
                await transaction.save();
            }
            catch (error) {
                console.log(error);
            }
        }));
        await this.recalculateProducts(Array.from(productIds), productMap);
    }
    async getUniqueDates() {
        const uniqueDates = await UploadedBankTransaction_model_1.UploadedBankTransaction.aggregate([
            {
                $group: {
                    _id: { $dateToString: { format: '%Y-%m-%d', date: '$date' } },
                },
            },
            {
                $sort: { _id: 1 },
            },
        ]);
        return uniqueDates.map(d => d._id);
    }
    async saveAllocatedBankTransactions(date, transactions) {
        const startDate = (0, dayjs_1.default)(date).utcOffset(0).startOf('day');
        const endDate = startDate.endOf('day');
        const existingAllocatedTransactions = await this.getAllocatedBankTransactions(startDate.toDate(), endDate.toDate());
        const existingTransactionIds = existingAllocatedTransactions.map((t) => t._id.toString());
        const transactionIds = transactions.map((t) => t._id.toString());
        const transactionIdsToDelete = existingTransactionIds.reduce((acc, id) => {
            if (transactionIds.includes(id)) {
                return acc;
            }
            return [...acc, id];
        }, []);
        await Promise.all(transactions.map(async (transaction, order) => {
            const { _id, ...rest } = transaction;
            if (_id.toString().includes('new_')) {
                const newTransaction = new AllocatedBankTransaction_model_1.AllocatedBankTransaction({ ...rest, order });
                await newTransaction.save();
            }
            else {
                await AllocatedBankTransaction_model_1.AllocatedBankTransaction.updateOne({ _id }, { ...rest, order });
            }
        }));
        await AllocatedBankTransaction_model_1.AllocatedBankTransaction.deleteMany({ _id: { $in: transactionIdsToDelete } });
        await this.updateLockStatus(date, true);
    }
    async createAllocatedBankTransaction(transaction) {
        const { _id, ...rest } = transaction;
        const startDate = (0, dayjs_1.default)(transaction.date).utcOffset(0).startOf('day');
        const endDate = startDate.endOf('day');
        const existingAllocatedTransactions = await this.getAllocatedBankTransactions(startDate.toDate(), endDate.toDate());
        const order = existingAllocatedTransactions.length > 0 ? existingAllocatedTransactions[existingAllocatedTransactions.length - 1].order + 1 : 0;
        const newTransaction = new AllocatedBankTransaction_model_1.AllocatedBankTransaction({ ...rest, order });
        await newTransaction.save();
        await this.updateLockStatus(transaction.date, true);
    }
    async updateLockStatus(date, isLocked) {
        const normalizedDate = (0, dayjs_1.default)(date).utcOffset(0).startOf('day');
        await AllocatedData_model_1.AllocatedData.updateOne({ date: normalizedDate.format(date_formats_constants_1.MAIN_DATE_FORMAT) }, { isLocked }, { upsert: true });
    }
}
exports.BankUploadedTransactionsService = BankUploadedTransactionsService;
