Bank Transaction matching

This commit is contained in:
David Bomba 2022-09-15 17:31:32 +10:00
parent a4d7d4af54
commit 02071e93d6
3 changed files with 148 additions and 11 deletions

View File

@ -32,7 +32,7 @@ class MatchBankTransactionRequest extends Request
return [
'*.id' => 'required|bail',
'*.invoice_id' => 'nullable|sometimes',
'*.expense_id' => 'nullable|sometimes',
'*.is_expense' => 'nullable|sometimes|bool',
'*.amount' => 'nullable|sometimes|numeric'
];

View File

@ -13,6 +13,8 @@ namespace App\Jobs\Bank;
use App\Events\Invoice\InvoiceWasPaid;
use App\Events\Payment\PaymentWasCreated;
use App\Factory\ExpenseCategoryFactory;
use App\Factory\ExpenseFactory;
use App\Factory\PaymentFactory;
use App\Helpers\Bank\Yodlee\Yodlee;
use App\Libraries\Currency\Conversion\CurrencyApi;
@ -21,10 +23,12 @@ use App\Models\BankIntegration;
use App\Models\BankTransaction;
use App\Models\Company;
use App\Models\Currency;
use App\Models\ExpenseCategory;
use App\Models\Invoice;
use App\Models\Payment;
use App\Services\Bank\BankService;
use App\Utils\Ninja;
use App\Utils\Traits\GeneratesCounter;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
@ -35,7 +39,7 @@ use Illuminate\Support\Carbon;
class MatchBankTransactions implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, GeneratesCounter;
private int $company_id;
@ -78,7 +82,7 @@ class MatchBankTransactions implements ShouldQueue
$yodlee = new Yodlee($this->company->account->bank_integration_account_id);
$_categories = collect($yodlee->getTransactionCategories());
$_categories = $yodlee->getTransactionCategories();
if($_categories)
$this->categories = collect($_categories->transactionCategory);
@ -87,7 +91,7 @@ class MatchBankTransactions implements ShouldQueue
{
if(array_key_exists('invoice_id', $match) && strlen($match['invoice_id']) > 1)
$this->matchInvoicePayment($match);
elseif(array_key_exists('expense_id', $match) && strlen($match['expense_id']) > 1)
elseif(array_key_exists('is_expense', $match) && $match['is_expense'])
$this->matchExpense($match);
}
@ -106,7 +110,7 @@ class MatchBankTransactions implements ShouldQueue
if($_invoice && $_invoice->isPayable()){
$this->createPayment($match['id'], $amount);
$this->createPayment($match['invoice_id'], $amount);
}
@ -117,7 +121,17 @@ class MatchBankTransactions implements ShouldQueue
//if there is a category id, pull it from Yodlee and insert - or just reuse!!
$this->bt = BankTransaction::find($match['id']);
$category_id = $this->resolveCategory();
$expense = ExpenseFactory::create($this->bt->company_id, $this->bt->user_id);
$expense->category_id = $this->resolveCategory();
$expense->amount = $this->bt->amount;
$expense->number = $this->getNextExpenseNumber($expense);
$expense->currency_id = $this->harvestCurrencyId();
$expense->date = Carbon::parse($this->bt->date);
$expense->public_notes = $this->bt->description;
$expense->save();
nlog($expense->toArray());
}
private function createPayment(int $invoice_id, float $amount) :void
@ -178,7 +192,7 @@ class MatchBankTransactions implements ShouldQueue
->save();
$payment->ledger()
->updatePaymentBalance($this->payable_balance * -1);
->updatePaymentBalance($amount * -1);
$this->invoice
->client
@ -201,7 +215,25 @@ class MatchBankTransactions implements ShouldQueue
private function resolveCategory() :?int
{
$this->categories->firstWhere('highLevelCategoryId', $this->bt->category_id)
$category = $this->categories->firstWhere('highLevelCategoryId', $this->bt->category_id);
$ec = ExpenseCategory::where('company_id', $this->bt->company_id)->where('bank_category_id', $this->bt->category_id)->first();
if($ec)
return $ec->id;
if($category)
{
$ec = ExpenseCategoryFactory::create($this->bt->company_id, $this->bt->user_id);
$ec->bank_category_id = $this->bt->category_id;
$ec->name = $category->highLevelCategoryName;
$ec->save();
return $ec->id;
}
return null;
}
private function harvestCurrencyId() :int
@ -228,7 +260,6 @@ class MatchBankTransactions implements ShouldQueue
$exchange_rate = new CurrencyApi();
$payment->exchange_rate = $exchange_rate->exchangeRate($client_currency, $company_currency, Carbon::parse($payment->date));
//$payment->exchange_currency_id = $client_currency; // 23/06/2021
$payment->exchange_currency_id = $company_currency;
$payment->saveQuietly();

View File

@ -12,10 +12,17 @@
namespace Tests\Feature\Bank;
use App\Factory\BankIntegrationFactory;
use App\Factory\BankTransactionFactory;
use App\Helpers\Bank\Yodlee\Yodlee;
use App\Helpers\Invoice\InvoiceSum;
use App\Jobs\Bank\MatchBankTransactions;
use App\Jobs\Bank\ProcessBankTransactions;
use App\Models\BankIntegration;
use App\Models\BankTransaction;
use App\Models\Expense;
use App\Models\Invoice;
use App\Models\Payment;
use App\Services\Bank\BankService;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\MockAccountData;
@ -37,6 +44,105 @@ class YodleeApiTest extends TestCase
}
public function testExpenseGenerationFromBankFeed()
{
$bi = BankIntegrationFactory::create($this->company->id, $this->user->id, $this->account->id);
$bi->save();
$bt = BankTransactionFactory::create($this->company->id, $this->user->id);
$bt->bank_integration_id = $bi->id;
$bt->description = 'Fuel';
$bt->amount = 10;
$bt->currency_code = $this->client->currency()->code;
$bt->date = now()->format('Y-m-d');
$bt->transaction_id = 1234567890;
$bt->category_id = 10000003;
$bt->save();
$data = [
[
'id' => $bt->id,
'is_expense' => true
]
];
MatchBankTransactions::dispatchSync($this->company->id, $this->company->db, $data);
$expense = Expense::where('public_notes', 'Fuel')->first();
$this->assertNotNull($expense);
$this->assertEquals(10, (int)$expense->amount);
}
public function testIncomeMatchingAndPaymentGeneration()
{
$this->account->bank_integration_account_id = 'sbMem62e1e69547bfb2';
$this->account->save();
$invoice = Invoice::factory()->create(['user_id' => $this->user->id, 'company_id' => $this->company->id, 'client_id' => $this->client->id]);
$invoice->status_id = Invoice::STATUS_DRAFT;
$invoice->line_items = $this->buildLineItems();
$invoice->uses_inclusive_taxes = false;
$invoice->tax_rate1 = 0;
$invoice->tax_rate2 = 0;
$invoice->tax_rate3 = 0;
$invoice->discount = 0;
$invoice->number = 'TESTMATCHING';
$invoice->date = now()->format('Y-m-d');
$invoice->save();
$invoice_calc = new InvoiceSum($invoice);
$invoice_calc->build();
$invoice = $invoice_calc->getInvoice();
$invoice->save();
$invoice = $invoice->service()->markSent()->save();
$this->assertEquals(Invoice::STATUS_SENT, $invoice->status_id);
$this->assertEquals(10, $invoice->amount);
$this->assertEquals(10, $invoice->balance);
$bi = BankIntegrationFactory::create($this->company->id, $this->user->id, $this->account->id);
$bi->save();
$bt = BankTransactionFactory::create($this->company->id, $this->user->id);
$bt->bank_integration_id = $bi->id;
$bt->description = $invoice->number;
$bt->amount = 10;
$bt->currency_code = $this->client->currency()->code;
$bt->date = now()->format('Y-m-d');
$bt->transaction_id = 123456;
$bt->save();
$data = [
[
'id' => $bt->id,
'invoice_id' => $invoice->id
]
];
MatchBankTransactions::dispatchSync($this->company->id, $this->company->db, $data);
$payment = Payment::where('transaction_reference', '123456')->first();
$this->assertNotNull($payment);
$this->assertEquals(10, (int)$payment->amount);
$this->assertEquals(4, $payment->status_id);
$this->assertEquals(1, $payment->invoices()->count());
$invoice = $invoice->fresh();
$this->assertEquals(Invoice::STATUS_PAID, $invoice->status_id);
$this->assertEquals(0, $invoice->balance);
}
public function testCategoryPropertyExists()
{
$yodlee = new Yodlee('sbMem62e1e69547bfb2');
@ -89,8 +195,8 @@ class YodleeApiTest extends TestCase
}
}
$this->assertGreaterThan(1, BankIntegration::count());
$this->assertGreaterThan(1, BankTransaction::count());
$this->assertGreaterThan(0, BankIntegration::count());
$this->assertGreaterThan(0, BankTransaction::count());
$this->invoice->number = "XXXXXX8501";
$this->invoice->save();