mirror of
				https://github.com/invoiceninja/invoiceninja.git
				synced 2025-10-31 16:07:34 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			320 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			320 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| 
 | |
| namespace App\Services;
 | |
| 
 | |
| use App\Libraries\Finance;
 | |
| use App\Libraries\Login;
 | |
| use App\Models\BankSubaccount;
 | |
| use App\Models\Expense;
 | |
| use App\Models\Vendor;
 | |
| use App\Ninja\Datatables\BankAccountDatatable;
 | |
| use App\Ninja\Repositories\BankAccountRepository;
 | |
| use App\Ninja\Repositories\ExpenseRepository;
 | |
| use App\Ninja\Repositories\VendorRepository;
 | |
| use Hash;
 | |
| use stdClass;
 | |
| use Utils;
 | |
| 
 | |
| /**
 | |
|  * Class BankAccountService.
 | |
|  */
 | |
| class BankAccountService extends BaseService
 | |
| {
 | |
|     /**
 | |
|      * @var BankAccountRepository
 | |
|      */
 | |
|     protected $bankAccountRepo;
 | |
| 
 | |
|     /**
 | |
|      * @var ExpenseRepository
 | |
|      */
 | |
|     protected $expenseRepo;
 | |
| 
 | |
|     /**
 | |
|      * @var VendorRepository
 | |
|      */
 | |
|     protected $vendorRepo;
 | |
| 
 | |
|     /**
 | |
|      * @var DatatableService
 | |
|      */
 | |
|     protected $datatableService;
 | |
| 
 | |
|     /**
 | |
|      * BankAccountService constructor.
 | |
|      *
 | |
|      * @param BankAccountRepository $bankAccountRepo
 | |
|      * @param ExpenseRepository     $expenseRepo
 | |
|      * @param VendorRepository      $vendorRepo
 | |
|      * @param DatatableService      $datatableService
 | |
|      */
 | |
|     public function __construct(BankAccountRepository $bankAccountRepo, ExpenseRepository $expenseRepo, VendorRepository $vendorRepo, DatatableService $datatableService)
 | |
|     {
 | |
|         $this->bankAccountRepo = $bankAccountRepo;
 | |
|         $this->vendorRepo = $vendorRepo;
 | |
|         $this->expenseRepo = $expenseRepo;
 | |
|         $this->datatableService = $datatableService;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @return BankAccountRepository
 | |
|      */
 | |
|     protected function getRepo()
 | |
|     {
 | |
|         return $this->bankAccountRepo;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @param null $bankId
 | |
|      *
 | |
|      * @return array
 | |
|      */
 | |
|     private function getExpenses($bankId = null)
 | |
|     {
 | |
|         $expenses = Expense::scope()
 | |
|                         ->bankId($bankId)
 | |
|                         ->where('transaction_id', '!=', '')
 | |
|                         ->withTrashed()
 | |
|                         ->get(['transaction_id'])
 | |
|                         ->toArray();
 | |
|         $expenses = array_flip(array_map(function ($val) {
 | |
|             return $val['transaction_id'];
 | |
|         }, $expenses));
 | |
| 
 | |
|         return $expenses;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @param $bankId
 | |
|      * @param $username
 | |
|      * @param $password
 | |
|      * @param bool $includeTransactions
 | |
|      *
 | |
|      * @return array|bool
 | |
|      */
 | |
|     public function loadBankAccounts($bankId, $username, $password, $includeTransactions = true)
 | |
|     {
 | |
|         if (! $bankId || ! $username || ! $password) {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         $expenses = $this->getExpenses();
 | |
|         $vendorMap = $this->createVendorMap();
 | |
|         $bankAccounts = BankSubaccount::scope()
 | |
|                             ->whereHas('bank_account', function ($query) use ($bankId) {
 | |
|                                 $query->where('bank_id', '=', $bankId);
 | |
|                             })
 | |
|                             ->get();
 | |
|         $bank = Utils::getFromCache($bankId, 'banks');
 | |
|         $data = [];
 | |
| 
 | |
|         // load OFX trnansactions
 | |
|         try {
 | |
|             $finance = new Finance();
 | |
|             $finance->banks[$bankId] = $bank->getOFXBank($finance);
 | |
|             $finance->banks[$bankId]->logins[] = new Login($finance->banks[$bankId], $username, $password);
 | |
| 
 | |
|             foreach ($finance->banks as $bank) {
 | |
|                 foreach ($bank->logins as $login) {
 | |
|                     $login->setup();
 | |
|                     foreach ($login->accounts as $account) {
 | |
|                         $account->setup($includeTransactions);
 | |
|                         if ($account = $this->parseBankAccount($account, $bankAccounts, $expenses, $includeTransactions, $vendorMap)) {
 | |
|                             $data[] = $account;
 | |
|                         }
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             return $data;
 | |
|         } catch (\Exception $e) {
 | |
|             return false;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @param $account
 | |
|      * @param $bankAccounts
 | |
|      * @param $expenses
 | |
|      * @param $includeTransactions
 | |
|      * @param $vendorMap
 | |
|      *
 | |
|      * @return bool|stdClass
 | |
|      */
 | |
|     private function parseBankAccount($account, $bankAccounts, $expenses, $includeTransactions, $vendorMap)
 | |
|     {
 | |
|         $obj = new stdClass();
 | |
|         $obj->account_name = '';
 | |
| 
 | |
|         // look up bank account name
 | |
|         foreach ($bankAccounts as $bankAccount) {
 | |
|             if (Hash::check($account->id, $bankAccount->account_number)) {
 | |
|                 $obj->account_name = $bankAccount->account_name;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // if we can't find a match skip the account
 | |
|         if (count($bankAccounts) && ! $obj->account_name) {
 | |
|             return false;
 | |
|         }
 | |
| 
 | |
|         $obj->masked_account_number = Utils::maskAccountNumber($account->id);
 | |
|         $obj->hashed_account_number = bcrypt($account->id);
 | |
|         $obj->type = $account->type;
 | |
|         $obj->balance = Utils::formatMoney($account->ledgerBalance, CURRENCY_DOLLAR);
 | |
| 
 | |
|         if ($includeTransactions) {
 | |
|             $obj = $this->parseTransactions($obj, $account->response, $expenses, $vendorMap);
 | |
|         }
 | |
| 
 | |
|         return $obj;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @param $account
 | |
|      * @param $data
 | |
|      * @param $expenses
 | |
|      * @param $vendorMap
 | |
|      *
 | |
|      * @return mixed
 | |
|      */
 | |
|     private function parseTransactions($account, $data, $expenses, $vendorMap)
 | |
|     {
 | |
|         $ofxParser = new \OfxParser\Parser();
 | |
|         $ofx = $ofxParser->loadFromString($data);
 | |
| 
 | |
|         $account->start_date = $ofx->BankAccount->Statement->startDate;
 | |
|         $account->end_date = $ofx->BankAccount->Statement->endDate;
 | |
|         $account->transactions = [];
 | |
| 
 | |
|         foreach ($ofx->BankAccount->Statement->transactions as $transaction) {
 | |
|             // ensure transactions aren't imported as expenses twice
 | |
|             if (isset($expenses[$transaction->uniqueId])) {
 | |
|                 continue;
 | |
|             }
 | |
|             if ($transaction->amount >= 0) {
 | |
|                 continue;
 | |
|             }
 | |
| 
 | |
|             // if vendor has already been imported use current name
 | |
|             $vendorName = trim(substr($transaction->name, 0, 20));
 | |
|             $key = strtolower($vendorName);
 | |
|             $vendor = isset($vendorMap[$key]) ? $vendorMap[$key] : null;
 | |
| 
 | |
|             $transaction->vendor = $vendor ? $vendor->name : $this->prepareValue($vendorName);
 | |
|             $transaction->info = $this->prepareValue(substr($transaction->name, 20));
 | |
|             $transaction->memo = $this->prepareValue($transaction->memo);
 | |
|             $transaction->date = \Auth::user()->account->formatDate($transaction->date);
 | |
|             $transaction->amount *= -1;
 | |
|             $account->transactions[] = $transaction;
 | |
|         }
 | |
| 
 | |
|         return $account;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @param $value
 | |
|      *
 | |
|      * @return string
 | |
|      */
 | |
|     private function prepareValue($value)
 | |
|     {
 | |
|         return ucwords(strtolower(trim($value)));
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @param $data
 | |
|      *
 | |
|      * @return mixed
 | |
|      */
 | |
|     public function parseOFX($data)
 | |
|     {
 | |
|         $account = new stdClass();
 | |
|         $expenses = $this->getExpenses();
 | |
|         $vendorMap = $this->createVendorMap();
 | |
| 
 | |
|         return $this->parseTransactions($account, $data, $expenses, $vendorMap);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @return array
 | |
|      */
 | |
|     private function createVendorMap()
 | |
|     {
 | |
|         $vendorMap = [];
 | |
|         $vendors = Vendor::scope()
 | |
|                         ->withTrashed()
 | |
|                         ->get(['id', 'name', 'transaction_name']);
 | |
|         foreach ($vendors as $vendor) {
 | |
|             $vendorMap[strtolower($vendor->name)] = $vendor;
 | |
|             $vendorMap[strtolower($vendor->transaction_name)] = $vendor;
 | |
|         }
 | |
| 
 | |
|         return $vendorMap;
 | |
|     }
 | |
| 
 | |
|     public function importExpenses($bankId, $input)
 | |
|     {
 | |
|         $vendorMap = $this->createVendorMap();
 | |
|         $countVendors = 0;
 | |
|         $countExpenses = 0;
 | |
| 
 | |
|         foreach ($input as $transaction) {
 | |
|             $vendorName = $transaction['vendor'];
 | |
|             $key = strtolower($vendorName);
 | |
|             $info = $transaction['info'];
 | |
| 
 | |
|             // find vendor otherwise create it
 | |
|             if (isset($vendorMap[$key])) {
 | |
|                 $vendor = $vendorMap[$key];
 | |
|             } else {
 | |
|                 $field = $this->determineInfoField($info);
 | |
|                 $vendor = $this->vendorRepo->save([
 | |
|                     $field => $info,
 | |
|                     'name' => $vendorName,
 | |
|                     'transaction_name' => $transaction['vendor_orig'],
 | |
|                     'vendor_contact' => [],
 | |
|                 ]);
 | |
|                 $vendorMap[$key] = $vendor;
 | |
|                 $vendorMap[$transaction['vendor_orig']] = $vendor;
 | |
|                 $countVendors++;
 | |
|             }
 | |
| 
 | |
|             // create the expense record
 | |
|             $this->expenseRepo->save([
 | |
|                 'vendor_id' => $vendor->id,
 | |
|                 'amount' => $transaction['amount'],
 | |
|                 'public_notes' => $transaction['memo'],
 | |
|                 'expense_date' => $transaction['date'],
 | |
|                 'transaction_id' => $transaction['id'],
 | |
|                 'bank_id' => $bankId,
 | |
|                 'should_be_invoiced' => true,
 | |
|             ]);
 | |
|             $countExpenses++;
 | |
|         }
 | |
| 
 | |
|         return trans('texts.imported_expenses', [
 | |
|             'count_vendors' => $countVendors,
 | |
|             'count_expenses' => $countExpenses,
 | |
|         ]);
 | |
|     }
 | |
| 
 | |
|     private function determineInfoField($value)
 | |
|     {
 | |
|         if (preg_match("/^[0-9\-\(\)\.]+$/", $value)) {
 | |
|             return 'work_phone';
 | |
|         } elseif (strpos($value, '.') !== false) {
 | |
|             return 'private_notes';
 | |
|         } else {
 | |
|             return 'city';
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     public function getDatatable($accountId)
 | |
|     {
 | |
|         $query = $this->bankAccountRepo->find($accountId);
 | |
| 
 | |
|         return $this->datatableService->createDatatable(new BankAccountDatatable(false), $query);
 | |
|     }
 | |
| }
 |