From 0efaf80ceef0c85b55af8f77e1789c7c20f3bfd1 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 20 Nov 2022 13:55:19 +1100 Subject: [PATCH] Tests for matching expenses --- .../BankTransactionRepository.php | 10 +- app/Services/Bank/BankMatchingService.php | 175 +------------- app/Services/Bank/BankService.php | 6 +- app/Services/Bank/ProcessBankRule.php | 27 --- app/Services/Bank/ProcessBankRules.php | 214 ++++++++++++++++++ .../Feature/Bank/BankTransactionRuleTest.php | 88 +++++++ 6 files changed, 312 insertions(+), 208 deletions(-) delete mode 100644 app/Services/Bank/ProcessBankRule.php create mode 100644 app/Services/Bank/ProcessBankRules.php create mode 100644 tests/Feature/Bank/BankTransactionRuleTest.php diff --git a/app/Repositories/BankTransactionRepository.php b/app/Repositories/BankTransactionRepository.php index 390b8f56ff..1a5bfba166 100644 --- a/app/Repositories/BankTransactionRepository.php +++ b/app/Repositories/BankTransactionRepository.php @@ -28,17 +28,11 @@ class BankTransactionRepository extends BaseRepository $bank_transaction->bank_integration_id = $data['bank_integration_id']; $bank_transaction->fill($data); - $bank_transaction->save(); - if($bank_transaction->base_type == 'CREDIT' && $invoice = $bank_transaction->service()->matchInvoiceNumber()) - { - $bank_transaction->invoice_ids = $invoice->hashed_id; - $bank_transaction->status_id = BankTransaction::STATUS_MATCHED; - $bank_transaction->save(); - } + $bank_transaction->service()->processRules(); - return $bank_transaction; + return $bank_transaction->fresh(); } } diff --git a/app/Services/Bank/BankMatchingService.php b/app/Services/Bank/BankMatchingService.php index c732a794c0..bd95296d4f 100644 --- a/app/Services/Bank/BankMatchingService.php +++ b/app/Services/Bank/BankMatchingService.php @@ -18,6 +18,7 @@ use App\Models\BankTransaction; use App\Models\Company; use App\Models\ExpenseCategory; use App\Models\Invoice; +use App\Services\Bank\BankService; use App\Utils\Traits\GeneratesCounter; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -42,23 +43,12 @@ class BankMatchingService implements ShouldQueue public $deleteWhenMissingModels = true; - protected $credit_rules; - - protected $debit_rules; - - protected $categories; - public function __construct($company_id, $db) { $this->company_id = $company_id; $this->db = $db; } - public function middleware() - { - return [new WithoutOverlapping($this->company->company_key)]; - } - public function handle() { @@ -66,180 +56,27 @@ class BankMatchingService implements ShouldQueue $this->company = Company::find($this->company_id); - $this->categories = collect(Cache::get('bank_categories')); - - $this->matchCredits(); + $this->matchTransactions(); - $this->matchDebits(); } - private function matchDebits() + private function matchTransactions() { - $this->debit_rules = $this->company->debit_rules(); - BankTransaction::where('company_id', $this->company->id) ->where('status_id', BankTransaction::STATUS_UNMATCHED) - ->where('base_type', 'DEBIT') ->cursor() ->each(function ($bt){ - $this->matchDebit($bt); + (new BankService($bt))->processRules(); }); } - private function matchDebit(BankTransaction $bank_transaction) + public function middleware() { - $matches = 0; - - foreach($this->debit_rules as $rule) - { - $rule_count = count($this->debit_rules); - - if($rule['search_key'] == 'description') - { - - if($this->matchStringOperator($bank_transaction->description, 'description', $rule['operator'])){ - $matches++; - } - - } - - if($rule['search_key'] == 'amount') - { - - if($this->matchNumberOperator($bank_transaction->description, 'amount', $rule['operator'])){ - $matches++; - } - - } - - if(($rule['matches_on_all'] && ($matches == $rule_count)) || (!$rule['matches_on_all'] &&$matches > 0)) - { - - $bank_transaction->client_id = empty($rule['client_id']) ? null : $rule['client_id']; - $bank_transaction->vendor_id = empty($rule['vendor_id']) ? null : $rule['vendor_id']; - $bank_transaction->ninja_category_id = empty($rule['category_id']) ? null : $rule['category_id']; - $bank_transaction->status_id = BankTransaction::STATUS_MATCHED; - $bank_transaction->save(); - - if($rule['auto_convert']) - { - - $expense = ExpenseFactory::create($bank_transaction->company_id, $bank_transaction->user_id); - $expense->category_id = $bank_transaction->ninja_category_id ?: $this->resolveCategory($bank_transaction); - $expense->amount = $bank_transaction->amount; - $expense->number = $this->getNextExpenseNumber($expense); - $expense->currency_id = $bank_transaction->currency_id; - $expense->date = Carbon::parse($bank_transaction->date); - $expense->payment_date = Carbon::parse($bank_transaction->date); - $expense->transaction_reference = $bank_transaction->description; - $expense->transaction_id = $bank_transaction->id; - $expense->vendor_id = $bank_transaction->vendor_id; - $expense->invoice_documents = $this->company->invoice_expense_documents; - $expense->should_be_invoiced = $this->company->mark_expenses_invoiceable; - $expense->save(); - - $bank_transaction->expense_id = $expense->id; - $bank_transaction->status_id = BankTransaction::STATUS_CONVERTED; - $bank_transaction->save(); - - break; - - } - - } - - } - + return [new WithoutOverlapping($this->company->company_key)]; } - - private function resolveCategory(BankTransaction $bank_transaction) - { - $category = $this->categories->firstWhere('highLevelCategoryId', $bank_transaction->category_id); - - $ec = ExpenseCategory::where('company_id', $this->company->id)->where('bank_category_id', $bank_transaction->category_id)->first(); - - if($ec) - return $ec->id; - - if($category) - { - $ec = ExpenseCategoryFactory::create($bank_transaction->company_id, $bank_transaction->user_id); - $ec->bank_category_id = $bank_transaction->category_id; - $ec->name = $category->highLevelCategoryName; - $ec->save(); - - return $ec->id; - } - } - - private function matchNumberOperator($bt_value, $rule_value, $operator) :bool - { - - return match ($operator) { - '>' => floatval($bt_value) > floatval($rule_value), - '>=' => floatval($bt_value) >= floatval($rule_value), - '=' => floatval($bt_value) == floatval($rule_value), - '<' => floatval($bt_value) < floatval($rule_value), - '<=' => floatval($bt_value) <= floatval($rule_value), - default => false, - }; - - } - - private function matchStringOperator($bt_value, $rule_value, $operator) :bool - { - $bt_value = strtolower(str_replace(" ", "", $bt_value)); - $rule_value = strtolower(str_replace(" ", "", $rule_value)); - $rule_length = iconv_strlen($rule_value); - - return match ($operator) { - 'is' => $bt_value == $rule_value, - 'contains' => str_contains($bt_value, $rule_value), - 'starts_with' => substr($bt_value, 0, $rule_length) == $rule_value, - 'is_empty' => empty($bt_value), - default => false, - }; - - } - - - - /* Currently we don't consider rules here, only matching on invoice number*/ - private function matchCredits() - { - $this->credit_rules = $this->company->credit_rules(); - - $this->invoices = Invoice::where('company_id', $this->company->id) - ->whereIn('status_id', [1,2,3]) - ->where('is_deleted', 0) - ->get(); - - BankTransaction::where('company_id', $this->company->id) - ->where('status_id', BankTransaction::STATUS_UNMATCHED) - ->where('base_type', 'CREDIT') - ->cursor() - ->each(function ($bt){ - - $invoice = $this->invoices->first(function ($value, $key) use ($bt){ - - return str_contains($bt->description, $value->number); - - }); - - if($invoice) - { - $bt->invoice_ids = $invoice->hashed_id; - $bt->status_id = BankTransaction::STATUS_MATCHED; - $bt->save(); - } - - }); - } - - } diff --git a/app/Services/Bank/BankService.php b/app/Services/Bank/BankService.php index 7a7f507591..5ff37a1ab6 100644 --- a/app/Services/Bank/BankService.php +++ b/app/Services/Bank/BankService.php @@ -40,11 +40,9 @@ class BankService } - public function processRule($rule) + public function processRules() { - (new ProcessBankRule($this->bank_transaction, $rule))->run(); - - return $this; + (new ProcessBankRules($this->bank_transaction))->run(); } } \ No newline at end of file diff --git a/app/Services/Bank/ProcessBankRule.php b/app/Services/Bank/ProcessBankRule.php deleted file mode 100644 index ffeedbb1f1..0000000000 --- a/app/Services/Bank/ProcessBankRule.php +++ /dev/null @@ -1,27 +0,0 @@ -bank_transaction->base_type == 'DEBIT') + $this->matchDebit(); + else + $this->matchCredit(); + } + + private function matchCredit() + { + $this->credit_rules = $this->bank_transaction->company->credit_rules(); + + $this->invoices = Invoice::where('company_id', $this->bank_transaction->company_id) + ->whereIn('status_id', [1,2,3]) + ->where('is_deleted', 0) + ->get(); + + $invoice = $this->invoices->first(function ($value, $key){ + + return str_contains($this->bank_transaction, $value->number); + + }); + + if($invoice) + { + $this->bank_transaction->invoice_ids = $invoice->hashed_id; + $this->bank_transaction->status_id = BankTransaction::STATUS_MATCHED; + $this->bank_transaction->save(); + return; + } + + //stub for credit rules + foreach($this->credit_rules as $rule) + { + + } + + } + + private function matchDebit() + { + + $this->debit_rules = $this->bank_transaction->company->debit_rules(); + + $this->categories = collect(Cache::get('bank_categories')); + + foreach($this->debit_rules as $bank_transaction_rule) + { + + $matches = 0; + + foreach($bank_transaction_rule['rules'] as $rule) + { + $rule_count = count($bank_transaction_rule['rules']); + + +nlog($rule_count); +nlog($rule); + + if($rule['search_key'] == 'description') + { + nlog("searching key"); + + if($this->matchStringOperator($this->bank_transaction->description, $rule['value'], $rule['operator'])){ + nlog("found key"); + $matches++; + } + + } + + if($rule['search_key'] == 'amount') + { + + if($this->matchNumberOperator($this->bank_transaction->description, 'amount', $rule['operator'])){ + $matches++; + } + + } + + if(($bank_transaction_rule['matches_on_all'] && ($matches == $rule_count)) || (!$bank_transaction_rule['matches_on_all'] && $matches > 0)) + { + + // $this->bank_transaction->client_id = empty($rule['client_id']) ? null : $rule['client_id']; + $this->bank_transaction->vendor_id = empty($rule['vendor_id']) ? null : $rule['vendor_id']; + $this->bank_transaction->ninja_category_id = empty($rule['category_id']) ? null : $rule['category_id']; + $this->bank_transaction->status_id = BankTransaction::STATUS_MATCHED; + $this->bank_transaction->save(); + + if($bank_transaction_rule['auto_convert']) + { + + $expense = ExpenseFactory::create($this->bank_transaction->company_id, $this->bank_transaction->user_id); + $expense->category_id = $this->bank_transaction->ninja_category_id ?: $this->resolveCategory(); + $expense->amount = $this->bank_transaction->amount; + $expense->number = $this->getNextExpenseNumber($expense); + $expense->currency_id = $this->bank_transaction->currency_id; + $expense->date = Carbon::parse($this->bank_transaction->date); + $expense->payment_date = Carbon::parse($this->bank_transaction->date); + $expense->transaction_reference = $this->bank_transaction->description; + $expense->transaction_id = $this->bank_transaction->id; + $expense->vendor_id = $this->bank_transaction->vendor_id; + $expense->invoice_documents = $this->bank_transaction->company->invoice_expense_documents; + $expense->should_be_invoiced = $this->bank_transaction->company->mark_expenses_invoiceable; + $expense->save(); + + $this->bank_transaction->expense_id = $expense->id; + $this->bank_transaction->status_id = BankTransaction::STATUS_CONVERTED; + $this->bank_transaction->save(); + + break; + + } + + } + } + + } + + } + + private function resolveCategory() + { + $category = $this->categories->firstWhere('highLevelCategoryId', $this->bank_transaction->category_id); + + $ec = ExpenseCategory::where('company_id', $this->bank_transaction->company_id)->where('bank_category_id', $this->bank_transaction->category_id)->first(); + + if($ec) + return $ec->id; + + if($category) + { + $ec = ExpenseCategoryFactory::create($this->bank_transaction->company_id, $this->bank_transaction->user_id); + $ec->bank_category_id = $this->bank_transaction->category_id; + $ec->name = $category->highLevelCategoryName; + $ec->save(); + + return $ec->id; + } + } + + private function matchNumberOperator($bt_value, $rule_value, $operator) :bool + { + + return match ($operator) { + '>' => floatval($bt_value) > floatval($rule_value), + '>=' => floatval($bt_value) >= floatval($rule_value), + '=' => floatval($bt_value) == floatval($rule_value), + '<' => floatval($bt_value) < floatval($rule_value), + '<=' => floatval($bt_value) <= floatval($rule_value), + default => false, + }; + + } + + private function matchStringOperator($bt_value, $rule_value, $operator) :bool + { + $bt_value = strtolower(str_replace(" ", "", $bt_value)); + $rule_value = strtolower(str_replace(" ", "", $rule_value)); + $rule_length = iconv_strlen($rule_value); + +nlog($bt_value); +nlog($rule_value); +nlog($rule_length); +nlog($operator); + + return match ($operator) { + 'is' => $bt_value == $rule_value, + 'contains' => str_contains($bt_value, $rule_value), + 'starts_with' => substr($bt_value, 0, $rule_length) == $rule_value, + 'is_empty' => empty($bt_value), + default => false, + }; + + } + + + + +} \ No newline at end of file diff --git a/tests/Feature/Bank/BankTransactionRuleTest.php b/tests/Feature/Bank/BankTransactionRuleTest.php new file mode 100644 index 0000000000..8981bc0bf0 --- /dev/null +++ b/tests/Feature/Bank/BankTransactionRuleTest.php @@ -0,0 +1,88 @@ +makeTestData(); + + $this->withoutMiddleware( + ThrottleRequests::class + ); + } + + public function testMatchingBankTransactionExpense() + { + // $this->expense->public_notes = "WaLLaBy"; + // $this->expense->save(); + + // $this->assertEquals('WaLLaBy', $this->expense->public_notes); + + $br = BankTransactionRule::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'matches_on_all' => false, + 'auto_convert' => true, + 'applies_to' => 'DEBIT', + 'client_id' => $this->client->id, + 'vendor_id' => $this->vendor->id, + 'rules' => [ + [ + 'search_key' => 'description', + 'operator' => 'is', + 'value' => 'wallaby', + ] + ] + ]); + + $bi = BankIntegration::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'account_id' => $this->account->id, + ]); + + $bt = BankTransaction::factory()->create([ + 'bank_integration_id' => $bi->id, + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'description' => 'WallABy', + 'base_type' => 'DEBIT', + ]); + + + $bt->service()->processRules(); + + $bt = $bt->fresh(); + + $this->assertNotNull($bt->expense_id); + } + +} \ No newline at end of file