mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-05-31 08:44:35 -04:00
Bank Transaction matching
This commit is contained in:
parent
a4d7d4af54
commit
02071e93d6
@ -32,7 +32,7 @@ class MatchBankTransactionRequest extends Request
|
|||||||
return [
|
return [
|
||||||
'*.id' => 'required|bail',
|
'*.id' => 'required|bail',
|
||||||
'*.invoice_id' => 'nullable|sometimes',
|
'*.invoice_id' => 'nullable|sometimes',
|
||||||
'*.expense_id' => 'nullable|sometimes',
|
'*.is_expense' => 'nullable|sometimes|bool',
|
||||||
'*.amount' => 'nullable|sometimes|numeric'
|
'*.amount' => 'nullable|sometimes|numeric'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -13,6 +13,8 @@ namespace App\Jobs\Bank;
|
|||||||
|
|
||||||
use App\Events\Invoice\InvoiceWasPaid;
|
use App\Events\Invoice\InvoiceWasPaid;
|
||||||
use App\Events\Payment\PaymentWasCreated;
|
use App\Events\Payment\PaymentWasCreated;
|
||||||
|
use App\Factory\ExpenseCategoryFactory;
|
||||||
|
use App\Factory\ExpenseFactory;
|
||||||
use App\Factory\PaymentFactory;
|
use App\Factory\PaymentFactory;
|
||||||
use App\Helpers\Bank\Yodlee\Yodlee;
|
use App\Helpers\Bank\Yodlee\Yodlee;
|
||||||
use App\Libraries\Currency\Conversion\CurrencyApi;
|
use App\Libraries\Currency\Conversion\CurrencyApi;
|
||||||
@ -21,10 +23,12 @@ use App\Models\BankIntegration;
|
|||||||
use App\Models\BankTransaction;
|
use App\Models\BankTransaction;
|
||||||
use App\Models\Company;
|
use App\Models\Company;
|
||||||
use App\Models\Currency;
|
use App\Models\Currency;
|
||||||
|
use App\Models\ExpenseCategory;
|
||||||
use App\Models\Invoice;
|
use App\Models\Invoice;
|
||||||
use App\Models\Payment;
|
use App\Models\Payment;
|
||||||
use App\Services\Bank\BankService;
|
use App\Services\Bank\BankService;
|
||||||
use App\Utils\Ninja;
|
use App\Utils\Ninja;
|
||||||
|
use App\Utils\Traits\GeneratesCounter;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
@ -35,7 +39,7 @@ use Illuminate\Support\Carbon;
|
|||||||
|
|
||||||
class MatchBankTransactions implements ShouldQueue
|
class MatchBankTransactions implements ShouldQueue
|
||||||
{
|
{
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, GeneratesCounter;
|
||||||
|
|
||||||
private int $company_id;
|
private int $company_id;
|
||||||
|
|
||||||
@ -78,7 +82,7 @@ class MatchBankTransactions implements ShouldQueue
|
|||||||
|
|
||||||
$yodlee = new Yodlee($this->company->account->bank_integration_account_id);
|
$yodlee = new Yodlee($this->company->account->bank_integration_account_id);
|
||||||
|
|
||||||
$_categories = collect($yodlee->getTransactionCategories());
|
$_categories = $yodlee->getTransactionCategories();
|
||||||
|
|
||||||
if($_categories)
|
if($_categories)
|
||||||
$this->categories = collect($_categories->transactionCategory);
|
$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)
|
if(array_key_exists('invoice_id', $match) && strlen($match['invoice_id']) > 1)
|
||||||
$this->matchInvoicePayment($match);
|
$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);
|
$this->matchExpense($match);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +110,7 @@ class MatchBankTransactions implements ShouldQueue
|
|||||||
|
|
||||||
if($_invoice && $_invoice->isPayable()){
|
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!!
|
//if there is a category id, pull it from Yodlee and insert - or just reuse!!
|
||||||
$this->bt = BankTransaction::find($match['id']);
|
$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
|
private function createPayment(int $invoice_id, float $amount) :void
|
||||||
@ -178,7 +192,7 @@ class MatchBankTransactions implements ShouldQueue
|
|||||||
->save();
|
->save();
|
||||||
|
|
||||||
$payment->ledger()
|
$payment->ledger()
|
||||||
->updatePaymentBalance($this->payable_balance * -1);
|
->updatePaymentBalance($amount * -1);
|
||||||
|
|
||||||
$this->invoice
|
$this->invoice
|
||||||
->client
|
->client
|
||||||
@ -201,7 +215,25 @@ class MatchBankTransactions implements ShouldQueue
|
|||||||
|
|
||||||
private function resolveCategory() :?int
|
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
|
private function harvestCurrencyId() :int
|
||||||
@ -228,7 +260,6 @@ class MatchBankTransactions implements ShouldQueue
|
|||||||
$exchange_rate = new CurrencyApi();
|
$exchange_rate = new CurrencyApi();
|
||||||
|
|
||||||
$payment->exchange_rate = $exchange_rate->exchangeRate($client_currency, $company_currency, Carbon::parse($payment->date));
|
$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->exchange_currency_id = $company_currency;
|
||||||
|
|
||||||
$payment->saveQuietly();
|
$payment->saveQuietly();
|
||||||
|
@ -12,10 +12,17 @@
|
|||||||
|
|
||||||
namespace Tests\Feature\Bank;
|
namespace Tests\Feature\Bank;
|
||||||
|
|
||||||
|
use App\Factory\BankIntegrationFactory;
|
||||||
|
use App\Factory\BankTransactionFactory;
|
||||||
use App\Helpers\Bank\Yodlee\Yodlee;
|
use App\Helpers\Bank\Yodlee\Yodlee;
|
||||||
|
use App\Helpers\Invoice\InvoiceSum;
|
||||||
|
use App\Jobs\Bank\MatchBankTransactions;
|
||||||
use App\Jobs\Bank\ProcessBankTransactions;
|
use App\Jobs\Bank\ProcessBankTransactions;
|
||||||
use App\Models\BankIntegration;
|
use App\Models\BankIntegration;
|
||||||
use App\Models\BankTransaction;
|
use App\Models\BankTransaction;
|
||||||
|
use App\Models\Expense;
|
||||||
|
use App\Models\Invoice;
|
||||||
|
use App\Models\Payment;
|
||||||
use App\Services\Bank\BankService;
|
use App\Services\Bank\BankService;
|
||||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
use Tests\MockAccountData;
|
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()
|
public function testCategoryPropertyExists()
|
||||||
{
|
{
|
||||||
$yodlee = new Yodlee('sbMem62e1e69547bfb2');
|
$yodlee = new Yodlee('sbMem62e1e69547bfb2');
|
||||||
@ -89,8 +195,8 @@ class YodleeApiTest extends TestCase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->assertGreaterThan(1, BankIntegration::count());
|
$this->assertGreaterThan(0, BankIntegration::count());
|
||||||
$this->assertGreaterThan(1, BankTransaction::count());
|
$this->assertGreaterThan(0, BankTransaction::count());
|
||||||
|
|
||||||
$this->invoice->number = "XXXXXX8501";
|
$this->invoice->number = "XXXXXX8501";
|
||||||
$this->invoice->save();
|
$this->invoice->save();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user