diff --git a/app/Models/AccountType.php b/app/Models/AccountType.php index 2a0c2fa7ca..1446d0d6e3 100644 --- a/app/Models/AccountType.php +++ b/app/Models/AccountType.php @@ -35,51 +35,19 @@ use Illuminate\Database\Eloquent\Relations\HasMany; */ class AccountType extends Model { - /** - * - */ - public const DEFAULT = 'Default account'; - /** - * - */ - public const CASH = 'Cash account'; - /** - * - */ - public const ASSET = 'Asset account'; - /** - * - */ - public const EXPENSE = 'Expense account'; - /** - * - */ - public const REVENUE = 'Revenue account'; - /** - * - */ + public const DEFAULT = 'Default account'; + public const CASH = 'Cash account'; + public const ASSET = 'Asset account'; + public const EXPENSE = 'Expense account'; + public const REVENUE = 'Revenue account'; public const INITIAL_BALANCE = 'Initial balance account'; - /** - * - */ - public const BENEFICIARY = 'Beneficiary account'; - /** - * - */ - public const IMPORT = 'Import account'; - /** - * - */ - public const RECONCILIATION = 'Reconciliation account'; - /** - * - */ - public const LOAN = 'Loan'; - /** - * The attributes that should be casted to native types. - * - * @var array - */ + public const BENEFICIARY = 'Beneficiary account'; + public const IMPORT = 'Import account'; + public const RECONCILIATION = 'Reconciliation account'; + public const LOAN = 'Loan'; + public const DEBT = 'Debt'; + public const MORTGAGE = 'Mortgage'; + public const CREDITCARD = 'Credit card'; protected $casts = [ 'created_at' => 'datetime', diff --git a/config/firefly.php b/config/firefly.php index 1af5d5b4a7..c996c32c76 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -197,10 +197,11 @@ return [ ], 'accountTypesByIdentifier' => [ - 'asset' => ['Default account', 'Asset account'], - 'expense' => ['Expense account', 'Beneficiary account'], - 'revenue' => ['Revenue account'], - 'import' => ['Import account'], + 'asset' => ['Default account', 'Asset account'], + 'expense' => ['Expense account', 'Beneficiary account'], + 'revenue' => ['Revenue account'], + 'import' => ['Import account'], + 'liabilities' => ['Loan', 'Debt', 'Credit card', 'Mortgage'], ], 'accountTypeByIdentifier' => [ @@ -267,7 +268,7 @@ return [ 'journalLink' => \FireflyIII\Models\TransactionJournalLink::class, 'currency' => \FireflyIII\Models\TransactionCurrency::class, 'piggyBank' => \FireflyIII\Models\PiggyBank::class, - 'preference' => \FireflyIII\Models\Preference::class, + 'preference' => \FireflyIII\Models\Preference::class, 'tj' => \FireflyIII\Models\TransactionJournal::class, 'tag' => \FireflyIII\Models\Tag::class, 'recurrence' => \FireflyIII\Models\Recurrence::class, diff --git a/database/seeds/AccountTypeSeeder.php b/database/seeds/AccountTypeSeeder.php index 0ebde97fb3..f695c310d8 100644 --- a/database/seeds/AccountTypeSeeder.php +++ b/database/seeds/AccountTypeSeeder.php @@ -41,12 +41,15 @@ class AccountTypeSeeder extends Seeder AccountType::IMPORT, AccountType::LOAN, AccountType::RECONCILIATION, + AccountType::DEBT, + AccountType::MORTGAGE, + AccountType::CREDITCARD, ]; foreach ($types as $type) { try { AccountType::create(['type' => $type]); } catch (PDOException $e) { - Log::warning(sprintf('Could not create account type "%s". It might exist already.', $type)); + Log::warning(sprintf('Could not create account type "%s". It might exist already: %s', $type , $e->getMessage())); } } } diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index e93a20952e..dd9f127293 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -700,6 +700,7 @@ return [ 'revenue_accounts' => 'Revenue accounts', 'cash_accounts' => 'Cash accounts', 'Cash account' => 'Cash account', + 'liabilities_accounts' => 'Liabilities', 'reconcile_account' => 'Reconcile account ":account"', 'overview_of_reconcile_modal' => 'Overview of reconciliation', 'delete_reconciliation' => 'Delete reconciliation', @@ -1187,6 +1188,10 @@ return [ 'no_accounts_intro_revenue' => 'You have no revenue accounts yet. Revenue accounts are the places where you receive money from, such as your employer.', 'no_accounts_imperative_revenue' => 'Revenue accounts are created automatically when you create transactions, but you can create one manually too, if you want. Let\'s create one now:', 'no_accounts_create_revenue' => 'Create a revenue account', + 'no_accounts_title_liabilities' => 'Let\'s create a liability!', + 'no_accounts_intro_liabilities' => 'You have no liabilities yet. Liabilities are the accounts that register your credit card(s), (student) loans and other debts.', + 'no_accounts_imperative_liabilities' => 'You don\'t have to use this feature, but it can be useful if you want to keep track of these things.', + 'no_accounts_create_liabilities' => 'Create a liability', 'no_budgets_title_default' => 'Let\'s create a budget', 'no_budgets_intro_default' => 'You have no budgets yet. Budgets are used to organise your expenses into logical groups, which you can give a soft-cap to limit your expenses.', 'no_budgets_imperative_default' => 'Budgets are the basic tools of financial management. Let\'s create one now:', diff --git a/resources/views/partials/menu-sidebar.twig b/resources/views/partials/menu-sidebar.twig index 8d959b7807..048c4d77d5 100644 --- a/resources/views/partials/menu-sidebar.twig +++ b/resources/views/partials/menu-sidebar.twig @@ -27,6 +27,11 @@ {{ 'revenue_accounts'|_ }} +
  • + + {{ 'liabilities_accounts'|_ }} + +
  • diff --git a/routes/web.php b/routes/web.php index 46fc37283b..89081aed4d 100755 --- a/routes/web.php +++ b/routes/web.php @@ -110,10 +110,10 @@ Route::group( ['middleware' => 'user-full-auth', 'namespace' => 'FireflyIII\Http\Controllers', 'prefix' => 'accounts', 'as' => 'accounts.'], function () { // show: - Route::get('{what}', ['uses' => 'Account\IndexController@index', 'as' => 'index'])->where('what', 'revenue|asset|expense'); + Route::get('{what}', ['uses' => 'Account\IndexController@index', 'as' => 'index'])->where('what', 'revenue|asset|expense|liabilities'); // create - Route::get('create/{what}', ['uses' => 'Account\CreateController@create', 'as' => 'create'])->where('what', 'revenue|asset|expense'); + Route::get('create/{what}', ['uses' => 'Account\CreateController@create', 'as' => 'create'])->where('what', 'revenue|asset|expense|liabilities'); Route::post('store', ['uses' => 'Account\CreateController@store', 'as' => 'store']); diff --git a/tests/Api/V1/Controllers/AvailableBudgetControllerTest.php b/tests/Api/V1/Controllers/AvailableBudgetControllerTest.php index ccd8c56aa6..2e2f3dd750 100644 --- a/tests/Api/V1/Controllers/AvailableBudgetControllerTest.php +++ b/tests/Api/V1/Controllers/AvailableBudgetControllerTest.php @@ -148,6 +148,38 @@ class AvailableBudgetControllerTest extends TestCase $response->assertHeader('Content-Type', 'application/vnd.api+json'); } + /** + * Store new available budget but the budget currency is invalid. + * + * @covers \FireflyIII\Api\V1\Controllers\AvailableBudgetController + * @covers \FireflyIII\Api\V1\Requests\AvailableBudgetRequest + */ + public function testStoreInvalidCurrency(): void + { + // mock stuff: + $repository = $this->mock(BudgetRepositoryInterface::class); + $currencyRepository = $this->mock(CurrencyRepositoryInterface::class); + + // mock calls: + $repository->shouldReceive('setUser')->once(); + $currencyRepository->shouldReceive('findNull')->andReturn(null); + + // data to submit + $data = [ + 'transaction_currency_id' => '1', + 'amount' => '100', + 'start_date' => '2018-01-01', + 'end_date' => '2018-01-31', + ]; + + + // test API + $response = $this->post('/api/v1/available_budgets', $data,['Accept' => 'application/json']); + $response->assertStatus(500); + $response->assertSee('Could not find the indicated currency.'); + $response->assertHeader('Content-Type', 'application/json'); + } + /** * Update available budget. * diff --git a/tests/Api/V1/Controllers/ConfigurationControllerTest.php b/tests/Api/V1/Controllers/ConfigurationControllerTest.php index f003a993ac..ba3c4add09 100644 --- a/tests/Api/V1/Controllers/ConfigurationControllerTest.php +++ b/tests/Api/V1/Controllers/ConfigurationControllerTest.php @@ -109,7 +109,7 @@ class ConfigurationControllerTest extends TestCase */ public function testUpdate(): void { - $data = [ + $data = [ 'name' => 'permission_update_check', 'value' => 1, @@ -135,7 +135,57 @@ class ConfigurationControllerTest extends TestCase FireflyConfig::shouldReceive('get')->withArgs(['permission_update_check'])->andReturn($permConfig)->once(); FireflyConfig::shouldReceive('get')->withArgs(['last_update_check'])->andReturn($lastConfig)->once(); FireflyConfig::shouldReceive('get')->withArgs(['single_user_mode'])->andReturn($singleConfig)->once(); - FireflyConfig::shouldReceive('set')->once()->withArgs(['permission_update_check',1]); + FireflyConfig::shouldReceive('set')->once()->withArgs(['permission_update_check', 1]); + + + $expected = [ + 'data' => [ + 'is_demo_site' => false, + 'permission_update_check' => -1, + 'last_update_check' => 123456789, + 'single_user_mode' => true, + ], + ]; + + $response = $this->post('/api/v1/configuration', $data); + $response->assertStatus(200); + $response->assertExactJson($expected); + } + + /** + * Set configuration variables. + * + * @covers \FireflyIII\Api\V1\Controllers\ConfigurationController + */ + public function testUpdateBoolean(): void + { + $data = [ + 'name' => 'single_user_mode', + 'value' => 'true', + + ]; + + $demoConfig = new Configuration; + $demoConfig->name = 'is_demo_site'; + $demoConfig->data = false; + + $permConfig = new Configuration; + $permConfig->name = 'permission_update_check'; + $permConfig->data = -1; + + $lastConfig = new Configuration; + $lastConfig->name = 'last_update_check'; + $lastConfig->data = 123456789; + + $singleConfig = new Configuration; + $singleConfig->name = 'single_user_mode'; + $singleConfig->data = true; + + FireflyConfig::shouldReceive('get')->withArgs(['is_demo_site'])->andReturn($demoConfig)->once(); + FireflyConfig::shouldReceive('get')->withArgs(['permission_update_check'])->andReturn($permConfig)->once(); + FireflyConfig::shouldReceive('get')->withArgs(['last_update_check'])->andReturn($lastConfig)->once(); + FireflyConfig::shouldReceive('get')->withArgs(['single_user_mode'])->andReturn($singleConfig)->once(); + FireflyConfig::shouldReceive('set')->once()->withArgs(['single_user_mode', true]); $expected = [ diff --git a/tests/Api/V1/Controllers/CurrencyExchangeRateControllerTest.php b/tests/Api/V1/Controllers/CurrencyExchangeRateControllerTest.php index 1ba9df9e72..d246883ef1 100644 --- a/tests/Api/V1/Controllers/CurrencyExchangeRateControllerTest.php +++ b/tests/Api/V1/Controllers/CurrencyExchangeRateControllerTest.php @@ -102,4 +102,72 @@ class CurrencyExchangeRateControllerTest extends TestCase ); $response->assertHeader('Content-Type', 'application/vnd.api+json'); } + + /** + * @covers \FireflyIII\Api\V1\Controllers\CurrencyExchangeRateController + */ + public function testIndexBadSource(): void + { + // mock repository + $repository = $this->mock(CurrencyRepositoryInterface::class); + $service = $this->mock(ExchangeRateInterface::class); + + $rate = new CurrencyExchangeRate(); + $rate->date = new Carbon(); + $rate->updated_at = new Carbon(); + $rate->created_at = new Carbon(); + $rate->rate = '0.5'; + $rate->to_currency_id = 1; + $rate->from_currency_id = 2; + + // mock calls: + $repository->shouldReceive('setUser')->once(); + $repository->shouldReceive('findByCodeNull')->withArgs(['EUR'])->andReturn(null)->once(); + $repository->shouldReceive('findByCodeNull')->withArgs(['USD'])->andReturn(TransactionCurrency::whereCode('USD')->first())->once(); + + // test API + $params = [ + 'from' => 'EUR', + 'to' => 'USD', + 'date' => '2018-01-01', + ]; + $response = $this->get('/api/v1/cer?' . http_build_query($params), ['Accept' => 'application/json']); + $response->assertStatus(500); + $response->assertSee('Unknown source currency.'); + $response->assertHeader('Content-Type', 'application/json'); + } + + /** + * @covers \FireflyIII\Api\V1\Controllers\CurrencyExchangeRateController + */ + public function testIndexBadDestination(): void + { + // mock repository + $repository = $this->mock(CurrencyRepositoryInterface::class); + $service = $this->mock(ExchangeRateInterface::class); + + $rate = new CurrencyExchangeRate(); + $rate->date = new Carbon(); + $rate->updated_at = new Carbon(); + $rate->created_at = new Carbon(); + $rate->rate = '0.5'; + $rate->to_currency_id = 1; + $rate->from_currency_id = 2; + + // mock calls: + $repository->shouldReceive('setUser')->once(); + $repository->shouldReceive('findByCodeNull')->withArgs(['EUR'])->andReturn(TransactionCurrency::whereCode('USD')->first())->once(); + $repository->shouldReceive('findByCodeNull')->withArgs(['USD'])->andReturn(null)->once(); + + // test API + $params = [ + 'from' => 'EUR', + 'to' => 'USD', + 'date' => '2018-01-01', + ]; + $response = $this->get('/api/v1/cer?' . http_build_query($params), ['Accept' => 'application/json']); + $response->assertStatus(500); + $response->assertSee('Unknown destination currency.'); + $response->assertHeader('Content-Type', 'application/json'); + } } diff --git a/tests/Api/V1/Controllers/JournalLinkControllerTest.php b/tests/Api/V1/Controllers/JournalLinkControllerTest.php index 5819cfadaa..858f252c2f 100644 --- a/tests/Api/V1/Controllers/JournalLinkControllerTest.php +++ b/tests/Api/V1/Controllers/JournalLinkControllerTest.php @@ -188,6 +188,53 @@ class JournalLinkControllerTest extends TestCase $response->assertHeader('Content-Type', 'application/vnd.api+json'); } + /** + * @covers \FireflyIII\Api\V1\Controllers\JournalLinkController + * @covers \FireflyIII\Api\V1\Requests\JournalLinkRequest + */ + public function testStoreWithNull(): void + { + $journalLink = TransactionJournalLink::first(); + $journal = $this->user()->transactionJournals()->find(1); + $transaction = Transaction::first(); + $transaction->date = new Carbon; + $transaction->transaction_type_type = 'Withdrawal'; + + // mock stuff: + $repository = $this->mock(LinkTypeRepositoryInterface::class); + $journalRepos = $this->mock(JournalRepositoryInterface::class); + $collector = $this->mock(JournalCollectorInterface::class); + + // mock calls: + $repository->shouldReceive('setUser')->once(); + $journalRepos->shouldReceive('setUser')->once(); + $collector->shouldReceive('setUser')->withAnyArgs(); + + $collector->shouldReceive('setUser')->withAnyArgs(); + $collector->shouldReceive('withOpposingAccount')->andReturnSelf(); + $collector->shouldReceive('withCategoryInformation')->andReturnSelf(); + $collector->shouldReceive('withBudgetInformation')->andReturnSelf(); + $collector->shouldReceive('setJournals')->andReturnSelf(); + $collector->shouldReceive('getJournals')->andReturn(new Collection([$transaction])); + + $journalRepos->shouldReceive('findNull')->andReturn(null); + + + // data to submit + $data = [ + 'link_type_id' => '1', + 'inward_id' => '1', + 'outward_id' => '2', + 'notes' => 'Some notes', + ]; + + // test API + $response = $this->post('/api/v1/journal_links', $data, ['Accept' => 'application/json']); + $response->assertStatus(500); + $response->assertSee('Source or destination is NULL.'); // error message + $response->assertHeader('Content-Type', 'application/json'); + } + /** * @covers \FireflyIII\Api\V1\Controllers\JournalLinkController * @covers \FireflyIII\Api\V1\Requests\JournalLinkRequest @@ -236,4 +283,53 @@ class JournalLinkControllerTest extends TestCase $response->assertSee($journalLink->created_at->toAtomString()); // the creation moment. $response->assertHeader('Content-Type', 'application/vnd.api+json'); } + + /** + * @covers \FireflyIII\Api\V1\Controllers\JournalLinkController + * @covers \FireflyIII\Api\V1\Requests\JournalLinkRequest + */ + public function testUpdateWithNull(): void + { + + // mock repositories + $repository = $this->mock(LinkTypeRepositoryInterface::class); + $journalRepos = $this->mock(JournalRepositoryInterface::class); + $collector = $this->mock(JournalCollectorInterface::class); + + $journalLink = TransactionJournalLink::first(); + $journal = $this->user()->transactionJournals()->find(1); + $transaction = Transaction::first(); + $transaction->date = new Carbon; + $transaction->transaction_type_type = 'Withdrawal'; + + + // mock calls: + $repository->shouldReceive('setUser'); + $journalRepos->shouldReceive('setUser')->once(); + $collector->shouldReceive('setUser')->withAnyArgs(); + + $collector->shouldReceive('setUser')->withAnyArgs(); + $collector->shouldReceive('withOpposingAccount')->andReturnSelf(); + $collector->shouldReceive('withCategoryInformation')->andReturnSelf(); + $collector->shouldReceive('withBudgetInformation')->andReturnSelf(); + $collector->shouldReceive('setJournals')->andReturnSelf(); + $collector->shouldReceive('getJournals')->andReturn(new Collection([$transaction])); + + $journalRepos->shouldReceive('findNull')->andReturn(null); + $repository->shouldReceive('updateLink')->once()->andReturn($journalLink); + + // data to submit + $data = [ + 'link_type_id' => '1', + 'inward_id' => '1', + 'outward_id' => '2', + 'notes' => 'Some notes', + ]; + + // test API + $response = $this->put('/api/v1/journal_links/' . $journalLink->id, $data, ['Accept' => 'application/json']); + $response->assertStatus(500); + $response->assertSee('Source or destination is NULL.'); // the creation moment. + $response->assertHeader('Content-Type', 'application/json'); + } }