import { AdminContextProvider } from '../../../admin-context/providers/admin-context.provider.js';
import { validateTransactionAgainstBusinessTrips } from '../../../business-trips/helpers/business-trips-expenses.helper.js';
import { BusinessTripAttendeesProvider } from '../../../business-trips/providers/business-trips-attendees.provider.js';
import { BusinessTripEmployeePaymentsProvider } from '../../../business-trips/providers/business-trips-employee-payments.provider.js';
import { BusinessTripExpensesProvider } from '../../../business-trips/providers/business-trips-expenses.provider.js';
import { BusinessTripsProvider } from '../../../business-trips/providers/business-trips.provider.js';
import { calculateTotalAmount, getChargeBusinesses, getChargeDocumentsMeta, getChargeTaxCategoryId, } from '../../../charges/helpers/common.helper.js';
import { DocumentsProvider } from '../../../documents/providers/documents.provider.js';
import { ExchangeProvider } from '../../../exchange-rates/providers/exchange.provider.js';
import { BusinessesProvider } from '../../../financial-entities/providers/businesses.provider.js';
import { TransactionsProvider } from '../../../transactions/providers/transactions.provider.js';
import { getExchangeDates, ledgerEntryFromDocument, } from '../../helpers/common-charge-ledger.helper.js';
import { handleCrossYearLedgerEntries } from '../../helpers/cross-year-ledger.helper.js';
import { validateExchangeRate } from '../../helpers/exchange-ledger.helper.js';
import { getEntriesFromFeeTransaction, splitFeeTransactions, } from '../../helpers/fee-transactions.js';
import { storeInitialGeneratedRecords } from '../../helpers/ledgrer-storage.helper.js';
import { generateMiscExpensesLedger } from '../../helpers/misc-expenses-ledger.helper.js';
import { generatePartialLedgerEntry, getFinancialAccountTaxCategoryId, getLedgerBalanceInfo, LedgerError, ledgerProtoToRecordsConverter, multipleForeignCurrenciesBalanceEntries, updateLedgerBalanceByEntry, validateTransactionRequiredVariables, } from '../../helpers/utils.helper.js';
import { UnbalancedBusinessesProvider } from '../../providers/unbalanced-businesses.provider.js';
const AMOUNT_REQUIRING_DOCUMENT = 1000;
const AMOUNT_FOR_SELF_CLOSING = 300;
export const generateLedgerRecordsForBusinessTrip = async (charge, { insertLedgerRecordsIfNotExists }, { injector }) => {
    const chargeId = charge.id;
    const { defaultLocalCurrency, defaultTaxCategoryId, general: { taxCategories: { incomeExchangeRateTaxCategoryId }, }, } = await injector.get(AdminContextProvider).getVerifiedAdminContext();
    const errors = new Set();
    try {
        const tripTaxCategory = await getChargeTaxCategoryId(charge.id, injector);
        if (!tripTaxCategory) {
            errors.add(`Business trip is missing tax category`);
        }
        // validate ledger records are balanced
        const ledgerBalance = new Map();
        const [{ invoiceCount, receiptCount }, formattedChargeAmount] = await Promise.all([
            getChargeDocumentsMeta(chargeId, injector),
            calculateTotalAmount(chargeId, injector, defaultLocalCurrency),
        ]);
        const gotRelevantDocuments = invoiceCount + receiptCount > 0;
        // Get all transactions and business trip transactions
        const transactionsPromise = injector
            .get(TransactionsProvider)
            .transactionsByChargeIDLoader.load(chargeId);
        const documentsPromise = injector
            .get(DocumentsProvider)
            .getDocumentsByChargeIdLoader.load(chargeId);
        const documentsTaxCategoryIdPromise = async () => {
            if (charge.tax_category_id) {
                return charge.tax_category_id;
            }
            const taxCategoryId = await getChargeTaxCategoryId(chargeId, injector);
            if (taxCategoryId) {
                return taxCategoryId;
            }
            if (!gotRelevantDocuments) {
                return undefined;
            }
            errors.add('Tax category not found');
            return defaultTaxCategoryId;
        };
        const businessTripExpensesPromise = injector
            .get(BusinessTripsProvider)
            .getBusinessTripsByChargeIdLoader.load(chargeId)
            .then(businessTrip => {
            if (businessTrip) {
                return injector
                    .get(BusinessTripExpensesProvider)
                    .getBusinessTripsExpensesByBusinessTripIdLoader.load(businessTrip.id);
            }
            return [];
        });
        const businessTripsEmployeePaymentsPromise = injector
            .get(BusinessTripEmployeePaymentsProvider)
            .getBusinessTripEmployeePaymentsByChargeIdLoader.load(chargeId);
        const businessTripAttendeesPromise = injector
            .get(BusinessTripAttendeesProvider)
            .getBusinessTripsAttendeesByChargeIdLoader.load(chargeId);
        const unbalancedBusinessesPromise = injector
            .get(UnbalancedBusinessesProvider)
            .getChargeUnbalancedBusinessesByChargeIds.load(chargeId);
        const [transactions, documents, documentsTaxCategoryId, businessTripExpenses, businessTripsEmployeePayments, businessTripAttendees, chargeUnbalancedBusinesses, { mainBusinessId },] = await Promise.all([
            transactionsPromise,
            documentsPromise,
            documentsTaxCategoryIdPromise(),
            businessTripExpensesPromise,
            businessTripsEmployeePaymentsPromise,
            businessTripAttendeesPromise,
            unbalancedBusinessesPromise,
            getChargeBusinesses(chargeId, injector),
        ]);
        // generate ledger from transactions
        const entriesPromises = [];
        const financialAccountLedgerEntries = [];
        const feeFinancialAccountLedgerEntries = [];
        const accountingLedgerEntries = [];
        const dates = new Set();
        const currencies = new Set();
        // generate ledger from transactions
        const { mainTransactions, feeTransactions } = splitFeeTransactions(transactions);
        let isSelfClosingLedger = mainTransactions.length === 1;
        let transactionsTotalLocalAmount = 0;
        // create ledger records for misc expenses
        const miscExpensesLedgerPromise = generateMiscExpensesLedger(charge, injector).then(entries => {
            entries.map(entry => {
                entry.ownerId = charge.owner_id;
                feeFinancialAccountLedgerEntries.push(entry);
                updateLedgerBalanceByEntry(entry, ledgerBalance, defaultLocalCurrency);
                dates.add(entry.valueDate.getTime());
                currencies.add(entry.currency);
            });
        });
        // for each transaction, create a ledger record
        const mainTransactionsPromises = mainTransactions.map(async (preValidatedTransaction) => {
            try {
                const transaction = validateTransactionRequiredVariables(preValidatedTransaction, {
                    skipBusinessId: isSelfClosingLedger,
                });
                if (transaction.business_id) {
                    isSelfClosingLedger = false;
                }
                const validateTransactionAgainstBusinessTripsPromise = validateTransactionAgainstBusinessTrips(injector, transaction).catch(e => {
                    if (e instanceof LedgerError) {
                        errors.add(e.message);
                    }
                    else {
                        throw e;
                    }
                });
                const ledgerEntryPromise = async () => {
                    // get tax category
                    const accountTaxCategoryId = await getFinancialAccountTaxCategoryId(injector, transaction);
                    // preparations for core ledger entries
                    let exchangeRate = undefined;
                    if (transaction.currency !== defaultLocalCurrency) {
                        // get exchange rate for currency
                        exchangeRate = await injector
                            .get(ExchangeProvider)
                            .getExchangeRates(transaction.currency, defaultLocalCurrency, transaction.debit_timestamp);
                    }
                    const partialEntry = generatePartialLedgerEntry(transaction, charge.owner_id, exchangeRate);
                    transactionsTotalLocalAmount +=
                        (partialEntry.isCreditorCounterparty ? 1 : -1) *
                            partialEntry.localCurrencyCreditAmount1;
                    isSelfClosingLedger &&= partialEntry.localCurrencyCreditAmount1 < AMOUNT_FOR_SELF_CLOSING;
                    const entryBusinessId = (isSelfClosingLedger ? tripTaxCategory : null) ?? transaction.business_id;
                    if (!entryBusinessId) {
                        throw new LedgerError(`Transaction reference "${transaction.source_reference}" is missing business ID`);
                    }
                    const ledgerEntry = {
                        ...partialEntry,
                        creditAccountID1: partialEntry.isCreditorCounterparty
                            ? entryBusinessId
                            : accountTaxCategoryId,
                        debitAccountID1: partialEntry.isCreditorCounterparty
                            ? accountTaxCategoryId
                            : entryBusinessId,
                    };
                    return ledgerEntry;
                };
                const [ledgerEntry] = await Promise.all([
                    ledgerEntryPromise(),
                    validateTransactionAgainstBusinessTripsPromise,
                ]);
                financialAccountLedgerEntries.push(ledgerEntry);
                updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency);
                dates.add(ledgerEntry.valueDate.getTime());
                currencies.add(ledgerEntry.currency);
            }
            catch (e) {
                if (e instanceof LedgerError) {
                    errors.add(e.message);
                }
                else {
                    throw e;
                }
            }
        });
        // create a ledger record for fee transactions
        const feeTransactionsPromises = feeTransactions.map(async (transaction) => {
            const ledgerEntries = await getEntriesFromFeeTransaction(transaction, charge, injector).catch(e => {
                if (e instanceof LedgerError) {
                    errors.add(e.message);
                }
                else {
                    throw e;
                }
            });
            if (!ledgerEntries) {
                return;
            }
            feeFinancialAccountLedgerEntries.push(...ledgerEntries);
            ledgerEntries.map(ledgerEntry => {
                updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency);
                dates.add(ledgerEntry.valueDate.getTime());
                currencies.add(ledgerEntry.currency);
            });
        });
        // generate ledger from documents
        if (gotRelevantDocuments) {
            // Get all relevant documents for charge
            const relevantDocuments = documents.filter(d => ['INVOICE', 'INVOICE_RECEIPT', 'CREDIT_INVOICE'].includes(d.type));
            // if no relevant documents found and business can settle with receipts, look for receipts
            if (!relevantDocuments.length && mainBusinessId) {
                const business = await injector
                    .get(BusinessesProvider)
                    .getBusinessByIdLoader.load(mainBusinessId);
                if (business?.can_settle_with_receipt) {
                    relevantDocuments.push(...documents.filter(d => d.type === 'RECEIPT'));
                }
            }
            // for each invoice - generate accounting ledger entry
            const documentsEntriesPromises = relevantDocuments.map(async (document) => {
                if (!documentsTaxCategoryId) {
                    return;
                }
                return ledgerEntryFromDocument(injector, document, charge, charge.owner_id, documentsTaxCategoryId)
                    .then(ledgerEntry => {
                    accountingLedgerEntries.push(ledgerEntry);
                    updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency);
                    dates.add(ledgerEntry.valueDate.getTime());
                    currencies.add(ledgerEntry.currency);
                })
                    .catch(e => {
                    if (e instanceof LedgerError) {
                        errors.add(e.message);
                    }
                    else {
                        throw e;
                    }
                });
            });
            entriesPromises.push(...documentsEntriesPromises);
        }
        entriesPromises.push(...mainTransactionsPromises, ...feeTransactionsPromises, miscExpensesLedgerPromise);
        await Promise.all(entriesPromises);
        if (Math.abs(transactionsTotalLocalAmount) > AMOUNT_REQUIRING_DOCUMENT &&
            accountingLedgerEntries.length === 0 &&
            !charge.documents_optional_flag) {
            errors.add(`Charge of more than ${AMOUNT_REQUIRING_DOCUMENT} ${defaultLocalCurrency} requires a document`);
        }
        // generate ledger from business trip transactions
        if (!isSelfClosingLedger && !accountingLedgerEntries.length) {
            if (businessTripsEmployeePayments.length) {
                const businessTripsEmployeePaymentsPromises = businessTripsEmployeePayments.map(async (businessTripsEmployeePayment) => {
                    if (!tripTaxCategory) {
                        return;
                    }
                    if (!businessTripsEmployeePayment.employee_business_id ||
                        !businessTripsEmployeePayment.date ||
                        !businessTripsEmployeePayment.amount ||
                        !businessTripsEmployeePayment.currency) {
                        errors.add(`Business trip flight transaction ID="${businessTripsEmployeePayment.id}" is missing required fields`);
                        return;
                    }
                    // preparations for core ledger entries
                    let exchangeRate = undefined;
                    if (businessTripsEmployeePayment.currency !== defaultLocalCurrency) {
                        // get exchange rate for currency
                        exchangeRate = await injector
                            .get(ExchangeProvider)
                            .getExchangeRates(businessTripsEmployeePayment.currency, defaultLocalCurrency, businessTripsEmployeePayment.date);
                    }
                    // set amounts
                    let amount = Number(businessTripsEmployeePayment.amount);
                    let foreignAmount = undefined;
                    if (exchangeRate) {
                        foreignAmount = amount;
                        // calculate amounts in ILS
                        amount = exchangeRate * amount;
                    }
                    const absAmount = Math.abs(amount);
                    const absForeignAmount = foreignAmount ? Math.abs(foreignAmount) : undefined;
                    const isCreditorCounterparty = amount > 0;
                    const ledgerEntry = {
                        id: businessTripsEmployeePayment.id,
                        invoiceDate: businessTripsEmployeePayment.date,
                        valueDate: businessTripsEmployeePayment.date,
                        currency: businessTripsEmployeePayment.currency,
                        creditAccountID1: isCreditorCounterparty
                            ? businessTripsEmployeePayment.employee_business_id
                            : tripTaxCategory,
                        creditAmount1: absForeignAmount,
                        localCurrencyCreditAmount1: absAmount,
                        debitAccountID1: isCreditorCounterparty
                            ? tripTaxCategory
                            : businessTripsEmployeePayment.employee_business_id,
                        debitAmount1: absForeignAmount,
                        localCurrencyDebitAmount1: absAmount,
                        reference: businessTripsEmployeePayment.id,
                        isCreditorCounterparty,
                        ownerId: charge.owner_id,
                        currencyRate: exchangeRate,
                        chargeId: charge.id,
                    };
                    accountingLedgerEntries.push(ledgerEntry);
                    updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency);
                    dates.add(ledgerEntry.valueDate.getTime());
                    currencies.add(ledgerEntry.currency);
                });
                await Promise.all(businessTripsEmployeePaymentsPromises);
            }
            else {
                const businessTripExpensesPromises = businessTripExpenses.map(async (businessTripExpense) => {
                    if (!tripTaxCategory) {
                        return;
                    }
                    const matchingEntry = financialAccountLedgerEntries.find(entry => businessTripExpense.transaction_ids?.includes(entry.id));
                    if (!matchingEntry) {
                        return;
                    }
                    const isCreditorCounterparty = !matchingEntry.isCreditorCounterparty;
                    const businessId = isCreditorCounterparty
                        ? matchingEntry.debitAccountID1
                        : matchingEntry.creditAccountID1;
                    const ledgerEntry = {
                        ...matchingEntry,
                        id: businessTripExpense.id,
                        isCreditorCounterparty,
                        creditAccountID1: isCreditorCounterparty ? businessId : tripTaxCategory,
                        debitAccountID1: isCreditorCounterparty ? tripTaxCategory : businessId,
                    };
                    accountingLedgerEntries.push(ledgerEntry);
                    updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency);
                });
                await Promise.all(businessTripExpensesPromises);
            }
        }
        const allowedUnbalancedBusinesses = new Set([
            ...businessTripAttendees.map(attendee => attendee.id),
            ...chargeUnbalancedBusinesses.map(({ business_id }) => business_id),
        ]);
        const ledgerBalanceInfo = await getLedgerBalanceInfo(injector, ledgerBalance, undefined, allowedUnbalancedBusinesses);
        const miscLedgerEntries = [];
        // multiple currencies balance
        const businessBalance = ledgerBalance.get(mainBusinessId ?? '');
        if (mainBusinessId &&
            Object.keys(businessBalance?.foreignAmounts ?? {}).length >
                (charge.invoice_payment_currency_diff ? 0 : 1)) {
            const transactionEntries = financialAccountLedgerEntries.filter(entry => [entry.creditAccountID1, entry.debitAccountID1].includes(mainBusinessId));
            const documentEntries = accountingLedgerEntries.filter(entry => [entry.creditAccountID1, entry.debitAccountID1].includes(mainBusinessId));
            try {
                const entries = await multipleForeignCurrenciesBalanceEntries(injector, defaultLocalCurrency, documentEntries, transactionEntries, charge, businessBalance.foreignAmounts, charge.invoice_payment_currency_diff ?? false);
                for (const ledgerEntry of entries) {
                    miscLedgerEntries.push(ledgerEntry);
                    updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency);
                }
            }
            catch (e) {
                if (e instanceof LedgerError) {
                    errors.add(e.message);
                }
                else {
                    throw e;
                }
            }
        }
        // check if exchange rate record is needed
        const hasMultipleDates = dates.size > 1;
        const foreignCurrencyCount = currencies.size - (currencies.has(defaultLocalCurrency) ? 1 : 0);
        const mightRequireExchangeRateRecord = (hasMultipleDates && !!foreignCurrencyCount) || foreignCurrencyCount >= 2;
        const unbalancedBusinesses = ledgerBalanceInfo.unbalancedEntities.filter(({ entityId }) => ledgerBalanceInfo.financialEntities.some(fe => fe.id === entityId && fe.type === 'business') && !chargeUnbalancedBusinesses.map(b => b.business_id).includes(entityId));
        if (mightRequireExchangeRateRecord && unbalancedBusinesses.length === 1) {
            const entryDate = getExchangeDates(financialAccountLedgerEntries.length
                ? financialAccountLedgerEntries
                : accountingLedgerEntries);
            const { entityId, balance } = unbalancedBusinesses[0];
            const amount = Math.abs(balance.raw);
            const isCreditorCounterparty = balance.raw < 0;
            const exchangeRateEntry = accountingLedgerEntries.find(entry => [entry.creditAccountID1, entry.debitAccountID1].includes(entityId));
            let exchangeRateTaxCategory = undefined;
            if (exchangeRateEntry) {
                exchangeRateTaxCategory =
                    exchangeRateEntry.debitAccountID1 === entityId
                        ? exchangeRateEntry.creditAccountID1
                        : exchangeRateEntry.debitAccountID1;
                const isIncomeCharge = !!formattedChargeAmount && formattedChargeAmount.raw > 0;
                if (isIncomeCharge) {
                    exchangeRateTaxCategory = incomeExchangeRateTaxCategoryId;
                }
            }
            // validate exchange rate
            const validation = validateExchangeRate(entityId, [
                ...financialAccountLedgerEntries,
                ...feeFinancialAccountLedgerEntries,
                ...accountingLedgerEntries,
                ...miscLedgerEntries,
            ], balance.raw, defaultLocalCurrency);
            if (validation === true) {
                if (exchangeRateTaxCategory) {
                    const ledgerEntry = {
                        id: 'exchange-ledger', // NOTE: this field is dummy
                        creditAccountID1: isCreditorCounterparty ? entityId : exchangeRateTaxCategory,
                        localCurrencyCreditAmount1: amount,
                        debitAccountID1: isCreditorCounterparty ? exchangeRateTaxCategory : entityId,
                        localCurrencyDebitAmount1: amount,
                        description: 'Exchange ledger record',
                        isCreditorCounterparty,
                        invoiceDate: entryDate,
                        valueDate: entryDate,
                        currency: defaultLocalCurrency,
                        ownerId: charge.owner_id,
                        chargeId,
                    };
                    miscLedgerEntries.push(ledgerEntry);
                    updateLedgerBalanceByEntry(ledgerEntry, ledgerBalance, defaultLocalCurrency);
                }
                else {
                    errors.add(`Failed to locate tax category for exchange rate for business ID="${entityId}"`);
                }
            }
            else {
                errors.add(validation);
            }
        }
        const crossYearLedgerEntries = await handleCrossYearLedgerEntries(charge, injector, accountingLedgerEntries);
        const records = [
            ...(crossYearLedgerEntries ?? accountingLedgerEntries),
            ...feeFinancialAccountLedgerEntries,
            ...financialAccountLedgerEntries,
            ...miscLedgerEntries,
        ];
        const updatedLedgerBalanceInfo = await getLedgerBalanceInfo(injector, ledgerBalance, errors, allowedUnbalancedBusinesses);
        if (insertLedgerRecordsIfNotExists) {
            await storeInitialGeneratedRecords(charge.id, records, injector);
        }
        return {
            records: ledgerProtoToRecordsConverter(records),
            charge,
            balance: updatedLedgerBalanceInfo,
            errors: Array.from(errors),
        };
    }
    catch (e) {
        return {
            __typename: 'CommonError',
            message: `Failed to generate ledger records for charge ID="${chargeId}"\n${e}`,
        };
    }
};
//# sourceMappingURL=business-trip-ledger-generation.resolver.js.map