Merge branch 'release/4.6.5'

This commit is contained in:
James Cole 2017-09-09 07:21:53 +02:00
commit 81bef28607
329 changed files with 11194 additions and 4962 deletions

View File

@ -2,6 +2,43 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/). This project adheres to [Semantic Versioning](http://semver.org/).
## [4.6.5] - 2017-09-09
### Added
- #616, The ability to link transactions
- #763, as suggested by @tannie
- #770, as suggested by @skibbipl
- #780, as suggested by @skibbipl
- #784, as suggested by @SmilingWorlock
- Lots of code for future support of automated Bunq imports
### Changed
- Rewrote the export routine
- #782, as suggested by @NiceGuyIT
- #800, as suggested by @jleeong
### Fixed
- #724, reported by @skibbipl
- #738, reported by @skibbipl
- #760, reported by @leander091
- #764, reported by @tannie
- #792, reported by @jleeong
- #793, reported by @nicoschreiner
- #797, reported by @leander091
- #801, reported by @pkoziol
- #803, reported by @pkoziol
- #805, reported by @pkoziol
- #806, reported by @pkoziol
- #807, reported by @pkoziol
- #808, reported by @pkoziol
- #809, reported by @pkoziol
- #814, reported by @nicoschreiner
- #818, reported by @gavu
- #819, reported by @DieBauer
- #820, reported by @simonsmiley
- Various other fixes
## [4.6.4] - 2017-08-13 ## [4.6.4] - 2017-08-13
### Added ### Added
- PHP7.1 support - PHP7.1 support

View File

@ -23,28 +23,18 @@ RUN docker-php-ext-install -j$(nproc) curl gd intl json mcrypt readline tidy zip
# Generate locales supported by firefly # Generate locales supported by firefly
RUN echo "en_US.UTF-8 UTF-8\nde_DE.UTF-8 UTF-8\nnl_NL.UTF-8 UTF-8\npt_BR.UTF-8 UTF-8" > /etc/locale.gen && locale-gen RUN echo "en_US.UTF-8 UTF-8\nde_DE.UTF-8 UTF-8\nnl_NL.UTF-8 UTF-8\npt_BR.UTF-8 UTF-8" > /etc/locale.gen && locale-gen
COPY docker/apache2.conf /etc/apache2/apache2.conf
# Enable apache mod rewrite.. # Enable apache mod rewrite..
RUN a2enmod rewrite RUN a2enmod rewrite
# Setup the Composer installer # Setup the Composer installer
RUN curl -o /tmp/composer-setup.php https://getcomposer.org/installer && \ run curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
curl -o /tmp/composer-setup.sig https://composer.github.io/installer.sig && \
php -r "if (hash('SHA384', file_get_contents('/tmp/composer-setup.php')) !== trim(file_get_contents('/tmp/composer-setup.sig'))) { unlink('/tmp/composer-setup.php'); echo 'Invalid installer' . PHP_EOL; exit(1); }" && \
chmod +x /tmp/composer-setup.php && \
php /tmp/composer-setup.php && \
mv composer.phar /usr/local/bin/composer && \
rm -f /tmp/composer-setup.{php,sig}
ADD . /var/www/firefly-iii RUN cd /var/www && composer create-project grumpydictator/firefly-iii --no-dev --prefer-dist firefly-iii 4.6.4
RUN chown -R www-data:www-data /var/www/ COPY docker/entrypoint.sh /var/www/firefly-iii/docker/entrypoint.sh
ADD docker/apache-firefly.conf /etc/apache2/sites-available/000-default.conf ADD docker/apache-firefly.conf /etc/apache2/sites-available/000-default.conf
RUN chown -R www-data:www-data /var/www && chmod -R 775 /var/www/firefly-iii/storage
USER www-data
WORKDIR /var/www/firefly-iii WORKDIR /var/www/firefly-iii
EXPOSE 80
RUN composer install --no-scripts --no-dev
USER root
ENTRYPOINT ["/var/www/firefly-iii/docker/entrypoint.sh"] ENTRYPOINT ["/var/www/firefly-iii/docker/entrypoint.sh"]

View File

@ -54,6 +54,8 @@ class CreateImport extends Command
} }
/** /**
* Run the command.
*
* @SuppressWarnings(PHPMD.ExcessiveMethodLength) // cannot be helped * @SuppressWarnings(PHPMD.ExcessiveMethodLength) // cannot be helped
* @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's five exactly. * @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's five exactly.
*/ */
@ -133,6 +135,8 @@ class CreateImport extends Command
} }
/** /**
* Verify user inserts correct arguments.
*
* @return bool * @return bool
* @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's five exactly. * @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's five exactly.
*/ */

View File

@ -51,7 +51,7 @@ class Import extends Command
} }
/** /**
* * Run the import routine.
*/ */
public function handle() public function handle()
{ {
@ -91,6 +91,8 @@ class Import extends Command
} }
/** /**
* Check if job is valid to be imported.
*
* @param ImportJob $job * @param ImportJob $job
* *
* @return bool * @return bool

View File

@ -20,22 +20,21 @@ use FireflyIII\Models\AccountMeta;
use FireflyIII\Models\AccountType; use FireflyIII\Models\AccountType;
use FireflyIII\Models\BudgetLimit; use FireflyIII\Models\BudgetLimit;
use FireflyIII\Models\LimitRepetition; use FireflyIII\Models\LimitRepetition;
use FireflyIII\Models\PiggyBankEvent;
use FireflyIII\Models\Transaction; use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType; use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Database\QueryException; use Illuminate\Database\QueryException;
use Illuminate\Support\Collection;
use Log; use Log;
use Preferences; use Preferences;
use Schema; use Schema;
use Steam;
/** /**
* Class UpgradeDatabase * Class UpgradeDatabase
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects) // it just touches a lot of things. * @SuppressWarnings(PHPMD.CouplingBetweenObjects) // it just touches a lot of things.
* *
* @package FireflyIII\Console\Commands * @package FireflyIII\Console\Commands
@ -71,76 +70,26 @@ class UpgradeDatabase extends Command
{ {
$this->setTransactionIdentifier(); $this->setTransactionIdentifier();
$this->migrateRepetitions(); $this->migrateRepetitions();
$this->repairPiggyBanks();
$this->updateAccountCurrencies(); $this->updateAccountCurrencies();
$this->updateJournalCurrencies(); $this->line('Updating currency information..');
$this->currencyInfoToTransactions(); $this->updateTransferCurrencies();
$this->verifyCurrencyInfo(); $this->updateOtherCurrencies();
$this->info('Firefly III database is up to date.'); $this->info('Firefly III database is up to date.');
return;
} }
/** /**
* Moves the currency id info to the transaction instead of the journal. * Migrate budget repetitions to new format where the end date is in the budget limit as well,
* * making the limit_repetition table obsolete.
* @SuppressWarnings(PHPMD.CyclomaticComplexity) // cannot be helped.
*/ */
private function currencyInfoToTransactions() public function migrateRepetitions(): void
{ {
$count = 0;
$set = TransactionJournal::with('transactions')->get();
/** @var TransactionJournal $journal */
foreach ($set as $journal) {
/** @var Transaction $transaction */
foreach ($journal->transactions as $transaction) {
if (is_null($transaction->transaction_currency_id)) {
$transaction->transaction_currency_id = $journal->transaction_currency_id;
$transaction->save();
$count++;
}
}
// read and use the foreign amounts when present.
if ($journal->hasMeta('foreign_amount')) {
$amount = Steam::positive($journal->getMeta('foreign_amount'));
// update both transactions:
foreach ($journal->transactions as $transaction) {
$transaction->foreign_amount = $amount;
if (bccomp($transaction->amount, '0') === -1) {
// update with negative amount:
$transaction->foreign_amount = bcmul($amount, '-1');
}
// set foreign currency id:
$transaction->foreign_currency_id = intval($journal->getMeta('foreign_currency_id'));
$transaction->save();
}
$journal->deleteMeta('foreign_amount');
$journal->deleteMeta('foreign_currency_id');
}
}
$this->line(sprintf('Updated currency information for %d transactions', $count));
}
/**
* Migrate budget repetitions to new format.
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's 5.
*/
private function migrateRepetitions()
{
if (!Schema::hasTable('budget_limits')) {
return;
}
// get all budget limits with end_date NULL
$set = BudgetLimit::whereNull('end_date')->get(); $set = BudgetLimit::whereNull('end_date')->get();
if ($set->count() > 0) {
$this->line(sprintf('Found %d budget limit(s) to update', $set->count()));
}
/** @var BudgetLimit $budgetLimit */ /** @var BudgetLimit $budgetLimit */
foreach ($set as $budgetLimit) { foreach ($set as $budgetLimit) {
// get limit repetition (should be just one):
/** @var LimitRepetition $repetition */ /** @var LimitRepetition $repetition */
$repetition = $budgetLimit->limitrepetitions()->first(); $repetition = $budgetLimit->limitrepetitions()->first();
if (!is_null($repetition)) { if (!is_null($repetition)) {
@ -150,59 +99,30 @@ class UpgradeDatabase extends Command
$repetition->delete(); $repetition->delete();
} }
} }
return;
} }
/** /**
* Make sure there are only transfers linked to piggy bank events. * This method gives all transactions which are part of a split journal (so more than 2) a sort of "order" so they are easier
* to easier to match to their counterpart. When a journal is split, it has two or three transactions: -3, -4 and -5 for example.
* *
* @SuppressWarnings(PHPMD.CyclomaticComplexity) // cannot be helped. * In the database this is reflected as 6 transactions: -3/+3, -4/+4, -5/+5.
*
* When either of these are the same amount, FF3 can't keep them apart: +3/-3, +3/-3, +3/-3. This happens more often than you would
* think. So each set gets a number (1,2,3) to keep them apart.
*/ */
private function repairPiggyBanks() public function setTransactionIdentifier(): void
{
// if table does not exist, return false
if (!Schema::hasTable('piggy_bank_events')) {
return;
}
$set = PiggyBankEvent::with(['PiggyBank', 'TransactionJournal', 'TransactionJournal.TransactionType'])->get();
/** @var PiggyBankEvent $event */
foreach ($set as $event) {
if (is_null($event->transaction_journal_id)) {
continue;
}
/** @var TransactionJournal $journal */
$journal = $event->transactionJournal()->first();
if (is_null($journal)) {
continue;
}
$type = $journal->transactionType->type;
if ($type !== TransactionType::TRANSFER) {
$event->transaction_journal_id = null;
$event->save();
$this->line(sprintf('Piggy bank #%d was referenced by an invalid event. This has been fixed.', $event->piggy_bank_id));
}
}
}
/**
* This is strangely complex, because the HAVING modifier is a no-no. And subqueries in Laravel are weird.
*/
private function setTransactionIdentifier()
{ {
// if table does not exist, return false // if table does not exist, return false
if (!Schema::hasTable('transaction_journals')) { if (!Schema::hasTable('transaction_journals')) {
return; return;
} }
$subQuery = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->whereNull('transaction_journals.deleted_at')
$subQuery = TransactionJournal::leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') ->whereNull('transactions.deleted_at')
->whereNull('transaction_journals.deleted_at') ->groupBy(['transaction_journals.id'])
->whereNull('transactions.deleted_at') ->select(['transaction_journals.id', DB::raw('COUNT(transactions.id) AS t_count')]);
->groupBy(['transaction_journals.id'])
->select(['transaction_journals.id', DB::raw('COUNT(transactions.id) AS t_count')]);
$result = DB::table(DB::raw('(' . $subQuery->toSql() . ') AS derived')) $result = DB::table(DB::raw('(' . $subQuery->toSql() . ') AS derived'))
->mergeBindings($subQuery->getQuery()) ->mergeBindings($subQuery->getQuery())
->where('t_count', '>', 2) ->where('t_count', '>', 2)
@ -210,55 +130,172 @@ class UpgradeDatabase extends Command
$journalIds = array_unique($result->pluck('id')->toArray()); $journalIds = array_unique($result->pluck('id')->toArray());
foreach ($journalIds as $journalId) { foreach ($journalIds as $journalId) {
$this->updateJournal(intval($journalId)); $this->updateJournalidentifiers(intval($journalId));
} }
return;
} }
/** /**
* Make sure all accounts have proper currency info. * Each (asset) account must have a reference to a preferred currency. If the account does not have one, it's forced upon the account.
*/ */
private function updateAccountCurrencies() public function updateAccountCurrencies(): void
{ {
$accounts = Account::leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') $accounts = Account::leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id')
->whereIn('account_types.type', [AccountType::DEFAULT, AccountType::ASSET])->get(['accounts.*']); ->whereIn('account_types.type', [AccountType::DEFAULT, AccountType::ASSET])->get(['accounts.*']);
/** @var Account $account */ $accounts->each(
foreach ($accounts as $account) { function (Account $account) {
// get users preference, fall back to system pref. // get users preference, fall back to system pref.
$defaultCurrencyCode = Preferences::getForUser($account->user, 'currencyPreference', config('firefly.default_currency', 'EUR'))->data; $defaultCurrencyCode = Preferences::getForUser($account->user, 'currencyPreference', config('firefly.default_currency', 'EUR'))->data;
$defaultCurrency = TransactionCurrency::where('code', $defaultCurrencyCode)->first(); $defaultCurrency = TransactionCurrency::where('code', $defaultCurrencyCode)->first();
$accountCurrency = intval($account->getMeta('currency_id')); $accountCurrency = intval($account->getMeta('currency_id'));
$openingBalance = $account->getOpeningBalance(); $openingBalance = $account->getOpeningBalance();
$obCurrency = intval($openingBalance->transaction_currency_id); $obCurrency = intval($openingBalance->transaction_currency_id);
// both 0? set to default currency: // both 0? set to default currency:
if ($accountCurrency === 0 && $obCurrency === 0) { if ($accountCurrency === 0 && $obCurrency === 0) {
AccountMeta::create(['account_id' => $account->id, 'name' => 'currency_id', 'data' => $defaultCurrency->id]); AccountMeta::create(['account_id' => $account->id, 'name' => 'currency_id', 'data' => $defaultCurrency->id]);
$this->line(sprintf('Account #%d ("%s") now has a currency setting (%s).', $account->id, $account->name, $defaultCurrencyCode)); $this->line(sprintf('Account #%d ("%s") now has a currency setting (%s).', $account->id, $account->name, $defaultCurrencyCode));
continue;
return true;
}
// account is set to 0, opening balance is not?
if ($accountCurrency === 0 && $obCurrency > 0) {
AccountMeta::create(['account_id' => $account->id, 'name' => 'currency_id', 'data' => $obCurrency]);
$this->line(sprintf('Account #%d ("%s") now has a currency setting (%s).', $account->id, $account->name, $defaultCurrencyCode));
return true;
}
// do not match and opening balance id is not null.
if ($accountCurrency !== $obCurrency && $openingBalance->id > 0) {
// update opening balance:
$openingBalance->transaction_currency_id = $accountCurrency;
$openingBalance->save();
$this->line(sprintf('Account #%d ("%s") now has a correct currency for opening balance.', $account->id, $account->name));
return true;
}
return true;
} }
);
// account is set to 0, opening balance is not? return;
if ($accountCurrency === 0 && $obCurrency > 0) { }
AccountMeta::create(['account_id' => $account->id, 'name' => 'currency_id', 'data' => $obCurrency]);
$this->line(sprintf('Account #%d ("%s") now has a currency setting (%s).', $account->id, $account->name, $defaultCurrencyCode)); /**
continue; * This routine verifies that withdrawals, deposits and opening balances have the correct currency settings for
* the accounts they are linked to.
*
* Both source and destination must match the respective currency preference of the related asset account.
* So FF3 must verify all transactions.
*/
public function updateOtherCurrencies(): void
{
/** @var CurrencyRepositoryInterface $repository */
$repository = app(CurrencyRepositoryInterface::class);
$set = TransactionJournal
::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->whereIn('transaction_types.type', [TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::OPENING_BALANCE])
->get(['transaction_journals.*']);
$set->each(
function (TransactionJournal $journal) use ($repository) {
// get the transaction with the asset account in it:
/** @var Transaction $transaction */
$transaction = $journal->transactions()
->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id')
->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id')
->whereIn('account_types.type', [AccountType::DEFAULT, AccountType::ASSET])->first(['transactions.*']);
/** @var Account $account */
$account = $transaction->account;
$currency = $repository->find(intval($account->getMeta('currency_id')));
$transactions = $journal->transactions()->get();
$transactions->each(
function (Transaction $transaction) use ($currency) {
if (is_null($transaction->transaction_currency_id)) {
$transaction->transaction_currency_id = $currency->id;
$transaction->save();
}
// when mismatch in transaction:
if ($transaction->transaction_currency_id !== $currency->id) {
$transaction->foreign_currency_id = $transaction->transaction_currency_id;
$transaction->foreign_amount = $transaction->amount;
$transaction->transaction_currency_id = $currency->id;
$transaction->save();
}
}
);
// also update the journal, of course:
$journal->transaction_currency_id = $currency->id;
$journal->save();
} }
);
// do not match: return;
if ($accountCurrency !== $obCurrency) { }
// update opening balance:
$openingBalance->transaction_currency_id = $accountCurrency; /**
$openingBalance->save(); * This routine verifies that transfers have the correct currency settings for the accounts they are linked to.
$this->line(sprintf('Account #%d ("%s") now has a correct currency for opening balance.', $account->id, $account->name)); * For transfers, this is can be a destructive routine since we FORCE them into a currency setting whether they
continue; * like it or not. Previous routines MUST have set the currency setting for both accounts for this to work.
*
* A transfer always has the
*
* Both source and destination must match the respective currency preference. So FF3 must verify ALL
* transactions.
*/
public function updateTransferCurrencies()
{
$set = TransactionJournal
::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->where('transaction_types.type', TransactionType::TRANSFER)
->get(['transaction_journals.*']);
$set->each(
function (TransactionJournal $transfer) {
// select all "source" transactions:
/** @var Collection $transactions */
$transactions = $transfer->transactions()->where('amount', '<', 0)->get();
$transactions->each(
function (Transaction $transaction) {
$this->updateTransactionCurrency($transaction);
$this->updateJournalCurrency($transaction);
}
);
} }
);
}
// opening balance 0, account not zero? just continue:
// both are equal, just continue:
/**
* This method makes sure that the transaction journal uses the currency given in the transaction.
*
* @param Transaction $transaction
*/
private function updateJournalCurrency(Transaction $transaction): void
{
/** @var CurrencyRepositoryInterface $repository */
$repository = app(CurrencyRepositoryInterface::class);
$currency = $repository->find(intval($transaction->account->getMeta('currency_id')));
$journal = $transaction->transactionJournal;
if (!(intval($currency->id) === intval($journal->transaction_currency_id))) {
$this->line(
sprintf(
'Transfer #%d ("%s") has been updated to use %s instead of %s.', $journal->id, $journal->description, $currency->code,
$journal->transactionCurrency->code
)
);
$journal->transaction_currency_id = $currency->id;
$journal->save();
} }
return;
} }
/** /**
@ -267,7 +304,7 @@ class UpgradeDatabase extends Command
* *
* @param int $journalId * @param int $journalId
*/ */
private function updateJournal(int $journalId) private function updateJournalidentifiers(int $journalId): void
{ {
$identifier = 0; $identifier = 0;
$processed = []; $processed = [];
@ -295,121 +332,128 @@ class UpgradeDatabase extends Command
if (!is_null($opposing)) { if (!is_null($opposing)) {
// give both a new identifier: // give both a new identifier:
$transaction->identifier = $identifier; $transaction->identifier = $identifier;
$opposing->identifier = $identifier;
$transaction->save(); $transaction->save();
$opposing->identifier = $identifier;
$opposing->save(); $opposing->save();
$processed[] = $transaction->id; $processed[] = $transaction->id;
$processed[] = $opposing->id; $processed[] = $opposing->id;
} }
$identifier++; $identifier++;
} }
return;
} }
/** /**
* Makes sure that withdrawals, deposits and transfers have * This method makes sure that the tranaction uses the same currency as the source account does.
* a currency setting matching their respective accounts * If not, the currency is updated to include a reference to its original currency as the "foreign" currency.
*/
private function updateJournalCurrencies()
{
$types = [
TransactionType::WITHDRAWAL => '<',
TransactionType::DEPOSIT => '>',
];
$repository = app(CurrencyRepositoryInterface::class);
$notification = '%s #%d uses %s but should use %s. It has been updated. Please verify this in Firefly III.';
$transfer = 'Transfer #%d has been updated to use the correct currencies. Please verify this in Firefly III.';
$driver = DB::connection()->getDriverName();
$pgsql = ['pgsql', 'postgresql'];
foreach ($types as $type => $operator) {
$query = TransactionJournal
::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')->leftJoin(
'transactions', function (JoinClause $join) use ($operator) {
$join->on('transaction_journals.id', '=', 'transactions.transaction_journal_id')->where('transactions.amount', $operator, '0');
}
)
->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id')
->leftJoin('account_meta', 'account_meta.account_id', '=', 'accounts.id')
->where('transaction_types.type', $type)
->where('account_meta.name', 'currency_id');
if (in_array($driver, $pgsql)) {
$query->where('transaction_journals.transaction_currency_id', '!=', DB::raw('cast(account_meta.data as int)'));
}
if (!in_array($driver, $pgsql)) {
$query->where('transaction_journals.transaction_currency_id', '!=', DB::raw('account_meta.data'));
}
$set = $query->get(['transaction_journals.*', 'account_meta.data as expected_currency_id', 'transactions.amount as transaction_amount']);
/** @var TransactionJournal $journal */
foreach ($set as $journal) {
$expectedCurrency = $repository->find(intval($journal->expected_currency_id));
$line = sprintf($notification, $type, $journal->id, $journal->transactionCurrency->code, $expectedCurrency->code);
$journal->setMeta('foreign_amount', $journal->transaction_amount);
$journal->setMeta('foreign_currency_id', $journal->transaction_currency_id);
$journal->transaction_currency_id = $expectedCurrency->id;
$journal->save();
$this->line($line);
}
}
/*
* For transfers it's slightly different. Both source and destination
* must match the respective currency preference. So we must verify ALL
* transactions.
*/
$set = TransactionJournal
::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->where('transaction_types.type', TransactionType::TRANSFER)
->get(['transaction_journals.*']);
/** @var TransactionJournal $journal */
foreach ($set as $journal) {
$updated = false;
/** @var Transaction $sourceTransaction */
$sourceTransaction = $journal->transactions()->where('amount', '<', 0)->first();
$sourceCurrency = $repository->find(intval($sourceTransaction->account->getMeta('currency_id')));
if ($sourceCurrency->id !== $journal->transaction_currency_id) {
$updated = true;
$journal->transaction_currency_id = $sourceCurrency->id;
$journal->save();
}
// destination
$destinationTransaction = $journal->transactions()->where('amount', '>', 0)->first();
$destinationCurrency = $repository->find(intval($destinationTransaction->account->getMeta('currency_id')));
if ($destinationCurrency->id !== $journal->transaction_currency_id) {
$updated = true;
$journal->deleteMeta('foreign_amount');
$journal->deleteMeta('foreign_currency_id');
$journal->setMeta('foreign_amount', $destinationTransaction->amount);
$journal->setMeta('foreign_currency_id', $destinationCurrency->id);
}
if ($updated) {
$line = sprintf($transfer, $journal->id);
$this->line($line);
}
}
}
/**
* *
* The transaction that is sent to this function MUST be the source transaction (amount negative).
*
* @param Transaction $transaction
*/ */
private function verifyCurrencyInfo() private function updateTransactionCurrency(Transaction $transaction): void
{ {
$count = 0; /** @var CurrencyRepositoryInterface $repository */
$transactions = Transaction::get(); $repository = app(CurrencyRepositoryInterface::class);
/** @var Transaction $transaction */ $currency = $repository->find(intval($transaction->account->getMeta('currency_id')));
foreach ($transactions as $transaction) {
$currencyId = intval($transaction->transaction_currency_id); // has no currency ID? Must have, so fill in using account preference:
$foreignId = intval($transaction->foreign_currency_id); if (is_null($transaction->transaction_currency_id)) {
if ($currencyId === $foreignId) { $transaction->transaction_currency_id = $currency->id;
$transaction->foreign_currency_id = null; Log::debug(sprintf('Transaction #%d has no currency setting, now set to %s', $transaction->id, $currency->code));
$transaction->foreign_amount = null; $transaction->save();
$transaction->save();
$count++;
}
} }
$this->line(sprintf('Updated currency information for %d transactions', $count));
// does not match the source account (see above)? Can be fixed
// when mismatch in transaction and NO foreign amount is set:
if ($transaction->transaction_currency_id !== $currency->id && is_null($transaction->foreign_amount)) {
Log::debug(
sprintf(
'Transaction #%d has a currency setting (#%d) that should be #%d. Amount remains %s, currency is changed.', $transaction->id,
$transaction->transaction_currency_id, $currency->id, $transaction->amount
)
);
$transaction->transaction_currency_id = $currency->id;
$transaction->save();
}
// grab opposing transaction:
/** @var TransactionJournal $journal */
$journal = $transaction->transactionJournal;
/** @var Transaction $opposing */
$opposing = $journal->transactions()->where('amount', '>', 0)->where('identifier', $transaction->identifier)->first();
$opposingCurrency = $repository->find(intval($opposing->account->getMeta('currency_id')));
if (is_null($opposingCurrency->id)) {
Log::error(sprintf('Account #%d ("%s") must have currency preference but has none.', $opposing->account->id, $opposing->account->name));
return;
}
// if the destination account currency is the same, both foreign_amount and foreign_currency_id must be NULL for both transactions:
if ($opposingCurrency->id === $currency->id) {
// update both transactions to match:
$transaction->foreign_amount = null;
$transaction->foreign_currency_id = null;
$opposing->foreign_amount = null;
$opposing->foreign_currency_id = null;
$opposing->transaction_currency_id = $currency->id;
$transaction->save();
$opposing->save();
Log::debug(sprintf('Cleaned up transaction #%d and #%d', $transaction->id, $opposing->id));
return;
}
// if destination account currency is different, both transactions must have this currency as foreign currency id.
if ($opposingCurrency->id !== $currency->id) {
$transaction->foreign_currency_id = $opposingCurrency->id;
$opposing->foreign_currency_id = $opposingCurrency->id;
$transaction->save();
$opposing->save();
Log::debug(sprintf('Verified foreign currency ID of transaction #%d and #%d', $transaction->id, $opposing->id));
}
// if foreign amount of one is null and the other is not, use this to restore:
if (is_null($transaction->foreign_amount) && !is_null($opposing->foreign_amount)) {
$transaction->foreign_amount = bcmul(strval($opposing->foreign_amount), '-1');
$transaction->save();
Log::debug(sprintf('Restored foreign amount of transaction (1) #%d to %s', $transaction->id, $transaction->foreign_amount));
}
// if foreign amount of one is null and the other is not, use this to restore (other way around)
if (is_null($opposing->foreign_amount) && !is_null($transaction->foreign_amount)) {
$opposing->foreign_amount = bcmul(strval($transaction->foreign_amount), '-1');
$opposing->save();
Log::debug(sprintf('Restored foreign amount of transaction (2) #%d to %s', $opposing->id, $opposing->foreign_amount));
}
// when both are zero, try to grab it from journal:
if (is_null($opposing->foreign_amount) && is_null($transaction->foreign_amount)) {
$foreignAmount = $journal->getMeta('foreign_amount');
if (is_null($foreignAmount)) {
Log::debug(sprintf('Journal #%d has missing foreign currency data, forced to do 1:1 conversion :(.', $transaction->transaction_journal_id));
$transaction->foreign_amount = bcmul(strval($transaction->amount), '-1');
$opposing->foreign_amount = bcmul(strval($opposing->amount), '-1');
$transaction->save();
$opposing->save();
return;
}
$foreignPositive = app('steam')->positive(strval($foreignAmount));
Log::debug(
sprintf(
'Journal #%d has missing foreign currency info, try to restore from meta-data ("%s").', $transaction->transaction_journal_id, $foreignAmount
)
);
$transaction->foreign_amount = bcmul($foreignPositive, '-1');
$opposing->foreign_amount = $foreignPositive;
$transaction->save();
$opposing->save();
}
return;
} }
} }

View File

@ -84,6 +84,9 @@ class UpgradeFireflyInstructions extends Command
} }
} }
/**
* Render instructions.
*/
private function installInstructions() private function installInstructions()
{ {
/** @var string $version */ /** @var string $version */
@ -102,7 +105,7 @@ class UpgradeFireflyInstructions extends Command
$this->boxed(''); $this->boxed('');
if (is_null($text)) { if (is_null($text)) {
$this->boxed(sprintf('Thank you for installin Firefly III, v%s!', $version)); $this->boxed(sprintf('Thank you for installing Firefly III, v%s!', $version));
$this->boxedInfo('There are no extra installation instructions.'); $this->boxedInfo('There are no extra installation instructions.');
$this->boxed('Firefly III should be ready for use.'); $this->boxed('Firefly III should be ready for use.');
$this->boxed(''); $this->boxed('');
@ -131,6 +134,9 @@ class UpgradeFireflyInstructions extends Command
} }
/**
* Render upgrade instructions.
*/
private function updateInstructions() private function updateInstructions()
{ {
/** @var string $version */ /** @var string $version */

View File

@ -1,4 +1,14 @@
<?php <?php
/**
* UseEncryption.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1); declare(strict_types=1);
/** /**
@ -16,6 +26,8 @@ use Illuminate\Support\Str;
/** /**
* Class UseEncryption * Class UseEncryption
*
* @package FireflyIII\Console\Commands
*/ */
class UseEncryption extends Command class UseEncryption extends Command
{ {
@ -46,7 +58,6 @@ class UseEncryption extends Command
*/ */
public function handle() public function handle()
{ {
//
$this->handleObjects('Account', 'name', 'encrypted'); $this->handleObjects('Account', 'name', 'encrypted');
$this->handleObjects('Bill', 'name', 'name_encrypted'); $this->handleObjects('Bill', 'name', 'name_encrypted');
$this->handleObjects('Bill', 'match', 'match_encrypted'); $this->handleObjects('Bill', 'match', 'match_encrypted');
@ -57,6 +68,8 @@ class UseEncryption extends Command
} }
/** /**
* Run each object and encrypt them (or not).
*
* @param string $class * @param string $class
* @param string $field * @param string $field
* @param string $indicator * @param string $indicator

View File

@ -17,6 +17,7 @@ use Crypt;
use FireflyIII\Models\Account; use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType; use FireflyIII\Models\AccountType;
use FireflyIII\Models\Budget; use FireflyIII\Models\Budget;
use FireflyIII\Models\PiggyBankEvent;
use FireflyIII\Models\Transaction; use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType; use FireflyIII\Models\TransactionType;
@ -94,6 +95,40 @@ class VerifyDatabase extends Command
// report on journals with the wrong types of accounts. // report on journals with the wrong types of accounts.
$this->reportIncorrectJournals(); $this->reportIncorrectJournals();
// report (and fix) piggy banks
$this->repairPiggyBanks();
}
/**
* Make sure there are only transfers linked to piggy bank events.
*/
private function repairPiggyBanks(): void
{
$set = PiggyBankEvent::with(['PiggyBank', 'TransactionJournal', 'TransactionJournal.TransactionType'])->get();
$set->each(
function (PiggyBankEvent $event) {
if (is_null($event->transaction_journal_id)) {
return true;
}
/** @var TransactionJournal $journal */
$journal = $event->transactionJournal()->first();
if (is_null($journal)) {
return true;
}
$type = $journal->transactionType->type;
if ($type !== TransactionType::TRANSFER) {
$event->transaction_journal_id = null;
$event->save();
$this->line(sprintf('Piggy bank #%d was referenced by an invalid event. This has been fixed.', $event->piggy_bank_id));
}
return true;
}
);
return;
} }
/** /**
@ -169,6 +204,9 @@ class VerifyDatabase extends Command
} }
} }
/**
* Report on journals with bad account types linked to them.
*/
private function reportIncorrectJournals() private function reportIncorrectJournals()
{ {
$configuration = [ $configuration = [
@ -235,7 +273,7 @@ class VerifyDatabase extends Command
} }
/** /**
* * Report on journals without transactions.
*/ */
private function reportNoTransactions() private function reportNoTransactions()
{ {
@ -253,6 +291,8 @@ class VerifyDatabase extends Command
} }
/** /**
* Report on things with no linked journals.
*
* @param string $name * @param string $name
*/ */
private function reportObject(string $name) private function reportObject(string $name)
@ -324,7 +364,7 @@ class VerifyDatabase extends Command
} }
/** /**
* * Report on transfers that have budgets.
*/ */
private function reportTransfersBudgets() private function reportTransfersBudgets()
{ {

View File

@ -15,6 +15,7 @@ namespace FireflyIII\Export\Collector;
use Crypt; use Crypt;
use Illuminate\Contracts\Encryption\DecryptException; use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Log; use Log;
use Storage; use Storage;
@ -50,14 +51,6 @@ class UploadCollector extends BasicCollector implements CollectorInterface
public function run(): bool public function run(): bool
{ {
Log::debug('Going to collect attachments', ['key' => $this->job->key]); Log::debug('Going to collect attachments', ['key' => $this->job->key]);
// file names associated with the old import routine.
$this->vintageFormat = sprintf('csv-upload-%d-', $this->job->user->id);
// collect old upload files (names beginning with "csv-upload".
$this->collectVintageUploads();
// then collect current upload files:
$this->collectModernUploads(); $this->collectModernUploads();
return true; return true;
@ -70,7 +63,8 @@ class UploadCollector extends BasicCollector implements CollectorInterface
*/ */
private function collectModernUploads(): bool private function collectModernUploads(): bool
{ {
$set = $this->job->user->importJobs()->where('status', 'import_complete')->get(['import_jobs.*']); $set = $this->job->user->importJobs()->whereIn('status', ['import_complete', 'finished'])->get(['import_jobs.*']);
Log::debug(sprintf('Found %d import jobs', $set->count()));
$keys = []; $keys = [];
if ($set->count() > 0) { if ($set->count() > 0) {
$keys = $set->pluck('key')->toArray(); $keys = $set->pluck('key')->toArray();
@ -83,59 +77,6 @@ class UploadCollector extends BasicCollector implements CollectorInterface
return true; return true;
} }
/**
* This method collects all the uploads that are uploaded using the "old" importer. So from before the summer of 2016.
*
* @return bool
*/
private function collectVintageUploads(): bool
{
// grab upload directory.
$files = $this->uploadDisk->files();
foreach ($files as $entry) {
$this->processVintageUpload($entry);
}
return true;
}
/**
* This method tells you when the vintage upload file was actually uploaded.
*
* @param string $entry
*
* @return string
*/
private function getVintageUploadDate(string $entry): string
{
// this is an original upload.
$parts = explode('-', str_replace(['.csv.encrypted', $this->vintageFormat], '', $entry));
$originalUpload = intval($parts[1]);
$date = date('Y-m-d \a\t H-i-s', $originalUpload);
return $date;
}
/**
* Tells you if a file name is a vintage upload.
*
* @param string $entry
*
* @return bool
*/
private function isVintageImport(string $entry): bool
{
$len = strlen($this->vintageFormat);
// file is part of the old import routine:
if (substr($entry, 0, $len) === $this->vintageFormat) {
return true;
}
return false;
}
/** /**
* @param string $key * @param string $key
* *
@ -153,7 +94,7 @@ class UploadCollector extends BasicCollector implements CollectorInterface
$content = ''; $content = '';
try { try {
$content = Crypt::decrypt($this->uploadDisk->get(sprintf('%s.upload', $key))); $content = Crypt::decrypt($this->uploadDisk->get(sprintf('%s.upload', $key)));
} catch (DecryptException $e) { } catch (FileNotFoundException | DecryptException $e) {
Log::error(sprintf('Could not decrypt old import file "%s". Skipped because: %s', $key, $e->getMessage())); Log::error(sprintf('Could not decrypt old import file "%s". Skipped because: %s', $key, $e->getMessage()));
} }
@ -168,47 +109,4 @@ class UploadCollector extends BasicCollector implements CollectorInterface
return true; return true;
} }
/**
* If the file is a vintage upload, process it.
*
* @param string $entry
*
* @return bool
*/
private function processVintageUpload(string $entry): bool
{
if ($this->isVintageImport($entry)) {
$this->saveVintageImportFile($entry);
return true;
}
return false;
}
/**
* This will store the content of the old vintage upload somewhere.
*
* @param string $entry
*/
private function saveVintageImportFile(string $entry)
{
$content = '';
try {
$content = Crypt::decrypt($this->uploadDisk->get($entry));
} catch (DecryptException $e) {
Log::error('Could not decrypt old CSV import file ' . $entry . '. Skipped because ' . $e->getMessage());
}
if (strlen($content) > 0) {
// add to export disk.
$date = $this->getVintageUploadDate($entry);
$file = $this->job->key . '-Old import dated ' . $date . '.csv';
$this->exportDisk->put($file, $content);
$this->getEntries()->push($file);
}
}
} }

View File

@ -13,6 +13,7 @@ declare(strict_types=1);
namespace FireflyIII\Export\Entry; namespace FireflyIII\Export\Entry;
use FireflyIII\Models\Transaction;
use Steam; use Steam;
/** /**
@ -37,24 +38,43 @@ final class Entry
{ {
// @formatter:off // @formatter:off
public $journal_id; public $journal_id;
public $transaction_id = 0;
public $date; public $date;
public $description; public $description;
public $currency_code; public $currency_code;
public $amount; public $amount;
public $foreign_currency_code = '';
public $foreign_amount = '0';
public $transaction_type; public $transaction_type;
public $asset_account_id; public $asset_account_id;
public $asset_account_name; public $asset_account_name;
public $asset_account_iban;
public $asset_account_bic;
public $asset_account_number;
public $asset_currency_code;
public $opposing_account_id; public $opposing_account_id;
public $opposing_account_name; public $opposing_account_name;
public $opposing_account_iban;
public $opposing_account_bic;
public $opposing_account_number;
public $opposing_currency_code;
public $budget_id; public $budget_id;
public $budget_name; public $budget_name;
public $category_id; public $category_id;
public $category_name; public $category_name;
public $bill_id;
public $bill_name;
public $notes;
public $tags;
// @formatter:on // @formatter:on
/** /**
@ -95,5 +115,75 @@ final class Entry
return $entry; return $entry;
} }
/**
* Converts a given transaction (as collected by the collector) into an export entry.
*
* @param Transaction $transaction
*
* @return Entry
*/
public static function fromTransaction(Transaction $transaction): Entry
{
$entry = new self;
$entry->journal_id = $transaction->journal_id;
$entry->transaction_id = $transaction->id;
$entry->date = $transaction->date->format('Ymd');
$entry->description = $transaction->description;
if (strlen(strval($transaction->transaction_description)) > 0) {
$entry->description = $transaction->transaction_description . '(' . $transaction->description . ')';
}
$entry->currency_code = $transaction->transactionCurrency->code;
$entry->amount = round($transaction->transaction_amount, $transaction->transactionCurrency->decimal_places);
$entry->foreign_currency_code = is_null($transaction->foreign_currency_id) ? null : $transaction->foreignCurrency->code;
$entry->foreign_amount = is_null($transaction->foreign_currency_id)
? null
: strval(
round(
$transaction->transaction_foreign_amount, $transaction->foreignCurrency->decimal_places
)
);
$entry->transaction_type = $transaction->transaction_type_type;
$entry->asset_account_id = $transaction->account_id;
$entry->asset_account_name = app('steam')->tryDecrypt($transaction->account_name);
$entry->asset_account_iban = $transaction->account_iban;
$entry->asset_account_number = $transaction->account_number;
$entry->asset_account_bic = $transaction->account_bic;
$entry->asset_currency_code = $transaction->account_currency_code;
$entry->opposing_account_id = $transaction->opposing_account_id;
$entry->opposing_account_name = app('steam')->tryDecrypt($transaction->opposing_account_name);
$entry->opposing_account_iban = $transaction->opposing_account_iban;
$entry->opposing_account_number = $transaction->opposing_account_number;
$entry->opposing_account_bic = $transaction->opposing_account_bic;
$entry->opposing_currency_code = $transaction->opposing_currency_code;
/** budget */
$entry->budget_id = $transaction->transaction_budget_id;
$entry->budget_name = app('steam')->tryDecrypt($transaction->transaction_budget_name);
if (is_null($transaction->transaction_budget_id)) {
$entry->budget_id = $transaction->transaction_journal_budget_id;
$entry->budget_name = app('steam')->tryDecrypt($transaction->transaction_journal_budget_name);
}
/** category */
$entry->category_id = $transaction->transaction_category_id;
$entry->category_name = app('steam')->tryDecrypt($transaction->transaction_category_name);
if (is_null($transaction->transaction_category_id)) {
$entry->category_id = $transaction->transaction_journal_category_id;
$entry->category_name = app('steam')->tryDecrypt($transaction->transaction_journal_category_name);
}
/** budget */
$entry->bill_id = $transaction->bill_id;
$entry->bill_name = app('steam')->tryDecrypt($transaction->bill_name);
$entry->tags = $transaction->tags;
$entry->notes = $transaction->notes;
return $entry;
}
} }

View File

@ -0,0 +1,339 @@
<?php
/**
* ExpandedProcessor.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Export;
use Crypt;
use DB;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Export\Collector\AttachmentCollector;
use FireflyIII\Export\Collector\UploadCollector;
use FireflyIII\Export\Entry\Entry;
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
use FireflyIII\Helpers\Filter\InternalTransferFilter;
use FireflyIII\Models\AccountMeta;
use FireflyIII\Models\ExportJob;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournalMeta;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use Illuminate\Support\Collection;
use Log;
use Storage;
use ZipArchive;
/**
* Class ExpandedProcessor
*
* @package FireflyIII\Export
*/
class ExpandedProcessor implements ProcessorInterface
{
/** @var Collection */
public $accounts;
/** @var string */
public $exportFormat;
/** @var bool */
public $includeAttachments;
/** @var bool */
public $includeOldUploads;
/** @var ExportJob */
public $job;
/** @var array */
public $settings;
/** @var Collection */
private $exportEntries;
/** @var Collection */
private $files;
/** @var Collection */
private $journals;
/**
* Processor constructor.
*/
public function __construct()
{
$this->journals = new Collection;
$this->exportEntries = new Collection;
$this->files = new Collection;
}
/**
* @return bool
*/
public function collectAttachments(): bool
{
/** @var AttachmentCollector $attachmentCollector */
$attachmentCollector = app(AttachmentCollector::class);
$attachmentCollector->setJob($this->job);
$attachmentCollector->setDates($this->settings['startDate'], $this->settings['endDate']);
$attachmentCollector->run();
$this->files = $this->files->merge($attachmentCollector->getEntries());
return true;
}
/**
* @return bool
*/
public function collectJournals(): bool
{
// use journal collector thing.
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAccounts($this->accounts)->setRange($this->settings['startDate'], $this->settings['endDate'])
->withOpposingAccount()->withBudgetInformation()->withCategoryInformation()
->removeFilter(InternalTransferFilter::class);
$transactions = $collector->getJournals();
// get some more meta data for each entry:
$ids = $transactions->pluck('journal_id')->toArray();
$assetIds = $transactions->pluck('account_id')->toArray();
$opposingIds = $transactions->pluck('opposing_account_id')->toArray();
$notes = $this->getNotes($ids);
$tags = $this->getTags($ids);
$ibans = $this->getIbans($assetIds) + $this->getIbans($opposingIds);
$currencies = $this->getAccountCurrencies($ibans);
$transactions->each(
function (Transaction $transaction) use ($notes, $tags, $ibans, $currencies) {
$journalId = intval($transaction->journal_id);
$accountId = intval($transaction->account_id);
$opposingId = intval($transaction->opposing_account_id);
$currencyId = $ibans[$accountId]['currency_id'] ?? 0;
$opposingCurrencyId = $ibans[$opposingId]['currency_id'] ?? 0;
$transaction->notes = $notes[$journalId] ?? '';
$transaction->tags = join(',', $tags[$journalId] ?? []);
$transaction->account_number = $ibans[$accountId]['accountNumber'] ?? '';
$transaction->account_bic = $ibans[$accountId]['BIC'] ?? '';
$transaction->account_currency_code = $currencies[$currencyId] ?? '';
$transaction->opposing_account_number = $ibans[$opposingId]['accountNumber'] ?? '';
$transaction->opposing_account_bic = $ibans[$opposingId]['BIC'] ?? '';
$transaction->opposing_currency_code = $currencies[$opposingCurrencyId] ?? '';
}
);
$this->journals = $transactions;
return true;
}
/**
* @return bool
*/
public function collectOldUploads(): bool
{
/** @var UploadCollector $uploadCollector */
$uploadCollector = app(UploadCollector::class);
$uploadCollector->setJob($this->job);
$uploadCollector->run();
$this->files = $this->files->merge($uploadCollector->getEntries());
return true;
}
/**
* @return bool
*/
public function convertJournals(): bool
{
$this->journals->each(
function (Transaction $transaction) {
$this->exportEntries->push(Entry::fromTransaction($transaction));
}
);
Log::debug(sprintf('Count %d entries in exportEntries (convertJournals)', $this->exportEntries->count()));
return true;
}
/**
* @return bool
* @throws FireflyException
*/
public function createZipFile(): bool
{
$zip = new ZipArchive;
$file = $this->job->key . '.zip';
$fullPath = storage_path('export') . '/' . $file;
if ($zip->open($fullPath, ZipArchive::CREATE) !== true) {
throw new FireflyException('Cannot store zip file.');
}
// for each file in the collection, add it to the zip file.
$disk = Storage::disk('export');
foreach ($this->getFiles() as $entry) {
// is part of this job?
$zipFileName = str_replace($this->job->key . '-', '', $entry);
$zip->addFromString($zipFileName, $disk->get($entry));
}
$zip->close();
// delete the files:
$this->deleteFiles();
return true;
}
/**
* @return bool
*/
public function exportJournals(): bool
{
$exporterClass = config('firefly.export_formats.' . $this->exportFormat);
$exporter = app($exporterClass);
$exporter->setJob($this->job);
$exporter->setEntries($this->exportEntries);
$exporter->run();
$this->files->push($exporter->getFileName());
return true;
}
/**
* @return Collection
*/
public function getFiles(): Collection
{
return $this->files;
}
/**
* Save export job settings to class.
*
* @param array $settings
*/
public function setSettings(array $settings)
{
// save settings
$this->settings = $settings;
$this->accounts = $settings['accounts'];
$this->exportFormat = $settings['exportFormat'];
$this->includeAttachments = $settings['includeAttachments'];
$this->includeOldUploads = $settings['includeOldUploads'];
$this->job = $settings['job'];
}
/**
*
*/
private function deleteFiles()
{
$disk = Storage::disk('export');
foreach ($this->getFiles() as $file) {
$disk->delete($file);
}
}
/**
* @param array $array
*
* @return array
*/
private function getAccountCurrencies(array $array): array
{
/** @var CurrencyRepositoryInterface $repository */
$repository = app(CurrencyRepositoryInterface::class);
$return = [];
$ids = [];
$repository->setUser($this->job->user);
foreach ($array as $value) {
$ids[] = $value['currency_id'] ?? 0;
}
$ids = array_unique($ids);
$result = $repository->getByIds($ids);
foreach ($result as $currency) {
$return[$currency->id] = $currency->code;
}
return $return;
}
/**
* Get all IBAN / SWIFT / account numbers
*
* @param array $array
*
* @return array
*/
private function getIbans(array $array): array
{
$array = array_unique($array);
$return = [];
$set = AccountMeta::whereIn('account_id', $array)
->leftJoin('accounts', 'accounts.id', 'account_meta.account_id')
->where('accounts.user_id', $this->job->user_id)
->whereIn('account_meta.name', ['accountNumber', 'BIC', 'currency_id'])
->get(['account_meta.id', 'account_meta.account_id', 'account_meta.name', 'account_meta.data']);
/** @var AccountMeta $meta */
foreach ($set as $meta) {
$id = intval($meta->account_id);
$return[$id][$meta->name] = $meta->data;
}
return $return;
}
/**
* Returns, if present, for the given journal ID's the notes.
*
* @param array $array
*
* @return array
*/
private function getNotes(array $array): array
{
$array = array_unique($array);
$set = TransactionJournalMeta::whereIn('journal_meta.transaction_journal_id', $array)
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id')
->where('transaction_journals.user_id', $this->job->user_id)
->where('journal_meta.name', 'notes')->get(
['journal_meta.transaction_journal_id', 'journal_meta.data', 'journal_meta.id']
);
$return = [];
/** @var TransactionJournalMeta $meta */
foreach ($set as $meta) {
$id = intval($meta->transaction_journal_id);
$return[$id] = $meta->data;
}
return $return;
}
/**
* Returns a comma joined list of all the users tags linked to these journals.
*
* @param array $array
*
* @return array
*/
private function getTags(array $array): array
{
$set = DB::table('tag_transaction_journal')
->whereIn('tag_transaction_journal.transaction_journal_id', $array)
->leftJoin('tags', 'tag_transaction_journal.tag_id', '=', 'tags.id')
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'tag_transaction_journal.transaction_journal_id')
->where('transaction_journals.user_id', $this->job->user_id)
->get(['tag_transaction_journal.transaction_journal_id', 'tags.tag']);
$result = [];
foreach ($set as $entry) {
$id = intval($entry->transaction_journal_id);
$result[$id] = isset($result[$id]) ? $result[$id] : [];
$result[$id][] = Crypt::decrypt($entry->tag);
}
return $result;
}
}

View File

@ -58,8 +58,12 @@ class CsvExporter extends BasicExporter implements ExporterInterface
// get field names for header row: // get field names for header row:
$first = $this->getEntries()->first(); $first = $this->getEntries()->first();
$headers = array_keys(get_object_vars($first)); $headers = [];
$rows[] = $headers; if (!is_null($first)) {
$headers = array_keys(get_object_vars($first));
}
$rows[] = $headers;
/** @var Entry $entry */ /** @var Entry $entry */
foreach ($this->getEntries() as $entry) { foreach ($this->getEntries() as $entry) {

View File

@ -15,7 +15,6 @@ namespace FireflyIII\Helpers\Collector;
use Carbon\Carbon; use Carbon\Carbon;
use Crypt;
use DB; use DB;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Filter\FilterInterface; use FireflyIII\Helpers\Filter\FilterInterface;
@ -31,7 +30,6 @@ use FireflyIII\Models\Tag;
use FireflyIII\Models\Transaction; use FireflyIII\Models\Transaction;
use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\User; use FireflyIII\User;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\JoinClause; use Illuminate\Database\Query\JoinClause;
use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\LengthAwarePaginator;
@ -86,7 +84,9 @@ class JournalCollector implements JournalCollectorInterface
'accounts.name as account_name', 'accounts.name as account_name',
'accounts.encrypted as account_encrypted', 'accounts.encrypted as account_encrypted',
'accounts.iban as account_iban',
'account_types.type as account_type', 'account_types.type as account_type',
]; ];
/** @var bool */ /** @var bool */
private $filterTransfers = false; private $filterTransfers = false;
@ -175,12 +175,10 @@ class JournalCollector implements JournalCollectorInterface
if (!is_null($transaction->bill_name)) { if (!is_null($transaction->bill_name)) {
$transaction->bill_name = Steam::decrypt(intval($transaction->bill_name_encrypted), $transaction->bill_name); $transaction->bill_name = Steam::decrypt(intval($transaction->bill_name_encrypted), $transaction->bill_name);
} }
$transaction->opposing_account_name = app('steam')->tryDecrypt($transaction->opposing_account_name);
$transaction->account_iban = app('steam')->tryDecrypt($transaction->account_iban);
$transaction->opposing_account_iban = app('steam')->tryDecrypt($transaction->opposing_account_iban);
try {
$transaction->opposing_account_name = Crypt::decrypt($transaction->opposing_account_name);
} catch (DecryptException $e) {
// if this fails its already decrypted.
}
} }
); );
@ -621,6 +619,8 @@ class JournalCollector implements JournalCollectorInterface
$this->query->leftJoin('budgets as transaction_journal_budgets', 'transaction_journal_budgets.id', '=', 'budget_transaction_journal.budget_id'); $this->query->leftJoin('budgets as transaction_journal_budgets', 'transaction_journal_budgets.id', '=', 'budget_transaction_journal.budget_id');
$this->query->leftJoin('budget_transaction', 'budget_transaction.transaction_id', '=', 'transactions.id'); $this->query->leftJoin('budget_transaction', 'budget_transaction.transaction_id', '=', 'transactions.id');
$this->query->leftJoin('budgets as transaction_budgets', 'transaction_budgets.id', '=', 'budget_transaction.budget_id'); $this->query->leftJoin('budgets as transaction_budgets', 'transaction_budgets.id', '=', 'budget_transaction.budget_id');
$this->query->whereNull('transaction_journal_budgets.deleted_at');
$this->query->whereNull('transaction_budgets.deleted_at');
$this->fields[] = 'budget_transaction_journal.budget_id as transaction_journal_budget_id'; $this->fields[] = 'budget_transaction_journal.budget_id as transaction_journal_budget_id';
$this->fields[] = 'transaction_journal_budgets.encrypted as transaction_journal_budget_encrypted'; $this->fields[] = 'transaction_journal_budgets.encrypted as transaction_journal_budget_encrypted';
@ -677,10 +677,12 @@ class JournalCollector implements JournalCollectorInterface
$this->query->leftJoin('account_types as opposing_account_types', 'opposing_accounts.account_type_id', '=', 'opposing_account_types.id'); $this->query->leftJoin('account_types as opposing_account_types', 'opposing_accounts.account_type_id', '=', 'opposing_account_types.id');
$this->query->whereNull('opposing.deleted_at'); $this->query->whereNull('opposing.deleted_at');
$this->fields[] = 'opposing.id as opposing_id'; $this->fields[] = 'opposing.id as opposing_id';
$this->fields[] = 'opposing.account_id as opposing_account_id'; $this->fields[] = 'opposing.account_id as opposing_account_id';
$this->fields[] = 'opposing_accounts.name as opposing_account_name'; $this->fields[] = 'opposing_accounts.name as opposing_account_name';
$this->fields[] = 'opposing_accounts.encrypted as opposing_account_encrypted'; $this->fields[] = 'opposing_accounts.encrypted as opposing_account_encrypted';
$this->fields[] = 'opposing_accounts.iban as opposing_account_iban';
$this->fields[] = 'opposing_account_types.type as opposing_account_type'; $this->fields[] = 'opposing_account_types.type as opposing_account_type';
$this->joinedOpposing = true; $this->joinedOpposing = true;
Log::debug('joinedOpposing is now true!'); Log::debug('joinedOpposing is now true!');

View File

@ -14,16 +14,12 @@ declare(strict_types=1);
namespace FireflyIII\Helpers\Report; namespace FireflyIII\Helpers\Report;
use Carbon\Carbon; use Carbon\Carbon;
use DB;
use FireflyIII\Helpers\Collection\Balance; use FireflyIII\Helpers\Collection\Balance;
use FireflyIII\Helpers\Collection\BalanceEntry; use FireflyIII\Helpers\Collection\BalanceEntry;
use FireflyIII\Helpers\Collection\BalanceHeader; use FireflyIII\Helpers\Collection\BalanceHeader;
use FireflyIII\Helpers\Collection\BalanceLine; use FireflyIII\Helpers\Collection\BalanceLine;
use FireflyIII\Models\BudgetLimit; use FireflyIII\Models\BudgetLimit;
use FireflyIII\Models\Tag;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Log; use Log;
@ -74,14 +70,9 @@ class BalanceReportHelper implements BalanceReportHelperInterface
$line = $this->createBalanceLine($budgetLimit, $accounts); $line = $this->createBalanceLine($budgetLimit, $accounts);
$balance->addBalanceLine($line); $balance->addBalanceLine($line);
} }
Log::debug('Create rest of the things.'); $noBudgetLine = $this->createNoBudgetLine($accounts, $start, $end);
$noBudgetLine = $this->createNoBudgetLine($accounts, $start, $end);
$coveredByTagLine = $this->createTagsBalanceLine($accounts, $start, $end);
$leftUnbalancedLine = $this->createLeftUnbalancedLine($noBudgetLine, $coveredByTagLine);
$balance->addBalanceLine($noBudgetLine); $balance->addBalanceLine($noBudgetLine);
$balance->addBalanceLine($coveredByTagLine);
$balance->addBalanceLine($leftUnbalancedLine);
$balance->setBalanceHeader($header); $balance->setBalanceHeader($header);
Log::debug('Clear unused budgets.'); Log::debug('Clear unused budgets.');
@ -93,54 +84,6 @@ class BalanceReportHelper implements BalanceReportHelperInterface
return $balance; return $balance;
} }
/**
* This method collects all transfers that are part of a "balancing act" tag
* and groups the amounts of those transfers by their destination account.
*
* This is used to indicate which expenses, usually outside of budgets, have been
* corrected by transfers from a savings account.
*
* @param Collection $accounts
* @param Carbon $start
* @param Carbon $end
*
* @return Collection
*/
private function allCoveredByBalancingActs(Collection $accounts, Carbon $start, Carbon $end): Collection
{
$ids = $accounts->pluck('id')->toArray();
$set = auth()->user()->tags()
->leftJoin('tag_transaction_journal', 'tag_transaction_journal.tag_id', '=', 'tags.id')
->leftJoin('transaction_journals', 'tag_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id')
->leftJoin('transaction_types', 'transaction_journals.transaction_type_id', '=', 'transaction_types.id')
->leftJoin(
'transactions AS t_source', function (JoinClause $join) {
$join->on('transaction_journals.id', '=', 't_source.transaction_journal_id')->where('t_source.amount', '<', 0);
}
)
->leftJoin(
'transactions AS t_destination', function (JoinClause $join) {
$join->on('transaction_journals.id', '=', 't_destination.transaction_journal_id')->where('t_destination.amount', '>', 0);
}
)
->where('tags.tagMode', 'balancingAct')
->where('transaction_types.type', TransactionType::TRANSFER)
->where('transaction_journals.date', '>=', $start->format('Y-m-d'))
->where('transaction_journals.date', '<=', $end->format('Y-m-d'))
->whereNull('transaction_journals.deleted_at')
->whereIn('t_source.account_id', $ids)
->whereIn('t_destination.account_id', $ids)
->groupBy('t_destination.account_id')
->get(
[
't_destination.account_id',
DB::raw('SUM(t_destination.amount) AS sum'),
]
);
return $set;
}
/** /**
* @param BudgetLimit $budgetLimit * @param BudgetLimit $budgetLimit
@ -168,40 +111,6 @@ class BalanceReportHelper implements BalanceReportHelperInterface
return $line; return $line;
} }
/**
* @param BalanceLine $noBudgetLine
* @param BalanceLine $coveredByTagLine
*
* @return BalanceLine
*/
private function createLeftUnbalancedLine(BalanceLine $noBudgetLine, BalanceLine $coveredByTagLine): BalanceLine
{
$line = new BalanceLine;
$line->setRole(BalanceLine::ROLE_DIFFROLE);
$noBudgetEntries = $noBudgetLine->getBalanceEntries();
$tagEntries = $coveredByTagLine->getBalanceEntries();
foreach ($noBudgetEntries as $entry) {
$account = $entry->getAccount();
$tagEntry = $tagEntries->filter(
function (BalanceEntry $current) use ($account) {
return $current->getAccount()->id === $account->id;
}
);
if ($tagEntry->first()) {
// found corresponding entry. As we should:
$newEntry = new BalanceEntry;
$newEntry->setAccount($account);
$spent = bcadd($tagEntry->first()->getLeft(), $entry->getSpent());
$newEntry->setSpent($spent);
$line->addBalanceEntry($newEntry);
}
}
return $line;
}
/** /**
* @param Collection $accounts * @param Collection $accounts
@ -227,41 +136,6 @@ class BalanceReportHelper implements BalanceReportHelperInterface
return $empty; return $empty;
} }
/**
* @param Collection $accounts
* @param Carbon $start
* @param Carbon $end
*
* @return BalanceLine
*/
private function createTagsBalanceLine(Collection $accounts, Carbon $start, Carbon $end): BalanceLine
{
$tags = new BalanceLine;
$tagsLeft = $this->allCoveredByBalancingActs($accounts, $start, $end);
$tags->setRole(BalanceLine::ROLE_TAGROLE);
foreach ($accounts as $account) {
$leftEntry = $tagsLeft->filter(
function (Tag $tag) use ($account) {
return $tag->account_id === $account->id;
}
);
$left = '0';
if (!is_null($leftEntry->first())) {
$left = $leftEntry->first()->sum;
}
// balanced by tags
$tagEntry = new BalanceEntry;
$tagEntry->setAccount($account);
$tagEntry->setLeft($left);
$tags->addBalanceEntry($tagEntry);
}
return $tags;
}
/** /**
* @param Balance $balance * @param Balance $balance

View File

@ -194,7 +194,7 @@ class AccountController extends Controller
return view( return view(
'accounts.edit', compact( 'accounts.edit', compact(
'allCurrencies', 'currencySelectList', 'account', 'currency', 'subTitle', 'subTitleIcon', 'what', 'roles' 'allCurrencies', 'currencySelectList', 'account', 'currency', 'subTitle', 'subTitleIcon', 'what', 'roles', 'preFilled'
) )
); );
} }

View File

@ -0,0 +1,229 @@
<?php
/**
* LinkController.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Admin;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Requests\LinkTypeFormRequest;
use FireflyIII\Models\LinkType;
use FireflyIII\Repositories\LinkType\LinkTypeRepositoryInterface;
use Illuminate\Http\Request;
use Preferences;
use View;
/**
* Class LinkController
*
* @package FireflyIII\Http\Controllers\Admin
*/
class LinkController extends Controller
{
/**
*
*/
public function __construct()
{
parent::__construct();
$this->middleware(
function ($request, $next) {
View::share('title', strval(trans('firefly.administration')));
View::share('mainTitleIcon', 'fa-hand-spock-o');
return $next($request);
}
);
}
/**
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function create()
{
$subTitle = trans('firefly.create_new_link_type');
$subTitleIcon = 'fa-link';
// put previous url in session if not redirect from store (not "create another").
if (session('link_types.create.fromStore') !== true) {
$this->rememberPreviousUri('link_types.create.uri');
}
return view('admin.link.create', compact('subTitle', 'subTitleIcon'));
}
/**
* @param Request $request
* @param LinkTypeRepositoryInterface $repository
* @param LinkType $linkType
*
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View
*/
public function delete(Request $request, LinkTypeRepositoryInterface $repository, LinkType $linkType)
{
if (!$linkType->editable) {
$request->session()->flash('error', strval(trans('firefly.cannot_edit_link_type', ['name' => $linkType->name])));
return redirect(route('admin.links.index'));
}
$subTitle = trans('firefly.delete_link_type', ['name' => $linkType->name]);
$otherTypes = $repository->get();
$count = $repository->countJournals($linkType);
$moveTo = [];
$moveTo[0] = trans('firefly.do_not_save_connection');
/** @var LinkType $otherType */
foreach ($otherTypes as $otherType) {
if ($otherType->id !== $linkType->id) {
$moveTo[$otherType->id] = sprintf('%s (%s / %s)', $otherType->name, $otherType->inward, $otherType->outward);
}
}
// put previous url in session
$this->rememberPreviousUri('link_types.delete.uri');
return view('admin.link.delete', compact('linkType', 'subTitle', 'moveTo', 'count'));
}
/**
* @param Request $request
* @param LinkTypeRepositoryInterface $repository
* @param LinkType $linkType
*
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function destroy(Request $request, LinkTypeRepositoryInterface $repository, LinkType $linkType)
{
$name = $linkType->name;
$moveTo = $repository->find(intval($request->get('move_link_type_before_delete')));
$repository->destroy($linkType, $moveTo);
$request->session()->flash('success', strval(trans('firefly.deleted_link_type', ['name' => $name])));
Preferences::mark();
return redirect($this->getPreviousUri('link_types.delete.uri'));
}
/**
* @param Request $request
* @param LinkType $linkType
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\View\View
*/
public function edit(Request $request, LinkType $linkType)
{
if (!$linkType->editable) {
$request->session()->flash('error', strval(trans('firefly.cannot_edit_link_type', ['name' => $linkType->name])));
return redirect(route('admin.links.index'));
}
$subTitle = trans('firefly.edit_link_type', ['name' => $linkType->name]);
$subTitleIcon = 'fa-link';
// put previous url in session if not redirect from store (not "return_to_edit").
if (session('link_types.edit.fromUpdate') !== true) {
$this->rememberPreviousUri('link_types.edit.uri');
}
$request->session()->forget('link_types.edit.fromUpdate');
return view('admin.link.edit', compact('subTitle', 'subTitleIcon', 'linkType'));
}
/**
* @param LinkTypeRepositoryInterface $repository
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function index(LinkTypeRepositoryInterface $repository)
{
$subTitle = trans('firefly.journal_link_configuration');
$subTitleIcon = 'fa-link';
$linkTypes = $repository->get();
$linkTypes->each(
function (LinkType $linkType) use ($repository) {
$linkType->journalCount = $repository->countJournals($linkType);
}
);
return view('admin.link.index', compact('subTitle', 'subTitleIcon', 'linkTypes'));
}
/**
* @param LinkType $linkType
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function show(LinkType $linkType)
{
$subTitle = trans('firefly.overview_for_link', ['name' => $linkType->name]);
$subTitleIcon = 'fa-link';
$links = $linkType->transactionJournalLinks()->get();
return view('admin.link.show', compact('subTitle', 'subTitleIcon', 'linkType', 'links'));
}
/**
* @param LinkTypeFormRequest $request
* @param LinkTypeRepositoryInterface $repository
*
* @return $this|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function store(LinkTypeFormRequest $request, LinkTypeRepositoryInterface $repository)
{
$data = [
'name' => $request->string('name'),
'inward' => $request->string('inward'),
'outward' => $request->string('outward'),
];
$linkType = $repository->store($data);
$request->session()->flash('success', strval(trans('firefly.stored_new_link_type', ['name' => $linkType->name])));
if (intval($request->get('create_another')) === 1) {
// set value so create routine will not overwrite URL:
$request->session()->put('link_types.create.fromStore', true);
return redirect(route('link_types.create', [$request->input('what')]))->withInput();
}
// redirect to previous URL.
return redirect($this->getPreviousUri('link_types.create.uri'));
}
public function update(LinkTypeFormRequest $request, LinkTypeRepositoryInterface $repository, LinkType $linkType)
{
if (!$linkType->editable) {
$request->session()->flash('error', strval(trans('firefly.cannot_edit_link_type', ['name' => $linkType->name])));
return redirect(route('admin.links.index'));
}
$data = [
'name' => $request->string('name'),
'inward' => $request->string('inward'),
'outward' => $request->string('outward'),
];
$repository->update($linkType, $data);
$request->session()->flash('success', strval(trans('firefly.updated_link_type', ['name' => $linkType->name])));
Preferences::mark();
if (intval($request->get('return_to_edit')) === 1) {
// set value so edit routine will not overwrite URL:
$request->session()->put('link_types.edit.fromUpdate', true);
return redirect(route('admin.links.edit', [$linkType->id]))->withInput(['return_to_edit' => 1]);
}
// redirect to previous URL.
return redirect($this->getPreviousUri('link_types.edit.uri'));
}
}

View File

@ -13,6 +13,7 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers; namespace FireflyIII\Http\Controllers;
use Amount;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Helpers\Collector\JournalCollectorInterface;
use FireflyIII\Http\Requests\BillFormRequest; use FireflyIII\Http\Requests\BillFormRequest;
@ -129,6 +130,11 @@ class BillController extends Controller
if (session('bills.edit.fromUpdate') !== true) { if (session('bills.edit.fromUpdate') !== true) {
$this->rememberPreviousUri('bills.edit.uri'); $this->rememberPreviousUri('bills.edit.uri');
} }
$currency = Amount::getDefaultCurrency();
$bill->amount_min = round($bill->amount_min, $currency->decimal_places);
$bill->amount_max = round($bill->amount_max, $currency->decimal_places);
$request->session()->forget('bills.edit.fromUpdate'); $request->session()->forget('bills.edit.fromUpdate');
$request->session()->flash('gaEventCategory', 'bills'); $request->session()->flash('gaEventCategory', 'bills');
$request->session()->flash('gaEventAction', 'edit'); $request->session()->flash('gaEventAction', 'edit');

View File

@ -75,11 +75,9 @@ class BudgetController extends Controller
*/ */
public function amount(Request $request, Budget $budget) public function amount(Request $request, Budget $budget)
{ {
$amount = intval($request->get('amount')); $amount = intval($request->get('amount'));
/** @var Carbon $start */ $start = Carbon::createFromFormat('Y-m-d', $request->get('start'));
$start = session('start', Carbon::now()->startOfMonth()); $end = Carbon::createFromFormat('Y-m-d', $request->get('end'));
/** @var Carbon $end */
$end = session('end', Carbon::now()->endOfMonth());
$budgetLimit = $this->repository->updateLimitAmount($budget, $start, $end, $amount); $budgetLimit = $this->repository->updateLimitAmount($budget, $start, $end, $amount);
if ($amount === 0) { if ($amount === 0) {
$budgetLimit = null; $budgetLimit = null;
@ -243,7 +241,7 @@ class BudgetController extends Controller
compact( compact(
'available', 'currentMonth', 'next', 'nextText', 'prev', 'prevText', 'available', 'currentMonth', 'next', 'nextText', 'prev', 'prevText',
'periodStart', 'periodEnd', 'budgetInformation', 'inactive', 'budgets', 'periodStart', 'periodEnd', 'budgetInformation', 'inactive', 'budgets',
'spent', 'budgeted', 'previousLoop', 'nextLoop', 'start' 'spent', 'budgeted', 'previousLoop', 'nextLoop', 'start', 'end'
) )
); );
} }
@ -313,15 +311,15 @@ class BudgetController extends Controller
*/ */
public function postUpdateIncome(BudgetIncomeRequest $request) public function postUpdateIncome(BudgetIncomeRequest $request)
{ {
$start = session('start', new Carbon); $start = Carbon::createFromFormat('Y-m-d', $request->string('start'));
$end = session('end', new Carbon); $end = Carbon::createFromFormat('Y-m-d', $request->string('end'));
$defaultCurrency = Amount::getDefaultCurrency(); $defaultCurrency = Amount::getDefaultCurrency();
$amount = $request->get('amount'); $amount = $request->get('amount');
$this->repository->setAvailableBudget($defaultCurrency, $start, $end, $amount); $this->repository->setAvailableBudget($defaultCurrency, $start, $end, $amount);
Preferences::mark(); Preferences::mark();
return redirect(route('budgets.index')); return redirect(route('budgets.index', [$start->format('Y-m-d')]));
} }
/** /**
@ -443,15 +441,16 @@ class BudgetController extends Controller
} }
/** /**
* @return View * @param Carbon $start
* @param Carbon $end
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/ */
public function updateIncome() public function updateIncome(Carbon $start, Carbon $end)
{ {
$start = session('start', new Carbon);
$end = session('end', new Carbon);
$defaultCurrency = Amount::getDefaultCurrency(); $defaultCurrency = Amount::getDefaultCurrency();
$available = $this->repository->getAvailableBudget($defaultCurrency, $start, $end); $available = $this->repository->getAvailableBudget($defaultCurrency, $start, $end);
$available = round($available, $defaultCurrency->decimal_places);
return view('budgets.income', compact('available', 'start', 'end')); return view('budgets.income', compact('available', 'start', 'end'));
} }

View File

@ -137,13 +137,14 @@ class ExportController extends Controller
public function postIndex(ExportFormRequest $request, AccountRepositoryInterface $repository, ExportJobRepositoryInterface $jobs) public function postIndex(ExportFormRequest $request, AccountRepositoryInterface $repository, ExportJobRepositoryInterface $jobs)
{ {
$job = $jobs->findByKey($request->get('job')); $job = $jobs->findByKey($request->get('job'));
$accounts = $request->get('accounts') ?? [];
$settings = [ $settings = [
'accounts' => $repository->getAccountsById($request->get('accounts')), 'accounts' => $repository->getAccountsById($accounts),
'startDate' => new Carbon($request->get('export_start_range')), 'startDate' => new Carbon($request->get('export_start_range')),
'endDate' => new Carbon($request->get('export_end_range')), 'endDate' => new Carbon($request->get('export_end_range')),
'exportFormat' => $request->get('exportFormat'), 'exportFormat' => $request->get('exportFormat'),
'includeAttachments' => intval($request->get('include_attachments')) === 1, 'includeAttachments' => $request->boolean('include_attachments'),
'includeOldUploads' => intval($request->get('include_old_uploads')) === 1, 'includeOldUploads' => $request->boolean('include_old_uploads'),
'job' => $job, 'job' => $job,
]; ];
@ -159,12 +160,14 @@ class ExportController extends Controller
$jobs->changeStatus($job, 'export_status_collecting_journals'); $jobs->changeStatus($job, 'export_status_collecting_journals');
$processor->collectJournals(); $processor->collectJournals();
$jobs->changeStatus($job, 'export_status_collected_journals'); $jobs->changeStatus($job, 'export_status_collected_journals');
/* /*
* Transform to exportable entries: * Transform to exportable entries:
*/ */
$jobs->changeStatus($job, 'export_status_converting_to_export_format'); $jobs->changeStatus($job, 'export_status_converting_to_export_format');
$processor->convertJournals(); $processor->convertJournals();
$jobs->changeStatus($job, 'export_status_converted_to_export_format'); $jobs->changeStatus($job, 'export_status_converted_to_export_format');
/* /*
* Transform to (temporary) file: * Transform to (temporary) file:
*/ */
@ -180,6 +183,7 @@ class ExportController extends Controller
$jobs->changeStatus($job, 'export_status_collected_attachments'); $jobs->changeStatus($job, 'export_status_collected_attachments');
} }
/* /*
* Collect old uploads * Collect old uploads
*/ */

View File

@ -82,6 +82,14 @@ class HomeController extends Controller
*/ */
public function displayError() public function displayError()
{ {
Log::debug('This is a test message at the DEBUG level.');
Log::info('This is a test message at the INFO level.');
Log::notice('This is a test message at the NOTICE level.');
Log::warning('This is a test message at the WARNING level.');
Log::error('This is a test message at the ERROR level.');
Log::critical('This is a test message at the CRITICAL level.');
Log::alert('This is a test message at the ALERT level.');
Log::emergency('This is a test message at the EMERGENCY level.');
throw new FireflyException('A very simple test error.'); throw new FireflyException('A very simple test error.');
} }

View File

@ -13,18 +13,126 @@ namespace FireflyIII\Http\Controllers\Import;
use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Support\Import\Information\InformationInterface;
use FireflyIII\Support\Import\Prerequisites\PrerequisitesInterface; use FireflyIII\Support\Import\Prerequisites\PrerequisitesInterface;
use Illuminate\Http\Request;
use Log;
use Session;
class BankController extends Controller class BankController extends Controller
{ {
public function postPrerequisites() /**
* This method must ask the user all parameters necessary to start importing data. This may not be enough
* to finish the import itself (ie. mapping) but it should be enough to begin: accounts to import from,
* accounts to import into, data ranges, etc.
*
* @param string $bank
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\View\View
*/
public function form(string $bank)
{ {
$class = config(sprintf('firefly.import_pre.%s', $bank));
/** @var PrerequisitesInterface $object */
$object = app($class);
$object->setUser(auth()->user());
if ($object->hasPrerequisites()) {
return redirect(route('import.bank.prerequisites', [$bank]));
}
$class = config(sprintf('firefly.import_info.%s', $bank));
/** @var InformationInterface $object */
$object = app($class);
$object->setUser(auth()->user());
$remoteAccounts = $object->getAccounts();
return view('import.bank.form', compact('remoteAccounts', 'bank'));
} }
/** /**
* With the information given in the submitted form Firefly III will call upon the bank's classes to return transaction
* information as requested. The user will be able to map unknown data and continue. Or maybe, it's put into some kind of
* fake CSV file and forwarded to the import routine.
*
* @param Request $request
* @param string $bank
*
* @return \Illuminate\Http\RedirectResponse|null
*/
public function postForm(Request $request, string $bank)
{
$class = config(sprintf('firefly.import_pre.%s', $bank));
/** @var PrerequisitesInterface $object */
$object = app($class);
$object->setUser(auth()->user());
if ($object->hasPrerequisites()) {
return redirect(route('import.bank.prerequisites', [$bank]));
}
$remoteAccounts = $request->get('do_import');
if (!is_array($remoteAccounts) || count($remoteAccounts) === 0) {
Session::flash('error', 'Must select accounts');
return redirect(route('import.bank.form', [$bank]));
}
$remoteAccounts = array_keys($remoteAccounts);
$class = config(sprintf('firefly.import_pre.%s', $bank));
// get import file
// get import config
}
/**
* This method processes the prerequisites the user has entered in the previous step.
*
* Whatever storePrerequisites does, it should make sure that the system is ready to continue immediately. So
* no extra calls or stuff, except maybe to open a session
*
* @see PrerequisitesInterface::storePrerequisites
*
* @param Request $request
* @param string $bank
*
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function postPrerequisites(Request $request, string $bank)
{
Log::debug(sprintf('Now in postPrerequisites for %s', $bank));
$class = config(sprintf('firefly.import_pre.%s', $bank));
/** @var PrerequisitesInterface $object */
$object = app($class);
$object->setUser(auth()->user());
if (!$object->hasPrerequisites()) {
Log::debug(sprintf('No more prerequisites for %s, move to form.', $bank));
return redirect(route('import.bank.form', [$bank]));
}
Log::debug('Going to store entered preprerequisites.');
// store post data
$result = $object->storePrerequisites($request);
if ($result->count() > 0) {
Session::flash('error', $result->first());
return redirect(route('import.bank.prerequisites', [$bank]));
}
return redirect(route('import.bank.form', [$bank]));
}
/**
* This method shows you, if necessary, a form that allows you to enter any required values, such as API keys,
* login passwords or other values.
*
* @param string $bank * @param string $bank
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\View\View
*/ */
public function prerequisites(string $bank) public function prerequisites(string $bank)
{ {
@ -40,10 +148,7 @@ class BankController extends Controller
return view($view, $parameters); return view($view, $parameters);
} }
if (!$object->hasPrerequisites()) { return redirect(route('import.bank.form', [$bank]));
echo 'redirect to import form.';
}
} }
} }

View File

@ -0,0 +1,171 @@
<?php
/**
* AutoCompleteController.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Json;
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Support\CacheProperties;
use Response;
/**
* Class AutoCompleteController
*
* @package FireflyIII\Http\Controllers\Json
*/
class AutoCompleteController extends Controller
{
/**
* Returns a JSON list of all accounts.
*
* @param AccountRepositoryInterface $repository
*
* @return \Illuminate\Http\JsonResponse
*
*/
public function allAccounts(AccountRepositoryInterface $repository)
{
$return = array_unique(
$repository->getAccountsByType(
[AccountType::REVENUE, AccountType::EXPENSE, AccountType::BENEFICIARY, AccountType::DEFAULT, AccountType::ASSET]
)->pluck('name')->toArray()
);
sort($return);
return Response::json($return);
}
/**
* @param JournalCollectorInterface $collector
*
* @return \Illuminate\Http\JsonResponse
*/
public function allTransactionJournals(JournalCollectorInterface $collector)
{
$collector->setLimit(250)->setPage(1);
$return = array_unique($collector->getJournals()->pluck('description')->toArray());
sort($return);
return Response::json($return);
}
/**
* Returns a JSON list of all beneficiaries.
*
* @param AccountRepositoryInterface $repository
*
* @return \Illuminate\Http\JsonResponse
*
*/
public function expenseAccounts(AccountRepositoryInterface $repository)
{
$set = $repository->getAccountsByType([AccountType::EXPENSE, AccountType::BENEFICIARY]);
$filtered = $set->filter(
function (Account $account) {
if ($account->active) {
return $account;
}
return false;
}
);
$return = array_unique($filtered->pluck('name')->toArray());
sort($return);
return Response::json($return);
}
/**
* @param JournalCollectorInterface $collector
*
* @param TransactionJournal $except
*
* @return \Illuminate\Http\JsonResponse|mixed
*/
public function journalsWithId(JournalCollectorInterface $collector, TransactionJournal $except)
{
$cache = new CacheProperties;
$cache->addProperty('recent-journals-id');
if ($cache->has()) {
return $cache->get(); // @codeCoverageIgnore
}
$collector->setLimit(400)->setPage(1);
$set = $collector->getJournals()->pluck('description', 'journal_id')->toArray();
$return = [];
foreach ($set as $id => $description) {
$id = intval($id);
if ($id !== $except->id) {
$return[] = [
'id' => $id,
'name' => $id . ': ' . $description,
];
}
}
$cache->store($return);
return Response::json($return);
}
/**
* @param AccountRepositoryInterface $repository
*
* @return \Illuminate\Http\JsonResponse
*
*/
public function revenueAccounts(AccountRepositoryInterface $repository)
{
$set = $repository->getAccountsByType([AccountType::REVENUE]);
$filtered = $set->filter(
function (Account $account) {
if ($account->active) {
return $account;
}
return false;
}
);
$return = array_unique($filtered->pluck('name')->toArray());
sort($return);
return Response::json($return);
}
/**
* @param JournalCollectorInterface $collector
* @param string $what
*
* @return \Illuminate\Http\JsonResponse
*/
public function transactionJournals(JournalCollectorInterface $collector, string $what)
{
$type = config('firefly.transactionTypesByWhat.' . $what);
$types = [$type];
$collector->setTypes($types)->setLimit(250)->setPage(1);
$return = array_unique($collector->getJournals()->pluck('description')->toArray());
sort($return);
return Response::json($return);
}
}

View File

@ -16,9 +16,7 @@ namespace FireflyIII\Http\Controllers;
use Amount; use Amount;
use Carbon\Carbon; use Carbon\Carbon;
use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Helpers\Collector\JournalCollectorInterface;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\TransactionType; use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Bill\BillRepositoryInterface; use FireflyIII\Repositories\Bill\BillRepositoryInterface;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\Category\CategoryRepositoryInterface; use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
@ -62,43 +60,6 @@ class JsonController extends Controller
return Response::json(['html' => $view]); return Response::json(['html' => $view]);
} }
/**
* Returns a JSON list of all accounts.
*
* @param AccountRepositoryInterface $repository
*
* @return \Illuminate\Http\JsonResponse
*
*/
public function allAccounts(AccountRepositoryInterface $repository)
{
$return = array_unique(
$repository->getAccountsByType(
[AccountType::REVENUE, AccountType::EXPENSE, AccountType::BENEFICIARY, AccountType::DEFAULT, AccountType::ASSET]
)->pluck('name')->toArray()
);
sort($return);
return Response::json($return);
}
/**
* @param JournalCollectorInterface $collector
*
* @return \Illuminate\Http\JsonResponse
*/
public function allTransactionJournals(JournalCollectorInterface $collector)
{
$collector->setLimit(100)->setPage(1);
$return = array_unique($collector->getJournals()->pluck('description')->toArray());
sort($return);
return Response::json($return);
}
/** /**
* @param BillRepositoryInterface $repository * @param BillRepositoryInterface $repository
* *
@ -235,36 +196,6 @@ class JsonController extends Controller
return Response::json($return); return Response::json($return);
} }
/**
* Returns a JSON list of all beneficiaries.
*
* @param AccountRepositoryInterface $repository
*
* @return \Illuminate\Http\JsonResponse
*
*/
public function expenseAccounts(AccountRepositoryInterface $repository)
{
$return = array_unique($repository->getAccountsByType([AccountType::EXPENSE, AccountType::BENEFICIARY])->pluck('name')->toArray());
sort($return);
return Response::json($return);
}
/**
* @param AccountRepositoryInterface $repository
*
* @return \Illuminate\Http\JsonResponse
*
*/
public function revenueAccounts(AccountRepositoryInterface $repository)
{
$return = array_unique($repository->getAccountsByType([AccountType::REVENUE])->pluck('name')->toArray());
sort($return);
return Response::json($return);
}
/** /**
* Returns a JSON list of all beneficiaries. * Returns a JSON list of all beneficiaries.
* *
@ -281,26 +212,6 @@ class JsonController extends Controller
} }
/**
* @param JournalCollectorInterface $collector
* @param string $what
*
* @return \Illuminate\Http\JsonResponse
*/
public function transactionJournals(JournalCollectorInterface $collector, string $what)
{
$type = config('firefly.transactionTypesByWhat.' . $what);
$types = [$type];
$collector->setTypes($types)->setLimit(100)->setPage(1);
$return = array_unique($collector->getJournals()->pluck('description')->toArray());
sort($return);
return Response::json($return);
}
/** /**
* @param JournalRepositoryInterface $repository * @param JournalRepositoryInterface $repository
* *
@ -329,6 +240,7 @@ class JsonController extends Controller
$triggers[$key] = trans('firefly.rule_trigger_' . $key . '_choice'); $triggers[$key] = trans('firefly.rule_trigger_' . $key . '_choice');
} }
} }
asort($triggers);
$view = view('rules.partials.trigger', compact('triggers', 'count'))->render(); $view = view('rules.partials.trigger', compact('triggers', 'count'))->render();

View File

@ -166,10 +166,13 @@ class RuleController extends Controller
*/ */
public function edit(Request $request, RuleRepositoryInterface $repository, Rule $rule) public function edit(Request $request, RuleRepositoryInterface $repository, Rule $rule)
{ {
$oldTriggers = $this->getCurrentTriggers($rule); /** @var RuleGroupRepositoryInterface $ruleGroupRepository */
$triggerCount = count($oldTriggers); $ruleGroupRepository = app(RuleGroupRepositoryInterface::class);
$oldActions = $this->getCurrentActions($rule); $oldTriggers = $this->getCurrentTriggers($rule);
$actionCount = count($oldActions); $triggerCount = count($oldTriggers);
$oldActions = $this->getCurrentActions($rule);
$actionCount = count($oldActions);
$ruleGroups = ExpandedForm::makeSelectList($ruleGroupRepository->get());
// has old input? // has old input?
if ($request->old()) { if ($request->old()) {
@ -191,7 +194,12 @@ class RuleController extends Controller
Session::flash('gaEventCategory', 'rules'); Session::flash('gaEventCategory', 'rules');
Session::flash('gaEventAction', 'edit-rule'); Session::flash('gaEventAction', 'edit-rule');
return view('rules.rule.edit', compact('rule', 'subTitle', 'primaryTrigger', 'oldTriggers', 'oldActions', 'triggerCount', 'actionCount')); return view(
'rules.rule.edit', compact(
'rule', 'subTitle',
'primaryTrigger', 'oldTriggers', 'oldActions', 'triggerCount', 'actionCount', 'ruleGroups'
)
);
} }
/** /**
@ -523,7 +531,7 @@ class RuleController extends Controller
$actions[] = view( $actions[] = view(
'rules.partials.action', 'rules.partials.action',
[ [
'oldTrigger' => $entry->action_type, 'oldAction' => $entry->action_type,
'oldValue' => $entry->action_value, 'oldValue' => $entry->action_value,
'oldChecked' => $entry->stop_processing, 'oldChecked' => $entry->stop_processing,
'count' => $count, 'count' => $count,

View File

@ -43,9 +43,6 @@ use View;
class TagController extends Controller class TagController extends Controller
{ {
/** @var array */
public $tagOptions = [];
/** @var TagRepositoryInterface */ /** @var TagRepositoryInterface */
protected $repository; protected $repository;
@ -60,17 +57,8 @@ class TagController extends Controller
$this->middleware( $this->middleware(
function ($request, $next) { function ($request, $next) {
$this->repository = app(TagRepositoryInterface::class); $this->repository = app(TagRepositoryInterface::class);
$this->tagOptions = [
'nothing' => trans('firefly.regular_tag'),
'balancingAct' => trans('firefly.balancing_act'),
'advancePayment' => trans('firefly.advance_payment'),
];
View::share('title', strval(trans('firefly.tags'))); View::share('title', strval(trans('firefly.tags')));
View::share('mainTitleIcon', 'fa-tags'); View::share('mainTitleIcon', 'fa-tags');
View::share('tagOptions', $this->tagOptions);
return $next($request); return $next($request);
} }
@ -168,41 +156,22 @@ class TagController extends Controller
*/ */
public function index(TagRepositoryInterface $repository) public function index(TagRepositoryInterface $repository)
{ {
$title = 'Tags';
$mainTitleIcon = 'fa-tags';
$types = ['nothing', 'balancingAct', 'advancePayment'];
$hasTypes = 0; // which types of tag the user actually has.
$counts = []; // how many of each type?
$count = $repository->count();
// loop each types and get the tags, group them by year. // collect tags by year:
$collection = []; /** @var Carbon $start */
foreach ($types as $type) { $start = clone(session('first'));
$now = new Carbon;
$clouds = [];
$clouds['no-date'] = $repository->tagCloud(null);
while ($now > $start) {
$year = $now->year;
$clouds[$year] = $repository->tagCloud($year);
/** @var Collection $tags */ $now->subYear();
$tags = $repository->getByType($type);
$tags = $tags->sortBy(
function (Tag $tag) {
$date = !is_null($tag->date) ? $tag->date->format('Ymd') : '000000';
return strtolower($date . $tag->tag);
}
);
if ($tags->count() > 0) {
$hasTypes++;
}
$counts[$type] = $tags->count();
/** @var Tag $tag */
foreach ($tags as $tag) {
$year = is_null($tag->date) ? trans('firefly.no_year') : $tag->date->year;
$monthFormatted = is_null($tag->date) ? trans('firefly.no_month') : $tag->date->formatLocalized($this->monthFormat);
$collection[$type][$year][$monthFormatted][] = $tag;
}
} }
$count = $repository->count();
return view('tags.index', compact('title', 'mainTitleIcon', 'counts', 'hasTypes', 'types', 'collection', 'count')); return view('tags.index', compact('clouds', 'count'));
} }
/** /**
@ -235,6 +204,7 @@ class TagController extends Controller
$start = $repository->firstUseDate($tag); $start = $repository->firstUseDate($tag);
$end = new Carbon; $end = new Carbon;
$sum = $repository->sumOfTag($tag, null, null); $sum = $repository->sumOfTag($tag, null, null);
$result = $repository->resultOfTag($tag, null, null);
$path = route('tags.show', [$tag->id, 'all']); $path = route('tags.show', [$tag->id, 'all']);
} }
@ -249,6 +219,7 @@ class TagController extends Controller
); );
$periods = $this->getPeriodOverview($tag); $periods = $this->getPeriodOverview($tag);
$sum = $repository->sumOfTag($tag, $start, $end); $sum = $repository->sumOfTag($tag, $start, $end);
$result = $repository->resultOfTag($tag, $start, $end);
$path = route('tags.show', [$tag->id, $moment]); $path = route('tags.show', [$tag->id, $moment]);
} }
@ -257,6 +228,8 @@ class TagController extends Controller
$start = clone session('start', Navigation::startOfPeriod(new Carbon, $range)); $start = clone session('start', Navigation::startOfPeriod(new Carbon, $range));
$end = clone session('end', Navigation::endOfPeriod(new Carbon, $range)); $end = clone session('end', Navigation::endOfPeriod(new Carbon, $range));
$periods = $this->getPeriodOverview($tag); $periods = $this->getPeriodOverview($tag);
$sum = $repository->sumOfTag($tag, $start, $end);
$result = $repository->resultOfTag($tag, $start, $end);
$subTitle = trans( $subTitle = trans(
'firefly.journals_in_period_for_tag', 'firefly.journals_in_period_for_tag',
['tag' => $tag->tag, 'start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)] ['tag' => $tag->tag, 'start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)]
@ -271,10 +244,9 @@ class TagController extends Controller
$journals->setPath($path); $journals->setPath($path);
return view('tags.show', compact('apiKey', 'tag', 'periods', 'subTitle', 'subTitleIcon', 'journals', 'sum', 'start', 'end', 'moment')); return view('tags.show', compact('apiKey', 'tag', 'result', 'periods', 'subTitle', 'subTitleIcon', 'journals', 'sum', 'start', 'end', 'moment'));
} }
/** /**
* @param TagFormRequest $request * @param TagFormRequest $request
* *
@ -371,4 +343,6 @@ class TagController extends Controller
return $collection; return $collection;
} }
} }

View File

@ -0,0 +1,143 @@
<?php
/**
* LinkController.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Transaction;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Requests\JournalLinkRequest;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionJournalLink;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Repositories\LinkType\LinkTypeRepositoryInterface;
use Log;
use Preferences;
use Session;
use URL;
use View;
/**
* Class LinkController
*
* @package FireflyIII\Http\Controllers\Transaction
*/
class LinkController extends Controller
{
/**
*
*/
public function __construct()
{
parent::__construct();
// some useful repositories:
$this->middleware(
function ($request, $next) {
View::share('title', trans('firefly.transactions'));
View::share('mainTitleIcon', 'fa-repeat');
return $next($request);
}
);
}
/**
* @param TransactionJournalLink $link
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function delete(TransactionJournalLink $link)
{
$subTitleIcon = 'fa-link';
$subTitle = trans('breadcrumbs.delete_journal_link');
$this->rememberPreviousUri('journal_links.delete.uri');
return view('transactions.links.delete', compact('link', 'subTitle', 'subTitleIcon'));
}
/**
* @param LinkTypeRepositoryInterface $repository
* @param TransactionJournalLink $link
*
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function destroy(LinkTypeRepositoryInterface $repository, TransactionJournalLink $link)
{
$repository->destroyLink($link);
Session::flash('success', strval(trans('firefly.deleted_link')));
Preferences::mark();
return redirect(strval(session('journal_links.delete.uri')));
}
/**
* @param JournalLinkRequest $request
* @param LinkTypeRepositoryInterface $repository
* @param JournalRepositoryInterface $journalRepository
* @param TransactionJournal $journal
*
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function store(
JournalLinkRequest $request, LinkTypeRepositoryInterface $repository, JournalRepositoryInterface $journalRepository, TransactionJournal $journal
) {
$linkInfo = $request->getLinkInfo();
$linkType = $repository->find($linkInfo['link_type_id']);
$other = $journalRepository->find($linkInfo['transaction_journal_id']);
$alreadyLinked = $repository->findLink($journal, $other);
if ($alreadyLinked) {
Session::flash('error', trans('firefly.journals_error_linked'));
return redirect(route('transactions.show', [$journal->id]));
}
Log::debug(sprintf('Journal is %d, opposing is %d', $journal->id, $other->id));
$journalLink = new TransactionJournalLink;
$journalLink->linkType()->associate($linkType);
if ($linkInfo['direction'] === 'inward') {
Log::debug(sprintf('Link type is inwards ("%s"), so %d is source and %d is destination.', $linkType->inward, $other->id, $journal->id));
$journalLink->source()->associate($other);
$journalLink->destination()->associate($journal);
}
if ($linkInfo['direction'] === 'outward') {
Log::debug(sprintf('Link type is inwards ("%s"), so %d is source and %d is destination.', $linkType->outward, $journal->id, $other->id));
$journalLink->source()->associate($journal);
$journalLink->destination()->associate($other);
}
$journalLink->comment = $linkInfo['comments'];
$journalLink->save();
Session::flash('success', trans('firefly.journals_linked'));
return redirect(route('transactions.show', [$journal->id]));
}
/**
* @param LinkTypeRepositoryInterface $repository
* @param TransactionJournalLink $link
*
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function switch(LinkTypeRepositoryInterface $repository, TransactionJournalLink $link)
{
$repository->switchLink($link);
return redirect(URL::previous());
}
}

View File

@ -14,12 +14,14 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Transaction; namespace FireflyIII\Http\Controllers\Transaction;
use Carbon\Carbon;
use ExpandedForm; use ExpandedForm;
use FireflyIII\Events\StoredTransactionJournal; use FireflyIII\Events\StoredTransactionJournal;
use FireflyIII\Events\UpdatedTransactionJournal; use FireflyIII\Events\UpdatedTransactionJournal;
use FireflyIII\Helpers\Attachments\AttachmentHelperInterface; use FireflyIII\Helpers\Attachments\AttachmentHelperInterface;
use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Requests\JournalFormRequest; use FireflyIII\Http\Requests\JournalFormRequest;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType; use FireflyIII\Models\AccountType;
use FireflyIII\Models\Transaction; use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournal;
@ -115,7 +117,7 @@ class SingleController extends Controller
'foreign_amount' => $foreignAmount, 'foreign_amount' => $foreignAmount,
'native_amount' => $foreignAmount, 'native_amount' => $foreignAmount,
'amount_currency_id_amount' => $transaction->foreign_currency_id ?? 0, 'amount_currency_id_amount' => $transaction->foreign_currency_id ?? 0,
'date' => $journal->date->format('Y-m-d'), 'date' => (new Carbon())->format('Y-m-d'),
'budget_id' => $budgetId, 'budget_id' => $budgetId,
'category' => $categoryName, 'category' => $categoryName,
'tags' => $tags, 'tags' => $tags,
@ -142,7 +144,7 @@ class SingleController extends Controller
{ {
$what = strtolower($what); $what = strtolower($what);
$uploadSize = min(Steam::phpBytes(ini_get('upload_max_filesize')), Steam::phpBytes(ini_get('post_max_size'))); $uploadSize = min(Steam::phpBytes(ini_get('upload_max_filesize')), Steam::phpBytes(ini_get('post_max_size')));
$assetAccounts = ExpandedForm::makeSelectList($this->accounts->getActiveAccountsByType([AccountType::DEFAULT, AccountType::ASSET])); $assetAccounts = $this->groupedActiveAccountList();
$budgets = ExpandedForm::makeSelectListWithEmpty($this->budgets->getActiveBudgets()); $budgets = ExpandedForm::makeSelectListWithEmpty($this->budgets->getActiveBudgets());
$piggyBanks = $this->piggyBanks->getPiggyBanksWithAmount(); $piggyBanks = $this->piggyBanks->getPiggyBanksWithAmount();
$piggies = ExpandedForm::makeSelectListWithEmpty($piggyBanks); $piggies = ExpandedForm::makeSelectListWithEmpty($piggyBanks);
@ -238,7 +240,7 @@ class SingleController extends Controller
} }
$what = strtolower($journal->transactionTypeStr()); $what = strtolower($journal->transactionTypeStr());
$assetAccounts = ExpandedForm::makeSelectList($this->accounts->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET])); $assetAccounts = $this->groupedAccountList();
$budgetList = ExpandedForm::makeSelectListWithEmpty($this->budgets->getBudgets()); $budgetList = ExpandedForm::makeSelectListWithEmpty($this->budgets->getBudgets());
// view related code // view related code
@ -408,6 +410,46 @@ class SingleController extends Controller
return redirect($this->getPreviousUri('transactions.edit.uri')); return redirect($this->getPreviousUri('transactions.edit.uri'));
} }
/**
* @return array
*/
private function groupedAccountList(): array
{
$accounts = $this->accounts->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]);
$return = [];
/** @var Account $account */
foreach ($accounts as $account) {
$type = $account->getMeta('accountRole');
if (strlen($type) === 0) {
$type = 'no_account_type';
}
$key = strval(trans('firefly.opt_group_' . $type));
$return[$key][$account->id] = $account->name;
}
return $return;
}
/**
* @return array
*/
private function groupedActiveAccountList(): array
{
$accounts = $this->accounts->getActiveAccountsByType([AccountType::DEFAULT, AccountType::ASSET]);
$return = [];
/** @var Account $account */
foreach ($accounts as $account) {
$type = $account->getMeta('accountRole');
if (strlen($type) === 0) {
$type = 'no_account_type';
}
$key = strval(trans('firefly.opt_group_' . $type));
$return[$key][$account->id] = $account->name;
}
return $return;
}
/** /**
* @param TransactionJournal $journal * @param TransactionJournal $journal
* *

View File

@ -18,6 +18,8 @@ use ExpandedForm;
use FireflyIII\Events\UpdatedTransactionJournal; use FireflyIII\Events\UpdatedTransactionJournal;
use FireflyIII\Helpers\Attachments\AttachmentHelperInterface; use FireflyIII\Helpers\Attachments\AttachmentHelperInterface;
use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Requests\SplitJournalFormRequest;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType; use FireflyIII\Models\AccountType;
use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Account\AccountRepositoryInterface;
@ -94,13 +96,23 @@ class SplitController extends Controller
$uploadSize = min(Steam::phpBytes(ini_get('upload_max_filesize')), Steam::phpBytes(ini_get('post_max_size'))); $uploadSize = min(Steam::phpBytes(ini_get('upload_max_filesize')), Steam::phpBytes(ini_get('post_max_size')));
$currencies = $this->currencies->get(); $currencies = $this->currencies->get();
$assetAccounts = ExpandedForm::makeSelectList($this->accounts->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET])); $accountList = $this->accounts->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]);
$assetAccounts = ExpandedForm::makeSelectList($accountList);
$optionalFields = Preferences::get('transaction_journal_optional_fields', [])->data; $optionalFields = Preferences::get('transaction_journal_optional_fields', [])->data;
$budgets = ExpandedForm::makeSelectListWithEmpty($this->budgets->getActiveBudgets()); $budgets = ExpandedForm::makeSelectListWithEmpty($this->budgets->getActiveBudgets());
$preFilled = $this->arrayFromJournal($request, $journal); $preFilled = $this->arrayFromJournal($request, $journal);
$subTitle = trans('breadcrumbs.edit_journal', ['description' => $journal->description]); $subTitle = trans('breadcrumbs.edit_journal', ['description' => $journal->description]);
$subTitleIcon = 'fa-pencil'; $subTitleIcon = 'fa-pencil';
$accountArray = [];
// account array to display currency info:
/** @var Account $account */
foreach ($accountList as $account) {
$accountArray[$account->id] = $account;
$accountArray[$account->id]['currency_id'] = intval($account->getMeta('currency_id'));
}
Session::flash('gaEventCategory', 'transactions'); Session::flash('gaEventCategory', 'transactions');
Session::flash('gaEventAction', 'edit-split-' . $preFilled['what']); Session::flash('gaEventAction', 'edit-split-' . $preFilled['what']);
@ -115,25 +127,24 @@ class SplitController extends Controller
compact( compact(
'subTitleIcon', 'currencies', 'optionalFields', 'subTitleIcon', 'currencies', 'optionalFields',
'preFilled', 'subTitle', 'uploadSize', 'assetAccounts', 'preFilled', 'subTitle', 'uploadSize', 'assetAccounts',
'budgets', 'journal' 'budgets', 'journal', 'accountArray', 'previous'
) )
); );
} }
/** /**
* @param Request $request * @param SplitJournalFormRequest $request
* @param JournalRepositoryInterface $repository * @param JournalRepositoryInterface $repository
* @param TransactionJournal $journal * @param TransactionJournal $journal
* *
* @return $this|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector * @return $this|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/ */
public function update(Request $request, JournalRepositoryInterface $repository, TransactionJournal $journal) public function update(SplitJournalFormRequest $request, JournalRepositoryInterface $repository, TransactionJournal $journal)
{ {
if ($this->isOpeningBalance($journal)) { if ($this->isOpeningBalance($journal)) {
return $this->redirectToAccount($journal); return $this->redirectToAccount($journal);
} }
$data = $this->arrayFromInput($request); $data = $this->arrayFromInput($request);
$journal = $repository->updateSplitJournal($journal, $data); $journal = $repository->updateSplitJournal($journal, $data);
/** @var array $files */ /** @var array $files */
@ -167,11 +178,11 @@ class SplitController extends Controller
} }
/** /**
* @param Request $request * @param SplitJournalFormRequest $request
* *
* @return array * @return array
*/ */
private function arrayFromInput(Request $request): array private function arrayFromInput(SplitJournalFormRequest $request): array
{ {
$array = [ $array = [
'journal_description' => $request->get('journal_description'), 'journal_description' => $request->get('journal_description'),
@ -200,8 +211,8 @@ class SplitController extends Controller
} }
/** /**
* @param Request $request * @param SplitJournalFormRequest|Request $request
* @param TransactionJournal $journal * @param TransactionJournal $journal
* *
* @return array * @return array
*/ */
@ -234,6 +245,8 @@ class SplitController extends Controller
// transactions. // transactions.
'transactions' => $this->getTransactionDataFromJournal($journal), 'transactions' => $this->getTransactionDataFromJournal($journal),
]; ];
// update transactions array with old request data.
$array['transactions'] = $this->updateWithPrevious($array['transactions'], $request->old());
return $array; return $array;
} }
@ -282,11 +295,11 @@ class SplitController extends Controller
} }
/** /**
* @param Request $request * @param SplitJournalFormRequest|Request $request
* *
* @return array * @return array
*/ */
private function getTransactionDataFromRequest(Request $request): array private function getTransactionDataFromRequest(SplitJournalFormRequest $request): array
{ {
$return = []; $return = [];
$transactions = $request->get('transactions'); $transactions = $request->get('transactions');
@ -312,5 +325,36 @@ class SplitController extends Controller
return $return; return $return;
} }
/**
* @param $array
* @param $old
*
* @return array
*/
private function updateWithPrevious($array, $old): array
{
if (count($old) === 0 || !isset($old['transactions'])) {
return $array;
}
$old = $old['transactions'];
foreach ($old as $index => $row) {
if (isset($array[$index])) {
$array[$index] = array_merge($array[$index], $row);
continue;
}
// take some info from first transaction, that should at least exist.
$array[$index] = $row;
$array[$index]['transaction_currency_id'] = $array[0]['transaction_currency_id'];
$array[$index]['transaction_currency_code'] = $array[0]['transaction_currency_code'];
$array[$index]['transaction_currency_symbol'] = $array[0]['transaction_currency_symbol'];
$array[$index]['foreign_amount'] = round($array[0]['foreign_destination_amount'] ?? '0', 12);
$array[$index]['foreign_currency_id'] = $array[0]['foreign_currency_id'];
$array[$index]['foreign_currency_code'] = $array[0]['foreign_currency_code'];
$array[$index]['foreign_currency_symbol'] = $array[0]['foreign_currency_symbol'];
}
return $array;
}
} }

View File

@ -17,9 +17,11 @@ use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Helpers\Collector\JournalCollectorInterface;
use FireflyIII\Helpers\Filter\InternalTransferFilter; use FireflyIII\Helpers\Filter\InternalTransferFilter;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournal;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalTaskerInterface; use FireflyIII\Repositories\Journal\JournalTaskerInterface;
use FireflyIII\Repositories\LinkType\LinkTypeRepositoryInterface;
use FireflyIII\Support\CacheProperties; use FireflyIII\Support\CacheProperties;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@ -27,7 +29,6 @@ use Log;
use Navigation; use Navigation;
use Preferences; use Preferences;
use Response; use Response;
use Steam;
use View; use View;
/** /**
@ -150,23 +151,26 @@ class TransactionController extends Controller
} }
/** /**
* @param TransactionJournal $journal * @param TransactionJournal $journal
* @param JournalTaskerInterface $tasker * @param JournalTaskerInterface $tasker
*
* @param LinkTypeRepositoryInterface $linkTypeRepository
* *
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View
*/ */
public function show(TransactionJournal $journal, JournalTaskerInterface $tasker) public function show(TransactionJournal $journal, JournalTaskerInterface $tasker, LinkTypeRepositoryInterface $linkTypeRepository)
{ {
if ($this->isOpeningBalance($journal)) { if ($this->isOpeningBalance($journal)) {
return $this->redirectToAccount($journal); return $this->redirectToAccount($journal);
} }
$linkTypes = $linkTypeRepository->get();
$links = $linkTypeRepository->getLinks($journal);
$events = $tasker->getPiggyBankEvents($journal); $events = $tasker->getPiggyBankEvents($journal);
$transactions = $tasker->getTransactionsOverview($journal); $transactions = $tasker->getTransactionsOverview($journal);
$what = strtolower($journal->transaction_type_type ?? $journal->transactionType->type); $what = strtolower($journal->transaction_type_type ?? $journal->transactionType->type);
$subTitle = trans('firefly.' . $what) . ' "' . e($journal->description) . '"'; $subTitle = trans('firefly.' . $what) . ' "' . e($journal->description) . '"';
return view('transactions.show', compact('journal', 'events', 'subTitle', 'what', 'transactions')); return view('transactions.show', compact('journal', 'events', 'subTitle', 'what', 'transactions', 'linkTypes', 'links'));
} }
@ -210,35 +214,24 @@ class TransactionController extends Controller
$collector = app(JournalCollectorInterface::class); $collector = app(JournalCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($end, $currentEnd)->withOpposingAccount()->setTypes($types); $collector->setAllAssetAccounts()->setRange($end, $currentEnd)->withOpposingAccount()->setTypes($types);
$collector->removeFilter(InternalTransferFilter::class); $collector->removeFilter(InternalTransferFilter::class);
$set = $collector->getJournals(); $journals = $collector->getJournals();
$sum = $set->sum('transaction_amount'); $sum = $journals->sum('transaction_amount');
$journals = $set->count();
// count per currency:
$sums = $this->sumPerCurrency($journals);
$dateStr = $end->format('Y-m-d'); $dateStr = $end->format('Y-m-d');
$dateName = Navigation::periodShow($end, $range); $dateName = Navigation::periodShow($end, $range);
$array = [ $array = [
'string' => $dateStr, 'string' => $dateStr,
'name' => $dateName, 'name' => $dateName,
'count' => $journals, 'sum' => $sum,
'spent' => 0, 'sums' => $sums,
'earned' => 0, 'date' => clone $end,
'transferred' => 0,
'date' => clone $end,
]; ];
Log::debug(sprintf('What is %s', $what)); Log::debug(sprintf('What is %s', $what));
switch ($what) { if ($journals->count() > 0) {
case 'withdrawal': $entries->push($array);
$array['spent'] = $sum;
break;
case 'deposit':
$array['earned'] = $sum;
break;
case 'transfers':
case 'transfer':
$array['transferred'] = Steam::positive($sum);
break;
} }
$entries->push($array);
$end = Navigation::subtractPeriod($end, $range, 1); $end = Navigation::subtractPeriod($end, $range, 1);
} }
Log::debug('End of loop'); Log::debug('End of loop');
@ -247,4 +240,41 @@ class TransactionController extends Controller
return $entries; return $entries;
} }
/**
* @param Collection $collection
*
* @return array
*/
private function sumPerCurrency(Collection $collection): array
{
$return = [];
/** @var Transaction $transaction */
foreach ($collection as $transaction) {
$currencyId = $transaction->transaction_currency_id;
// save currency information:
if (!isset($return[$currencyId])) {
$currencySymbol = $transaction->transaction_currency_symbol;
$decimalPlaces = $transaction->transaction_currency_dp;
$currencyCode = $transaction->transaction_currency_code;
$return[$currencyId] = [
'currency' => [
'id' => $currencyId,
'code' => $currencyCode,
'symbol' => $currencySymbol,
'dp' => $decimalPlaces,
],
'sum' => '0',
'count' => 0,
];
}
// save amount:
$return[$currencyId]['sum'] = bcadd($return[$currencyId]['sum'], $transaction->transaction_amount);
$return[$currencyId]['count']++;
}
asort($return);
return $return;
}
} }

View File

@ -37,6 +37,8 @@ class BudgetIncomeRequest extends Request
{ {
return [ return [
'amount' => 'numeric|required|min:0', 'amount' => 'numeric|required|min:0',
'start' => 'required|date|before:end',
'end' => 'required|date|after:start',
]; ];
} }
} }

View File

@ -38,10 +38,9 @@ class ExportFormRequest extends Request
public function rules() public function rules()
{ {
$sessionFirst = clone session('first'); $sessionFirst = clone session('first');
$first = $sessionFirst->subDay()->format('Y-m-d');
$first = $sessionFirst->subDay()->format('Y-m-d'); $today = Carbon::create()->addDay()->format('Y-m-d');
$today = Carbon::create()->addDay()->format('Y-m-d'); $formats = join(',', array_keys(config('firefly.export_formats')));
$formats = join(',', array_keys(config('firefly.export_formats')));
return [ return [
'export_start_range' => 'required|date|after:' . $first, 'export_start_range' => 'required|date|after:' . $first,

View File

@ -110,7 +110,7 @@ class JournalFormRequest extends Request
// foreign currency amounts // foreign currency amounts
'native_amount' => 'numeric|more:0', 'native_amount' => 'numeric|more:0',
'source_amount' => 'numeric|more:0', 'source_amount' => 'numeric|more:0',
'destination_amount' => 'numeric|more:0', 'destination_amount' => 'numeric',
]; ];
// some rules get an upgrade depending on the type of data: // some rules get an upgrade depending on the type of data:

View File

@ -0,0 +1,74 @@
<?php
/**
* JournalLinkRequest.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Http\Requests;
use FireflyIII\Models\LinkType;
/**
* Class JournalLink
*
*
* @package FireflyIII\Http\Requests
*/
class JournalLinkRequest extends Request
{
/**
* @return bool
*/
public function authorize()
{
// Only allow logged in users
return auth()->check();
}
/**
* @return array
*/
public function getLinkInfo(): array
{
$return = [];
$linkType = $this->get('link_type');
$parts = explode('_', $linkType);
$return['link_type_id'] = intval($parts[0]);
$return['transaction_journal_id'] = $this->integer('link_journal_id');
$return['comments'] = strlen($this->string('comments')) > 0 ? $this->string('comments') : null;
$return['direction'] = $parts[1];
if ($return['transaction_journal_id'] === 0 && ctype_digit($this->string('link_other'))) {
$return['transaction_journal_id'] = $this->integer('link_other');
}
return $return;
}
/**
* @return array
*/
public function rules()
{
// all possible combinations of link types and inward / outward:
$combinations = [];
$linkTypes = LinkType::get(['id']);
/** @var LinkType $type */
foreach ($linkTypes as $type) {
$combinations[] = sprintf('%d_inward', $type->id);
$combinations[] = sprintf('%d_outward', $type->id);
}
$string = join(',', $combinations);
return [
'link_type' => sprintf('required|in:%s', $string),
'link_other' => 'belongsToUser:transaction_journals',
'link_journal_id' => 'belongsToUser:transaction_journals',
];
}
}

View File

@ -0,0 +1,57 @@
<?php
/**
* LinkTypeFormRequest.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Http\Requests;
use FireflyIII\Repositories\LinkType\LinkTypeRepositoryInterface;
/**
* Class BillFormRequest
*
*
* @package FireflyIII\Http\Requests
*/
class LinkTypeFormRequest extends Request
{
/**
* @return bool
*/
public function authorize()
{
// Only allow logged and admins
return auth()->check() && auth()->user()->hasRole('owner');
}
/**
* @return array
*/
public function rules()
{
/** @var LinkTypeRepositoryInterface $repository */
$repository = app(LinkTypeRepositoryInterface::class);
$nameRule = 'required|min:1|unique:link_types,name';
$idRule = '';
if (!is_null($repository->find($this->integer('id'))->id)) {
$idRule = 'exists:link_types,id';
$nameRule = 'required|min:1';
}
$rules = [
'id' => $idRule,
'name' => $nameRule,
'inward' => 'required|min:1|different:outward',
'outward' => 'required|min:1|different:inward',
];
return $rules;
}
}

View File

@ -28,64 +28,17 @@ class Request extends FormRequest
* *
* @return bool * @return bool
*/ */
protected function boolean(string $field): bool public function boolean(string $field): bool
{ {
return intval($this->input($field)) === 1; return intval($this->input($field)) === 1;
} }
/**
* @param string $field
*
* @return Carbon|null
*/
protected function date(string $field)
{
return $this->get($field) ? new Carbon($this->get($field)) : null;
}
/**
* @param string $field
*
* @return float
*/
protected function float(string $field): float
{
return round($this->input($field), 12);
}
/**
* @param string $field
* @param string $type
*
* @return array
*/
protected function getArray(string $field, string $type): array
{
$original = $this->get($field);
$return = [];
foreach ($original as $index => $value) {
$return[$index] = $this->$type($value);
}
return $return;
}
/**
* @param string $field
*
* @return int
*/
protected function integer(string $field): int
{
return intval($this->get($field));
}
/** /**
* @param string $field * @param string $field
* *
* @return string * @return string
*/ */
protected function string(string $field): string public function string(string $field): string
{ {
$string = $this->get($field) ?? ''; $string = $this->get($field) ?? '';
$search = [ $search = [
@ -140,4 +93,51 @@ class Request extends FormRequest
return trim($string); return trim($string);
} }
/**
* @param string $field
*
* @return Carbon|null
*/
protected function date(string $field)
{
return $this->get($field) ? new Carbon($this->get($field)) : null;
}
/**
* @param string $field
*
* @return float
*/
protected function float(string $field): float
{
return round($this->input($field), 12);
}
/**
* @param string $field
* @param string $type
*
* @return array
*/
protected function getArray(string $field, string $type): array
{
$original = $this->get($field);
$return = [];
foreach ($original as $index => $value) {
$return[$index] = $this->$type($value);
}
return $return;
}
/**
* @param string $field
*
* @return int
*/
protected function integer(string $field): int
{
return intval($this->get($field));
}
} }

View File

@ -13,7 +13,7 @@ declare(strict_types=1);
namespace FireflyIII\Http\Requests; namespace FireflyIII\Http\Requests;
use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface; use FireflyIII\Repositories\Rule\RuleRepositoryInterface;
/** /**
* Class RuleFormRequest * Class RuleFormRequest
@ -39,6 +39,7 @@ class RuleFormRequest extends Request
{ {
return [ return [
'title' => $this->string('title'), 'title' => $this->string('title'),
'rule_group_id' => $this->integer('rule_group_id'),
'active' => $this->boolean('active'), 'active' => $this->boolean('active'),
'trigger' => $this->string('trigger'), 'trigger' => $this->string('trigger'),
'description' => $this->string('description'), 'description' => $this->string('description'),
@ -57,19 +58,18 @@ class RuleFormRequest extends Request
*/ */
public function rules() public function rules()
{ {
/** @var RuleGroupRepositoryInterface $repository */ /** @var RuleRepositoryInterface $repository */
$repository = app(RuleGroupRepositoryInterface::class); $repository = app(RuleRepositoryInterface::class);
$validTriggers = array_keys(config('firefly.rule-triggers')); $validTriggers = array_keys(config('firefly.rule-triggers'));
$validActions = array_keys(config('firefly.rule-actions')); $validActions = array_keys(config('firefly.rule-actions'));
// some actions require text: // some actions require text:
$contextActions = join(',', config('firefly.rule-actions-text')); $contextActions = join(',', config('firefly.rule-actions-text'));
$titleRule = 'required|between:1,100|uniqueObjectForUser:rule_groups,title'; $titleRule = 'required|between:1,100|uniqueObjectForUser:rules,title';
if (!is_null($repository->find(intval($this->get('id')))->id)) { if (!is_null($repository->find(intval($this->get('id')))->id)) {
$titleRule = 'required|between:1,100|uniqueObjectForUser:rule_groups,title,' . intval($this->get('id')); $titleRule = 'required|between:1,100|uniqueObjectForUser:rules,title,' . intval($this->get('id'));
} }
$rules = [ $rules = [
'title' => $titleRule, 'title' => $titleRule,
'description' => 'between:1,5000', 'description' => 'between:1,5000',

View File

@ -61,23 +61,23 @@ class SplitJournalFormRequest extends Request
public function rules(): array public function rules(): array
{ {
return [ return [
'what' => 'required|in:withdrawal,deposit,transfer', 'what' => 'required|in:withdrawal,deposit,transfer',
'journal_description' => 'required|between:1,255', 'journal_description' => 'required|between:1,255',
'id' => 'numeric|belongsToUser:transaction_journals,id', 'id' => 'numeric|belongsToUser:transaction_journals,id',
'journal_source_account_id' => 'numeric|belongsToUser:accounts,id', 'journal_source_account_id' => 'numeric|belongsToUser:accounts,id',
'journal_source_account_name.*' => 'between:1,255', 'journal_source_account_name.*' => 'between:1,255',
'journal_currency_id' => 'required|exists:transaction_currencies,id', 'journal_currency_id' => 'required|exists:transaction_currencies,id',
'date' => 'required|date', 'date' => 'required|date',
'interest_date' => 'date', 'interest_date' => 'date',
'book_date' => 'date', 'book_date' => 'date',
'process_date' => 'date', 'process_date' => 'date',
'description.*' => 'required|between:1,255', 'transactions.*.description' => 'required|between:1,255',
'destination_account_id.*' => 'numeric|belongsToUser:accounts,id', 'transactions.*.destination_account_id' => 'numeric|belongsToUser:accounts,id',
'destination_account_name.*' => 'between:1,255', 'transactions.*.destination_account_name' => 'between:1,255',
'amount.*' => 'required|numeric', 'transactions.*.amount' => 'required|numeric',
'budget_id.*' => 'belongsToUser:budgets,id', 'transactions.*.budget_id' => 'belongsToUser:budgets,id',
'category.*' => 'between:1,255', 'transactions.*.category' => 'between:1,255',
'piggy_bank_id.*' => 'between:1,255', 'transactions.*.piggy_bank_id' => 'between:1,255',
]; ];
} }

View File

@ -37,14 +37,13 @@ class TagFormRequest extends Request
*/ */
public function collectTagData(): array public function collectTagData(): array
{ {
$latitude = null;
$longitude = null;
$zoomLevel = null;
if ($this->get('setTag') === 'true') { if ($this->get('setTag') === 'true') {
$latitude = $this->string('latitude'); $latitude = $this->string('latitude');
$longitude = $this->string('longitude'); $longitude = $this->string('longitude');
$zoomLevel = $this->integer('zoomLevel'); $zoomLevel = $this->integer('zoomLevel');
} else {
$latitude = null;
$longitude = null;
$zoomLevel = null;
} }
$data = [ $data = [

View File

@ -21,12 +21,14 @@ use FireflyIII\Models\Budget;
use FireflyIII\Models\BudgetLimit; use FireflyIII\Models\BudgetLimit;
use FireflyIII\Models\Category; use FireflyIII\Models\Category;
use FireflyIII\Models\ImportJob; use FireflyIII\Models\ImportJob;
use FireflyIII\Models\LinkType;
use FireflyIII\Models\PiggyBank; use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\Rule; use FireflyIII\Models\Rule;
use FireflyIII\Models\RuleGroup; use FireflyIII\Models\RuleGroup;
use FireflyIII\Models\Tag; use FireflyIII\Models\Tag;
use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionJournalLink;
use FireflyIII\Models\TransactionType; use FireflyIII\Models\TransactionType;
use FireflyIII\User; use FireflyIII\User;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@ -152,6 +154,49 @@ Breadcrumbs::register(
); );
Breadcrumbs::register(
'admin.links.index', function (BreadCrumbGenerator $breadcrumbs) {
$breadcrumbs->parent('admin.index');
$breadcrumbs->push(trans('firefly.journal_link_configuration'), route('admin.links.index'));
}
);
Breadcrumbs::register(
'admin.links.create', function (BreadCrumbGenerator $breadcrumbs) {
$breadcrumbs->parent('admin.links.index');
$breadcrumbs->push(trans('firefly.create_new_link_type'), route('admin.links.create'));
}
);
Breadcrumbs::register(
'admin.links.show', function (BreadCrumbGenerator $breadcrumbs, LinkType $linkType) {
$breadcrumbs->parent('admin.links.index');
$breadcrumbs->push(trans('firefly.overview_for_link', [$linkType->name]), route('admin.links.show', [$linkType->id]));
}
);
Breadcrumbs::register(
'admin.links.edit', function (BreadCrumbGenerator $breadcrumbs, LinkType $linkType) {
$breadcrumbs->parent('admin.links.index');
$breadcrumbs->push(trans('firefly.edit_link_type', ['name' => $linkType->name]), route('admin.links.edit', [$linkType->id]));
}
);
Breadcrumbs::register(
'admin.links.delete', function (BreadCrumbGenerator $breadcrumbs, LinkType $linkType) {
$breadcrumbs->parent('admin.links.index');
$breadcrumbs->push(trans('firefly.delete_link_type', ['name' => $linkType->name]), route('admin.links.delete', [$linkType->id]));
}
);
Breadcrumbs::register(
'transactions.link.delete', function (BreadCrumbGenerator $breadcrumbs, TransactionJournalLink $link) {
$breadcrumbs->parent('home');
$breadcrumbs->push(trans('breadcrumbs.delete_journal_link'), route('transactions.link.delete', $link->id));
}
);
/** /**
* ATTACHMENTS * ATTACHMENTS
*/ */

View File

@ -13,6 +13,8 @@ declare(strict_types=1);
namespace FireflyIII\Import\Converter; namespace FireflyIII\Import\Converter;
use Log;
/** /**
* Class RabobankDebetCredit * Class RabobankDebetCredit
* *
@ -34,31 +36,40 @@ class Amount implements ConverterInterface
*/ */
public function convert($value): string public function convert($value): string
{ {
Log::debug(sprintf('Start with amount "%s"', $value));
$len = strlen($value); $len = strlen($value);
$decimalPosition = $len - 3; $decimalPosition = $len - 3;
$decimal = null; $decimal = null;
if (($len > 2 && $value{$decimalPosition} === '.') || ($len > 2 && strpos($value, '.') > $decimalPosition)) { if (($len > 2 && $value{$decimalPosition} === '.') || ($len > 2 && strpos($value, '.') > $decimalPosition)) {
$decimal = '.'; $decimal = '.';
Log::debug(sprintf('Decimal character in "%s" seems to be a dot.', $value));
} }
if ($len > 2 && $value{$decimalPosition} === ',') { if ($len > 2 && $value{$decimalPosition} === ',') {
$decimal = ','; $decimal = ',';
Log::debug(sprintf('Decimal character in "%s" seems to be a comma.', $value));
} }
// if decimal is dot, replace all comma's and spaces with nothing. then parse as float (round to 4 pos) // if decimal is dot, replace all comma's and spaces with nothing. then parse as float (round to 4 pos)
if ($decimal === '.') { if ($decimal === '.') {
$search = [',', ' ']; $search = [',', ' '];
$value = str_replace($search, '', $value); $oldValue = $value;
$value = str_replace($search, '', $value);
Log::debug(sprintf('Converted amount from "%s" to "%s".', $oldValue, $value));
} }
if ($decimal === ',') { if ($decimal === ',') {
$search = ['.', ' ']; $search = ['.', ' '];
$value = str_replace($search, '', $value); $oldValue = $value;
$value = str_replace(',', '.', $value); $value = str_replace($search, '', $value);
$value = str_replace(',', '.', $value);
Log::debug(sprintf('Converted amount from "%s" to "%s".', $oldValue, $value));
} }
if (is_null($decimal)) { if (is_null($decimal)) {
// replace all: // replace all:
$search = ['.', ' ', ',']; $search = ['.', ' ', ','];
$value = str_replace($search, '', $value); $oldValue = $value;
$value = str_replace($search, '', $value);
Log::debug(sprintf('No decimal character found. Converted amount from "%s" to "%s".', $oldValue, $value));
} }
return strval(round(floatval($value), 12)); return strval(round(floatval($value), 12));

View File

@ -151,10 +151,14 @@ class CsvProcessor implements FileProcessorInterface
*/ */
private function getImportArray(): Iterator private function getImportArray(): Iterator
{ {
$content = $this->job->uploadFileContents(); $content = $this->job->uploadFileContents();
$config = $this->job->configuration; $config = $this->job->configuration;
$reader = Reader::createFromString($content); $reader = Reader::createFromString($content);
$reader->setDelimiter($config['delimiter']); $delimiter = $config['delimiter'];
if ($delimiter === 'tab') {
$delimiter = "\t";
}
$reader->setDelimiter($delimiter);
$start = $config['has-headers'] ? 1 : 0; $start = $config['has-headers'] ? 1 : 0;
$results = $reader->setOffset($start)->fetch(); $results = $reader->setOffset($start)->fetch();
Log::debug(sprintf('Created a CSV reader starting at offset %d', $start)); Log::debug(sprintf('Created a CSV reader starting at offset %d', $start));

View File

@ -37,6 +37,8 @@ class ImportJournal
public $budget; public $budget;
/** @var ImportCategory */ /** @var ImportCategory */
public $category; public $category;
/** @var ImportCurrency */
public $currency;
/** @var string */ /** @var string */
public $description = ''; public $description = '';
/** @var string */ /** @var string */
@ -51,8 +53,8 @@ class ImportJournal
public $tags = []; public $tags = [];
/** @var string */ /** @var string */
private $amount; private $amount;
/** @var ImportCurrency */ /** @var string */
public $currency; private $convertedAmount = null;
/** @var string */ /** @var string */
private $date = ''; private $date = '';
/** @var string */ /** @var string */
@ -85,25 +87,37 @@ class ImportJournal
/** /**
* @return string * @return string
* @throws FireflyException
*/ */
public function getAmount(): string public function getAmount(): string
{ {
if (is_null($this->amount)) { Log::debug('Now in getAmount()');
if (is_null($this->convertedAmount)) {
Log::debug('convertedAmount is NULL');
/** @var ConverterInterface $amountConverter */ /** @var ConverterInterface $amountConverter */
$amountConverter = app(Amount::class); $amountConverter = app(Amount::class);
$this->amount = $amountConverter->convert($this->amount); $this->convertedAmount = $amountConverter->convert($this->amount);
Log::debug(sprintf('First attempt to convert gives "%s"', $this->convertedAmount));
// modify // modify
foreach ($this->modifiers as $modifier) { foreach ($this->modifiers as $modifier) {
$class = sprintf('FireflyIII\Import\Converter\%s', config(sprintf('csv.import_roles.%s.converter', $modifier['role']))); $class = sprintf('FireflyIII\Import\Converter\%s', config(sprintf('csv.import_roles.%s.converter', $modifier['role'])));
/** @var ConverterInterface $converter */ /** @var ConverterInterface $converter */
$converter = app($class); $converter = app($class);
Log::debug(sprintf('Now launching converter %s', $class));
if ($converter->convert($modifier['value']) === -1) { if ($converter->convert($modifier['value']) === -1) {
$this->amount = Steam::negative($this->amount); $this->convertedAmount = Steam::negative($this->convertedAmount);
} }
Log::debug(sprintf('convertedAmount after conversion is %s', $this->convertedAmount));
} }
Log::debug(sprintf('After modifiers the result is: "%s"', $this->convertedAmount));
}
Log::debug(sprintf('convertedAmount is: "%s"', $this->convertedAmount));
if (bccomp($this->convertedAmount, '0') === 0) {
throw new FireflyException('Amount is zero.');
} }
return $this->amount; return $this->convertedAmount;
} }
/** /**

View File

@ -59,7 +59,8 @@ class RabobankDescription implements SpecificInterface
); );
$row[6] = $alternateName; $row[6] = $alternateName;
$row[10] = ''; $row[10] = '';
} else { }
if (!(strlen($oppositeAccount) < 1 && strlen($oppositeName) < 1)) {
Log::debug('Rabobank specific: either opposite account or name are filled.'); Log::debug('Rabobank specific: either opposite account or name are filled.');
} }

View File

@ -11,6 +11,8 @@ declare(strict_types=1);
namespace FireflyIII\Import\Storage; namespace FireflyIII\Import\Storage;
use ErrorException;
use Exception;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Import\Object\ImportJournal; use FireflyIII\Import\Object\ImportJournal;
use FireflyIII\Models\ImportJob; use FireflyIII\Models\ImportJob;
@ -95,7 +97,7 @@ class ImportStorage
function (ImportJournal $importJournal, int $index) { function (ImportJournal $importJournal, int $index) {
try { try {
$this->storeImportJournal($index, $importJournal); $this->storeImportJournal($index, $importJournal);
} catch (FireflyException $e) { } catch (FireflyException | ErrorException | Exception $e) {
$this->errors->push($e->getMessage()); $this->errors->push($e->getMessage());
Log::error(sprintf('Cannot import row #%d because: %s', $index, $e->getMessage())); Log::error(sprintf('Cannot import row #%d because: %s', $index, $e->getMessage()));
} }
@ -142,7 +144,10 @@ class ImportStorage
if ($this->isDoubleTransfer($parameters) || $this->hashAlreadyImported($importJournal->hash)) { if ($this->isDoubleTransfer($parameters) || $this->hashAlreadyImported($importJournal->hash)) {
$this->job->addStepsDone(3); $this->job->addStepsDone(3);
// throw error // throw error
throw new FireflyException('Detected a possible duplicate, skip this one.'); $message = sprintf('Detected a possible duplicate, skip this one (hash: %s).', $importJournal->hash);
Log::error($message, $parameters);
throw new FireflyException($message);
} }
unset($parameters); unset($parameters);
@ -202,25 +207,32 @@ class ImportStorage
return false; return false;
} }
$amount = app('steam')->positive($parameters['amount']); $amount = app('steam')->positive($parameters['amount']);
$names = [$parameters['asset'], $parameters['opposing']]; $names = [$parameters['asset'], $parameters['opposing']];
$transfer = [];
$hit = false;
sort($names); sort($names);
foreach ($this->transfers as $transfer) { foreach ($this->transfers as $transfer) {
if ($parameters['description'] !== $transfer['description']) { if ($parameters['description'] === $transfer['description']) {
return false; $hit = true;
} }
if ($names !== $transfer['names']) { if ($names === $transfer['names']) {
return false; $hit = true;
} }
if (bccomp($amount, $transfer['amount']) !== 0) { if (bccomp($amount, $transfer['amount']) === 0) {
return false; $hit = true;
} }
if ($parameters['date'] !== $transfer['date']) { if ($parameters['date'] === $transfer['date']) {
return false; $hit = true;
} }
} }
if ($hit === true) {
Log::error(
'There already is a transfer imported with these properties. Compare existing with new. ', ['existing' => $transfer, 'new' => $parameters]
);
}
return true; return $hit;
} }
} }

View File

@ -214,7 +214,7 @@ trait ImportSupport
*/ */
private function getTransactionType(string $amount, Account $account): string private function getTransactionType(string $amount, Account $account): string
{ {
$transactionType = ''; $transactionType = TransactionType::WITHDRAWAL;
// amount is negative, it's a withdrawal, opposing is an expense: // amount is negative, it's a withdrawal, opposing is an expense:
if (bccomp($amount, '0') === -1) { if (bccomp($amount, '0') === -1) {
$transactionType = TransactionType::WITHDRAWAL; $transactionType = TransactionType::WITHDRAWAL;
@ -296,12 +296,15 @@ trait ImportSupport
*/ */
private function hashAlreadyImported(string $hash): bool private function hashAlreadyImported(string $hash): bool
{ {
$json = json_encode($hash); $json = json_encode($hash);
/** @var TransactionJournalMeta $entry */
$entry = TransactionJournalMeta::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id') $entry = TransactionJournalMeta::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id')
->where('data', $json) ->where('data', $json)
->where('name', 'importHash') ->where('name', 'importHash')
->first(); ->first();
if (!is_null($entry)) { if (!is_null($entry)) {
Log::error(sprintf('A journal with hash %s has already been imported (spoiler: it\'s journal #%d)', $hash, $entry->transaction_journal_id));
return true; return true;
} }
@ -441,4 +444,4 @@ trait ImportSupport
return; return;
} }
} }

View File

@ -1,6 +1,15 @@
<?php <?php
declare(strict_types=1);
/**
* RegisteredUser.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
/** /**

View File

@ -1,6 +1,15 @@
<?php <?php
declare(strict_types=1);
/**
* RequestedNewPassword.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
/** /**

View File

@ -17,6 +17,7 @@ use Crypt;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Watson\Validating\ValidatingTrait; use Watson\Validating\ValidatingTrait;
@ -28,7 +29,7 @@ use Watson\Validating\ValidatingTrait;
class Bill extends Model class Bill extends Model
{ {
use ValidatingTrait; use SoftDeletes, ValidatingTrait;
/** /**
* The attributes that should be casted to native types. * The attributes that should be casted to native types.
* *

View File

@ -13,12 +13,15 @@ declare(strict_types=1);
namespace FireflyIII\Models; namespace FireflyIII\Models;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/** /**
* Class ExportJob * Class ExportJob
* *
* @property User $user
*
* @package FireflyIII\Models * @package FireflyIII\Models
*/ */
class ExportJob extends Model class ExportJob extends Model

62
app/Models/LinkType.php Normal file
View File

@ -0,0 +1,62 @@
<?php
/**
* LinkType.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Models;
use Illuminate\Database\Eloquent\Model;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* @property int $journalCount
* Class LinkType
*
* @package FireflyIII\Models
*/
class LinkType extends Model
{
/**
* The attributes that should be casted to native types.
*
* @var array
*/
protected $casts
= [
'created_at' => 'date',
'updated_at' => 'date',
'deleted_at' => 'date',
'editable' => 'boolean',
];
/**
* @param $value
*
* @return mixed
* @throws NotFoundHttpException
*/
public static function routeBinder($value)
{
if (auth()->check()) {
$model = self::where('id', $value)->first();
if (!is_null($model)) {
return $model;
}
}
throw new NotFoundHttpException;
}
public function transactionJournalLinks()
{
return $this->hasMany(TransactionJournalLink::class);
}
}

View File

@ -84,6 +84,7 @@ class PiggyBank extends Model
return $this->currentRep; return $this->currentRep;
} }
// repeating piggy banks are no longer supported. // repeating piggy banks are no longer supported.
/** @var PiggyBankRepetition $rep */
$rep = $this->piggyBankRepetitions()->first(['piggy_bank_repetitions.*']); $rep = $this->piggyBankRepetitions()->first(['piggy_bank_repetitions.*']);
if (is_null($rep)) { if (is_null($rep)) {
return new PiggyBankRepetition(); return new PiggyBankRepetition();

View File

@ -14,6 +14,7 @@ declare(strict_types=1);
namespace FireflyIII\Models; namespace FireflyIII\Models;
use Crypt; use Crypt;
use Exception;
use FireflyIII\Exceptions\FireflyException; use FireflyIII\Exceptions\FireflyException;
use Illuminate\Contracts\Encryption\DecryptException; use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
@ -56,7 +57,15 @@ class Preference extends Model
sprintf('Could not decrypt preference #%d. If this error persists, please run "php artisan cache:clear" on the command line.', $this->id) sprintf('Could not decrypt preference #%d. If this error persists, please run "php artisan cache:clear" on the command line.', $this->id)
); );
} }
$unserialized = false;
try {
$unserialized = unserialize($data);
} catch (Exception $e) {
// don't care, assume is false.
}
if (!($unserialized === false)) {
return $unserialized;
}
return json_decode($data, true); return json_decode($data, true);
} }
@ -66,7 +75,7 @@ class Preference extends Model
*/ */
public function setDataAttribute($value) public function setDataAttribute($value)
{ {
$this->attributes['data'] = Crypt::encrypt(json_encode($value)); $this->attributes['data'] = Crypt::encrypt(serialize($value));
} }
/** /**

View File

@ -22,6 +22,48 @@ use Watson\Validating\ValidatingTrait;
/** /**
* Class Transaction * Class Transaction
* *
* @property-read int $journal_id
* @property-read Carbon $date
* @property-read string $transaction_description
* @property-read string $transaction_amount
* @property-read string $transaction_foreign_amount
* @property-read string $transaction_type_type
*
* @property-read int $account_id
* @property-read string $account_name
* @property string $account_iban
* @property string $account_number
* @property string $account_bic
* @property string $account_currency_code
*
* @property-read int $opposing_account_id
* @property string $opposing_account_name
* @property string $opposing_account_iban
* @property string $opposing_account_number
* @property string $opposing_account_bic
* @property string $opposing_currency_code
*
*
* @property-read int $transaction_budget_id
* @property-read string $transaction_budget_name
* @property-read int $transaction_journal_budget_id
* @property-read string $transaction_journal_budget_name
*
* @property-read int $transaction_category_id
* @property-read string $transaction_category_name
* @property-read int $transaction_journal_category_id
* @property-read string $transaction_journal_category_name
*
* @property-read int $bill_id
* @property string $bill_name
*
* @property string $notes
* @property string $tags
*
* @property string $transaction_currency_symbol
* @property int $transaction_currency_dp
* @property string $transaction_currency_code
*
* @package FireflyIII\Models * @package FireflyIII\Models
*/ */
class Transaction extends Model class Transaction extends Model

View File

@ -140,6 +140,14 @@ class TransactionJournal extends Model
return true; return true;
} }
/**
* @return HasMany
*/
public function destinationJournalLinks(): HasMany
{
return $this->hasMany(TransactionJournalLink::class, 'destination_id');
}
/** /**
* *
* @param $value * @param $value
@ -372,6 +380,14 @@ class TransactionJournal extends Model
return $entry; return $entry;
} }
/**
* @return HasMany
*/
public function sourceJournalLinks(): HasMany
{
return $this->hasMany(TransactionJournalLink::class, 'source_id');
}
/** /**
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/ */

View File

@ -0,0 +1,105 @@
<?php
/**
* TransactionJournalLink.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Models;
use Crypt;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Class TransactionJournalLink
*
* @package FireflyIII\Models
*/
class TransactionJournalLink extends Model
{
protected $table = 'journal_links';
/**
* @param $value
*
* @return mixed
* @throws NotFoundHttpException
*/
public static function routeBinder($value)
{
if (auth()->check()) {
$model = self::where('journal_links.id', $value)
->leftJoin('transaction_journals as t_a', 't_a.id', '=', 'source_id')
->leftJoin('transaction_journals as t_b', 't_b.id', '=', 'destination_id')
->where('t_a.user_id', auth()->user()->id)
->where('t_b.user_id', auth()->user()->id)
->first(['journal_links.*']);
if (!is_null($model)) {
return $model;
}
}
throw new NotFoundHttpException;
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function destination()
{
return $this->belongsTo(TransactionJournal::class, 'destination_id');
}
/**
* @param $value
*
* @return null|string
*/
public function getCommentAttribute($value): ?string
{
if (!is_null($value)) {
return Crypt::decrypt($value);
}
return null;
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function linkType(): BelongsTo
{
return $this->belongsTo(LinkType::class);
}
/**
*
* @param $value
*/
public function setCommentAttribute($value): void
{
if (!is_null($value) && strlen($value) > 0) {
$this->attributes['comment'] = Crypt::encrypt($value);
return;
}
$this->attributes['comment'] = null;
}
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function source()
{
return $this->belongsTo(TransactionJournal::class, 'source_id');
}
}

View File

@ -0,0 +1,63 @@
<?php
/**
* AdminServiceProvider.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Providers;
use FireflyIII\Repositories\LinkType\LinkTypeRepository;
use FireflyIII\Repositories\LinkType\LinkTypeRepositoryInterface;
use Illuminate\Foundation\Application;
use Illuminate\Support\ServiceProvider;
class AdminServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* @return void
*/
public function boot()
{
}
/**
* Register the application services.
*
* @return void
*/
public function register()
{
$this->linkType();
}
/**
*
*/
private function linkType()
{
$this->app->bind(
LinkTypeRepositoryInterface::class,
function (Application $app) {
/** @var LinkTypeRepository $repository */
$repository = app(LinkTypeRepository::class);
if ($app->auth->check()) {
$repository->setUser(auth()->user());
}
return $repository;
}
);
}
}

View File

@ -13,7 +13,7 @@ declare(strict_types=1);
namespace FireflyIII\Providers; namespace FireflyIII\Providers;
use FireflyIII\Export\Processor; use FireflyIII\Export\ExpandedProcessor;
use FireflyIII\Export\ProcessorInterface; use FireflyIII\Export\ProcessorInterface;
use FireflyIII\Generator\Chart\Basic\ChartJsGenerator; use FireflyIII\Generator\Chart\Basic\ChartJsGenerator;
use FireflyIII\Generator\Chart\Basic\GeneratorInterface; use FireflyIII\Generator\Chart\Basic\GeneratorInterface;
@ -138,7 +138,8 @@ class FireflyServiceProvider extends ServiceProvider
); );
// other generators // other generators
$this->app->bind(ProcessorInterface::class, Processor::class); // export:
$this->app->bind(ProcessorInterface::class, ExpandedProcessor::class);
$this->app->bind(UserRepositoryInterface::class, UserRepository::class); $this->app->bind(UserRepositoryInterface::class, UserRepository::class);
$this->app->bind(AttachmentHelperInterface::class, AttachmentHelper::class); $this->app->bind(AttachmentHelperInterface::class, AttachmentHelper::class);

View File

@ -205,10 +205,12 @@ trait FindAccountsTrait
*/ */
public function getCashAccount(): Account public function getCashAccount(): Account
{ {
$type = AccountType::where('type', AccountType::CASH)->first(); $type = AccountType::where('type', AccountType::CASH)->first();
$account = Account::firstOrCreateEncrypted( $account = Account::firstOrCreateEncrypted(
['user_id' => $this->user->id, 'account_type_id' => $type->id, 'name' => 'Cash account', 'active' => 1] ['user_id' => $this->user->id, 'account_type_id' => $type->id, 'name' => 'Cash account']
); );
$account->active = true;
$account->save();
return $account; return $account;
} }

View File

@ -116,7 +116,7 @@ class BillRepository implements BillRepositoryInterface
$set = $set->sortBy( $set = $set->sortBy(
function (Bill $bill) { function (Bill $bill) {
$int = $bill->active === 1 ? 0 : 1; $int = $bill->active ? 0 : 1;
return $int . strtolower($bill->name); return $int . strtolower($bill->name);
} }

View File

@ -166,6 +166,16 @@ class CurrencyRepository implements CurrencyRepositoryInterface
return TransactionCurrency::get(); return TransactionCurrency::get();
} }
/**
* @param array $ids
*
* @return Collection
*/
public function getByIds(array $ids): Collection
{
return TransactionCurrency::whereIn('id', $ids)->get();
}
/** /**
* @param Preference $preference * @param Preference $preference
* *

View File

@ -90,6 +90,13 @@ interface CurrencyRepositoryInterface
*/ */
public function get(): Collection; public function get(): Collection;
/**
* @param array $ids
*
* @return Collection
*/
public function getByIds(array $ids): Collection;
/** /**
* @param Preference $preference * @param Preference $preference
* *

View File

@ -117,8 +117,11 @@ trait SupportJournalsTrait
if (strlen($data['source_account_name']) > 0) { if (strlen($data['source_account_name']) > 0) {
$sourceType = AccountType::where('type', 'Revenue account')->first(); $sourceType = AccountType::where('type', 'Revenue account')->first();
$sourceAccount = Account::firstOrCreateEncrypted( $sourceAccount = Account::firstOrCreateEncrypted(
['user_id' => $user->id, 'account_type_id' => $sourceType->id, 'name' => $data['source_account_name'], 'active' => 1] ['user_id' => $user->id, 'account_type_id' => $sourceType->id, 'name' => $data['source_account_name']]
); );
// always make account active
$sourceAccount->active = true;
$sourceAccount->save();
Log::debug(sprintf('source account name is "%s", account is %d', $data['source_account_name'], $sourceAccount->id)); Log::debug(sprintf('source account name is "%s", account is %d', $data['source_account_name'], $sourceAccount->id));
@ -132,8 +135,11 @@ trait SupportJournalsTrait
$sourceType = AccountType::where('type', AccountType::CASH)->first(); $sourceType = AccountType::where('type', AccountType::CASH)->first();
$sourceAccount = Account::firstOrCreateEncrypted( $sourceAccount = Account::firstOrCreateEncrypted(
['user_id' => $user->id, 'account_type_id' => $sourceType->id, 'name' => 'Cash account', 'active' => 1] ['user_id' => $user->id, 'account_type_id' => $sourceType->id, 'name' => 'Cash account']
); );
// always make account active
$sourceAccount->active = true;
$sourceAccount->save();
return [ return [
'source' => $sourceAccount, 'source' => $sourceAccount,
@ -161,10 +167,13 @@ trait SupportJournalsTrait
'user_id' => $user->id, 'user_id' => $user->id,
'account_type_id' => $destinationType->id, 'account_type_id' => $destinationType->id,
'name' => $data['destination_account_name'], 'name' => $data['destination_account_name'],
'active' => 1,
] ]
); );
// always make account active
$destinationAccount->active = true;
$destinationAccount->save();
Log::debug(sprintf('destination account name is "%s", account is %d', $data['destination_account_name'], $destinationAccount->id)); Log::debug(sprintf('destination account name is "%s", account is %d', $data['destination_account_name'], $destinationAccount->id));
return [ return [
@ -175,8 +184,11 @@ trait SupportJournalsTrait
Log::debug('destination_account_name is empty, so default to cash account!'); Log::debug('destination_account_name is empty, so default to cash account!');
$destinationType = AccountType::where('type', AccountType::CASH)->first(); $destinationType = AccountType::where('type', AccountType::CASH)->first();
$destinationAccount = Account::firstOrCreateEncrypted( $destinationAccount = Account::firstOrCreateEncrypted(
['user_id' => $user->id, 'account_type_id' => $destinationType->id, 'name' => 'Cash account', 'active' => 1] ['user_id' => $user->id, 'account_type_id' => $destinationType->id, 'name' => 'Cash account']
); );
// always make account active
$destinationAccount->active = true;
$destinationAccount->save();
return [ return [
'source' => $sourceAccount, 'source' => $sourceAccount,

View File

@ -0,0 +1,181 @@
<?php
/**
* LinkTypeRepository.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Repositories\LinkType;
use FireflyIII\Models\LinkType;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionJournalLink;
use FireflyIII\User;
use Illuminate\Support\Collection;
/**
* Class LinkTypeRepository
*
* @package FireflyIII\Repositories\LinkType
*/
class LinkTypeRepository implements LinkTypeRepositoryInterface
{
/** @var User */
private $user;
/**
* @param LinkType $linkType
*
* @return int
*/
public function countJournals(LinkType $linkType): int
{
return $linkType->transactionJournalLinks()->count() * 2;
}
/**
* @param LinkType $linkType
* @param LinkType $moveTo
*
* @return bool
*/
public function destroy(LinkType $linkType, LinkType $moveTo): bool
{
if (!is_null($moveTo->id)) {
TransactionJournalLink::where('link_type_id', $linkType->id)->update(['link_type_id' => $moveTo->id]);
}
$linkType->delete();
return true;
}
/**
* @param TransactionJournalLink $link
*
* @return bool
*/
public function destroyLink(TransactionJournalLink $link): bool
{
$link->delete();
return true;
}
/**
* @param int $id
*
* @return LinkType
*/
public function find(int $id): LinkType
{
$linkType = LinkType::find($id);
if (is_null($linkType)) {
return new LinkType;
}
return $linkType;
}
/**
* Check if link exists between journals.
*
* @param TransactionJournal $one
* @param TransactionJournal $two
*
* @return bool
*/
public function findLink(TransactionJournal $one, TransactionJournal $two): bool
{
$count = TransactionJournalLink::whereDestinationId($one->id)->whereSourceId($two->id)->count();
$opposingCount = TransactionJournalLink::whereDestinationId($two->id)->whereSourceId($one->id)->count();
return ($count + $opposingCount > 0);
}
/**
* @return Collection
*/
public function get(): Collection
{
return LinkType::orderBy('name', 'ASC')->get();
}
/**
* Return list of existing connections.
*
* @param TransactionJournal $journal
*
* @return Collection
*/
public function getLinks(TransactionJournal $journal): Collection
{
$outward = TransactionJournalLink::whereSourceId($journal->id)->get();
$inward = TransactionJournalLink::whereDestinationId($journal->id)->get();
return $outward->merge($inward);
}
/**
* @param User $user
*/
public function setUser(User $user)
{
$this->user = $user;
}
/**
* @param array $data
*
* @return LinkType
*/
public function store(array $data): LinkType
{
$linkType = new LinkType;
$linkType->name = $data['name'];
$linkType->inward = $data['inward'];
$linkType->outward = $data['outward'];
$linkType->editable = true;
$linkType->save();
return $linkType;
}
/**
* @param TransactionJournalLink $link
*
* @return bool
*/
public function switchLink(TransactionJournalLink $link): bool
{
$source = $link->source_id;
$link->source_id = $link->destination_id;
$link->destination_id = $source;
$link->save();
return true;
}
/**
* @param LinkType $linkType
* @param array $data
*
* @return LinkType
*/
public function update(LinkType $linkType, array $data): LinkType
{
$linkType->name = $data['name'];
$linkType->inward = $data['inward'];
$linkType->outward = $data['outward'];
$linkType->save();
return $linkType;
}
}

View File

@ -0,0 +1,102 @@
<?php
/**
* LinkTypeRepositoryInterface.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Repositories\LinkType;
use FireflyIII\Models\LinkType;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionJournalLink;
use Illuminate\Support\Collection;
/**
* Interface LinkTypeRepositoryInterface
*
* @package FireflyIII\Repositories\LinkType
*/
interface LinkTypeRepositoryInterface
{
/**
* @param LinkType $linkType
*
* @return int
*/
public function countJournals(LinkType $linkType): int;
/**
* @param LinkType $linkType
* @param LinkType $moveTo
*
* @return bool
*/
public function destroy(LinkType $linkType, LinkType $moveTo): bool;
/**
* @param TransactionJournalLink $link
*
* @return bool
*/
public function destroyLink(TransactionJournalLink $link): bool;
/**
* @param int $id
*
* @return LinkType
*/
public function find(int $id): LinkType;
/**
* Check if link exists between journals.
*
* @param TransactionJournal $one
* @param TransactionJournal $two
*
* @return bool
*/
public function findLink(TransactionJournal $one, TransactionJournal $two): bool;
/**
* @return Collection
*/
public function get(): Collection;
/**
* Return list of existing connections.
*
* @param TransactionJournal $journal
*
* @return Collection
*/
public function getLinks(TransactionJournal $journal): Collection;
/**
* @param array $data
*
* @return LinkType
*/
public function store(array $data): LinkType;
/**
* @param TransactionJournalLink $link
*
* @return bool
*/
public function switchLink(TransactionJournalLink $link): bool;
/**
* @param LinkType $linkType
* @param array $data
*
* @return LinkType
*/
public function update(LinkType $linkType, array $data): LinkType;
}

View File

@ -56,6 +56,21 @@ class RuleRepository implements RuleRepositoryInterface
return true; return true;
} }
/**
* @param int $ruleId
*
* @return Rule
*/
public function find(int $ruleId): Rule
{
$rule = $this->user->rules()->find($ruleId);
if (is_null($rule)) {
return new Rule;
}
return $rule;
}
/** /**
* FIxXME can return null * FIxXME can return null
* *
@ -286,7 +301,7 @@ class RuleRepository implements RuleRepositoryInterface
$ruleTrigger->active = 1; $ruleTrigger->active = 1;
$ruleTrigger->stop_processing = $values['stopProcessing']; $ruleTrigger->stop_processing = $values['stopProcessing'];
$ruleTrigger->trigger_type = $values['action']; $ruleTrigger->trigger_type = $values['action'];
$ruleTrigger->trigger_value = $values['value']; $ruleTrigger->trigger_value = is_null($values['value']) ? '' : $values['value'];
$ruleTrigger->save(); $ruleTrigger->save();
return $ruleTrigger; return $ruleTrigger;
@ -301,6 +316,7 @@ class RuleRepository implements RuleRepositoryInterface
public function update(Rule $rule, array $data): Rule public function update(Rule $rule, array $data): Rule
{ {
// update rule: // update rule:
$rule->rule_group_id = $data['rule_group_id'];
$rule->active = $data['active']; $rule->active = $data['active'];
$rule->stop_processing = $data['stop_processing']; $rule->stop_processing = $data['stop_processing'];
$rule->title = $data['title']; $rule->title = $data['title'];

View File

@ -38,6 +38,14 @@ interface RuleRepositoryInterface
*/ */
public function destroy(Rule $rule): bool; public function destroy(Rule $rule): bool;
/**
* @param int $ruleId
*
* @return Rule
*/
public function find(int $ruleId): Rule;
/** /**
* @return RuleGroup * @return RuleGroup
*/ */

View File

@ -186,6 +186,34 @@ class TagRepository implements TagRepositoryInterface
return new Carbon; return new Carbon;
} }
/**
* Same as sum of tag but substracts income instead of adding it as well.
*
* @param Tag $tag
* @param Carbon|null $start
* @param Carbon|null $end
*
* @return string
*/
public function resultOfTag(Tag $tag, ?Carbon $start, ?Carbon $end): string
{
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
if (!is_null($start) && !is_null($end)) {
$collector->setRange($start, $end);
}
$collector->setAllAssetAccounts()->setTag($tag);
$journals = $collector->getJournals();
$sum = '0';
foreach ($journals as $journal) {
$sum = bcadd($sum, strval($journal->transaction_amount));
}
return strval($sum);
}
/** /**
* @param User $user * @param User $user
*/ */
@ -253,11 +281,71 @@ class TagRepository implements TagRepositoryInterface
} }
$collector->setAllAssetAccounts()->setTag($tag); $collector->setAllAssetAccounts()->setTag($tag);
$sum = $collector->getJournals()->sum('transaction_amount'); $journals = $collector->getJournals();
$sum = '0';
foreach ($journals as $journal) {
$sum = bcadd($sum, app('steam')->positive(strval($journal->transaction_amount)));
}
return strval($sum); return strval($sum);
} }
/**
* Generates a tag cloud.
*
* @param int|null $year
*
* @return array
*/
public function tagCloud(?int $year): array
{
$min = null;
$max = 0;
$query = $this->user->tags();
$return = [];
Log::debug('Going to build tag-cloud');
if (!is_null($year)) {
Log::debug(sprintf('Year is not null: %d', $year));
$start = $year . '-01-01';
$end = $year . '-12-31';
$query->where('date', '>=', $start)->where('date', '<=', $end);
}
if (is_null($year)) {
$query->whereNull('date');
Log::debug('Year is NULL');
}
$tags = $query->orderBy('id', 'desc')->get();
$temporary = [];
Log::debug(sprintf('Found %d tags', $tags->count()));
/** @var Tag $tag */
foreach ($tags as $tag) {
$amount = floatval($this->sumOfTag($tag, null, null));
$min = $amount < $min || is_null($min) ? $amount : $min;
$max = $amount > $max ? $amount : $max;
$temporary[] = [
'amount' => $amount,
'tag' => $tag,
];
Log::debug(sprintf('Now working on tag %s with total amount %s', $tag->tag, $amount));
Log::debug(sprintf('Minimum is now %f, maximum is %f', $min, $max));
}
/** @var array $entry */
foreach ($temporary as $entry) {
$scale = $this->cloudScale([12, 20], $entry['amount'], $min, $max);
$tagId = $entry['tag']->id;
$return[$tagId] = [
'scale' => $scale,
'tag' => $entry['tag'],
];
}
Log::debug('DONE with tagcloud');
return $return;
}
/** /**
* @param Tag $tag * @param Tag $tag
* @param array $data * @param array $data
@ -277,4 +365,38 @@ class TagRepository implements TagRepositoryInterface
return $tag; return $tag;
} }
/**
* @param array $range
* @param float $amount
* @param float $min
* @param float $max
*
* @return int
*/
private function cloudScale(array $range, float $amount, float $min, float $max): int
{
Log::debug(sprintf('Now in cloudScale with %s as amount and %f min, %f max', $amount, $min, $max));
$amountDiff = $max - $min;
Log::debug(sprintf('AmountDiff is %f', $amountDiff));
// no difference? Every tag same range:
if ($amountDiff === 0.0) {
Log::debug(sprintf('AmountDiff is zero, return %d', $range[0]));
return $range[0];
}
$diff = $range[1] - $range[0];
$step = 1;
if ($diff != 0) {
$step = $amountDiff / $diff;
}
if ($step == 0) {
$step = 1;
}
$extra = round($amount / $step);
return intval($range[0] + $extra);
}
} }

View File

@ -103,6 +103,15 @@ interface TagRepositoryInterface
*/ */
public function lastUseDate(Tag $tag): Carbon; public function lastUseDate(Tag $tag): Carbon;
/**
* @param Tag $tag
* @param Carbon|null $start
* @param Carbon|null $end
*
* @return string
*/
public function resultOfTag(Tag $tag, ?Carbon $start, ?Carbon $end): string;
/** /**
* @param User $user * @param User $user
*/ */
@ -135,6 +144,15 @@ interface TagRepositoryInterface
*/ */
public function sumOfTag(Tag $tag, ?Carbon $start, ?Carbon $end): string; public function sumOfTag(Tag $tag, ?Carbon $start, ?Carbon $end): string;
/**
* Generates a tag cloud.
*
* @param int|null $year
*
* @return array
*/
public function tagCloud(?int $year): array;
/** /**
* Update a tag. * Update a tag.
* *

View File

@ -118,7 +118,8 @@ final class Processor
{ {
$self = new self; $self = new self;
foreach ($triggers as $entry) { foreach ($triggers as $entry) {
$trigger = TriggerFactory::makeTriggerFromStrings($entry['type'], $entry['value'], $entry['stopProcessing']); $entry['value'] = is_null($entry['value']) ? '' : $entry['value'];
$trigger = TriggerFactory::makeTriggerFromStrings($entry['type'], $entry['value'], $entry['stopProcessing']);
$self->triggers->push($trigger); $self->triggers->push($trigger);
} }

View File

@ -0,0 +1,65 @@
<?php
/**
* HasAnyBudget.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Rules\Triggers;
use FireflyIII\Models\TransactionJournal;
use Log;
/**
* Class HasAnyBudget
*
* @package FireflyIII\Rules\Triggers
*/
final class HasAnyBudget extends AbstractTrigger implements TriggerInterface
{
/**
* A trigger is said to "match anything", or match any given transaction,
* when the trigger value is very vague or has no restrictions. Easy examples
* are the "AmountMore"-trigger combined with an amount of 0: any given transaction
* has an amount of more than zero! Other examples are all the "Description"-triggers
* which have hard time handling empty trigger values such as "" or "*" (wild cards).
*
* If the user tries to create such a trigger, this method MUST return true so Firefly III
* can stop the storing / updating the trigger. If the trigger is in any way restrictive
* (even if it will still include 99.9% of the users transactions), this method MUST return
* false.
*
* @param null $value
*
* @return bool
*/
public static function willMatchEverything($value = null)
{
return false;
}
/**
* @param TransactionJournal $journal
*
* @return bool
*/
public function triggered(TransactionJournal $journal): bool
{
$count = $journal->budgets()->count();
if ($count > 0) {
Log::debug(sprintf('RuleTrigger HasAnyBudget for journal #%d: count is %d, return true.', $journal->id, $count));
return true;
}
Log::debug(sprintf('RuleTrigger HasAnyBudget for journal #%d: count is %d, return false.', $journal->id, $count));
return false;
}
}

View File

@ -0,0 +1,65 @@
<?php
/**
* HasAnyCategory.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Rules\Triggers;
use FireflyIII\Models\TransactionJournal;
use Log;
/**
* Class HasAnyCategory
*
* @package FireflyIII\Rules\Triggers
*/
final class HasAnyCategory extends AbstractTrigger implements TriggerInterface
{
/**
* A trigger is said to "match anything", or match any given transaction,
* when the trigger value is very vague or has no restrictions. Easy examples
* are the "AmountMore"-trigger combined with an amount of 0: any given transaction
* has an amount of more than zero! Other examples are all the "Description"-triggers
* which have hard time handling empty trigger values such as "" or "*" (wild cards).
*
* If the user tries to create such a trigger, this method MUST return true so Firefly III
* can stop the storing / updating the trigger. If the trigger is in any way restrictive
* (even if it will still include 99.9% of the users transactions), this method MUST return
* false.
*
* @param null $value
*
* @return bool
*/
public static function willMatchEverything($value = null)
{
return false;
}
/**
* @param TransactionJournal $journal
*
* @return bool
*/
public function triggered(TransactionJournal $journal): bool
{
$count = $journal->categories()->count();
if ($count > 0) {
Log::debug(sprintf('RuleTrigger HasAnyCategory for journal #%d: count is %d, return true.', $journal->id, $count));
return true;
}
Log::debug(sprintf('RuleTrigger HasAnyCategory for journal #%d: count is %d, return false.', $journal->id, $count));
return false;
}
}

View File

@ -0,0 +1,65 @@
<?php
/**
* HasAnyTag.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Rules\Triggers;
use FireflyIII\Models\TransactionJournal;
use Log;
/**
* Class HasAnyTag
*
* @package FireflyIII\Rules\Triggers
*/
final class HasAnyTag extends AbstractTrigger implements TriggerInterface
{
/**
* A trigger is said to "match anything", or match any given transaction,
* when the trigger value is very vague or has no restrictions. Easy examples
* are the "AmountMore"-trigger combined with an amount of 0: any given transaction
* has an amount of more than zero! Other examples are all the "Description"-triggers
* which have hard time handling empty trigger values such as "" or "*" (wild cards).
*
* If the user tries to create such a trigger, this method MUST return true so Firefly III
* can stop the storing / updating the trigger. If the trigger is in any way restrictive
* (even if it will still include 99.9% of the users transactions), this method MUST return
* false.
*
* @param null $value
*
* @return bool
*/
public static function willMatchEverything($value = null)
{
return false;
}
/**
* @param TransactionJournal $journal
*
* @return bool
*/
public function triggered(TransactionJournal $journal): bool
{
$count = $journal->tags()->count();
if ($count > 0) {
Log::debug(sprintf('RuleTrigger HasAnyTag for journal #%d: count is %d, return true.', $journal->id, $count));
return true;
}
Log::debug(sprintf('RuleTrigger HasAnyTag for journal #%d: count is %d, return false.', $journal->id, $count));
return false;
}
}

View File

@ -0,0 +1,65 @@
<?php
/**
* HasNoBudget.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Rules\Triggers;
use FireflyIII\Models\TransactionJournal;
use Log;
/**
* Class HasNoBudget
*
* @package FireflyIII\Rules\Triggers
*/
final class HasNoBudget extends AbstractTrigger implements TriggerInterface
{
/**
* A trigger is said to "match anything", or match any given transaction,
* when the trigger value is very vague or has no restrictions. Easy examples
* are the "AmountMore"-trigger combined with an amount of 0: any given transaction
* has an amount of more than zero! Other examples are all the "Description"-triggers
* which have hard time handling empty trigger values such as "" or "*" (wild cards).
*
* If the user tries to create such a trigger, this method MUST return true so Firefly III
* can stop the storing / updating the trigger. If the trigger is in any way restrictive
* (even if it will still include 99.9% of the users transactions), this method MUST return
* false.
*
* @param null $value
*
* @return bool
*/
public static function willMatchEverything($value = null)
{
return false;
}
/**
* @param TransactionJournal $journal
*
* @return bool
*/
public function triggered(TransactionJournal $journal): bool
{
$count = $journal->budgets()->count();
if ($count === 0) {
Log::debug(sprintf('RuleTrigger HasNoBudget for journal #%d: count is %d, return true.', $journal->id, $count));
return true;
}
Log::debug(sprintf('RuleTrigger HasNoBudget for journal #%d: count is %d, return false.', $journal->id, $count));
return false;
}
}

View File

@ -0,0 +1,65 @@
<?php
/**
* HasNoCategory.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Rules\Triggers;
use FireflyIII\Models\TransactionJournal;
use Log;
/**
* Class HasNoCategory
*
* @package FireflyIII\Rules\Triggers
*/
final class HasNoCategory extends AbstractTrigger implements TriggerInterface
{
/**
* A trigger is said to "match anything", or match any given transaction,
* when the trigger value is very vague or has no restrictions. Easy examples
* are the "AmountMore"-trigger combined with an amount of 0: any given transaction
* has an amount of more than zero! Other examples are all the "Description"-triggers
* which have hard time handling empty trigger values such as "" or "*" (wild cards).
*
* If the user tries to create such a trigger, this method MUST return true so Firefly III
* can stop the storing / updating the trigger. If the trigger is in any way restrictive
* (even if it will still include 99.9% of the users transactions), this method MUST return
* false.
*
* @param null $value
*
* @return bool
*/
public static function willMatchEverything($value = null)
{
return false;
}
/**
* @param TransactionJournal $journal
*
* @return bool
*/
public function triggered(TransactionJournal $journal): bool
{
$count = $journal->categories()->count();
if ($count === 0) {
Log::debug(sprintf('RuleTrigger HasNoCategory for journal #%d: count is %d, return true.', $journal->id, $count));
return true;
}
Log::debug(sprintf('RuleTrigger HasNoCategory for journal #%d: count is %d, return false.', $journal->id, $count));
return false;
}
}

View File

@ -0,0 +1,65 @@
<?php
/**
* HasNoTag.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Rules\Triggers;
use FireflyIII\Models\TransactionJournal;
use Log;
/**
* Class HasNoTag
*
* @package FireflyIII\Rules\Triggers
*/
final class HasNoTag extends AbstractTrigger implements TriggerInterface
{
/**
* A trigger is said to "match anything", or match any given transaction,
* when the trigger value is very vague or has no restrictions. Easy examples
* are the "AmountMore"-trigger combined with an amount of 0: any given transaction
* has an amount of more than zero! Other examples are all the "Description"-triggers
* which have hard time handling empty trigger values such as "" or "*" (wild cards).
*
* If the user tries to create such a trigger, this method MUST return true so Firefly III
* can stop the storing / updating the trigger. If the trigger is in any way restrictive
* (even if it will still include 99.9% of the users transactions), this method MUST return
* false.
*
* @param null $value
*
* @return bool
*/
public static function willMatchEverything($value = null)
{
return false;
}
/**
* @param TransactionJournal $journal
*
* @return bool
*/
public function triggered(TransactionJournal $journal): bool
{
$count = $journal->tags()->count();
if ($count === 0) {
Log::debug(sprintf('RuleTrigger HasNoTag for journal #%d: count is %d, return true.', $journal->id, $count));
return true;
}
Log::debug(sprintf('RuleTrigger HasNoTag for journal #%d: count is %d, return false.', $journal->id, $count));
return false;
}
}

View File

@ -0,0 +1,43 @@
<?php
/**
* BunqId.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Id;
/**
* Class BunqId
*
* @package FireflyIII\Services\Bunq\Id
*/
class BunqId
{
/** @var int */
private $id = 0;
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
/**
* @param int $id
*/
public function setId(int $id)
{
$this->id = $id;
}
}

View File

@ -0,0 +1,25 @@
<?php
/**
* DeviceServerId.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Id;
/**
* Class DeviceServerId
*
* @package Bunq\Id
*/
class DeviceServerId extends BunqId
{
}

View File

@ -0,0 +1,24 @@
<?php
/**
* DeviceSessionId.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Id;
/**
* Class DeviceSessionId
*
* @package Bunq\Id
*/
class DeviceSessionId extends BunqId
{
}

View File

@ -0,0 +1,24 @@
<?php
/**
* InstallationId.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Id;
/**
* Class InstallationId
*
* @package Bunq\Id
*/
class InstallationId extends BunqId
{
}

View File

@ -0,0 +1,68 @@
<?php
/**
* Alias.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Object;
/**
* Class Alias
*
* @package FireflyIII\Services\Bunq\Object
*/
class Alias extends BunqObject
{
/** @var string */
private $name = '';
/** @var string */
private $type = '';
/** @var string */
private $value = '';
/**
* Alias constructor.
*
* @param array $data
*/
public function __construct(array $data)
{
$this->type = $data['type'];
$this->name = $data['name'];
$this->value = $data['value'];
return;
}
/**
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* @return string
*/
public function getValue(): string
{
return $this->value;
}
}

View File

@ -0,0 +1,57 @@
<?php
/**
* Currency.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Object;
/**
* Class Amount
*
* @package FireflyIII\Services\Bunq\Object
*/
class Amount extends BunqObject
{
/** @var string */
private $currency = '';
/** @var string */
private $value = '';
/**
* Amount constructor.
*
* @param array $data
*/
public function __construct(array $data)
{
$this->currency = $data['currency'];
$this->value = $data['value'];
return;
}
/**
* @return string
*/
public function getCurrency(): string
{
return $this->currency;
}
/**
* @return string
*/
public function getValue(): string
{
return $this->value;
}
}

View File

@ -0,0 +1,23 @@
<?php
/**
* Avatar.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Object;
/**
* Class Avatar
*
* @package FireflyIII\Services\Bunq\Object
*/
class Avatar extends BunqObject
{
}

View File

@ -0,0 +1,23 @@
<?php
/**
* BunqObject.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Object;
/**
* Class BunqObject
*
* @package FireflyIII\Services\Bunq\Object
*/
class BunqObject
{
}

View File

@ -0,0 +1,63 @@
<?php
/**
* DeviceServer.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Object;
use Carbon\Carbon;
use FireflyIII\Services\Bunq\Id\DeviceServerId;
class DeviceServer extends BunqObject
{
/** @var Carbon */
private $created;
/** @var string */
private $description;
/** @var DeviceServerId */
private $id;
/** @var string */
private $ip;
/** @var string */
private $status;
/** @var Carbon */
private $updated;
public function __construct(array $data)
{
$id = new DeviceServerId();
$id->setId($data['id']);
$this->id = $id;
$this->created = Carbon::createFromFormat('Y-m-d H:i:s.u', $data['created']);
$this->updated = Carbon::createFromFormat('Y-m-d H:i:s.u', $data['updated']);
$this->ip = $data['ip'];
$this->description = $data['description'];
$this->status = $data['status'];
}
/**
* @return DeviceServerId
*/
public function getId(): DeviceServerId
{
return $this->id;
}
/**
* @return string
*/
public function getIp(): string
{
return $this->ip;
}
}

View File

@ -0,0 +1,151 @@
<?php
/**
* MonetaryAccountBank.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Object;
use Carbon\Carbon;
/**
* Class MonetaryAccountBank
*
* @package FireflyIII\Services\Bunq\Object
*/
class MonetaryAccountBank extends BunqObject
{
/** @var array */
private $aliases = [];
/** @var Avatar */
private $avatar;
/** @var Amount */
private $balance;
/** @var Carbon */
private $created;
/** @var string */
private $currency = '';
/** @var Amount */
private $dailyLimit;
/** @var Amount */
private $dailySpent;
/** @var string */
private $description = '';
/** @var int */
private $id = 0;
/** @var MonetaryAccountProfile */
private $monetaryAccountProfile;
/** @var array */
private $notificationFilters = [];
/** @var Amount */
private $overdraftLimit;
/** @var string */
private $publicUuid = '';
/** @var string */
private $reason = '';
/** @var string */
private $reasonDescription = '';
/** @var MonetaryAccountSetting */
private $setting;
/** @var string */
private $status = '';
/** @var string */
private $subStatus = '';
/** @var Carbon */
private $updated;
/** @var int */
private $userId = 0;
/**
* MonetaryAccountBank constructor.
*
* @param array $data
*/
public function __construct(array $data)
{
$this->id = $data['id'];
$this->created = Carbon::createFromFormat('Y-m-d H:i:s.u', $data['created']);
$this->updated = Carbon::createFromFormat('Y-m-d H:i:s.u', $data['updated']);
$this->balance = new Amount($data['balance']);
$this->currency = $data['currency'];
$this->dailyLimit = new Amount($data['daily_limit']);
$this->dailySpent = new Amount($data['daily_spent']);
$this->description = $data['description'];
$this->publicUuid = $data['public_uuid'];
$this->status = $data['status'];
$this->subStatus = $data['sub_status'];
$this->userId = $data['user_id'];
$this->status = $data['status'];
$this->subStatus = $data['sub_status'];
$this->monetaryAccountProfile = new MonetaryAccountProfile($data['monetary_account_profile']);
$this->setting = new MonetaryAccountSetting($data['setting']);
$this->overdraftLimit = new Amount($data['overdraft_limit']);
$this->publicUuid = $data['public_uuid'];
// create aliases:
foreach ($data['alias'] as $alias) {
$this->aliases[] = new Alias($alias);
}
foreach ($data['notification_filters'] as $filter) {
$this->notificationFilters = new NotificationFilter($filter);
}
return;
}
/**
* @return array
*/
public function getAliases(): array
{
return $this->aliases;
}
/**
* @return Amount
*/
public function getBalance(): Amount
{
return $this->balance;
}
/**
* @return string
*/
public function getCurrency(): string
{
return $this->currency;
}
/**
* @return string
*/
public function getDescription(): string
{
return $this->description;
}
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
/**
* @return MonetaryAccountSetting
*/
public function getSetting(): MonetaryAccountSetting
{
return $this->setting;
}
}

View File

@ -0,0 +1,44 @@
<?php
/**
* MonetaryAccountProfile.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Object;
/**
* Class MonetaryAccountProfile
*
* @package FireflyIII\Services\Bunq\Object
*/
class MonetaryAccountProfile extends BunqObject
{
/** @var string */
private $profileActionRequired = '';
/** @var Amount */
private $profileAmountRequired;
private $profileDrain;
private $profileFill;
/**
* MonetaryAccountProfile constructor.
*
* @param array $data
*/
public function __construct(array $data)
{
$this->profileDrain = null;
$this->profileFill = null;
$this->profileActionRequired = $data['profile_action_required'];
$this->profileAmountRequired = new Amount($data['profile_amount_required']);
return;
}
}

View File

@ -0,0 +1,68 @@
<?php
/**
* MonetaryAccountSetting.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Object;
/**
* Class MonetaryAccountSetting
*
* @package FireflyIII\Services\Bunq\Object
*/
class MonetaryAccountSetting extends BunqObject
{
/** @var string */
private $color = '';
/** @var string */
private $defaultAvatarStatus = '';
/** @var string */
private $restrictionChat = '';
/**
* MonetaryAccountSetting constructor.
*
* @param array $data
*/
public function __construct(array $data)
{
$this->color = $data['color'];
$this->defaultAvatarStatus = $data['default_avatar_status'];
$this->restrictionChat = $data['restriction_chat'];
return;
}
/**
* @return string
*/
public function getColor(): string
{
return $this->color;
}
/**
* @return string
*/
public function getDefaultAvatarStatus(): string
{
return $this->defaultAvatarStatus;
}
/**
* @return string
*/
public function getRestrictionChat(): string
{
return $this->restrictionChat;
}
}

View File

@ -0,0 +1,31 @@
<?php
/**
* NotificationFilter.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Object;
/**
* Class NotificationFilter
*
* @package FireflyIII\Services\Bunq\Object
*/
class NotificationFilter extends BunqObject
{
/**
* NotificationFilter constructor.
*
* @param array $data
*/
public function __construct(array $data)
{
}
}

View File

@ -0,0 +1,52 @@
<?php
/**
* ServerPublicKey.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Object;
/**
* Class ServerPublicKey
*
* @package Bunq\Object
*/
class ServerPublicKey extends BunqObject
{
/** @var string */
private $publicKey = '';
/**
* ServerPublicKey constructor.
*
* @param array $response
*/
public function __construct(array $response)
{
$this->publicKey = $response['server_public_key'];
}
/**
* @return string
*/
public function getPublicKey(): string
{
return $this->publicKey;
}
/**
* @param string $publicKey
*/
public function setPublicKey(string $publicKey)
{
$this->publicKey = $publicKey;
}
}

View File

@ -0,0 +1,106 @@
<?php
/**
* UserCompany.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Object;
use Carbon\Carbon;
/**
* Class UserCompany
*
* @package FireflyIII\Services\Bunq\Object
*/
class UserCompany extends BunqObject
{
private $addressMain;
private $addressPostal;
/** @var array */
private $aliases = [];
private $avatar;
/** @var string */
private $cocNumber = '';
/** @var string */
private $counterBankIban = '';
/** @var Carbon */
private $created;
private $dailyLimit;
private $directorAlias;
/** @var string */
private $displayName = '';
/** @var int */
private $id = 0;
/** @var string */
private $language = '';
/** @var string */
private $name = '';
/** @var array */
private $notificationFilters = [];
/** @var string */
private $publicNickName = '';
/** @var string */
private $publicUuid = '';
/** @var string */
private $region = '';
/** @var string */
private $sectorOfIndustry = '';
/** @var int */
private $sessionTimeout = 0;
/** @var string */
private $status = '';
/** @var string */
private $subStatus = '';
/** @var string */
private $typeOfBusinessEntity = '';
/** @var array */
private $ubos = [];
/** @var Carbon */
private $updated;
/** @var int */
private $versionTos = 0;
/**
* UserCompany constructor.
*
* @param array $data
*/
public function __construct(array $data)
{
$this->id = intval($data['id']);
$this->created = Carbon::createFromFormat('Y-m-d H:i:s.u', $data['created']);
$this->updated = Carbon::createFromFormat('Y-m-d H:i:s.u', $data['updated']);
$this->status = $data['status'];
$this->subStatus = $data['sub_status'];
$this->publicUuid = $data['public_uuid'];
$this->displayName = $data['display_name'];
$this->publicNickName = $data['public_nick_name'];
$this->language = $data['language'];
$this->region = $data['region'];
$this->sessionTimeout = intval($data['session_timeout']);
$this->versionTos = intval($data['version_terms_of_service']);
$this->cocNumber = $data['chamber_of_commerce_number'];
$this->typeOfBusinessEntity = $data['type_of_business_entity'] ?? '';
$this->sectorOfIndustry = $data['sector_of_industry'] ?? '';
$this->counterBankIban = $data['counter_bank_iban'];
$this->name = $data['name'];
}
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
}

View File

@ -0,0 +1,70 @@
<?php
/**
* UserLight.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Object;
use Carbon\Carbon;
/**
* Class UserLight
*
* @package FireflyIII\Services\Bunq\Object
*/
class UserLight extends BunqObject
{
/** @var array */
private $aliases = [];
/** @var Carbon */
private $created;
/** @var string */
private $displayName = '';
/** @var string */
private $firstName = '';
/** @var int */
private $id = 0;
/** @var string */
private $lastName = '';
/** @var string */
private $legalName = '';
/** @var string */
private $middleName = '';
/** @var string */
private $publicNickName = '';
/** @var string */
private $publicUuid = '';
/** @var Carbon */
private $updated;
/**
* UserLight constructor.
*
* @param array $data
*/
public function __construct(array $data)
{
if (count($data) === 0) {
return;
}
$this->id = intval($data['id']);
$this->created = Carbon::createFromFormat('Y-m-d H:i:s.u', $data['created']);
$this->updated = Carbon::createFromFormat('Y-m-d H:i:s.u', $data['updated']);
$this->publicUuid = $data['public_uuid'];
$this->displayName = $data['display_name'];
$this->publicNickName = $data['public_nick_name'];
$this->firstName = $data['first_name'];
$this->middleName = $data['middle_name'];
$this->lastName = $data['last_name'];
$this->legalName = $data['legal_name'];
// aliases
}
}

View File

@ -0,0 +1,141 @@
<?php
/**
* UserPerson.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Object;
use Carbon\Carbon;
/**
* Class UserPerson
*
* @package Bunq\Object
*/
class UserPerson extends BunqObject
{
private $addressMain;
private $addressPostal;
/** @var array */
private $aliases = [];
private $avatar;
/** @var array */
private $billingContracts = [];
/** @var string */
private $countryOfBirth = '';
/** @var Carbon */
private $created;
private $customer;
private $customerLimit;
private $dailyLimit;
/** @var Carbon */
private $dateOfBirth;
/** @var string */
private $displayName = '';
/** @var string */
private $documentCountry = '';
/** @var string */
private $documentNumber = '';
/** @var string */
private $documentType = '';
/** @var string */
private $firstName = '';
/** @var string */
private $gender = '';
/** @var int */
private $id = 0;
/** @var string */
private $language = '';
/** @var string */
private $lastName = '';
/** @var string */
private $legalName = '';
/** @var string */
private $middleName = '';
/** @var string */
private $nationality = '';
/** @var array */
private $notificationFilters = [];
/** @var string */
private $placeOfBirth = '';
/** @var string */
private $publicNickName = '';
/** @var string */
private $publicUuid = '';
private $region;
/** @var int */
private $sessionTimeout = 0;
/** @var string */
private $status = '';
/** @var string */
private $subStatus = '';
/** @var string */
private $taxResident = '';
/** @var Carbon */
private $updated;
/** @var int */
private $versionTos = 0;
/**
* UserPerson constructor.
*
* @param array $data
*/
public function __construct(array $data)
{
if (count($data) === 0) {
return;
}
$this->id = intval($data['id']);
$this->created = Carbon::createFromFormat('Y-m-d H:i:s.u', $data['created']);
$this->updated = Carbon::createFromFormat('Y-m-d H:i:s.u', $data['updated']);
$this->status = $data['status'];
$this->subStatus = $data['sub_status'];
$this->publicUuid = $data['public_uuid'];
$this->displayName = $data['display_name'];
$this->publicNickName = $data['public_nick_name'];
$this->language = $data['language'];
$this->region = $data['region'];
$this->sessionTimeout = intval($data['session_timeout']);
$this->firstName = $data['first_name'];
$this->middleName = $data['middle_name'];
$this->lastName = $data['last_name'];
$this->legalName = $data['legal_name'];
$this->taxResident = $data['tax_resident'];
$this->dateOfBirth = Carbon::createFromFormat('Y-m-d', $data['date_of_birth']);
$this->placeOfBirth = $data['place_of_birth'];
$this->countryOfBirth = $data['country_of_birth'];
$this->nationality = $data['nationality'];
$this->gender = $data['gender'];
$this->versionTos = intval($data['version_terms_of_service']);
$this->documentNumber = $data['document_number'];
$this->documentType = $data['document_type'];
$this->documentCountry = $data['document_country_of_issuance'];
// create aliases
// create avatar
// create daily limit
// create notification filters
// create address main, postal
// document front, back attachment
// customer, customer_limit
// billing contracts
}
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
}

View File

@ -0,0 +1,451 @@
<?php
/**
* BunqRequest.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Request;
use Exception;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Services\Bunq\Object\ServerPublicKey;
use Log;
use Requests;
use Requests_Exception;
/**
* Class BunqRequest
*
* @package Bunq\Request
*/
abstract class BunqRequest
{
/** @var string */
protected $secret = '';
/** @var string */
private $privateKey = '';
/** @var string */
private $server = '';
/** @var ServerPublicKey */
private $serverPublicKey;
private $upperCaseHeaders
= [
'x-bunq-client-response-id' => 'X-Bunq-Client-Response-Id',
'x-bunq-client-request-id' => 'X-Bunq-Client-Request-Id',
];
/**
* BunqRequest constructor.
*/
public function __construct()
{
$this->server = config('firefly.bunq.server');
}
/**
*
*/
abstract public function call(): void;
/**
* @return string
*/
public function getServer(): string
{
return $this->server;
}
/**
* @param string $privateKey
*/
public function setPrivateKey(string $privateKey)
{
$this->privateKey = $privateKey;
}
/**
* @param string $secret
*/
public function setSecret(string $secret)
{
$this->secret = $secret;
}
/**
* @param ServerPublicKey $serverPublicKey
*/
public function setServerPublicKey(ServerPublicKey $serverPublicKey)
{
$this->serverPublicKey = $serverPublicKey;
}
/**
* @param string $method
* @param string $uri
* @param array $headers
* @param string $data
*
* @return string
* @throws FireflyException
*/
protected function generateSignature(string $method, string $uri, array $headers, string $data): string
{
if (strlen($this->privateKey) === 0) {
throw new FireflyException('No private key present.');
}
if (strtolower($method) === 'get' || strtolower($method) === 'delete') {
$data = '';
}
$uri = str_replace(['https://api.bunq.com', 'https://sandbox.public.api.bunq.com'], '', $uri);
$toSign = sprintf("%s %s\n", strtoupper($method), $uri);
$headersToSign = ['Cache-Control', 'User-Agent'];
ksort($headers);
foreach ($headers as $name => $value) {
if (in_array($name, $headersToSign) || substr($name, 0, 7) === 'X-Bunq-') {
$toSign .= sprintf("%s: %s\n", $name, $value);
}
}
$toSign .= "\n" . $data;
$signature = '';
openssl_sign($toSign, $signature, $this->privateKey, OPENSSL_ALGO_SHA256);
$signature = base64_encode($signature);
return $signature;
}
/**
* @param string $key
* @param array $response
*
* @return array
*/
protected function getArrayFromResponse(string $key, array $response): array
{
$result = [];
if (isset($response['Response'])) {
foreach ($response['Response'] as $entry) {
$currentKey = key($entry);
$data = current($entry);
if ($currentKey === $key) {
$result[] = $data;
}
}
}
return $result;
}
protected function getDefaultHeaders(): array
{
$userAgent = sprintf('FireflyIII v%s', config('firefly.version'));
return [
'X-Bunq-Client-Request-Id' => uniqid('FFIII'),
'Cache-Control' => 'no-cache',
'User-Agent' => $userAgent,
'X-Bunq-Language' => 'en_US',
'X-Bunq-Region' => 'nl_NL',
'X-Bunq-Geolocation' => '0 0 0 0 NL',
];
}
/**
* @param string $key
* @param array $response
*
* @return array
*/
protected function getKeyFromResponse(string $key, array $response): array
{
if (isset($response['Response'])) {
foreach ($response['Response'] as $entry) {
$currentKey = key($entry);
$data = current($entry);
if ($currentKey === $key) {
return $data;
}
}
}
return [];
}
/**
* @param string $uri
* @param array $headers
*
* @return array
* @throws Exception
*/
protected function sendSignedBunqDelete(string $uri, array $headers): array
{
if (strlen($this->server) === 0) {
throw new FireflyException('No bunq server defined');
}
$fullUri = $this->server . $uri;
$signature = $this->generateSignature('delete', $uri, $headers, '');
$headers['X-Bunq-Client-Signature'] = $signature;
try {
$response = Requests::delete($fullUri, $headers);
} catch (Requests_Exception $e) {
return ['Error' => [0 => ['error_description' => $e->getMessage(), 'error_description_translated' => $e->getMessage()],]];
}
$body = $response->body;
$array = json_decode($body, true);
$responseHeaders = $response->headers->getAll();
$statusCode = $response->status_code;
$array['ResponseHeaders'] = $responseHeaders;
$array['ResponseStatusCode'] = $statusCode;
Log::debug(sprintf('Response to DELETE %s is %s', $fullUri, $body));
if ($this->isErrorResponse($array)) {
$this->throwResponseError($array);
}
if (!$this->verifyServerSignature($body, $responseHeaders, $statusCode)) {
throw new FireflyException(sprintf('Could not verify signature for request to "%s"', $uri));
}
return $array;
}
/**
* @param string $uri
* @param array $data
* @param array $headers
*
* @return array
* @throws Exception
*/
protected function sendSignedBunqGet(string $uri, array $data, array $headers): array
{
if (strlen($this->server) === 0) {
throw new FireflyException('No bunq server defined');
}
$body = json_encode($data);
$fullUri = $this->server . $uri;
$signature = $this->generateSignature('get', $uri, $headers, $body);
$headers['X-Bunq-Client-Signature'] = $signature;
try {
$response = Requests::get($fullUri, $headers);
} catch (Requests_Exception $e) {
return ['Error' => [0 => ['error_description' => $e->getMessage(), 'error_description_translated' => $e->getMessage()],]];
}
$body = $response->body;
$array = json_decode($body, true);
$responseHeaders = $response->headers->getAll();
$statusCode = $response->status_code;
$array['ResponseHeaders'] = $responseHeaders;
$array['ResponseStatusCode'] = $statusCode;
if ($this->isErrorResponse($array)) {
$this->throwResponseError($array);
}
if (!$this->verifyServerSignature($body, $responseHeaders, $statusCode)) {
throw new FireflyException(sprintf('Could not verify signature for request to "%s"', $uri));
}
return $array;
}
/**
* @param string $uri
* @param array $data
* @param array $headers
*
* @return array
* @throws Exception
*/
protected function sendSignedBunqPost(string $uri, array $data, array $headers): array
{
$body = json_encode($data);
$fullUri = $this->server . $uri;
$signature = $this->generateSignature('post', $uri, $headers, $body);
$headers['X-Bunq-Client-Signature'] = $signature;
try {
$response = Requests::post($fullUri, $headers, $body);
} catch (Requests_Exception $e) {
return ['Error' => [0 => ['error_description' => $e->getMessage(), 'error_description_translated' => $e->getMessage()],]];
}
$body = $response->body;
$array = json_decode($body, true);
$responseHeaders = $response->headers->getAll();
$statusCode = $response->status_code;
$array['ResponseHeaders'] = $responseHeaders;
$array['ResponseStatusCode'] = $statusCode;
if ($this->isErrorResponse($array)) {
$this->throwResponseError($array);
}
if (!$this->verifyServerSignature($body, $responseHeaders, $statusCode)) {
throw new FireflyException(sprintf('Could not verify signature for request to "%s"', $uri));
}
return $array;
}
/**
* @param string $uri
* @param array $headers
*
* @return array
*/
protected function sendUnsignedBunqDelete(string $uri, array $headers): array
{
$fullUri = $this->server . $uri;
try {
$response = Requests::delete($fullUri, $headers);
} catch (Requests_Exception $e) {
return ['Error' => [0 => ['error_description' => $e->getMessage(), 'error_description_translated' => $e->getMessage()],]];
}
$body = $response->body;
$array = json_decode($body, true);
$responseHeaders = $response->headers->getAll();
$statusCode = $response->status_code;
$array['ResponseHeaders'] = $responseHeaders;
$array['ResponseStatusCode'] = $statusCode;
if ($this->isErrorResponse($array)) {
$this->throwResponseError($array);
}
return $array;
}
/**
* @param string $uri
* @param array $data
* @param array $headers
*
* @return array
*/
protected function sendUnsignedBunqPost(string $uri, array $data, array $headers): array
{
$body = json_encode($data);
$fullUri = $this->server . $uri;
try {
$response = Requests::post($fullUri, $headers, $body);
} catch (Requests_Exception $e) {
return ['Error' => [0 => ['error_description' => $e->getMessage(), 'error_description_translated' => $e->getMessage()],]];
}
$body = $response->body;
$array = json_decode($body, true);
$responseHeaders = $response->headers->getAll();
$statusCode = $response->status_code;
$array['ResponseHeaders'] = $responseHeaders;
$array['ResponseStatusCode'] = $statusCode;
if ($this->isErrorResponse($array)) {
$this->throwResponseError($array);
}
return $array;
}
/**
* @param array $response
*
* @return bool
*/
private function isErrorResponse(array $response): bool
{
$key = key($response);
if ($key === 'Error') {
return true;
}
return false;
}
/**
* @param array $response
*
* @throws Exception
*/
private function throwResponseError(array $response)
{
$message = [];
if (isset($response['Error'])) {
foreach ($response['Error'] as $error) {
$message[] = $error['error_description'];
}
}
throw new FireflyException('Bunq ERROR ' . $response['ResponseStatusCode'] . ': ' . join(', ', $message));
}
/**
* @param string $body
* @param array $headers
* @param int $statusCode
*
* @return bool
* @throws Exception
*/
private function verifyServerSignature(string $body, array $headers, int $statusCode): bool
{
Log::debug('Going to verify signature for body+headers+status');
$dataToVerify = $statusCode . "\n";
$verifyHeaders = [];
// false when no public key is present
if (is_null($this->serverPublicKey)) {
Log::error('No public key present in class, so return FALSE.');
return false;
}
foreach ($headers as $header => $value) {
// skip non-bunq headers or signature
if (substr($header, 0, 7) !== 'x-bunq-' || $header === 'x-bunq-server-signature') {
continue;
}
// need to have upper case variant of header:
if (!isset($this->upperCaseHeaders[$header])) {
throw new FireflyException(sprintf('No upper case variant for header "%s"', $header));
}
$header = $this->upperCaseHeaders[$header];
$verifyHeaders[$header] = $value[0];
}
// sort verification headers:
ksort($verifyHeaders);
// add them to data to sign:
foreach ($verifyHeaders as $header => $value) {
$dataToVerify .= $header . ': ' . trim($value) . "\n";
}
$signature = $headers['x-bunq-server-signature'][0];
$dataToVerify .= "\n" . $body;
$result = openssl_verify($dataToVerify, base64_decode($signature), $this->serverPublicKey->getPublicKey(), OPENSSL_ALGO_SHA256);
if (is_int($result) && $result < 1) {
Log::error(sprintf('Result of verification is %d, return false.', $result));
return false;
}
if (!is_int($result)) {
Log::error(sprintf('Result of verification is a boolean (%d), return false.', $result));
}
Log::info('Signature is a match, return true.');
return true;
}
}

View File

@ -0,0 +1,49 @@
<?php
/**
* DeleteDeviceSessionRequest.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Request;
use FireflyIII\Services\Bunq\Token\SessionToken;
use Log;
/**
* Class DeleteDeviceSessionRequest
*
* @package FireflyIII\Services\Bunq\Request
*/
class DeleteDeviceSessionRequest extends BunqRequest
{
/** @var SessionToken */
private $sessionToken;
/**
*
*/
public function call(): void
{
Log::debug('Going to send bunq delete session request.');
$uri = sprintf('/v1/session/%d', $this->sessionToken->getId());
$headers = $this->getDefaultHeaders();
$headers['X-Bunq-Client-Authentication'] = $this->sessionToken->getToken();
$this->sendSignedBunqDelete($uri, $headers);
return;
}
/**
* @param SessionToken $sessionToken
*/
public function setSessionToken(SessionToken $sessionToken)
{
$this->sessionToken = $sessionToken;
}
}

View File

@ -0,0 +1,82 @@
<?php
/**
* DeviceServerRequest.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Request;
use FireflyIII\Services\Bunq\Id\DeviceServerId;
use FireflyIII\Services\Bunq\Token\InstallationToken;
/**
* Class DeviceServerRequest
*
* @package Bunq\Request
*/
class DeviceServerRequest extends BunqRequest
{
/** @var string */
private $description = '';
/** @var DeviceServerId */
private $deviceServerId;
/** @var InstallationToken */
private $installationToken;
/** @var array */
private $permittedIps = [];
/**
*
*/
public function call(): void
{
$uri = '/v1/device-server';
$data = ['description' => $this->description, 'secret' => $this->secret, 'permitted_ips' => $this->permittedIps];
$headers = $this->getDefaultHeaders();
$headers['X-Bunq-Client-Authentication'] = $this->installationToken->getToken();
$response = $this->sendSignedBunqPost($uri, $data, $headers);
$deviceServerId = new DeviceServerId;
$deviceServerId->setId(intval($response['Response'][0]['Id']['id']));
$this->deviceServerId = $deviceServerId;
return;
}
/**
* @return DeviceServerId
*/
public function getDeviceServerId(): DeviceServerId
{
return $this->deviceServerId;
}
/**
* @param string $description
*/
public function setDescription(string $description)
{
$this->description = $description;
}
/**
* @param InstallationToken $installationToken
*/
public function setInstallationToken(InstallationToken $installationToken)
{
$this->installationToken = $installationToken;
}
/**
* @param array $permittedIps
*/
public function setPermittedIps(array $permittedIps)
{
$this->permittedIps = $permittedIps;
}
}

View File

@ -0,0 +1,149 @@
<?php
/**
* DeviceSessionRequest.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Request;
use FireflyIII\Services\Bunq\Id\DeviceSessionId;
use FireflyIII\Services\Bunq\Object\UserCompany;
use FireflyIII\Services\Bunq\Object\UserPerson;
use FireflyIII\Services\Bunq\Token\InstallationToken;
use FireflyIII\Services\Bunq\Token\SessionToken;
use Log;
/**
* Class DeviceSessionRequest
*
* @package FireflyIII\Services\Bunq\Request
*/
class DeviceSessionRequest extends BunqRequest
{
/** @var DeviceSessionId */
private $deviceSessionId;
/** @var InstallationToken */
private $installationToken;
/** @var SessionToken */
private $sessionToken;
/** @var UserCompany */
private $userCompany;
/** @var UserPerson */
private $userPerson;
/**
*
*/
public function call(): void
{
$uri = '/v1/session-server';
$data = ['secret' => $this->secret];
$headers = $this->getDefaultHeaders();
$headers['X-Bunq-Client-Authentication'] = $this->installationToken->getToken();
$response = $this->sendSignedBunqPost($uri, $data, $headers);
$this->deviceSessionId = $this->extractDeviceSessionId($response);
$this->sessionToken = $this->extractSessionToken($response);
$this->userPerson = $this->extractUserPerson($response);
$this->userCompany = $this->extractUserCompany($response);
Log::debug(sprintf('Session ID: %s', serialize($this->deviceSessionId)));
Log::debug(sprintf('Session token: %s', serialize($this->sessionToken)));
Log::debug(sprintf('Session user person: %s', serialize($this->userPerson)));
Log::debug(sprintf('Session user company: %s', serialize($this->userCompany)));
return;
}
/**
* @return DeviceSessionId
*/
public function getDeviceSessionId(): DeviceSessionId
{
return $this->deviceSessionId;
}
/**
* @return SessionToken
*/
public function getSessionToken(): SessionToken
{
return $this->sessionToken;
}
/**
* @return UserPerson
*/
public function getUserPerson(): UserPerson
{
return $this->userPerson;
}
/**
* @param InstallationToken $installationToken
*/
public function setInstallationToken(InstallationToken $installationToken)
{
$this->installationToken = $installationToken;
}
/**
* @param array $response
*
* @return DeviceSessionId
*/
private function extractDeviceSessionId(array $response): DeviceSessionId
{
$data = $this->getKeyFromResponse('Id', $response);
$deviceSessionId = new DeviceSessionId;
$deviceSessionId->setId(intval($data['id']));
return $deviceSessionId;
}
private function extractSessionToken(array $response): SessionToken
{
$data = $this->getKeyFromResponse('Token', $response);
$sessionToken = new SessionToken($data);
return $sessionToken;
}
/**
* @param $response
*
* @return UserCompany
*/
private function extractUserCompany($response): UserCompany
{
$data = $this->getKeyFromResponse('UserCompany', $response);
$userCompany = new UserCompany($data);
return $userCompany;
}
/**
* @param $response
*
* @return UserPerson
*/
private function extractUserPerson($response): UserPerson
{
$data = $this->getKeyFromResponse('UserPerson', $response);
$userPerson = new UserPerson($data);
return $userPerson;
}
}

View File

@ -0,0 +1,147 @@
<?php
/**
* InstallationTokenRequest.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Request;
use FireflyIII\Services\Bunq\Id\InstallationId;
use FireflyIII\Services\Bunq\Object\ServerPublicKey;
use FireflyIII\Services\Bunq\Token\InstallationToken;
use Log;
/**
* Class InstallationTokenRequest
*
* @package FireflyIII\Services\Bunq\Request
*/
class InstallationTokenRequest extends BunqRequest
{
/** @var InstallationId */
private $installationId;
/** @var InstallationToken */
private $installationToken;
/** @var string */
private $publicKey = '';
/** @var ServerPublicKey */
private $serverPublicKey;
/**
*
*/
public function call(): void
{
$uri = '/v1/installation';
$data = ['client_public_key' => $this->publicKey,];
$headers = $this->getDefaultHeaders();
$response = $this->sendUnsignedBunqPost($uri, $data, $headers);
Log::debug('Installation request response', $response);
$this->installationId = $this->extractInstallationId($response);
$this->serverPublicKey = $this->extractServerPublicKey($response);
$this->installationToken = $this->extractInstallationToken($response);
Log::debug(sprintf('Installation ID: %s', serialize($this->installationId)));
Log::debug(sprintf('Installation token: %s', serialize($this->installationToken)));
Log::debug(sprintf('server public key: %s', serialize($this->serverPublicKey)));
return;
}
/**
* @return InstallationId
*/
public function getInstallationId(): InstallationId
{
return $this->installationId;
}
/**
* @return InstallationToken
*/
public function getInstallationToken(): InstallationToken
{
return $this->installationToken;
}
/**
* @return string
*/
public function getPublicKey(): string
{
return $this->publicKey;
}
/**
* @param string $publicKey
*/
public function setPublicKey(string $publicKey)
{
$this->publicKey = $publicKey;
}
/**
* @return ServerPublicKey
*/
public function getServerPublicKey(): ServerPublicKey
{
return $this->serverPublicKey;
}
/**
* @param bool $fake
*/
public function setFake(bool $fake)
{
$this->fake = $fake;
}
/**
* @param array $response
*
* @return InstallationId
*/
private function extractInstallationId(array $response): InstallationId
{
$installationId = new InstallationId;
$data = $this->getKeyFromResponse('Id', $response);
$installationId->setId(intval($data['id']));
return $installationId;
}
/**
* @param array $response
*
* @return InstallationToken
*/
private function extractInstallationToken(array $response): InstallationToken
{
$data = $this->getKeyFromResponse('Token', $response);
$installationToken = new InstallationToken($data);
return $installationToken;
}
/**
* @param array $response
*
* @return ServerPublicKey
*/
private function extractServerPublicKey(array $response): ServerPublicKey
{
$data = $this->getKeyFromResponse('ServerPublicKey', $response);
$serverPublicKey = new ServerPublicKey($data);
return $serverPublicKey;
}
}

View File

@ -0,0 +1,73 @@
<?php
/**
* ListDeviceServerRequest.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Request;
use FireflyIII\Services\Bunq\Object\DeviceServer;
use FireflyIII\Services\Bunq\Token\InstallationToken;
use Illuminate\Support\Collection;
/**
* Class ListDeviceServerRequest
*
* @package FireflyIII\Services\Bunq\Request
*/
class ListDeviceServerRequest extends BunqRequest
{
/** @var Collection */
private $devices;
/** @var InstallationToken */
private $installationToken;
public function __construct()
{
parent::__construct();
$this->devices = new Collection;
}
/**
*
*/
public function call(): void
{
$uri = '/v1/device-server';
$data = [];
$headers = $this->getDefaultHeaders();
$headers['X-Bunq-Client-Authentication'] = $this->installationToken->getToken();
$response = $this->sendSignedBunqGet($uri, $data, $headers);
// create device server objects:
$raw = $this->getArrayFromResponse('DeviceServer', $response);
/** @var array $entry */
foreach ($raw as $entry) {
$this->devices->push(new DeviceServer($entry));
}
return;
}
/**
* @return Collection
*/
public function getDevices(): Collection
{
return $this->devices;
}
/**
* @param InstallationToken $installationToken
*/
public function setInstallationToken(InstallationToken $installationToken)
{
$this->installationToken = $installationToken;
}
}

View File

@ -0,0 +1,79 @@
<?php
/**
* ListMonetaryAccountRequest.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Request;
use FireflyIII\Services\Bunq\Object\MonetaryAccountBank;
use FireflyIII\Services\Bunq\Token\SessionToken;
use Illuminate\Support\Collection;
/**
* Class ListMonetaryAccountRequest
*
* @package FireflyIII\Services\Bunq\Request
*/
class ListMonetaryAccountRequest extends BunqRequest
{
/** @var Collection */
private $monetaryAccounts;
/** @var SessionToken */
private $sessionToken;
/** @var int */
private $userId = 0;
/**
*
*/
public function call(): void
{
$this->monetaryAccounts = new Collection;
$uri = sprintf('/v1/user/%d/monetary-account', $this->userId);
$data = [];
$headers = $this->getDefaultHeaders();
$headers['X-Bunq-Client-Authentication'] = $this->sessionToken->getToken();
$response = $this->sendSignedBunqGet($uri, $data, $headers);
// create device server objects:
$raw = $this->getArrayFromResponse('MonetaryAccountBank', $response);
foreach ($raw as $entry) {
$account = new MonetaryAccountBank($entry);
$this->monetaryAccounts->push($account);
}
return;
}
/**
* @return Collection
*/
public function getMonetaryAccounts(): Collection
{
return $this->monetaryAccounts;
}
/**
* @param SessionToken $sessionToken
*/
public function setSessionToken(SessionToken $sessionToken)
{
$this->sessionToken = $sessionToken;
}
/**
* @param int $userId
*/
public function setUserId(int $userId)
{
$this->userId = $userId;
}
}

View File

@ -0,0 +1,90 @@
<?php
/**
* ListUserRequest.php
* Copyright (c) 2017 thegrumpydictator@gmail.com
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Bunq\Request;
use FireflyIII\Services\Bunq\Object\UserCompany;
use FireflyIII\Services\Bunq\Object\UserLight;
use FireflyIII\Services\Bunq\Object\UserPerson;
use FireflyIII\Services\Bunq\Token\SessionToken;
/**
* Class ListUserRequest
*
* @package FireflyIII\Services\Bunq\Request
*/
class ListUserRequest extends BunqRequest
{
/** @var SessionToken */
private $sessionToken;
/** @var UserCompany */
private $userCompany;
/** @var UserLight */
private $userLight;
/** @var UserPerson */
private $userPerson;
/**
*
*/
public function call(): void
{
$uri = '/v1/user';
$data = [];
$headers = $this->getDefaultHeaders();
$headers['X-Bunq-Client-Authentication'] = $this->sessionToken->getToken();
$response = $this->sendSignedBunqGet($uri, $data, $headers);
// create user objects:
$light = $this->getKeyFromResponse('UserLight', $response);
$company = $this->getKeyFromResponse('UserCompany', $response);
$person = $this->getKeyFromResponse('UserPerson', $response);
$this->userLight = new UserLight($light);
$this->userCompany = new UserCompany($company);
$this->userPerson = new UserPerson($person);
return;
}
/**
* @return UserCompany
*/
public function getUserCompany(): UserCompany
{
return $this->userCompany;
}
/**
* @return UserLight
*/
public function getUserLight(): UserLight
{
return $this->userLight;
}
/**
* @return UserPerson
*/
public function getUserPerson(): UserPerson
{
return $this->userPerson;
}
/**
* @param SessionToken $sessionToken
*/
public function setSessionToken(SessionToken $sessionToken)
{
$this->sessionToken = $sessionToken;
}
}

Some files were not shown because too many files have changed in this diff Show More