From b123860304101b644bc11314faf5c3e20d4ca79f Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 17 May 2015 12:49:09 +0200 Subject: [PATCH] More stuff. --- app/Helpers/Collection/BalanceEntry.php | 21 +++++ app/Helpers/Collection/BalanceLine.php | 84 +++++++++++++++-- app/Helpers/Report/ReportHelper.php | 46 ++++++++-- app/Helpers/Report/ReportQuery.php | 3 +- app/Helpers/Report/ReportQueryInterface.php | 3 +- app/Http/Controllers/TagController.php | 6 +- app/Http/Requests/TagFormRequest.php | 1 + app/Models/TransactionJournal.php | 24 ++++- app/Repositories/Bill/BillRepository.php | 6 +- .../Journal/JournalRepository.php | 92 ++++++++++++------- app/Repositories/Tag/TagRepository.php | 38 +++++++- .../Tag/TagRepositoryInterface.php | 19 ++++ resources/twig/partials/reports/balance.twig | 33 +++++-- tests/controllers/AccountControllerTest.php | 5 +- tests/controllers/BillControllerTest.php | 1 + 15 files changed, 301 insertions(+), 81 deletions(-) diff --git a/app/Helpers/Collection/BalanceEntry.php b/app/Helpers/Collection/BalanceEntry.php index 6ceb36b5f3..8ba9a734a0 100644 --- a/app/Helpers/Collection/BalanceEntry.php +++ b/app/Helpers/Collection/BalanceEntry.php @@ -21,6 +21,9 @@ class BalanceEntry /** @var float */ protected $spent = 0.0; + /** @var float */ + protected $left = 0.0; + /** * @return AccountModel @@ -54,5 +57,23 @@ class BalanceEntry $this->spent = $spent; } + /** + * @return float + */ + public function getLeft() + { + return $this->left; + } + + /** + * @param float $left + */ + public function setLeft($left) + { + $this->left = $left; + } + + + } \ No newline at end of file diff --git a/app/Helpers/Collection/BalanceLine.php b/app/Helpers/Collection/BalanceLine.php index cf0f8e9d02..4e045eca8a 100644 --- a/app/Helpers/Collection/BalanceLine.php +++ b/app/Helpers/Collection/BalanceLine.php @@ -3,6 +3,7 @@ namespace FireflyIII\Helpers\Collection; use FireflyIII\Models\Budget as BudgetModel; +use FireflyIII\Models\LimitRepetition; use Illuminate\Support\Collection; /** @@ -15,14 +16,20 @@ use Illuminate\Support\Collection; class BalanceLine { + const ROLE_DEFAULTROLE = 1; + const ROLE_TAGROLE = 2; + const ROLE_DIFFROLE = 3; + /** @var Collection */ protected $balanceEntries; /** @var BudgetModel */ protected $budget; - /** @var float */ - protected $budgetAmount = 0.0; + /** @var LimitRepetition */ + protected $repetition; + + protected $role = self::ROLE_DEFAULTROLE; /** * @@ -40,6 +47,25 @@ class BalanceLine $this->balanceEntries->push($balanceEntry); } + /** + * @return string + */ + public function getTitle() + { + if ($this->getBudget() instanceof BudgetModel) { + return $this->getBudget()->name; + } + if ($this->getRole() == self::ROLE_DEFAULTROLE) { + return trans('firefly.noBudget'); + } + if ($this->getRole() == self::ROLE_TAGROLE) { + return trans('firefly.coveredWithTags'); + } + if ($this->getRole() == self::ROLE_DIFFROLE) { + return trans('firefly.leftUnbalanced'); + } + } + /** * @return BudgetModel */ @@ -57,11 +83,32 @@ class BalanceLine } /** + * @return int + */ + public function getRole() + { + return $this->role; + } + + /** + * @param int $role + */ + public function setRole($role) + { + $this->role = $role; + } + + /** + * If a BalanceLine has a budget/repetition, each BalanceEntry in this BalanceLine + * should have a "spent" value, which is the amount of money that has been spent + * on the given budget/repetition. If you subtract all those amounts from the budget/repetition's + * total amount, this is returned: + * * @return float */ - public function left() + public function leftOfRepetition() { - $start = $this->getBudgetAmount(); + $start = $this->getRepetition() ? $this->getRepetition()->amount : 0; /** @var BalanceEntry $balanceEntry */ foreach ($this->getBalanceEntries() as $balanceEntry) { $start += $balanceEntry->getSpent(); @@ -71,19 +118,19 @@ class BalanceLine } /** - * @return float + * @return LimitRepetition */ - public function getBudgetAmount() + public function getRepetition() { - return $this->budgetAmount; + return $this->repetition; } /** - * @param float $budgetAmount + * @param LimitRepetition $repetition */ - public function setBudgetAmount($budgetAmount) + public function setRepetition($repetition) { - $this->budgetAmount = $budgetAmount; + $this->repetition = $repetition; } /** @@ -102,5 +149,22 @@ class BalanceLine $this->balanceEntries = $balanceEntries; } + /** + * If the BalanceEntries for a BalanceLine have a "left" value, the amount + * of money left in the entire BalanceLine is returned here: + * + * @return float + */ + public function sumOfLeft() + { + $sum = 0.0; + /** @var BalanceEntry $balanceEntry */ + foreach ($this->getBalanceEntries() as $balanceEntry) { + $sum += $balanceEntry->getLeft(); + } + + return $sum; + } + } \ No newline at end of file diff --git a/app/Helpers/Report/ReportHelper.php b/app/Helpers/Report/ReportHelper.php index 8c6440e6b3..5dbbbed26a 100644 --- a/app/Helpers/Report/ReportHelper.php +++ b/app/Helpers/Report/ReportHelper.php @@ -98,8 +98,9 @@ class ReportHelper implements ReportHelperInterface */ public function getBalanceReport(Carbon $start, Carbon $end, $shared) { - $repository = App::make('FireflyIII\Repositories\Budget\BudgetRepositoryInterface'); - $balance = new Balance; + $repository = App::make('FireflyIII\Repositories\Budget\BudgetRepositoryInterface'); + $tagRepository = App::make('FireflyIII\Repositories\Tag\TagRepositoryInterface'); + $balance = new Balance; // build a balance header: $header = new BalanceHeader; @@ -117,9 +118,7 @@ class ReportHelper implements ReportHelperInterface // get budget amount for current period: $rep = $repository->getCurrentRepetition($budget, $start); - if ($rep) { - $line->setBudgetAmount($rep->amount); - } + $line->setRepetition($rep); // loop accounts: foreach ($accounts as $account) { @@ -127,7 +126,7 @@ class ReportHelper implements ReportHelperInterface $balanceEntry->setAccount($account); // get spent: - $spent = $this->query->spentInBudget($account, $budget, $start, $end, $shared); // I think shared is irrelevant. + $spent = $this->query->spentInBudget($account, $budget, $start, $end); // I think shared is irrelevant. $balanceEntry->setSpent($spent); $line->addBalanceEntry($balanceEntry); @@ -137,15 +136,42 @@ class ReportHelper implements ReportHelperInterface } // then a new line for without budget. + // and one for the tags: $empty = new BalanceLine; + $tags = new BalanceLine; + $diffLine = new BalanceLine; + + $tags->setRole(BalanceLine::ROLE_TAGROLE); + $diffLine->setRole(BalanceLine::ROLE_DIFFROLE); + foreach ($accounts as $account) { $spent = $this->query->spentNoBudget($account, $start, $end); - $balanceEntry = new BalanceEntry; - $balanceEntry->setAccount($account); - $balanceEntry->setSpent($spent); - $empty->addBalanceEntry($balanceEntry); + $left = $tagRepository->coveredByBalancingActs($account, $start, $end); + $diff = $spent + $left; + + // budget + $budgetEntry = new BalanceEntry; + $budgetEntry->setAccount($account); + $budgetEntry->setSpent($spent); + $empty->addBalanceEntry($budgetEntry); + + // balanced by tags + $tagEntry = new BalanceEntry; + $tagEntry->setAccount($account); + $tagEntry->setLeft($left); + $tags->addBalanceEntry($tagEntry); + + // difference: + $diffEntry = new BalanceEntry; + $diffEntry->setAccount($account); + $diffEntry->setSpent($diff); + $diffLine->addBalanceEntry($diffEntry); + } + $balance->addBalanceLine($empty); + $balance->addBalanceLine($tags); + $balance->addBalanceLine($diffLine); $balance->setBalanceHeader($header); diff --git a/app/Helpers/Report/ReportQuery.php b/app/Helpers/Report/ReportQuery.php index ab5696e562..38fce48ebb 100644 --- a/app/Helpers/Report/ReportQuery.php +++ b/app/Helpers/Report/ReportQuery.php @@ -196,11 +196,10 @@ class ReportQuery implements ReportQueryInterface * @param Budget $budget * @param Carbon $start * @param Carbon $end - * @param bool $shared * * @return float */ - public function spentInBudget(Account $account, Budget $budget, Carbon $start, Carbon $end, $shared = false) + public function spentInBudget(Account $account, Budget $budget, Carbon $start, Carbon $end) { return floatval( diff --git a/app/Helpers/Report/ReportQueryInterface.php b/app/Helpers/Report/ReportQueryInterface.php index 09478d885b..bcf36e45da 100644 --- a/app/Helpers/Report/ReportQueryInterface.php +++ b/app/Helpers/Report/ReportQueryInterface.php @@ -61,11 +61,10 @@ interface ReportQueryInterface * @param Budget $budget * @param Carbon $start * @param Carbon $end - * @param bool $shared * * @return float */ - public function spentInBudget(Account $account, Budget $budget, Carbon $start, Carbon $end, $shared = false); + public function spentInBudget(Account $account, Budget $budget, Carbon $start, Carbon $end); /** * @param Account $account diff --git a/app/Http/Controllers/TagController.php b/app/Http/Controllers/TagController.php index 77a5daf1f8..90cd841829 100644 --- a/app/Http/Controllers/TagController.php +++ b/app/Http/Controllers/TagController.php @@ -289,9 +289,9 @@ class TagController extends Controller public function update(TagFormRequest $request, TagRepositoryInterface $repository, Tag $tag) { if (Input::get('setTag') == 'true') { - $latitude = strlen($request->get('latitude')) > 0 ? $request->get('latitude') : null; - $longitude = strlen($request->get('longitude')) > 0 ? $request->get('longitude') : null; - $zoomLevel = strlen($request->get('zoomLevel')) > 0 ? $request->get('zoomLevel') : null; + $latitude = $request->get('latitude'); + $longitude = $request->get('longitude'); + $zoomLevel = $request->get('zoomLevel'); } else { $latitude = null; $longitude = null; diff --git a/app/Http/Requests/TagFormRequest.php b/app/Http/Requests/TagFormRequest.php index 777288d5a8..2d0aefc35a 100644 --- a/app/Http/Requests/TagFormRequest.php +++ b/app/Http/Requests/TagFormRequest.php @@ -48,6 +48,7 @@ class TagFormRequest extends Request 'date' => 'date', 'latitude' => 'numeric|min:-90|max:90', 'longitude' => 'numeric|min:-90|max:90', + 'zoomLevel' => 'numeric|min:0|max:80', 'tagMode' => 'required|in:nothing,balancingAct,advancePayment' ]; } diff --git a/app/Models/TransactionJournal.php b/app/Models/TransactionJournal.php index b90726afb0..a102244030 100644 --- a/app/Models/TransactionJournal.php +++ b/app/Models/TransactionJournal.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Database\Query\JoinClause; use Watson\Validating\ValidatingTrait; /** @@ -74,7 +75,7 @@ class TransactionJournal extends Model } /** - * @return Account|mixed + * @return Account */ public function getAssetAccountAttribute() { @@ -93,9 +94,10 @@ class TransactionJournal extends Model } - return $this->transactions()->first(); + return $this->transactions()->first()->account; } + /** * @codeCoverageIgnore * @return \Illuminate\Database\Eloquent\Relations\HasMany @@ -211,6 +213,24 @@ class TransactionJournal extends Model return $query->where('date', '=', $date->format('Y-m-d')); } + /** + * Returns the account to which the money was moved. + * + * @codeCoverageIgnore + * + * @param EloquentBuilder $query + * @param Account $account + */ + public function scopeToAccountIs(EloquentBuilder $query, Account $account) + { + $query->leftJoin( + 'transactions', function (JoinClause $join) { + $join->on('transactions.transaction_journal_id', '=', 'transaction_journals.id')->where('transactions.amount', '>', 0); + } + ); + $query->where('transactions.account_id', $account->id); + } + /** * @codeCoverageIgnore * diff --git a/app/Repositories/Bill/BillRepository.php b/app/Repositories/Bill/BillRepository.php index 8445cda471..d384b8da8a 100644 --- a/app/Repositories/Bill/BillRepository.php +++ b/app/Repositories/Bill/BillRepository.php @@ -272,13 +272,10 @@ class BillRepository implements BillRepositoryInterface */ public function scan(Bill $bill, TransactionJournal $journal) { - /* - * Match words. - */ + $amountMatch = false; $wordMatch = false; $matches = explode(',', $bill->match); $description = strtolower($journal->description); - Log::debug('Now scanning ' . $description); /* * Attach expense account to description for more narrow matching. @@ -316,7 +313,6 @@ class BillRepository implements BillRepositoryInterface * Match amount. */ - $amountMatch = false; if (count($transactions) > 1) { $amount = max(floatval($transactions[0]->amount), floatval($transactions[1]->amount)); diff --git a/app/Repositories/Journal/JournalRepository.php b/app/Repositories/Journal/JournalRepository.php index 05ed690581..c13c79adb7 100644 --- a/app/Repositories/Journal/JournalRepository.php +++ b/app/Repositories/Journal/JournalRepository.php @@ -338,55 +338,31 @@ class JournalRepository implements JournalRepositoryInterface */ protected function storeAccounts(TransactionType $type, array $data) { - $from = null; - $to = null; + $fromAccount = null; + $toAccount = null; switch ($type->type) { case 'Withdrawal': - - $from = Account::find($data['account_id']); - - if (strlen($data['expense_account']) > 0) { - $toType = AccountType::where('type', 'Expense account')->first(); - $to = Account::firstOrCreateEncrypted( - ['user_id' => $data['user'], 'account_type_id' => $toType->id, 'name' => $data['expense_account'], 'active' => 1] - ); - } else { - $toType = AccountType::where('type', 'Cash account')->first(); - $to = Account::firstOrCreateEncrypted( - ['user_id' => $data['user'], 'account_type_id' => $toType->id, 'name' => 'Cash account', 'active' => 1] - ); - } + list($fromAccount, $toAccount) = $this->storeWithdrawalAccounts($data); break; case 'Deposit': - $to = Account::find($data['account_id']); - - if (strlen($data['revenue_account']) > 0) { - $fromType = AccountType::where('type', 'Revenue account')->first(); - $from = Account::firstOrCreateEncrypted( - ['user_id' => $data['user'], 'account_type_id' => $fromType->id, 'name' => $data['revenue_account'], 'active' => 1] - ); - } else { - $toType = AccountType::where('type', 'Cash account')->first(); - $from = Account::firstOrCreateEncrypted( - ['user_id' => $data['user'], 'account_type_id' => $toType->id, 'name' => 'Cash account', 'active' => 1] - ); - } + list($fromAccount, $toAccount) = $this->storeDepositAccounts($data); break; case 'Transfer': - $from = Account::find($data['account_from_id']); - $to = Account::find($data['account_to_id']); + $fromAccount = Account::find($data['account_from_id']); + $toAccount = Account::find($data['account_to_id']); break; } - if (is_null($to) || (!is_null($to) && is_null($to->id))) { + + if (is_null($toAccount)) { Log::error('"to"-account is null, so we cannot continue!'); App::abort(500, '"to"-account is null, so we cannot continue!'); // @codeCoverageIgnoreStart } // @codeCoverageIgnoreEnd - if (is_null($from) || (!is_null($from) && is_null($from->id))) { + if (is_null($fromAccount)) { Log::error('"from"-account is null, so we cannot continue!'); App::abort(500, '"from"-account is null, so we cannot continue!'); // @codeCoverageIgnoreStart @@ -394,6 +370,54 @@ class JournalRepository implements JournalRepositoryInterface // @codeCoverageIgnoreEnd - return [$from, $to]; + return [$fromAccount, $toAccount]; + } + + /** + * @param array $data + * + * @return array + */ + protected function storeWithdrawalAccounts(array $data) + { + $fromAccount = Account::find($data['account_id']); + + if (strlen($data['expense_account']) > 0) { + $toType = AccountType::where('type', 'Expense account')->first(); + $toAccount = Account::firstOrCreateEncrypted( + ['user_id' => $data['user'], 'account_type_id' => $toType->id, 'name' => $data['expense_account'], 'active' => 1] + ); + } else { + $toType = AccountType::where('type', 'Cash account')->first(); + $toAccount = Account::firstOrCreateEncrypted( + ['user_id' => $data['user'], 'account_type_id' => $toType->id, 'name' => 'Cash account', 'active' => 1] + ); + } + + return [$fromAccount, $toAccount]; + } + + /** + * @param array $data + * + * @return array + */ + protected function storeDepositAccounts(array $data) + { + $toAccount = Account::find($data['account_id']); + + if (strlen($data['revenue_account']) > 0) { + $fromType = AccountType::where('type', 'Revenue account')->first(); + $fromAccount = Account::firstOrCreateEncrypted( + ['user_id' => $data['user'], 'account_type_id' => $fromType->id, 'name' => $data['revenue_account'], 'active' => 1] + ); + } else { + $toType = AccountType::where('type', 'Cash account')->first(); + $fromAccount = Account::firstOrCreateEncrypted( + ['user_id' => $data['user'], 'account_type_id' => $toType->id, 'name' => 'Cash account', 'active' => 1] + ); + } + + return [$fromAccount, $toAccount]; } } diff --git a/app/Repositories/Tag/TagRepository.php b/app/Repositories/Tag/TagRepository.php index c8e172e68d..753d35f23e 100644 --- a/app/Repositories/Tag/TagRepository.php +++ b/app/Repositories/Tag/TagRepository.php @@ -4,6 +4,8 @@ namespace FireflyIII\Repositories\Tag; use Auth; +use Carbon\Carbon; +use FireflyIII\Models\Account; use FireflyIII\Models\Tag; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; @@ -17,6 +19,7 @@ use Illuminate\Support\Collection; class TagRepository implements TagRepositoryInterface { + /** * @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's five. * @@ -52,6 +55,38 @@ class TagRepository implements TagRepositoryInterface return false; } + /** + * This method scans the transaction journals from or to the given asset account + * and checks if these are part of a balancing act. If so, it will sum up the amounts + * transferred into the balancing act (if any) and return this amount. + * + * This method effectively tells you the amount of money that has been balanced out + * correctly in the given period for the given account. + * + * @param Account $account + * @param Carbon $start + * @param Carbon $end + * + * @return float + */ + public function coveredByBalancingActs(Account $account, Carbon $start, Carbon $end) + { + // the quickest way to do this is by scanning all balancingAct tags + // because there will be less of them any way. + $tags = Auth::user()->tags()->where('tagMode', 'balancingAct')->get(); + $amount = 0; + + /** @var Tag $tag */ + foreach ($tags as $tag) { + $transfer = $tag->transactionjournals()->after($start)->before($end)->toAccountIs($account)->transactionTypes(['Transfer'])->first(); + if ($transfer) { + $amount += $transfer->amount; + } + } + + return $amount; + } + /** * @param Tag $tag * @@ -63,6 +98,7 @@ class TagRepository implements TagRepositoryInterface return true; } + // @codeCoverageIgnoreEnd /** * @return Collection @@ -79,7 +115,6 @@ class TagRepository implements TagRepositoryInterface return $tags; } - // @codeCoverageIgnoreEnd /** * @param array $data @@ -223,6 +258,7 @@ class TagRepository implements TagRepositoryInterface return true; } + return false; } } diff --git a/app/Repositories/Tag/TagRepositoryInterface.php b/app/Repositories/Tag/TagRepositoryInterface.php index 94d5e44cb2..95cbe558e5 100644 --- a/app/Repositories/Tag/TagRepositoryInterface.php +++ b/app/Repositories/Tag/TagRepositoryInterface.php @@ -2,6 +2,8 @@ namespace FireflyIII\Repositories\Tag; +use Carbon\Carbon; +use FireflyIII\Models\Account; use FireflyIII\Models\Tag; use FireflyIII\Models\TransactionJournal; use Illuminate\Support\Collection; @@ -15,6 +17,23 @@ use Illuminate\Support\Collection; interface TagRepositoryInterface { + + /** + * This method scans the transaction journals from or to the given asset account + * and checks if these are part of a balancing act. If so, it will sum up the amounts + * transferred into the balancing act (if any) and return this amount. + * + * This method effectively tells you the amount of money that has been balanced out + * correctly in the given period for the given account. + * + * @param Account $account + * @param Carbon $start + * @param Carbon $end + * + * @return float + */ + public function coveredByBalancingActs(Account $account, Carbon $start, Carbon $end); + /** * @param array $data * diff --git a/resources/twig/partials/reports/balance.twig b/resources/twig/partials/reports/balance.twig index 3536d21e01..cddb047bce 100644 --- a/resources/twig/partials/reports/balance.twig +++ b/resources/twig/partials/reports/balance.twig @@ -10,31 +10,44 @@ {% for account in balance.getBalanceHeader.getAccounts %} {{ account.name }} {% endfor %} - + {{ 'leftInBudget'|_ }} + {{ 'sum'|_ }} {% for balanceLine in balance.getBalanceLines %} + + {% if balanceLine.getBudget %} - {% if balanceLine.getBudget %} - {{ balanceLine.getBudget.name }} - {% else %} - {{ 'noBudget'|_ }} - {% endif %} + {{ balanceLine.getTitle }} - {{ balanceLine.getBudgetAmount|formatAmount }} + {{ balanceLine.getRepetition.amount|formatAmount }} + {% else %} + {{ balanceLine.getTitle }} + {% endif %} + {% for balanceEntry in balanceLine.getBalanceEntries %} - + {% if balanceEntry.getSpent != 0 %} - {{ (balanceEntry.getSpent*-1)|formatAmountPlain }} + {{ (balanceEntry.getSpent*-1)|formatAmountPlain }} + {% endif %} + {% if balanceEntry.getLeft != 0 %} + {{ (balanceEntry.getLeft)|formatAmountPlain }} {% endif %} {% endfor %} - {{ balanceLine.left|formatAmount }} + {% if balanceLine.leftOfRepetition != 0 %} + {{ balanceLine.leftOfRepetition|formatAmount }} + {% endif %} + + + {% if balanceLine.sumOfLeft != 0 %} + {{ balanceLine.sumOfLeft|formatAmount }} + {% endif %} {% endfor %} diff --git a/tests/controllers/AccountControllerTest.php b/tests/controllers/AccountControllerTest.php index e90b554e0f..ffa4a1740d 100644 --- a/tests/controllers/AccountControllerTest.php +++ b/tests/controllers/AccountControllerTest.php @@ -9,6 +9,7 @@ use Illuminate\Support\Collection; use League\FactoryMuffin\Facade as FactoryMuffin; /** + * @SuppressWarnings(PHPMD.TooManyMethods) * Class AccountControllerTest */ class AccountControllerTest extends TestCase @@ -118,8 +119,8 @@ class AccountControllerTest extends TestCase $repository->shouldReceive('openingBalanceTransaction')->andReturn($openingBalance); // create a transaction that will be returned for the opening balance transaction: - $openingBalanceTransaction = FactoryMuffin::create('FireflyIII\Models\Transaction'); - $repository->shouldReceive('getFirstTransaction')->andReturn($openingBalanceTransaction); + $opening = FactoryMuffin::create('FireflyIII\Models\Transaction'); + $repository->shouldReceive('getFirstTransaction')->andReturn($opening); // CURRENCY: $currency = FactoryMuffin::create('FireflyIII\Models\TransactionCurrency'); diff --git a/tests/controllers/BillControllerTest.php b/tests/controllers/BillControllerTest.php index fb27206afb..65a1a4566d 100644 --- a/tests/controllers/BillControllerTest.php +++ b/tests/controllers/BillControllerTest.php @@ -4,6 +4,7 @@ use Illuminate\Support\Collection; use League\FactoryMuffin\Facade as FactoryMuffin; /** + * @SuppressWarnings(PHPMD.TooManyMethods) * Class BillControllerTest */ class BillControllerTest extends TestCase