import { userDataPath } from '@/app-globals' ; import { EnrichedTransaction } from '@/backend/commonTypes' ; import * as configManager from '@/backend/configManager/configManager' ; import * as bankScraper from '@/backend/import/bankScraper' ; import { ScaperScrapingResult , Transaction } from '@/backend/import/bankScraper' ; import * as categoryCalculation from '@/backend/import/categoryCalculationScript' ; import _ from 'lodash' ; import moment from 'moment' ; import { AccountStatus , BudgetTrackingEventEmitter , DownalodChromeEvent , EventNames , EventPublisher , ImporterEvent } from '../eventEmitters/EventEmitter' ; import { calculateTransactionHash } from '../transactions/transactions' ; import getChrome from './downloadChromium' ; type AccountToScrapeConfig = configManager . AccountToScrapeConfig ; type Config = configManager . Config ; type ScrapingConfig = Config [ 'scraping' ] ; const TRANSACTION_STATUS_COMPLETED = 'completed' ; export async function scrapeFinancialAccountsAndFetchTransactions ( scrapingConfig , startDate , eventPublisher ) { const dowloadedChrome = await getChrome ( userDataPath , ( percent ) => emitChromeDownload ( eventPublisher , percent ) ) ; const scrapePromises = scrapingConfig . accountsToScrape . filter ( ( accountToScrape ) => accountToScrape . active !== false ) . map ( async ( accountToScrape ) => ( { id : accountToScrape . id , transactions : await fetchTransactions ( accountToScrape , startDate , scrapingConfig . showBrowser , eventPublisher , dowloadedChrome ) } ) ) ; const promiseResults = await Promise . allSettled ( scrapePromises ) ; const companyIdToTransactions = promiseResults . reduce ( ( idToTrxAcc , scrapeRes ) => { if ( scrapeRes . status === 'fulfilled' ) { const { id , transactions } = scrapeRes . value ; idToTrxAcc [ id ] = transactions ; } return idToTrxAcc ; } , { } as Record < string , EnrichedTransaction [ ] > ) ; return companyIdToTransactions ; } function buildImporterEvent ( accountConfig , additionalParams ) { return new ImporterEvent ( { message : additionalParams . message , importerKey : accountConfig . key , error : additionalParams . error , status : additionalParams . status } ) ; } function emitChromeDownload ( eventPublisher , percent ) { eventPublisher . emit ( EventNames . DOWNLOAD_CHROME , new DownalodChromeEvent ( percent ) ) ; } export async function getFinancialAccountNumbers ( config ) { const eventEmitter = new BudgetTrackingEventEmitter ( ) ; const startDate = moment ( ) . subtract ( 30 , 'days' ) . startOf ( 'day' ) . toDate ( ) ; const companyIdToTransactions = await scrapeFinancialAccountsAndFetchTransactions ( config . scraping , startDate , eventEmitter ) ; const companyIdToAccountNumbers = { } ; Object . keys ( companyIdToTransactions ) . forEach ( ( companyId ) => { let accountNumbers = companyIdToTransactions [ companyId ] . map ( ( transaction ) => transaction . accountNumber ) ; accountNumbers = _ . uniq ( accountNumbers ) ; companyIdToAccountNumbers [ companyId ] = accountNumbers ; } ) ; return companyIdToAccountNumbers ; } async function fetchTransactions ( account , startDate , showBrowser , eventPublisher , chromePath ) { try { await eventPublisher . emit ( EventNames . IMPORTER_START , buildImporterEvent ( account , { message : 'Importer start' } ) ) ; const emitImporterProgressEvent = async ( eventCompanyId , message ) => { await eventPublisher . emit ( EventNames . IMPORTER_PROGRESS , buildImporterEvent ( account , { message } ) ) ; } ; const scrapeResult = await bankScraper . scrape ( { companyId : account . key , credentials : account . loginFields , startDate , showBrowser , } , emitImporterProgressEvent , chromePath ) ; if ( ! scrapeResult . success ) { throw new Error ( scrapeResult . errorMessage || scrapeResult . errorType ) ; } const transactions = await postProcessTransactions ( account , scrapeResult ) ; await eventPublisher . emit ( EventNames . IMPORTER_END , buildImporterEvent ( account , { message : 'Importer end' , status : AccountStatus . DONE } ) ) ; return transactions ; } catch ( error ) { await eventPublisher . emit ( EventNames . IMPORTER_ERROR , buildImporterEvent ( account , { message : 'Importer error' , error , status : AccountStatus . ERROR } ) ) ; throw error ; } } async function postProcessTransactions ( accountToScrape , scrapeResult ) { if ( scrapeResult . accounts ) { let transactions = scrapeResult . accounts . flatMap ( ( transactionAccount ) => { return transactionAccount . txns . map ( ( transaction ) => enrichTransaction ( transaction , accountToScrape . key , transactionAccount . accountNumber ) ) ; } ) ; transactions = transactions . filter ( ( transaction ) => transaction . status === TRANSACTION_STATUS_COMPLETED ) ; transactions . sort ( transactionsDateComparator ) ; return transactions ; } return [ ] ; } function enrichTransaction ( transaction , companyId , accountNumber ) { const hash = calculateTransactionHash ( transaction , companyId , accountNumber ) ; const category = categoryCalculation . getCategoryNameByTransactionDescription ( transaction . description ) ; const enrichedTransaction = { ... transaction , accountNumber , category , hash } ; return enrichedTransaction ; } function transactionsDateComparator ( t1 , t2 ) { const date1 = moment ( t1 . date ) ; const date2 = moment ( t2 . date ) ; if ( date1 . isAfter ( date2 ) ) { return 1 ; } if ( date1 . isBefore ( date2 ) ) { return - 1 ; } return 1 ; }