diff --git a/.deploy/docker/cronjob.conf b/.deploy/docker/cronjob.conf
new file mode 100644
index 0000000000..71aacd84eb
--- /dev/null
+++ b/.deploy/docker/cronjob.conf
@@ -0,0 +1,11 @@
+[program:cron]
+command=/usr/sbin/cron -f -L 15
+user=root
+autostart=true
+autorestart=true
+stdout_events_enabled=true
+stderr_events_enabled=true
+stdout_logfile=/dev/stdout
+stdout_logfile_maxbytes=0
+startsecs=10
+startretries=3
diff --git a/.deploy/docker/entrypoint.sh b/.deploy/docker/entrypoint.sh
index 2a6f7cf013..ff808c1990 100755
--- a/.deploy/docker/entrypoint.sh
+++ b/.deploy/docker/entrypoint.sh
@@ -22,8 +22,8 @@ chmod -R 775 $FIREFLY_PATH/storage
# remove any lingering files that may break upgrades:
rm -f $FIREFLY_PATH/storage/logs/laravel.log
-cat .env.docker | envsubst > .env && cat .env
+cat .env.docker | envsubst > .env
composer dump-autoload
php artisan package:discover
php artisan firefly:instructions install
-exec apache2-foreground
+exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf --nodaemon
\ No newline at end of file
diff --git a/.deploy/docker/firefly-iii.conf b/.deploy/docker/firefly-iii.conf
new file mode 100644
index 0000000000..5ee1c2eb28
--- /dev/null
+++ b/.deploy/docker/firefly-iii.conf
@@ -0,0 +1,6 @@
+[program:apache2]
+command=/bin/bash -c "source /etc/apache2/envvars && exec /usr/sbin/apache2 -DFOREGROUND"
+stdout_events_enabled=true
+stderr_events_enabled=true
+stdout_logfile=/dev/stdout
+stdout_logfile_maxbytes=0
\ No newline at end of file
diff --git a/.deploy/docker/supervisord.conf b/.deploy/docker/supervisord.conf
new file mode 100644
index 0000000000..479120ed38
--- /dev/null
+++ b/.deploy/docker/supervisord.conf
@@ -0,0 +1,30 @@
+# supervisor config file
+# Adapted from the config file distributed with the supervisor package on Debian
+# Jessie
+
+# Enable supervisord in non-daemon mode. Disable the logfile as we receive
+# log messages via stdout/err. Set up the child process log directory in case
+# the user doesn't set logging to stdout/err.
+[supervisord]
+nodaemon = true
+logfile = NONE
+pidfile = /var/run/supervisord.pid
+childlogdir = /var/log/supervisor
+loglevel=debug
+
+# Enable supervisorctl via RPC interface over Unix socket
+[unix_http_server]
+file = /var/run/supervisor.sock
+chmod = 0700
+
+[rpcinterface:supervisor]
+supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
+
+[supervisorctl]
+serverurl = unix:///var/run/supervisor.sock
+
+
+# Include conf files for child processes
+# Debian/Ubuntu packages use /etc/supervisor/conf.d
+[include]
+files = /etc/supervisor/conf.d/*.conf
\ No newline at end of file
diff --git a/.deploy/docker/vhost.conf b/.deploy/docker/vhost.conf
new file mode 100644
index 0000000000..f5a07256b0
--- /dev/null
+++ b/.deploy/docker/vhost.conf
@@ -0,0 +1,26 @@
+server {
+ listen 80 default_server;
+
+ server_name _ *.vm docker;
+
+ root "/app/public";
+ index index.php;
+
+ include /opt/docker/etc/nginx/vhost.common.d/*.conf;
+}
+
+##############
+# SSL
+##############
+
+server {
+ listen 443 default_server;
+
+ server_name _ *.vm docker;
+
+ root "/app/public";
+ index index.php;
+
+ include /opt/docker/etc/nginx/vhost.common.d/*.conf;
+ include /opt/docker/etc/nginx/vhost.ssl.conf;
+}
\ No newline at end of file
diff --git a/.env.docker b/.env.docker
index 502b81ef49..d88c6915a7 100644
--- a/.env.docker
+++ b/.env.docker
@@ -13,15 +13,31 @@ SITE_OWNER=${SITE_OWNER}
# Change it to a string of exactly 32 chars or use command `php artisan key:generate` to generate it
APP_KEY=${FF_APP_KEY}
-# APP_URL and TRUSTED_PROXIES are useful when using Docker and/or a reverse proxy.
+# Change this value to your preferred time zone.
+# Example: Europe/Amsterdam
+TZ=UTC
+
+# This variable must match your installation's external address but keep in mind that
+# it's only used on the command line as a fallback value.
APP_URL=${APP_URL}
+
+# TRUSTED_PROXIES is a useful variable when using Docker and/or a reverse proxy.
TRUSTED_PROXIES=${TRUSTED_PROXIES}
# The log channel defines where your log entries go to.
-LOG_CHANNEL=${LOG_CHANNEL}
+# 'daily' is the default logging mode giving you 5 daily rotated log files in /storage/logs/.
+# Several other options exist. You can use 'single' for one big fat error log (not recommended).
+# Also available are 'syslog', 'errorlog' and 'stdout' which will log to the system itself.
+LOG_CHANNEL=stdout
+
+# Log level. You can set this from least severe to most severe:
+# debug, info, notice, warning, error, critical, alert, emergency
+# If you set it to debug your logs will grow large, and fast. If you set it to emergency probably
+# nothing will get logged, ever.
+APP_LOG_LEVEL=${APP_LOG_LEVEL}
# Database credentials. Make sure the database exists. I recommend a dedicated user for Firefly III
-# If you use SQLite, set connection to `sqlite` and remove the database, username and password settings.
+# For other database types, please see the FAQ: http://firefly-iii.readthedocs.io/en/latest/support/faq.html
DB_CONNECTION=${FF_DB_CONNECTION}
DB_HOST=${FF_DB_HOST}
DB_PORT=${FF_DB_PORT}
@@ -29,17 +45,6 @@ DB_DATABASE=${FF_DB_NAME}
DB_USERNAME=${FF_DB_USER}
DB_PASSWORD=${FF_DB_PASSWORD}
-# 'daily' is the default logging mode giving you 5 daily rotated log files in /storage/logs/.
-# Several other options exist. You can use 'single' for one big fat error log (not recommended).
-# Also available are 'syslog' and 'errorlog' which will log to the system itself.
-APP_LOG=syslog
-
-# Log level. You can set this from least severe to most severe:
-# debug, info, notice, warning, error, critical, alert, emergency
-# If you set it to debug your logs will grow large, and fast. If you set it to emergency probably
-# nothing will get logged, ever.
-APP_LOG_LEVEL=info
-
# If you're looking for performance improvements, you could install memcached.
CACHE_DRIVER=file
SESSION_DRIVER=file
@@ -103,4 +108,5 @@ IS_DOCKER=true
IS_SANDSTORM=false
IS_HEROKU=false
BUNQ_USE_SANDBOX=false
-TZ=${TZ}
+MAILGUN_DOMAIN=
+MAILGUN_SECRET=
diff --git a/.env.example b/.env.example
index 73afaf0470..571115cda6 100644
--- a/.env.example
+++ b/.env.example
@@ -15,15 +15,27 @@ APP_KEY=SomeRandomStringOf32CharsExactly
# Change this value to your preferred time zone.
# Example: Europe/Amsterdam
-TZ=UTC
+TZ=${TZ}
-# APP_URL and TRUSTED_PROXIES are useful when using Docker and/or a reverse proxy.
+# This variable must match your installation's external address but keep in mind that
+# it's only used on the command line as a fallback value.
APP_URL=http://localhost
+
+# TRUSTED_PROXIES is a useful variable when using Docker and/or a reverse proxy.
TRUSTED_PROXIES=
# The log channel defines where your log entries go to.
+# 'daily' is the default logging mode giving you 5 daily rotated log files in /storage/logs/.
+# Several other options exist. You can use 'single' for one big fat error log (not recommended).
+# Also available are 'syslog', 'errorlog' and 'stdout' which will log to the system itself.
LOG_CHANNEL=daily
+# Log level. You can set this from least severe to most severe:
+# debug, info, notice, warning, error, critical, alert, emergency
+# If you set it to debug your logs will grow large, and fast. If you set it to emergency probably
+# nothing will get logged, ever.
+APP_LOG_LEVEL=notice
+
# Database credentials. Make sure the database exists. I recommend a dedicated user for Firefly III
# For other database types, please see the FAQ: http://firefly-iii.readthedocs.io/en/latest/support/faq.html
DB_CONNECTION=mysql
@@ -33,17 +45,6 @@ DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret
-# 'daily' is the default logging mode giving you 5 daily rotated log files in /storage/logs/.
-# Several other options exist. You can use 'single' for one big fat error log (not recommended).
-# Also available are 'syslog' and 'errorlog' which will log to the system itself.
-APP_LOG=daily
-
-# Log level. You can set this from least severe to most severe:
-# debug, info, notice, warning, error, critical, alert, emergency
-# If you set it to debug your logs will grow large, and fast. If you set it to emergency probably
-# nothing will get logged, ever.
-APP_LOG_LEVEL=notice
-
# If you're looking for performance improvements, you could install memcached.
CACHE_DRIVER=file
SESSION_DRIVER=file
@@ -105,7 +106,7 @@ DEMO_USERNAME=
DEMO_PASSWORD=
IS_DOCKER=false
IS_SANDSTORM=false
-BUNQ_USE_SANDBOX=false
IS_HEROKU=false
+BUNQ_USE_SANDBOX=false
MAILGUN_DOMAIN=
MAILGUN_SECRET=
diff --git a/.env.heroku b/.env.heroku
index 55237bd98b..c16376ea76 100644
--- a/.env.heroku
+++ b/.env.heroku
@@ -17,12 +17,24 @@ APP_KEY=7ahyYVPVsmxjdhsweWCauGeJfwc92NP2
# Example: Europe/Amsterdam
TZ=UTC
-# APP_URL and TRUSTED_PROXIES are useful when using Docker and/or a reverse proxy.
+# This variable must match your installation's external address but keep in mind that
+# it's only used on the command line as a fallback value.
APP_URL=http://localhost
-TRUSTED_PROXIES=
+
+# TRUSTED_PROXIES is a useful variable when using Docker and/or a reverse proxy.
+TRUSTED_PROXIES=**
# The log channel defines where your log entries go to.
-LOG_CHANNEL=syslog
+# 'daily' is the default logging mode giving you 5 daily rotated log files in /storage/logs/.
+# Several other options exist. You can use 'single' for one big fat error log (not recommended).
+# Also available are 'syslog', 'errorlog' and 'stdout' which will log to the system itself.
+LOG_CHANNEL=stdout
+
+# Log level. You can set this from least severe to most severe:
+# debug, info, notice, warning, error, critical, alert, emergency
+# If you set it to debug your logs will grow large, and fast. If you set it to emergency probably
+# nothing will get logged, ever.
+APP_LOG_LEVEL=debug
# Database credentials. Make sure the database exists. I recommend a dedicated user for Firefly III
# If you use SQLite, set connection to `sqlite` and remove the database, username and password settings.
@@ -33,17 +45,6 @@ DB_CONNECTION=pgsql
-# 'daily' is the default logging mode giving you 5 daily rotated log files in /storage/logs/.
-# Several other options exist. You can use 'single' for one big fat error log (not recommended).
-# Also available are 'syslog' and 'errorlog' which will log to the system itself.
-APP_LOG=errorlog
-
-# Log level. You can set this from least severe to most severe:
-# debug, info, notice, warning, error, critical, alert, emergency
-# If you set it to debug your logs will grow large, and fast. If you set it to emergency probably
-# nothing will get logged, ever.
-APP_LOG_LEVEL=debug
-
# If you're looking for performance improvements, you could install memcached.
CACHE_DRIVER=file
SESSION_DRIVER=file
@@ -105,7 +106,7 @@ DEMO_USERNAME=
DEMO_PASSWORD=
IS_DOCKER=false
IS_SANDSTORM=false
-BUNQ_USE_SANDBOX=false
IS_HEROKU=true
+BUNQ_USE_SANDBOX=false
MAILGUN_DOMAIN=
MAILGUN_SECRET=
diff --git a/.env.sandstorm b/.env.sandstorm
index 5895f63467..ce73089fe8 100755
--- a/.env.sandstorm
+++ b/.env.sandstorm
@@ -17,12 +17,24 @@ APP_KEY=SomeRandomStringOf32CharsExactly
# Example: Europe/Amsterdam
TZ=UTC
-# APP_URL and TRUSTED_PROXIES are useful when using Docker and/or a reverse proxy.
+# This variable must match your installation's external address but keep in mind that
+# it's only used on the command line as a fallback value.
APP_URL=http://localhost
+
+# TRUSTED_PROXIES is a useful variable when using Docker and/or a reverse proxy.
TRUSTED_PROXIES=
# The log channel defines where your log entries go to.
-LOG_CHANNEL=syslog
+# 'daily' is the default logging mode giving you 5 daily rotated log files in /storage/logs/.
+# Several other options exist. You can use 'single' for one big fat error log (not recommended).
+# Also available are 'syslog', 'errorlog' and 'stdout' which will log to the system itself.
+LOG_CHANNEL=stdout
+
+# Log level. You can set this from least severe to most severe:
+# debug, info, notice, warning, error, critical, alert, emergency
+# If you set it to debug your logs will grow large, and fast. If you set it to emergency probably
+# nothing will get logged, ever.
+APP_LOG_LEVEL=debug
# Database credentials. Make sure the database exists. I recommend a dedicated user for Firefly III
# If you use SQLite, set connection to `sqlite` and remove the database, username and password settings.
@@ -33,17 +45,6 @@ DB_DATABASE=firefly
DB_USERNAME=firefly
DB_PASSWORD=firefly
-# 'daily' is the default logging mode giving you 5 daily rotated log files in /storage/logs/.
-# Several other options exist. You can use 'single' for one big fat error log (not recommended).
-# Also available are 'syslog' and 'errorlog' which will log to the system itself.
-APP_LOG=syslog
-
-# Log level. You can set this from least severe to most severe:
-# debug, info, notice, warning, error, critical, alert, emergency
-# If you set it to debug your logs will grow large, and fast. If you set it to emergency probably
-# nothing will get logged, ever.
-APP_LOG_LEVEL=info
-
# If you're looking for performance improvements, you could install memcached.
CACHE_DRIVER=file
SESSION_DRIVER=file
@@ -105,7 +106,7 @@ DEMO_USERNAME=
DEMO_PASSWORD=
IS_DOCKER=false
IS_SANDSTORM=true
-BUNQ_USE_SANDBOX=false
IS_HEROKU=false
+BUNQ_USE_SANDBOX=false
MAILGUN_DOMAIN=
MAILGUN_SECRET=
diff --git a/.env.testing b/.env.testing
index 6eec082239..27d1d955d5 100644
--- a/.env.testing
+++ b/.env.testing
@@ -17,21 +17,18 @@ APP_KEY=TestTestTestTestTestTestTestTest
# Example: Europe/Amsterdam
TZ=Europe/Amsterdam
-# APP_URL and TRUSTED_PROXIES are useful when using Docker and/or a reverse proxy.
+# This variable must match your installation's external address but keep in mind that
+# it's only used on the command line as a fallback value.
APP_URL=http://localhost
+
+# TRUSTED_PROXIES is a useful variable when using Docker and/or a reverse proxy.
TRUSTED_PROXIES=
# The log channel defines where your log entries go to.
-LOG_CHANNEL=dailytest
-
-# Database credentials. Make sure the database exists. I recommend a dedicated user for Firefly III
-# For other database types, please see the FAQ: http://firefly-iii.readthedocs.io/en/latest/support/faq.html
-DB_CONNECTION=sqlite
-
# 'daily' is the default logging mode giving you 5 daily rotated log files in /storage/logs/.
# Several other options exist. You can use 'single' for one big fat error log (not recommended).
-# Also available are 'syslog' and 'errorlog' which will log to the system itself.
-APP_LOG=daily
+# Also available are 'syslog', 'errorlog' and 'stdout' which will log to the system itself.
+LOG_CHANNEL=dailytest
# Log level. You can set this from least severe to most severe:
# debug, info, notice, warning, error, critical, alert, emergency
@@ -39,6 +36,15 @@ APP_LOG=daily
# nothing will get logged, ever.
APP_LOG_LEVEL=debug
+# Database credentials. Make sure the database exists. I recommend a dedicated user for Firefly III
+# For other database types, please see the FAQ: http://firefly-iii.readthedocs.io/en/latest/support/faq.html
+DB_CONNECTION=sqlite
+
+
+
+
+
+
# If you're looking for performance improvements, you could install memcached.
CACHE_DRIVER=file
SESSION_DRIVER=file
@@ -64,6 +70,9 @@ MAILGUN_SECRET=
MANDRILL_SECRET=
SPARKPOST_SECRET=
+# Firefly III can send you the following messages
+SEND_REGISTRATION_MAIL=true
+SEND_ERROR_MESSAGE=false
# Set a Mapbox API key here (see mapbox.com) so there might be a map available at various places.
MAPBOX_API_KEY=
@@ -97,7 +106,7 @@ DEMO_USERNAME=
DEMO_PASSWORD=
IS_DOCKER=false
IS_SANDSTORM=false
-BUNQ_USE_SANDBOX=true
IS_HEROKU=false
+BUNQ_USE_SANDBOX=true
MAILGUN_DOMAIN=
MAILGUN_SECRET=
diff --git a/.sandstorm/changelog.md b/.sandstorm/changelog.md
index cbf34ab81c..39054f0536 100644
--- a/.sandstorm/changelog.md
+++ b/.sandstorm/changelog.md
@@ -1,3 +1,46 @@
+# 4.7.6
+- [Issue 145](https://github.com/firefly-iii/firefly-iii/issues/145) You can now download transactions from YNAB.
+- [Issue 306](https://github.com/firefly-iii/firefly-iii/issues/306) You can now add liabilities to Firefly III.
+- [Issue 740](https://github.com/firefly-iii/firefly-iii/issues/740) Various charts are now currency aware.
+- [Issue 833](https://github.com/firefly-iii/firefly-iii/issues/833) Bills can use non-default currencies.
+- [Issue 1578](https://github.com/firefly-iii/firefly-iii/issues/1578) Firefly III will notify you if the cron job hasn't fired.
+- [Issue 1623](https://github.com/firefly-iii/firefly-iii/issues/1623) New transactions will link back from the success message.
+- [Issue 1624](https://github.com/firefly-iii/firefly-iii/issues/1624) transactions will link to the object.
+- You can call the cron job over the web now (see docs).
+- You don't need to call the cron job every minute any more.
+- Various charts are now red/green to signify income and expenses.
+- Option to add or remove accounts from the net worth calculations.
+- This will be the last release on PHP 7.1. Future versions will require PHP 7.2.
+- [Issue 1460](https://github.com/firefly-iii/firefly-iii/issues/1460) Downloading transactions from bunq should go more smoothly.
+- [Issue 1464](https://github.com/firefly-iii/firefly-iii/issues/1464) Fixed the docker file to work on Raspberry Pi's.
+- [Issue 1540](https://github.com/firefly-iii/firefly-iii/issues/1540) The Docker file now has a working cron job for recurring transactions.
+- [Issue 1564](https://github.com/firefly-iii/firefly-iii/issues/1564) Fix double transfers when importing from bunq.
+- [Issue 1575](https://github.com/firefly-iii/firefly-iii/issues/1575) Some views would give a XSRF token warning
+- [Issue 1576](https://github.com/firefly-iii/firefly-iii/issues/1576) Fix assigning budgets
+- [Issue 1580](https://github.com/firefly-iii/firefly-iii/issues/1580) Missing string for translation
+- [Issue 1581](https://github.com/firefly-iii/firefly-iii/issues/1581) Expand help text
+- [Issue 1584](https://github.com/firefly-iii/firefly-iii/issues/1584) Link to administration is back.
+- [Issue 1586](https://github.com/firefly-iii/firefly-iii/issues/1586) Date fields in import were mislabeled.
+- [Issue 1593](https://github.com/firefly-iii/firefly-iii/issues/1593) Link types are translatable.
+- [Issue 1594](https://github.com/firefly-iii/firefly-iii/issues/1594) Very long breadcrumbs are weird.
+- [Issue 1598](https://github.com/firefly-iii/firefly-iii/issues/1598) Fix budget calculations.
+- [Issue 1597](https://github.com/firefly-iii/firefly-iii/issues/1597) Piggy banks are always inactive.
+- [Issue 1605](https://github.com/firefly-iii/firefly-iii/issues/1605) System will ignore foreign currency setting if user doesn't indicate the amount.
+- [Issue 1607](https://github.com/firefly-iii/firefly-iii/issues/1607) Firefly III trusts the Heroku load balancer, fixing deployment on Heroku.
+- [Issue 1608](https://github.com/firefly-iii/firefly-iii/issues/1608) Spelling error in command line import.
+- [Issue 1609](https://github.com/firefly-iii/firefly-iii/issues/1609) Link to budgets page was absolute.
+- [Issue 1615](https://github.com/firefly-iii/firefly-iii/issues/1615) Fix currency bug in transactions.
+- [Issue 1616](https://github.com/firefly-iii/firefly-iii/issues/1616) Fix null pointer exception in pie charts.
+- [Issue 1617](https://github.com/firefly-iii/firefly-iii/issues/1617) Fix for complex tag names in URL's.
+- [Issue 1620](https://github.com/firefly-iii/firefly-iii/issues/1620) Fixed index reference in API.
+- [Issue 1642](https://github.com/firefly-iii/firefly-iii/issues/1642) Fix issue with split journals.
+- [Issue 1643](https://github.com/firefly-iii/firefly-iii/issues/1643) Fix reconciliation issue.
+- Users can no longer give expenses a budget.
+- Fix bug in Spectre import.
+- Heroku would not make you owner.
+- Add `.htaccess` files to all public directories.
+- New secure headers will make Firefly III slightly more secure.
+
# 4.7.5.3
- [Issue 1527](https://github.com/firefly-iii/firefly-iii/issues/1527), fixed views for transactions without a budget.
- [Issue 1553](https://github.com/firefly-iii/firefly-iii/issues/1553), report could not handle transactions before the first one in the system.
diff --git a/.sandstorm/sandstorm-pkgdef.capnp b/.sandstorm/sandstorm-pkgdef.capnp
index 8d7814ae0d..8f55d0c5d9 100644
--- a/.sandstorm/sandstorm-pkgdef.capnp
+++ b/.sandstorm/sandstorm-pkgdef.capnp
@@ -15,8 +15,8 @@ const pkgdef :Spk.PackageDefinition = (
manifest = (
appTitle = (defaultText = "Firefly III"),
- appVersion = 15,
- appMarketingVersion = (defaultText = "4.7.5.3"),
+ appVersion = 16,
+ appMarketingVersion = (defaultText = "4.7.6"),
actions = [
# Define your "new document" handlers here.
diff --git a/Dockerfile b/Dockerfile
index 6d6ca1799b..15dcc12043 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,12 +1,16 @@
-# use PHP 7.1 and Apache as a base.
FROM php:7.1-apache
-# set working dir
-ENV FIREFLY_PATH /var/www/firefly-iii
+# If building on a RPi, use --build-arg cores=3 to use all cores when compiling
+# to speed up the image build
+ARG CORES
+ENV CORES ${CORES:-1}
+
+ENV FIREFLY_PATH /var/www/firefly-iii/
ENV CURL_VERSION 7.60.0
ENV OPENSSL_VERSION 1.1.1-pre6
-WORKDIR $FIREFLY_PATH
-ADD . $FIREFLY_PATH
+
+LABEL version="1.0" maintainer="thegrumpydictator@gmail.com"
+
# install packages
RUN apt-get update -y && \
@@ -19,48 +23,62 @@ RUN apt-get update -y && \
libedit-dev \
libtidy-dev \
libxml2-dev \
+ unzip \
libsqlite3-dev \
+ nano \
libpq-dev \
libbz2-dev \
gettext-base \
cron \
+ rsyslog \
+ supervisor \
locales && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
-# Setup the Composer installer
-RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
-
# Install latest curl
RUN cd /tmp && \
wget https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz && \
tar -xvf openssl-${OPENSSL_VERSION}.tar.gz && \
cd openssl-${OPENSSL_VERSION} && \
./config && \
- make && \
+ make -j${CORES} && \
make install
RUN cd /tmp && \
wget https://curl.haxx.se/download/curl-${CURL_VERSION}.tar.gz && \
tar -xvf curl-${CURL_VERSION}.tar.gz && \
cd curl-${CURL_VERSION} && \
- ./configure --with-ssl && \
- make && \
+ ./configure --with-ssl --host=$(gcc -dumpmachine) && \
+ make -j${CORES} && \
make install
+# Make sure that libcurl is using the newer curl libaries
+RUN echo "/usr/local/lib" >> /etc/ld.so.conf.d/00-curl.conf && ldconfig
-# Create the log file to be able to run tail
-RUN touch /var/log/cron.log
+# Mimic the Debian/Ubuntu config file structure for supervisor
+COPY .deploy/docker/supervisord.conf /etc/supervisor/supervisord.conf
+RUN mkdir -p /etc/supervisor/conf.d /var/log/supervisor
-# Setup cron job
-RUN (crontab -l ; echo "* * * * * root $FIREFLY_PATH/artisan schedule:run >> /var/log/cron.log") | crontab
+# copy Firefly III supervisor conf file.
+COPY ./.deploy/docker/firefly-iii.conf /etc/supervisor/conf.d/firefly-iii.conf
+# copy cron job supervisor conf file.
+COPY ./.deploy/docker/cronjob.conf /etc/supervisor/conf.d/cronjob.conf
+
+# test crons added via crontab
+RUN echo "0 3 * * * /usr/local/bin/php /var/www/firefly-iii/artisan firefly:cron" | crontab -
+#RUN (crontab -l ; echo "*/1 * * * * free >> /var/www/firefly-iii/public/cron.html") 2>&1 | crontab -
# Install PHP exentions.
-RUN docker-php-ext-install -j$(nproc) curl gd intl json readline tidy zip bcmath xml mbstring pdo_sqlite pdo_mysql bz2 pdo_pgsql
+RUN docker-php-ext-install -j$(nproc) gd intl tidy zip bcmath pdo_mysql bz2 pdo_pgsql
+
+# Install composer
+RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
# Generate locales supported by Firefly III
RUN echo "en_US.UTF-8 UTF-8\nde_DE.UTF-8 UTF-8\nfr_FR.UTF-8 UTF-8\nit_IT.UTF-8 UTF-8\nnl_NL.UTF-8 UTF-8\npl_PL.UTF-8 UTF-8\npt_BR.UTF-8 UTF-8\nru_RU.UTF-8 UTF-8\ntr_TR.UTF-8 UTF-8\n\n" > /etc/locale.gen && locale-gen
+
# copy Apache config to correct spot.
COPY ./.deploy/docker/apache2.conf /etc/apache2/apache2.conf
@@ -70,7 +88,7 @@ RUN a2enmod rewrite
# Enable apache mod ssl..
RUN a2enmod ssl
-# Create volumes for several directories:
+# Create volumes
VOLUME $FIREFLY_PATH/storage/export $FIREFLY_PATH/storage/upload
# Enable default site (Firefly III)
@@ -79,15 +97,16 @@ COPY ./.deploy/docker/apache-firefly.conf /etc/apache2/sites-available/000-defau
# Make sure we own Firefly III directory
RUN chown -R www-data:www-data /var/www && chmod -R 775 $FIREFLY_PATH/storage
+# Copy in Firefly Source
+WORKDIR $FIREFLY_PATH
+ADD . $FIREFLY_PATH
+
# Run composer
+ENV COMPOSER_ALLOW_SUPERUSER 1
RUN composer install --prefer-dist --no-dev --no-scripts --no-suggest
# Expose port 80
EXPOSE 80
-# Run the command on container startup
-CMD cron
-
# Run entrypoint thing
ENTRYPOINT [".deploy/docker/entrypoint.sh"]
-
diff --git a/app/Api/V1/Controllers/AccountController.php b/app/Api/V1/Controllers/AccountController.php
index 9fef764538..bf3213cbd2 100644
--- a/app/Api/V1/Controllers/AccountController.php
+++ b/app/Api/V1/Controllers/AccountController.php
@@ -221,6 +221,11 @@ class AccountController extends Controller
'special' => [AccountType::CASH, AccountType::INITIAL_BALANCE, AccountType::IMPORT, AccountType::RECONCILIATION,
AccountType::LOAN,],
'hidden' => [AccountType::INITIAL_BALANCE, AccountType::IMPORT, AccountType::RECONCILIATION, AccountType::LOAN,],
+ 'liability' => [AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::CREDITCARD],
+ 'liabilities' => [AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::CREDITCARD],
+ 'cc' => [AccountType::CREDITCARD],
+ 'creditcard' => [AccountType::CREDITCARD],
+ 'credit_card' => [AccountType::CREDITCARD],
AccountType::DEFAULT => [AccountType::DEFAULT],
AccountType::CASH => [AccountType::CASH],
AccountType::ASSET => [AccountType::ASSET],
@@ -231,6 +236,10 @@ class AccountController extends Controller
AccountType::IMPORT => [AccountType::IMPORT],
AccountType::RECONCILIATION => [AccountType::RECONCILIATION],
AccountType::LOAN => [AccountType::LOAN],
+ AccountType::MORTGAGE => [AccountType::MORTGAGE],
+ AccountType::DEBT => [AccountType::DEBT],
+ AccountType::CREDITCARD => [AccountType::CREDITCARD],
+
];
$return = $types['all'];
if (isset($types[$type])) {
diff --git a/app/Api/V1/Controllers/AvailableBudgetController.php b/app/Api/V1/Controllers/AvailableBudgetController.php
index fa2697dbeb..13c56f1495 100644
--- a/app/Api/V1/Controllers/AvailableBudgetController.php
+++ b/app/Api/V1/Controllers/AvailableBudgetController.php
@@ -152,7 +152,10 @@ class AvailableBudgetController extends Controller
public function store(AvailableBudgetRequest $request): JsonResponse
{
$data = $request->getAll();
- $currency = $this->currencyRepository->findNull($data['transaction_currency_id']);
+ $currency = $this->currencyRepository->findNull($data['currency_id']);
+ if (null === $currency) {
+ $currency = $this->currencyRepository->findByCodeNull($data['currency_code']);
+ }
if (null === $currency) {
throw new FireflyException('Could not find the indicated currency.');
}
@@ -166,6 +169,8 @@ class AvailableBudgetController extends Controller
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json');
}
+
+
/**
* Update the specified resource in storage.
*
diff --git a/app/Api/V1/Controllers/BudgetLimitController.php b/app/Api/V1/Controllers/BudgetLimitController.php
index 16cf1d095a..99127989cb 100644
--- a/app/Api/V1/Controllers/BudgetLimitController.php
+++ b/app/Api/V1/Controllers/BudgetLimitController.php
@@ -34,7 +34,6 @@ use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
-
use League\Fractal\Manager;
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
use League\Fractal\Resource\Collection as FractalCollection;
diff --git a/app/Api/V1/Controllers/ConfigurationController.php b/app/Api/V1/Controllers/ConfigurationController.php
index aee513f7cb..c21b430b0c 100644
--- a/app/Api/V1/Controllers/ConfigurationController.php
+++ b/app/Api/V1/Controllers/ConfigurationController.php
@@ -54,6 +54,7 @@ class ConfigurationController extends Controller
$admin = auth()->user();
if (!$this->repository->hasRole($admin, 'owner')) {
+ /** @noinspection ExceptionsAnnotatingAndHandlingInspection */
throw new FireflyException('No access to method.'); // @codeCoverageIgnore
}
diff --git a/app/Api/V1/Controllers/JournalLinkController.php b/app/Api/V1/Controllers/JournalLinkController.php
index 7ae6b12792..c428fc0146 100644
--- a/app/Api/V1/Controllers/JournalLinkController.php
+++ b/app/Api/V1/Controllers/JournalLinkController.php
@@ -175,8 +175,7 @@ class JournalLinkController extends Controller
$data['direction'] = 'inward';
$journalLink = $this->repository->storeLink($data, $inward, $outward);
-
- $resource = new Item($journalLink, new JournalLinkTransformer($this->parameters), 'journal_links');
+ $resource = new Item($journalLink, new JournalLinkTransformer($this->parameters), 'journal_links');
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json');
diff --git a/app/Api/V1/Controllers/PiggyBankController.php b/app/Api/V1/Controllers/PiggyBankController.php
index 4b1da3528f..8ab73dbcdc 100644
--- a/app/Api/V1/Controllers/PiggyBankController.php
+++ b/app/Api/V1/Controllers/PiggyBankController.php
@@ -161,7 +161,7 @@ class PiggyBankController extends Controller
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json');
}
- throw new FireflyException('Could not store new piggy bank.'); // @codeCoverageIgnore
+ throw new FireflyException('Could not store new piggy bank.');
}
diff --git a/app/Api/V1/Controllers/RuleController.php b/app/Api/V1/Controllers/RuleController.php
index 42e016332a..07ff7c870c 100644
--- a/app/Api/V1/Controllers/RuleController.php
+++ b/app/Api/V1/Controllers/RuleController.php
@@ -101,7 +101,7 @@ class RuleController extends Controller
// make paginator:
$paginator = new LengthAwarePaginator($rules, $count, $pageSize, $this->parameters->get('page'));
- $paginator->setPath(route('api.v1.piggy_banks.index') . $this->buildParams());
+ $paginator->setPath(route('api.v1.rules.index') . $this->buildParams());
// present to user.
$manager->setSerializer(new JsonApiSerializer($baseUrl));
diff --git a/app/Api/V1/Controllers/RuleGroupController.php b/app/Api/V1/Controllers/RuleGroupController.php
index 83185cec11..d0d9a8d07c 100644
--- a/app/Api/V1/Controllers/RuleGroupController.php
+++ b/app/Api/V1/Controllers/RuleGroupController.php
@@ -95,7 +95,7 @@ class RuleGroupController extends Controller
// types to get, page size:
$pageSize = (int)app('preferences')->getForUser(auth()->user(), 'listPageSize', 50)->data;
- // get list of budgets. Count it and split it.
+ // get list of rule groups. Count it and split it.
$collection = $this->ruleGroupRepository->get();
$count = $collection->count();
$ruleGroups = $collection->slice(($this->parameters->get('page') - 1) * $pageSize, $pageSize);
diff --git a/app/Api/V1/Controllers/TransactionController.php b/app/Api/V1/Controllers/TransactionController.php
index 91210a8525..9df8330527 100644
--- a/app/Api/V1/Controllers/TransactionController.php
+++ b/app/Api/V1/Controllers/TransactionController.php
@@ -26,7 +26,7 @@ namespace FireflyIII\Api\V1\Controllers;
use FireflyIII\Api\V1\Requests\TransactionRequest;
use FireflyIII\Exceptions\FireflyException;
-use FireflyIII\Helpers\Collector\JournalCollectorInterface;
+use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Helpers\Filter\InternalTransferFilter;
use FireflyIII\Helpers\Filter\NegativeAmountFilter;
use FireflyIII\Helpers\Filter\PositiveAmountFilter;
@@ -108,8 +108,8 @@ class TransactionController extends Controller
/** @var User $admin */
$admin = auth()->user();
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setUser($admin);
$collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation();
$collector->setAllAssetAccounts();
@@ -123,7 +123,7 @@ class TransactionController extends Controller
}
$collector->setLimit($pageSize)->setPage($this->parameters->get('page'));
$collector->setTypes($types);
- $paginator = $collector->getPaginatedJournals();
+ $paginator = $collector->getPaginatedTransactions();
$paginator->setPath(route('api.v1.transactions.index') . $this->buildParams());
$transactions = $paginator->getCollection();
@@ -155,7 +155,7 @@ class TransactionController extends Controller
$manager->parseIncludes($include);
// collect transactions using the journal collector
- $collector = app(JournalCollectorInterface::class);
+ $collector = app(TransactionCollectorInterface::class);
$collector->setUser(auth()->user());
$collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation();
// filter on specific journals.
@@ -170,7 +170,7 @@ class TransactionController extends Controller
$collector->addFilter(NegativeAmountFilter::class);
}
- $transactions = $collector->getJournals();
+ $transactions = $collector->getTransactions();
$resource = new FractalCollection($transactions, new TransactionTransformer($this->parameters), 'transactions');
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json');
@@ -201,7 +201,7 @@ class TransactionController extends Controller
$manager->parseIncludes($include);
// collect transactions using the journal collector
- $collector = app(JournalCollectorInterface::class);
+ $collector = app(TransactionCollectorInterface::class);
$collector->setUser(auth()->user());
$collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation();
// filter on specific journals.
@@ -216,7 +216,7 @@ class TransactionController extends Controller
$collector->addFilter(NegativeAmountFilter::class);
}
- $transactions = $collector->getJournals();
+ $transactions = $collector->getTransactions();
$resource = new FractalCollection($transactions, new TransactionTransformer($this->parameters), 'transactions');
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json');
@@ -247,7 +247,7 @@ class TransactionController extends Controller
// needs a lot of extra data to match the journal collector. Or just expand that one.
// collect transactions using the journal collector
- $collector = app(JournalCollectorInterface::class);
+ $collector = app(TransactionCollectorInterface::class);
$collector->setUser(auth()->user());
$collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation();
// filter on specific journals.
@@ -262,7 +262,7 @@ class TransactionController extends Controller
$collector->addFilter(NegativeAmountFilter::class);
}
- $transactions = $collector->getJournals();
+ $transactions = $collector->getTransactions();
$resource = new FractalCollection($transactions, new TransactionTransformer($this->parameters), 'transactions');
return response()->json($manager->createData($resource)->toArray())->header('Content-Type', 'application/vnd.api+json');
diff --git a/app/Api/V1/Requests/AccountRequest.php b/app/Api/V1/Requests/AccountRequest.php
index db0feadf7e..52d1db7726 100644
--- a/app/Api/V1/Requests/AccountRequest.php
+++ b/app/Api/V1/Requests/AccountRequest.php
@@ -50,6 +50,7 @@ class AccountRequest extends Request
$data = [
'name' => $this->string('name'),
'active' => $this->boolean('active'),
+ 'include_net_worth' => $this->boolean('include_net_worth'),
'accountType' => $this->string('type'),
'account_type_id' => null,
'currency_id' => $this->integer('currency_id'),
@@ -64,7 +65,21 @@ class AccountRequest extends Request
'ccType' => $this->string('cc_type'),
'ccMonthlyPaymentDate' => $this->string('cc_monthly_payment_date'),
'notes' => $this->string('notes'),
+ 'interest' => $this->string('interest'),
+ 'interest_period' => $this->string('interest_period'),
];
+ // new fields for liabilities
+ // 'liability_type' => $this->string('liability_type'),
+ // 'liability_start_date' => $this->date('liability_start_date'),
+
+
+ //];
+ if ('liability' === $data['accountType']) {
+ $data['openingBalance'] = bcmul($this->string('liability_amount'), '-1');
+ $data['openingBalanceDate'] = $this->date('liability_start_date');
+ $data['accountType'] = $this->string('liability_type');
+ $data['account_type_id'] = null;
+ }
return $data;
}
@@ -91,10 +106,18 @@ class AccountRequest extends Request
'account_number' => 'between:1,255|nullable|uniqueAccountNumberForUser',
'account_role' => 'in:' . $accountRoles . '|required_if:type,asset',
'active' => 'required|boolean',
+ 'include_net_worth' => 'required|boolean',
'cc_type' => 'in:' . $ccPaymentTypes . '|required_if:account_role,ccAsset',
'cc_monthly_payment_date' => 'date' . '|required_if:account_role,ccAsset|required_if:cc_type,monthlyFull',
'type' => 'required|in:' . $types,
'notes' => 'min:0|max:65536',
+ // required fields for liabilities:
+ 'liability_type' => 'required_if:type,liability|in:loan,debt,mortgage,credit card',
+ 'liability_amount' => 'required_if:type,liability|min:0|numeric',
+ 'liability_start_date' => 'required_if:type,liability|date',
+ 'interest' => 'required_if:type,liability|between:0,100|numeric',
+ 'interest_period' => 'required_if:type,liability|in:daily,monthly,yearly',
+
];
switch ($this->method()) {
default:
diff --git a/app/Api/V1/Requests/AvailableBudgetRequest.php b/app/Api/V1/Requests/AvailableBudgetRequest.php
index dd6daf4e65..865b2b7fd5 100644
--- a/app/Api/V1/Requests/AvailableBudgetRequest.php
+++ b/app/Api/V1/Requests/AvailableBudgetRequest.php
@@ -47,10 +47,11 @@ class AvailableBudgetRequest extends Request
public function getAll(): array
{
return [
- 'transaction_currency_id' => $this->integer('transaction_currency_id'),
- 'amount' => $this->string('amount'),
- 'start_date' => $this->date('start_date'),
- 'end_date' => $this->date('end_date'),
+ 'currency_id' => $this->integer('currency_id'),
+ 'currency_code' => $this->string('currency_code'),
+ 'amount' => $this->string('amount'),
+ 'start_date' => $this->date('start_date'),
+ 'end_date' => $this->date('end_date'),
];
}
@@ -62,10 +63,11 @@ class AvailableBudgetRequest extends Request
public function rules(): array
{
$rules = [
- 'transaction_currency_id' => 'required|numeric|exists:transaction_currencies,id',
- 'amount' => 'required|numeric|more:0',
- 'start_date' => 'required|date|before:end_date',
- 'end_date' => 'required|date|after:start_date',
+ 'currency_id' => 'numeric|exists:transaction_currencies,id|required_without:currency_code',
+ 'currency_code' => 'min:3|max:3|exists:transaction_currencies,code|required_without:currency_id',
+ 'amount' => 'required|numeric|more:0',
+ 'start_date' => 'required|date|before:end_date',
+ 'end_date' => 'required|date|after:start_date',
];
return $rules;
diff --git a/app/Api/V1/Requests/JournalLinkRequest.php b/app/Api/V1/Requests/JournalLinkRequest.php
index 19e5f8f430..2c31e0d889 100644
--- a/app/Api/V1/Requests/JournalLinkRequest.php
+++ b/app/Api/V1/Requests/JournalLinkRequest.php
@@ -23,6 +23,9 @@ declare(strict_types=1);
namespace FireflyIII\Api\V1\Requests;
+use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
+use FireflyIII\Repositories\LinkType\LinkTypeRepositoryInterface;
+use Illuminate\Validation\Validator;
/**
*
@@ -49,10 +52,11 @@ class JournalLinkRequest extends Request
public function getAll(): array
{
return [
- 'link_type_id' => $this->integer('link_type_id'),
- 'inward_id' => $this->integer('inward_id'),
- 'outward_id' => $this->integer('outward_id'),
- 'notes' => $this->string('notes'),
+ 'link_type_id' => $this->integer('link_type_id'),
+ 'link_type_name' => $this->string('link_type_name'),
+ 'inward_id' => $this->integer('inward_id'),
+ 'outward_id' => $this->integer('outward_id'),
+ 'notes' => $this->string('notes'),
];
}
@@ -65,11 +69,63 @@ class JournalLinkRequest extends Request
public function rules(): array
{
return [
- 'link_type_id' => 'required|exists:link_types,id',
- 'inward_id' => 'required|belongsToUser:transaction_journals,id',
- 'outward_id' => 'required|belongsToUser:transaction_journals,id',
- 'notes' => 'between:0,65000',
+ 'link_type_id' => 'exists:link_types,id|required_without:link_type_name',
+ 'link_type_name' => 'exists:link_types,name|required_without:link_type_id',
+ 'inward_id' => 'required|belongsToUser:transaction_journals,id',
+ 'outward_id' => 'required|belongsToUser:transaction_journals,id',
+ 'notes' => 'between:0,65000',
];
}
+ /**
+ * Configure the validator instance.
+ *
+ * @param Validator $validator
+ *
+ * @return void
+ */
+ public function withValidator(Validator $validator): void
+ {
+ $validator->after(
+ function (Validator $validator) {
+ $this->validateExistingLink($validator);
+ }
+ );
+ }
+
+ /**
+ * @param Validator $validator
+ */
+ private function validateExistingLink(Validator $validator): void
+ {
+ /** @var LinkTypeRepositoryInterface $repository */
+ $repository = app(LinkTypeRepositoryInterface::class);
+ $repository->setUser(auth()->user());
+
+ /** @var JournalRepositoryInterface $journalRepos */
+ $journalRepos = app(JournalRepositoryInterface::class);
+ $journalRepos->setUser(auth()->user());
+
+ $data = $validator->getData();
+ $inwardId = (int)($data['inward_id'] ?? 0);
+ $outwardId = (int)($data['outward_id'] ?? 0);
+ $inward = $journalRepos->findNull($inwardId);
+ $outward = $journalRepos->findNull($outwardId);
+
+ if (null === $inward) {
+ $validator->errors()->add('inward_id', 'Invalid inward ID.');
+
+ return;
+ }
+ if (null === $outward) {
+ $validator->errors()->add('outward_id', 'Invalid outward ID.');
+
+ return;
+ }
+
+ if ($repository->findLink($inward, $outward)) {
+ $validator->errors()->add('outward_id', 'Already have a link between inward and outward.');
+ $validator->errors()->add('inward_id', 'Already have a link between inward and outward.');
+ }
+ }
}
diff --git a/app/Api/V1/Requests/PiggyBankRequest.php b/app/Api/V1/Requests/PiggyBankRequest.php
index b113fa8fe2..68ef285527 100644
--- a/app/Api/V1/Requests/PiggyBankRequest.php
+++ b/app/Api/V1/Requests/PiggyBankRequest.php
@@ -57,7 +57,7 @@ class PiggyBankRequest extends Request
'current_amount' => $this->string('current_amount'),
'start_date' => $this->date('start_date'),
'target_date' => $this->date('target_date'),
- 'note' => $this->string('notes'),
+ 'notes' => $this->string('notes'),
];
}
diff --git a/app/Api/V1/Requests/RecurrenceRequest.php b/app/Api/V1/Requests/RecurrenceRequest.php
index fd348c733d..77adb8d242 100644
--- a/app/Api/V1/Requests/RecurrenceRequest.php
+++ b/app/Api/V1/Requests/RecurrenceRequest.php
@@ -84,7 +84,7 @@ class RecurrenceRequest extends Request
*/
public function rules(): array
{
- $today = Carbon::create()->addDay();
+ $today = Carbon::now()->addDay();
return [
'type' => 'required|in:withdrawal,transfer,deposit',
diff --git a/app/Api/V1/Requests/RuleRequest.php b/app/Api/V1/Requests/RuleRequest.php
index d44833852e..f8e50e4da2 100644
--- a/app/Api/V1/Requests/RuleRequest.php
+++ b/app/Api/V1/Requests/RuleRequest.php
@@ -56,27 +56,12 @@ class RuleRequest extends Request
'rule_group_title' => $this->string('rule_group_title'),
'trigger' => $this->string('trigger'),
'strict' => $this->boolean('strict'),
- 'stop-processing' => $this->boolean('stop_processing'),
+ 'stop_processing' => $this->boolean('stop_processing'),
'active' => $this->boolean('active'),
- 'rule-triggers' => [],
- 'rule-actions' => [],
+ 'rule_triggers' => $this->getRuleTriggers(),
+ 'rule_actions' => $this->getRuleActions(),
];
- foreach ($this->get('rule-triggers') as $trigger) {
- $data['rule-triggers'][] = [
- 'name' => $trigger['name'],
- 'value' => $trigger['value'],
- 'stop-processing' => 1 === (int)($trigger['stop-processing'] ?? 0),
- ];
- }
- foreach ($this->get('rule-actions') as $action) {
- $data['rule-actions'][] = [
- 'name' => $action['name'],
- 'value' => $action['value'],
- 'stop-processing' => 1 === (int)($action['stop-processing'] ?? 0),
- ];
- }
-
return $data;
}
@@ -99,12 +84,12 @@ class RuleRequest extends Request
'rule_group_id' => 'required|belongsToUser:rule_groups|required_without:rule_group_title',
'rule_group_title' => 'nullable|between:1,255|required_without:rule_group_id|belongsToUser:rule_groups,title',
'trigger' => 'required|in:store-journal,update-journal',
- 'rule-triggers.*.name' => 'required|in:' . implode(',', $validTriggers),
- 'rule-triggers.*.stop-processing' => 'boolean',
- 'rule-triggers.*.value' => 'required|min:1|ruleTriggerValue', //
- 'rule-actions.*.name' => 'required|in:' . implode(',', $validActions),
- 'rule-actions.*.value' => 'required_if:rule-action.*.type,' . $contextActions . '|ruleActionValue',
- 'rule-actions.*.stop-processing' => 'boolean',
+ 'rule_triggers.*.name' => 'required|in:' . implode(',', $validTriggers),
+ 'rule_triggers.*.stop_processing' => 'boolean',
+ 'rule_triggers.*.value' => 'required|min:1|ruleTriggerValue',
+ 'rule_actions.*.name' => 'required|in:' . implode(',', $validActions),
+ 'rule_actions.*.value' => 'required_if:rule_actions.*.type,' . $contextActions . '|ruleActionValue',
+ 'rule_actions.*.stop_processing' => 'boolean',
'strict' => 'required|boolean',
'stop_processing' => 'required|boolean',
'active' => 'required|boolean',
@@ -138,7 +123,7 @@ class RuleRequest extends Request
protected function atLeastOneAction(Validator $validator): void
{
$data = $validator->getData();
- $repetitions = $data['rule-actions'] ?? [];
+ $repetitions = $data['rule_actions'] ?? [];
// need at least one transaction
if (0 === \count($repetitions)) {
$validator->errors()->add('title', (string)trans('validation.at_least_one_action'));
@@ -153,10 +138,50 @@ class RuleRequest extends Request
protected function atLeastOneTrigger(Validator $validator): void
{
$data = $validator->getData();
- $repetitions = $data['rule-triggers'] ?? [];
+ $repetitions = $data['rule_triggers'] ?? [];
// need at least one transaction
if (0 === \count($repetitions)) {
$validator->errors()->add('title', (string)trans('validation.at_least_one_trigger'));
}
}
+
+ /**
+ * @return array
+ */
+ private function getRuleActions(): array
+ {
+ $actions = $this->get('rule_actions');
+ $return = [];
+ if (\is_array($actions)) {
+ foreach ($actions as $action) {
+ $return[] = [
+ 'name' => $action['name'],
+ 'value' => $action['value'],
+ 'stop_processing' => 1 === (int)($action['stop-processing'] ?? '0'),
+ ];
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * @return array
+ */
+ private function getRuleTriggers(): array
+ {
+ $triggers = $this->get('rule_triggers');
+ $return = [];
+ if (\is_array($triggers)) {
+ foreach ($triggers as $trigger) {
+ $return[] = [
+ 'name' => $trigger['name'],
+ 'value' => $trigger['value'],
+ 'stop_processing' => 1 === (int)($trigger['stop-processing'] ?? '0'),
+ ];
+ }
+ }
+
+ return $return;
+ }
}
diff --git a/app/Console/Commands/CreateExport.php b/app/Console/Commands/CreateExport.php
index 6896b409cf..73b4d3384f 100644
--- a/app/Console/Commands/CreateExport.php
+++ b/app/Console/Commands/CreateExport.php
@@ -39,6 +39,8 @@ use Storage;
* Class CreateExport.
*
* Generates export from the command line.
+ *
+ * @codeCoverageIgnore
*/
class CreateExport extends Command
{
diff --git a/app/Console/Commands/CreateImport.php b/app/Console/Commands/CreateImport.php
index 74910381e2..15234cb3aa 100644
--- a/app/Console/Commands/CreateImport.php
+++ b/app/Console/Commands/CreateImport.php
@@ -37,6 +37,8 @@ use Log;
/**
* Class CreateImport.
+ *
+ * @codeCoverageIgnore
*/
class CreateImport extends Command
{
@@ -150,7 +152,7 @@ class CreateImport extends Command
if (true === $this->option('start')) {
- $this->infoLine('The has started. The process is not visible. Please wait.');
+ $this->infoLine('The import routine has started. The process is not visible. Please wait.');
Log::debug('Go for import!');
// run it!
diff --git a/app/Console/Commands/Cron.php b/app/Console/Commands/Cron.php
new file mode 100644
index 0000000000..807de0c39f
--- /dev/null
+++ b/app/Console/Commands/Cron.php
@@ -0,0 +1,67 @@
+fire();
+ } catch (FireflyException $e) {
+ $this->error($e->getMessage());
+
+ return 0;
+ }
+ if (false === $result) {
+ $this->line('The recurring transaction cron job did not fire.');
+ }
+ if (true === $result) {
+ $this->line('The recurring transaction cron job fired successfully.');
+ }
+
+ $this->info('More feedback on the cron jobs can be found in the log files.');
+
+ return 0;
+ }
+
+
+}
diff --git a/app/Console/Commands/DecryptAttachment.php b/app/Console/Commands/DecryptAttachment.php
index dfa57386e5..450eedd8bd 100644
--- a/app/Console/Commands/DecryptAttachment.php
+++ b/app/Console/Commands/DecryptAttachment.php
@@ -31,6 +31,8 @@ use Log;
/**
* Class DecryptAttachment.
+ *
+ * @codeCoverageIgnore
*/
class DecryptAttachment extends Command
{
diff --git a/app/Console/Commands/EncryptFile.php b/app/Console/Commands/EncryptFile.php
index 64b19820a5..08d9683afa 100644
--- a/app/Console/Commands/EncryptFile.php
+++ b/app/Console/Commands/EncryptFile.php
@@ -29,6 +29,8 @@ use Illuminate\Console\Command;
/**
* Class EncryptFile.
+ *
+ * @codeCoverageIgnore
*/
class EncryptFile extends Command
{
diff --git a/app/Console/Commands/Import.php b/app/Console/Commands/Import.php
index 0f952e6af3..7ee59ea03b 100644
--- a/app/Console/Commands/Import.php
+++ b/app/Console/Commands/Import.php
@@ -35,6 +35,8 @@ use Log;
/**
* Class Import.
+ *
+ * @codeCoverageIgnore
*/
class Import extends Command
{
@@ -64,7 +66,7 @@ class Import extends Command
Log::debug('Start start-import command');
$jobKey = (string)$this->argument('key');
/** @var ImportJob $job */
- $job = ImportJob::where('key', $jobKey)->first();
+ $job = ImportJob::where('key', $jobKey)->first();
if (null === $job) {
$this->errorLine(sprintf('No job found with key "%s"', $jobKey));
diff --git a/app/Console/Commands/ScanAttachments.php b/app/Console/Commands/ScanAttachments.php
index 0a77363a0a..034a1c2fa3 100644
--- a/app/Console/Commands/ScanAttachments.php
+++ b/app/Console/Commands/ScanAttachments.php
@@ -34,6 +34,8 @@ use Storage;
/**
* Class ScanAttachments.
+ *
+ * @codeCoverageIgnore
*/
class ScanAttachments extends Command
{
@@ -82,6 +84,7 @@ class ScanAttachments extends Command
$attachment->save();
$this->line(sprintf('Fixed attachment #%d', $attachment->id));
}
+
return 0;
}
}
diff --git a/app/Console/Commands/UpgradeDatabase.php b/app/Console/Commands/UpgradeDatabase.php
index b126e38850..b1b393398b 100644
--- a/app/Console/Commands/UpgradeDatabase.php
+++ b/app/Console/Commands/UpgradeDatabase.php
@@ -63,6 +63,7 @@ use UnexpectedValueException;
* Upgrade user database.
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ * @codeCoverageIgnore
*/
class UpgradeDatabase extends Command
{
diff --git a/app/Console/Commands/UpgradeFireflyInstructions.php b/app/Console/Commands/UpgradeFireflyInstructions.php
index d2e4730204..37cd28f4ea 100644
--- a/app/Console/Commands/UpgradeFireflyInstructions.php
+++ b/app/Console/Commands/UpgradeFireflyInstructions.php
@@ -27,6 +27,8 @@ use Illuminate\Console\Command;
/**
* Class UpgradeFireflyInstructions.
+ *
+ * @codeCoverageIgnore
*/
class UpgradeFireflyInstructions extends Command
{
@@ -54,6 +56,7 @@ class UpgradeFireflyInstructions extends Command
if ('install' === (string)$this->argument('task')) {
$this->installInstructions();
}
+
return 0;
}
diff --git a/app/Console/Commands/UseEncryption.php b/app/Console/Commands/UseEncryption.php
index bfc04db91c..55ef87eee9 100644
--- a/app/Console/Commands/UseEncryption.php
+++ b/app/Console/Commands/UseEncryption.php
@@ -28,6 +28,7 @@ use Illuminate\Support\Str;
/**
* Class UseEncryption.
+ * @codeCoverageIgnore
*/
class UseEncryption extends Command
{
@@ -62,6 +63,7 @@ class UseEncryption extends Command
$this->handleObjects('Category', 'name', 'encrypted');
$this->handleObjects('PiggyBank', 'name', 'encrypted');
$this->handleObjects('TransactionJournal', 'description', 'encrypted');
+
return 0;
}
diff --git a/app/Console/Commands/VerifiesAccessToken.php b/app/Console/Commands/VerifiesAccessToken.php
index 857aeaa30b..a0224ae0d6 100644
--- a/app/Console/Commands/VerifiesAccessToken.php
+++ b/app/Console/Commands/VerifiesAccessToken.php
@@ -30,6 +30,7 @@ use Log;
* Trait VerifiesAccessToken.
*
* Verifies user access token for sensitive commands.
+ * @codeCoverageIgnore
*/
trait VerifiesAccessToken
{
diff --git a/app/Console/Commands/VerifyDatabase.php b/app/Console/Commands/VerifyDatabase.php
index 88635664ae..31b8b89658 100644
--- a/app/Console/Commands/VerifyDatabase.php
+++ b/app/Console/Commands/VerifyDatabase.php
@@ -50,6 +50,7 @@ use stdClass;
*
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ * @codeCoverageIgnore
*/
class VerifyDatabase extends Command
{
diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index 3c1fe481de..1b3fdb5311 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -24,13 +24,13 @@ declare(strict_types=1);
namespace FireflyIII\Console;
-use Carbon\Carbon;
-use FireflyIII\Jobs\CreateRecurringTransactions;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
+use Log;
/**
* File to make sure commands work.
+ * @codeCoverageIgnore
*/
class Kernel extends ConsoleKernel
{
@@ -52,6 +52,21 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule): void
{
- $schedule->job(new CreateRecurringTransactions(new Carbon))->daily();
+ $schedule->call(
+ function () {
+ Log::error('Firefly III no longer users the Laravel scheduler to do cron jobs! Please read the instructions at https://firefly-iii.readthedocs.io/en/latest/');
+ echo "\n";
+ echo '------------';
+ echo "\n";
+ echo wordwrap('Firefly III no longer users the Laravel scheduler to do cron jobs! Please read the instructions here:');
+ echo "\n";
+ echo 'https://firefly-iii.readthedocs.io/en/latest/';
+ echo "\n\n";
+ echo 'Disable this cron job!';
+ echo "\n";
+ echo '------------';
+ echo "\n";
+ }
+ )->everyMinute();
}
}
diff --git a/app/Events/AdminRequestedTestMessage.php b/app/Events/AdminRequestedTestMessage.php
index 800ffb909f..0a82c434cc 100644
--- a/app/Events/AdminRequestedTestMessage.php
+++ b/app/Events/AdminRequestedTestMessage.php
@@ -30,6 +30,7 @@ use Log;
/**
* Class AdminRequestedTestMessage.
+ * @codeCoverageIgnore
*/
class AdminRequestedTestMessage extends Event
{
diff --git a/app/Events/Event.php b/app/Events/Event.php
index 41b3b0437c..84ea6bdb2b 100644
--- a/app/Events/Event.php
+++ b/app/Events/Event.php
@@ -26,6 +26,7 @@ namespace FireflyIII\Events;
/**
* Class Event.
+ * @codeCoverageIgnore
*/
abstract class Event
{
diff --git a/app/Events/RegisteredUser.php b/app/Events/RegisteredUser.php
index 1bbb4b25dd..c576b16f94 100644
--- a/app/Events/RegisteredUser.php
+++ b/app/Events/RegisteredUser.php
@@ -29,6 +29,7 @@ use Illuminate\Queue\SerializesModels;
/**
* Class RegisteredUser.
+ * @codeCoverageIgnore
*/
class RegisteredUser extends Event
{
diff --git a/app/Events/RequestedNewPassword.php b/app/Events/RequestedNewPassword.php
index 4452a8dfeb..7dce83c05d 100644
--- a/app/Events/RequestedNewPassword.php
+++ b/app/Events/RequestedNewPassword.php
@@ -29,6 +29,7 @@ use Illuminate\Queue\SerializesModels;
/**
* Class RequestedNewPassword.
+ * @codeCoverageIgnore
*/
class RequestedNewPassword extends Event
{
@@ -36,10 +37,10 @@ class RequestedNewPassword extends Event
/** @var string The users IP address */
public $ipAddress;
- /** @var User The user */
- public $user;
/** @var string The token */
public $token;
+ /** @var User The user */
+ public $user;
/**
* Create a new event instance. This event is triggered when a users tries to reset his or her password.
diff --git a/app/Events/RequestedReportOnJournals.php b/app/Events/RequestedReportOnJournals.php
index 9d9c0444a6..0bd2ac4c55 100644
--- a/app/Events/RequestedReportOnJournals.php
+++ b/app/Events/RequestedReportOnJournals.php
@@ -31,6 +31,8 @@ use Log;
/**
* Class RequestedReportOnJournals
+ *
+ * @codeCoverageIgnore
*/
class RequestedReportOnJournals
{
diff --git a/app/Events/RequestedVersionCheckStatus.php b/app/Events/RequestedVersionCheckStatus.php
index dabe9ca7b8..a490ea0e38 100644
--- a/app/Events/RequestedVersionCheckStatus.php
+++ b/app/Events/RequestedVersionCheckStatus.php
@@ -30,6 +30,8 @@ use Illuminate\Queue\SerializesModels;
/**
* Class RequestedVersionCheckStatus
+ *
+ * @codeCoverageIgnore
*/
class RequestedVersionCheckStatus extends Event
{
diff --git a/app/Events/UserChangedEmail.php b/app/Events/UserChangedEmail.php
index 917eef90ae..12e7aab446 100644
--- a/app/Events/UserChangedEmail.php
+++ b/app/Events/UserChangedEmail.php
@@ -29,6 +29,8 @@ use Illuminate\Queue\SerializesModels;
/**
* Class UserChangedEmail.
+ *
+ * @codeCoverageIgnore
*/
class UserChangedEmail extends Event
{
diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php
index 5d6899c52b..72caac1261 100644
--- a/app/Exceptions/Handler.php
+++ b/app/Exceptions/Handler.php
@@ -38,6 +38,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Class Handler
+ * @codeCoverageIgnore
*/
class Handler extends ExceptionHandler
{
diff --git a/app/Export/Collector/AttachmentCollector.php b/app/Export/Collector/AttachmentCollector.php
index 9eecde245b..a0a354451b 100644
--- a/app/Export/Collector/AttachmentCollector.php
+++ b/app/Export/Collector/AttachmentCollector.php
@@ -28,13 +28,15 @@ use Crypt;
use FireflyIII\Models\Attachment;
use FireflyIII\Repositories\Attachment\AttachmentRepositoryInterface;
use Illuminate\Contracts\Encryption\DecryptException;
-use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Support\Collection;
use Log;
use Storage;
/**
* Class AttachmentCollector.
+ *
+ * @deprecated
+ * @codeCoverageIgnore
*/
class AttachmentCollector extends BasicCollector implements CollectorInterface
{
@@ -108,7 +110,7 @@ class AttachmentCollector extends BasicCollector implements CollectorInterface
if ($this->uploadDisk->exists($file)) {
try {
$decrypted = Crypt::decrypt($this->uploadDisk->get($file));
- } catch (FileNotFoundException|DecryptException $e) {
+ } catch (DecryptException $e) {
Log::error('Catchable error: could not decrypt attachment #' . $attachment->id . ' because: ' . $e->getMessage());
return false;
diff --git a/app/Export/Collector/BasicCollector.php b/app/Export/Collector/BasicCollector.php
index 10215d6c58..c1db2a82a3 100644
--- a/app/Export/Collector/BasicCollector.php
+++ b/app/Export/Collector/BasicCollector.php
@@ -30,6 +30,9 @@ use Illuminate\Support\Collection;
/**
* Class BasicCollector.
+ *
+ * @codeCoverageIgnore
+ * @deprecated
*/
class BasicCollector
{
diff --git a/app/Export/Collector/CollectorInterface.php b/app/Export/Collector/CollectorInterface.php
index b93dc1617b..c58e538ee1 100644
--- a/app/Export/Collector/CollectorInterface.php
+++ b/app/Export/Collector/CollectorInterface.php
@@ -29,6 +29,9 @@ use Illuminate\Support\Collection;
/**
* Interface CollectorInterface.
+ *
+ * @codeCoverageIgnore
+ * @deprecated
*/
interface CollectorInterface
{
diff --git a/app/Export/Collector/UploadCollector.php b/app/Export/Collector/UploadCollector.php
index 5e1e6df02b..e2637b940f 100644
--- a/app/Export/Collector/UploadCollector.php
+++ b/app/Export/Collector/UploadCollector.php
@@ -31,6 +31,9 @@ use Storage;
/**
* Class UploadCollector.
+ *
+ * @codeCoverageIgnore
+ * @deprecated
*/
class UploadCollector extends BasicCollector implements CollectorInterface
{
diff --git a/app/Export/Entry/Entry.php b/app/Export/Entry/Entry.php
index 8543c75646..9e0064d58e 100644
--- a/app/Export/Entry/Entry.php
+++ b/app/Export/Entry/Entry.php
@@ -43,6 +43,9 @@ use FireflyIII\Models\Transaction;
*
* @SuppressWarnings(PHPMD.LongVariable)
* @SuppressWarnings(PHPMD.TooManyFields)
+ *
+ * @codeCoverageIgnore
+ * @deprecated
*/
final class Entry
{
diff --git a/app/Export/ExpandedProcessor.php b/app/Export/ExpandedProcessor.php
index 27fdc83822..4adb9499cf 100644
--- a/app/Export/ExpandedProcessor.php
+++ b/app/Export/ExpandedProcessor.php
@@ -32,7 +32,7 @@ use FireflyIII\Export\Collector\AttachmentCollector;
use FireflyIII\Export\Collector\UploadCollector;
use FireflyIII\Export\Entry\Entry;
use FireflyIII\Export\Exporter\ExporterInterface;
-use FireflyIII\Helpers\Collector\JournalCollectorInterface;
+use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Helpers\Filter\InternalTransferFilter;
use FireflyIII\Models\AccountMeta;
use FireflyIII\Models\ExportJob;
@@ -49,12 +49,15 @@ use ZipArchive;
* Class ExpandedProcessor.
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects) // its doing a lot.
+ *
+ * @codeCoverageIgnore
+ * @deprecated
*/
class ExpandedProcessor implements ProcessorInterface
{
/** @var Collection All accounts */
public $accounts;
- /** @var string The export format*/
+ /** @var string The export format */
public $exportFormat;
/** @var bool Should include attachments */
public $includeAttachments;
@@ -62,7 +65,7 @@ class ExpandedProcessor implements ProcessorInterface
public $includeOldUploads;
/** @var ExportJob The export job itself */
public $job;
- /** @var array The settings*/
+ /** @var array The settings */
public $settings;
/** @var Collection The entries to export. */
private $exportEntries;
@@ -107,13 +110,13 @@ class ExpandedProcessor implements ProcessorInterface
public function collectJournals(): bool
{
// use journal collector thing.
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setUser($this->job->user);
$collector->setAccounts($this->accounts)->setRange($this->settings['startDate'], $this->settings['endDate'])
->withOpposingAccount()->withBudgetInformation()->withCategoryInformation()
->removeFilter(InternalTransferFilter::class);
- $transactions = $collector->getJournals();
+ $transactions = $collector->getTransactions();
// get some more meta data for each entry:
$ids = $transactions->pluck('journal_id')->toArray();
$assetIds = $transactions->pluck('account_id')->toArray();
@@ -222,7 +225,7 @@ class ExpandedProcessor implements ProcessorInterface
{
$exporterClass = config('firefly.export_formats.' . $this->exportFormat);
/** @var ExporterInterface $exporter */
- $exporter = app($exporterClass);
+ $exporter = app($exporterClass);
$exporter->setJob($this->job);
$exporter->setEntries($this->exportEntries);
$exporter->run();
@@ -260,7 +263,7 @@ class ExpandedProcessor implements ProcessorInterface
/**
* Delete files.
*/
- private function deleteFiles():void
+ private function deleteFiles(): void
{
$disk = Storage::disk('export');
foreach ($this->getFiles() as $file) {
diff --git a/app/Export/Exporter/BasicExporter.php b/app/Export/Exporter/BasicExporter.php
index fd1003ddfa..de7eaed474 100644
--- a/app/Export/Exporter/BasicExporter.php
+++ b/app/Export/Exporter/BasicExporter.php
@@ -29,6 +29,9 @@ use Illuminate\Support\Collection;
/**
* Class BasicExporter.
+ *
+ * @codeCoverageIgnore
+ * @deprecated
*/
class BasicExporter
{
diff --git a/app/Export/Exporter/CsvExporter.php b/app/Export/Exporter/CsvExporter.php
index bbc66b8345..6e4a7b96d1 100644
--- a/app/Export/Exporter/CsvExporter.php
+++ b/app/Export/Exporter/CsvExporter.php
@@ -30,6 +30,9 @@ use Storage;
/**
* Class CsvExporter.
+ *
+ * @codeCoverageIgnore
+ * @deprecated
*/
class CsvExporter extends BasicExporter implements ExporterInterface
{
diff --git a/app/Export/Exporter/ExporterInterface.php b/app/Export/Exporter/ExporterInterface.php
index cc44b69c08..998510f65b 100644
--- a/app/Export/Exporter/ExporterInterface.php
+++ b/app/Export/Exporter/ExporterInterface.php
@@ -29,6 +29,9 @@ use Illuminate\Support\Collection;
/**
* Interface ExporterInterface.
+ *
+ * @codeCoverageIgnore
+ * @deprecated
*/
interface ExporterInterface
{
diff --git a/app/Export/ProcessorInterface.php b/app/Export/ProcessorInterface.php
index 641e26eaeb..330e69c2d2 100644
--- a/app/Export/ProcessorInterface.php
+++ b/app/Export/ProcessorInterface.php
@@ -28,6 +28,9 @@ use Illuminate\Support\Collection;
/**
* Interface ProcessorInterface.
+ *
+ * @codeCoverageIgnore
+ * @deprecated
*/
interface ProcessorInterface
{
diff --git a/app/Factory/AccountFactory.php b/app/Factory/AccountFactory.php
index aeb9b62b3b..6be9b42330 100644
--- a/app/Factory/AccountFactory.php
+++ b/app/Factory/AccountFactory.php
@@ -32,6 +32,7 @@ use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
use FireflyIII\Services\Internal\Support\AccountServiceTrait;
use FireflyIII\User;
+use Log;
/**
* Factory to create or return accounts.
@@ -80,8 +81,9 @@ class AccountFactory
'iban' => $data['iban'],
];
- // remove virtual balance when not an asset account:
- if ($type->type !== AccountType::ASSET) {
+ // remove virtual balance when not an asset account or a liability
+ $canHaveVirtual = [AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::CREDITCARD];
+ if (!\in_array($type->type, $canHaveVirtual, true)) {
$databaseData['virtual_balance'] = '0';
}
@@ -93,7 +95,7 @@ class AccountFactory
$return = Account::create($databaseData);
$this->updateMetaData($return, $data);
- if ($type->type === AccountType::ASSET) {
+ if (\in_array($type->type, $canHaveVirtual, true)) {
if ($this->validIBData($data)) {
$this->updateIB($return, $data);
}
@@ -130,6 +132,7 @@ class AccountFactory
}
/**
+ *
* @param string $accountName
* @param string $accountType
*
@@ -188,9 +191,13 @@ class AccountFactory
$result = AccountType::find($accountTypeId);
}
if (null === $result) {
- /** @var string $type */
- $type = (string)config('firefly.accountTypeByIdentifier.' . $accountType);
- $result = AccountType::whereType($type)->first();
+ Log::debug(sprintf('No account type found by ID, continue search for "%s".', $accountType));
+ /** @var array $types */
+ $types = config('firefly.accountTypeByIdentifier.' . $accountType) ?? [];
+ if (\count($types) > 0) {
+ Log::debug(sprintf('%d accounts in list from config', \count($types)), $types);
+ $result = AccountType::whereIn('type', $types)->first();
+ }
if (null === $result && null !== $accountType) {
// try as full name:
$result = AccountType::whereType($accountType)->first();
diff --git a/app/Factory/AccountMetaFactory.php b/app/Factory/AccountMetaFactory.php
index 67ec707046..5861e81b4d 100644
--- a/app/Factory/AccountMetaFactory.php
+++ b/app/Factory/AccountMetaFactory.php
@@ -65,7 +65,7 @@ class AccountMetaFactory
// if $data has field and $entry is null, create new one:
if (null === $entry) {
Log::debug(sprintf('Created meta-field "%s":"%s" for account #%d ("%s") ', $field, $value, $account->id, $account->name));
- $this->create(['account_id' => $account->id, 'name' => $field, 'data' => $value]);
+ return $this->create(['account_id' => $account->id, 'name' => $field, 'data' => $value]);
}
// if $data has field and $entry is not null, update $entry:
@@ -75,12 +75,13 @@ class AccountMetaFactory
Log::debug(sprintf('Updated meta-field "%s":"%s" for #%d ("%s") ', $field, $value, $account->id, $account->name));
}
}
- if ('' === $value && null !== $entry && isset($data[$field])) {
+ if ('' === $value && null !== $entry) {
try {
$entry->delete();
- } catch (Exception $e) {
- Log::debug(sprintf('Could not delete entry: %s', $e->getMessage()));
+ } catch (Exception $e) { // @codeCoverageIgnore
+ Log::debug(sprintf('Could not delete entry: %s', $e->getMessage())); // @codeCoverageIgnore
}
+ return null;
}
return $entry;
diff --git a/app/Factory/BillFactory.php b/app/Factory/BillFactory.php
index a3de276298..c08cea5a8d 100644
--- a/app/Factory/BillFactory.php
+++ b/app/Factory/BillFactory.php
@@ -25,6 +25,7 @@ declare(strict_types=1);
namespace FireflyIII\Factory;
use FireflyIII\Models\Bill;
+use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Services\Internal\Support\BillServiceTrait;
use FireflyIII\User;
use Illuminate\Support\Collection;
@@ -46,6 +47,10 @@ class BillFactory
*/
public function create(array $data): ?Bill
{
+ /** @var TransactionCurrencyFactory $factory */
+ $factory = app(TransactionCurrencyFactory::class);
+ /** @var TransactionCurrency $currency */
+ $currency = $factory->find((int)$data['currency_id'], (string)$data['currency_code']);
/** @var Bill $bill */
$bill = Bill::create(
[
@@ -53,7 +58,7 @@ class BillFactory
'match' => 'MIGRATED_TO_RULES',
'amount_min' => $data['amount_min'],
'user_id' => $this->user->id,
- 'transaction_currency_id' => $data['transaction_currency_id'],
+ 'transaction_currency_id' => $currency->id,
'amount_max' => $data['amount_max'],
'date' => $data['date'],
'repeat_freq' => $data['repeat_freq'],
diff --git a/app/Factory/TransactionFactory.php b/app/Factory/TransactionFactory.php
index 39d483b46a..9168702251 100644
--- a/app/Factory/TransactionFactory.php
+++ b/app/Factory/TransactionFactory.php
@@ -59,10 +59,10 @@ class TransactionFactory
$currencyId = isset($data['currency']) ? $data['currency']->id : $currencyId;
if ('' === $data['amount']) {
Log::error('Empty string in data.', $data);
- throw new FireflyException('Amount is an empty string, which Firefly III cannot handle. Apologies.'); // @codeCoverageIgnore
+ throw new FireflyException('Amount is an empty string, which Firefly III cannot handle. Apologies.');
}
if (null === $currencyId) {
- throw new FireflyException('Cannot store transaction without currency information.'); // @codeCoverageIgnore
+ throw new FireflyException('Cannot store transaction without currency information.');
}
$data['foreign_amount'] = '' === (string)$data['foreign_amount'] ? null : $data['foreign_amount'];
Log::debug(sprintf('Create transaction for account #%d ("%s") with amount %s', $data['account']->id, $data['account']->name, $data['amount']));
@@ -121,13 +121,9 @@ class TransactionFactory
}
Log::debug(sprintf('Source type is "%s", destination type is "%s"', $sourceAccount->accountType->type, $destinationAccount->accountType->type));
- // throw big fat error when source type === dest type and it's not a transfer or reconciliation.
- if ($sourceAccount->accountType->type === $destinationAccount->accountType->type && $journal->transactionType->type !== TransactionType::TRANSFER) {
- throw new FireflyException(sprintf('Source and destination account cannot be both of the type "%s"', $destinationAccount->accountType->type));
- }
- if ($sourceAccount->accountType->type !== AccountType::ASSET && $destinationAccount->accountType->type !== AccountType::ASSET) {
- throw new FireflyException('At least one of the accounts must be an asset account.');
- }
+
+ // based on the source type, destination type and transaction type, the system can start throwing FireflyExceptions.
+ $this->validateTransaction($sourceAccount->accountType->type, $destinationAccount->accountType->type, $journal->transactionType->type);
$source = $this->create(
[
@@ -154,7 +150,7 @@ class TransactionFactory
]
);
if (null === $source || null === $dest) {
- throw new FireflyException('Could not create transactions.');
+ throw new FireflyException('Could not create transactions.'); // @codeCoverageIgnore
}
// set foreign currency
@@ -194,5 +190,29 @@ class TransactionFactory
$this->user = $user;
}
+ /**
+ * @param string $sourceType
+ * @param string $destinationType
+ * @param string $transactionType
+ *
+ * @throws FireflyException
+ */
+ private function validateTransaction(string $sourceType, string $destinationType, string $transactionType): void
+ {
+ // throw big fat error when source type === dest type and it's not a transfer or reconciliation.
+ if ($sourceType === $destinationType && $transactionType !== TransactionType::TRANSFER) {
+ throw new FireflyException(sprintf('Source and destination account cannot be both of the type "%s"', $destinationType));
+ }
+ // source must be in this list AND dest must be in this list:
+ $list = [AccountType::DEFAULT, AccountType::ASSET, AccountType::CASH, AccountType::DEBT, AccountType::MORTGAGE, AccountType::LOAN, AccountType::MORTGAGE];
+ if (
+ !\in_array($sourceType, $list, true) &&
+ !\in_array($destinationType, $list, true)) {
+ throw new FireflyException(sprintf('At least one of the accounts must be an asset account (%s, %s).', $sourceType, $destinationType));
+ }
+ // either of these must be asset or default account.
+
+ }
+
}
diff --git a/app/Factory/TransactionJournalFactory.php b/app/Factory/TransactionJournalFactory.php
index 9096d8099d..1dbd565934 100644
--- a/app/Factory/TransactionJournalFactory.php
+++ b/app/Factory/TransactionJournalFactory.php
@@ -104,7 +104,7 @@ class TransactionJournalFactory
// store date meta fields (if present):
$fields = ['sepa-cc', 'sepa-ct-op', 'sepa-ct-id', 'sepa-db', 'sepa-country', 'sepa-ep', 'sepa-ci', 'interest_date', 'book_date', 'process_date',
'due_date', 'recurrence_id', 'payment_date', 'invoice_date', 'internal_reference', 'bunq_payment_id', 'importHash', 'importHashV2',
- 'external_id','sepa-batch-id'];
+ 'external_id', 'sepa-batch-id'];
foreach ($fields as $field) {
$this->storeMeta($journal, $data, $field);
diff --git a/app/Factory/TransactionTypeFactory.php b/app/Factory/TransactionTypeFactory.php
index 05f0fdf67d..d684555390 100644
--- a/app/Factory/TransactionTypeFactory.php
+++ b/app/Factory/TransactionTypeFactory.php
@@ -28,7 +28,6 @@ namespace FireflyIII\Factory;
use FireflyIII\Models\TransactionType;
/**
- * @codeCoverageIgnore
* Class TransactionTypeFactory
*/
class TransactionTypeFactory
diff --git a/app/Generator/Chart/Basic/ChartJsGenerator.php b/app/Generator/Chart/Basic/ChartJsGenerator.php
index c21522586e..28862b1706 100644
--- a/app/Generator/Chart/Basic/ChartJsGenerator.php
+++ b/app/Generator/Chart/Basic/ChartJsGenerator.php
@@ -39,6 +39,8 @@ class ChartJsGenerator implements GeneratorInterface
* 'type' => bar or line, optional
* 'yAxisID' => ID of yAxis, optional, will not be included when unused.
* 'fill' => if to fill a line? optional, will not be included when unused.
+ * 'currency_symbol' => 'x',
+ * 'backgroundColor' => 'x',
* 'entries' =>
* [
* 'label-of-entry' => 'value'
@@ -120,7 +122,7 @@ class ChartJsGenerator implements GeneratorInterface
// different sort when values are positive and when they're negative.
asort($data);
$next = next($data);
- if (!\is_bool($next) && 1 === bccomp($next, '0')) {
+ if (!\is_bool($next) && 1 === bccomp((string)$next, '0')) {
// next is positive, sort other way around.
arsort($data);
}
@@ -129,7 +131,7 @@ class ChartJsGenerator implements GeneratorInterface
$index = 0;
foreach ($data as $key => $value) {
// make larger than 0
- $chartData['datasets'][0]['data'][] = (float)app('steam')->positive($value);
+ $chartData['datasets'][0]['data'][] = (float)app('steam')->positive((string)$value);
$chartData['datasets'][0]['backgroundColor'][] = ChartColour::getColour($index);
$chartData['labels'][] = $key;
++$index;
@@ -138,6 +140,46 @@ class ChartJsGenerator implements GeneratorInterface
return $chartData;
}
+ /**
+ * Expects data as:.
+ *
+ * key => [value => x, 'currency_symbol' => 'x']
+ *
+ * @param array $data
+ *
+ * @return array
+ */
+ public function multiCurrencyPieChart(array $data): array
+ {
+ $chartData = [
+ 'datasets' => [
+ 0 => [],
+ ],
+ 'labels' => [],
+ ];
+
+ $amounts = array_column($data, 'amount');
+ $next = next($amounts);
+ $sortFlag = SORT_ASC;
+ if (!\is_bool($next) && 1 === bccomp((string)$next, '0')) {
+ $sortFlag = SORT_DESC;
+ }
+ array_multisort($amounts, $sortFlag, $data);
+ unset($next, $sortFlag, $amounts);
+
+ $index = 0;
+ foreach ($data as $key => $valueArray) {
+ // make larger than 0
+ $chartData['datasets'][0]['data'][] = (float)app('steam')->positive((string)$valueArray['amount']);
+ $chartData['datasets'][0]['backgroundColor'][] = ChartColour::getColour($index);
+ $chartData['datasets'][0]['currency_symbol'][] = $valueArray['currency_symbol'];
+ $chartData['labels'][] = $key;
+ ++$index;
+ }
+
+ return $chartData;
+ }
+
/**
* Will generate a (ChartJS) compatible array from the given input. Expects this format:.
*
diff --git a/app/Generator/Chart/Basic/GeneratorInterface.php b/app/Generator/Chart/Basic/GeneratorInterface.php
index 5cfdd4d94f..cc7d55626c 100644
--- a/app/Generator/Chart/Basic/GeneratorInterface.php
+++ b/app/Generator/Chart/Basic/GeneratorInterface.php
@@ -27,6 +27,13 @@ namespace FireflyIII\Generator\Chart\Basic;
*/
interface GeneratorInterface
{
+ /**
+ * @param array $data
+ *
+ * @return array
+ */
+ public function multiCurrencyPieChart(array $data): array;
+
/**
* Will generate a Chart JS compatible array from the given input. Expects this format.
*
diff --git a/app/Generator/Report/Account/MonthReportGenerator.php b/app/Generator/Report/Account/MonthReportGenerator.php
index 49f0cbfbd7..f9a53e5979 100644
--- a/app/Generator/Report/Account/MonthReportGenerator.php
+++ b/app/Generator/Report/Account/MonthReportGenerator.php
@@ -25,9 +25,13 @@ namespace FireflyIII\Generator\Report\Account;
use Carbon\Carbon;
use FireflyIII\Generator\Report\ReportGeneratorInterface;
use Illuminate\Support\Collection;
+use Log;
+use Throwable;
/**
* Class MonthReportGenerator.
+ *
+ * @codeCoverageIgnore
*/
class MonthReportGenerator implements ReportGeneratorInterface
{
@@ -44,7 +48,6 @@ class MonthReportGenerator implements ReportGeneratorInterface
* Generate the report.
*
* @return string
- * @throws \Throwable
*/
public function generate(): string
{
@@ -52,11 +55,17 @@ class MonthReportGenerator implements ReportGeneratorInterface
$expenseIds = implode(',', $this->expense->pluck('id')->toArray());
$reportType = 'account';
$preferredPeriod = $this->preferredPeriod();
+ try {
+ $result = view(
+ 'reports.account.report',
+ compact('accountIds', 'reportType', 'expenseIds', 'preferredPeriod')
+ )->with('start', $this->start)->with('end', $this->end)->render();
+ } catch (Throwable $e) {
+ Log::error(sprintf('Cannot render reports.account.report: %s', $e->getMessage()));
+ $result = 'Could not render report view.';
+ }
- return view(
- 'reports.account.report',
- compact('accountIds', 'reportType', 'expenseIds', 'preferredPeriod')
- )->with('start', $this->start)->with('end', $this->end)->render();
+ return $result;
}
/**
diff --git a/app/Generator/Report/Account/MultiYearReportGenerator.php b/app/Generator/Report/Account/MultiYearReportGenerator.php
index a4d17d0d83..ada1211667 100644
--- a/app/Generator/Report/Account/MultiYearReportGenerator.php
+++ b/app/Generator/Report/Account/MultiYearReportGenerator.php
@@ -24,6 +24,8 @@ namespace FireflyIII\Generator\Report\Account;
/**
* Class MultiYearReportGenerator.
+ *
+ * @codeCoverageIgnore
*/
class MultiYearReportGenerator extends MonthReportGenerator
{
diff --git a/app/Generator/Report/Account/YearReportGenerator.php b/app/Generator/Report/Account/YearReportGenerator.php
index c1e890ba19..bdee367fed 100644
--- a/app/Generator/Report/Account/YearReportGenerator.php
+++ b/app/Generator/Report/Account/YearReportGenerator.php
@@ -24,6 +24,8 @@ namespace FireflyIII\Generator\Report\Account;
/**
* Class YearReportGenerator.
+ *
+ * @codeCoverageIgnore
*/
class YearReportGenerator extends MonthReportGenerator
{
diff --git a/app/Generator/Report/Audit/MonthReportGenerator.php b/app/Generator/Report/Audit/MonthReportGenerator.php
index 386f27ded2..cadc73b157 100644
--- a/app/Generator/Report/Audit/MonthReportGenerator.php
+++ b/app/Generator/Report/Audit/MonthReportGenerator.php
@@ -28,12 +28,14 @@ namespace FireflyIII\Generator\Report\Audit;
use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Generator\Report\ReportGeneratorInterface;
-use FireflyIII\Helpers\Collector\JournalCollectorInterface;
+use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Models\Account;
use FireflyIII\Models\Transaction;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use Illuminate\Support\Collection;
+use Log;
+use Throwable;
/**
* Class MonthReportGenerator.
@@ -52,7 +54,7 @@ class MonthReportGenerator implements ReportGeneratorInterface
*
* @return string
* @throws FireflyException
- * @throws \Throwable
+ * @codeCoverageIgnore
*/
public function generate(): string
{
@@ -78,100 +80,16 @@ class MonthReportGenerator implements ReportGeneratorInterface
'internal_reference', 'notes',
'create_date', 'update_date',
];
+ try {
+ $result = view('reports.audit.report', compact('reportType', 'accountIds', 'auditData', 'hideable', 'defaultShow'))
+ ->with('start', $this->start)->with('end', $this->end)->with('accounts', $this->accounts)
+ ->render();
+ } catch (Throwable $e) {
+ Log::error(sprintf('Cannot render reports.audit.report: %s', $e->getMessage()));
+ $result = 'Could not render report view.';
+ }
- return view('reports.audit.report', compact('reportType', 'accountIds', 'auditData', 'hideable', 'defaultShow'))
- ->with('start', $this->start)->with('end', $this->end)->with('accounts', $this->accounts)
- ->render();
- }
-
- /**
- * Account collection setter.
- *
- * @param Collection $accounts
- *
- * @return ReportGeneratorInterface
- */
- public function setAccounts(Collection $accounts): ReportGeneratorInterface
- {
- $this->accounts = $accounts;
-
- return $this;
- }
-
- /**
- * Budget collection setter.
- *
- * @param Collection $budgets
- *
- * @return ReportGeneratorInterface
- */
- public function setBudgets(Collection $budgets): ReportGeneratorInterface
- {
- return $this;
- }
-
- /**
- * Category collection setter.
- *
- * @param Collection $categories
- *
- * @return ReportGeneratorInterface
- */
- public function setCategories(Collection $categories): ReportGeneratorInterface
- {
- return $this;
- }
-
- /**
- * End date setter.
- *
- * @param Carbon $date
- *
- * @return ReportGeneratorInterface
- */
- public function setEndDate(Carbon $date): ReportGeneratorInterface
- {
- $this->end = $date;
-
- return $this;
- }
-
- /**
- * Expenses collection setter.
- *
- * @param Collection $expense
- *
- * @return ReportGeneratorInterface
- */
- public function setExpense(Collection $expense): ReportGeneratorInterface
- {
- return $this;
- }
-
- /**
- * Start date collection setter.
- *
- * @param Carbon $date
- *
- * @return ReportGeneratorInterface
- */
- public function setStartDate(Carbon $date): ReportGeneratorInterface
- {
- $this->start = $date;
-
- return $this;
- }
-
- /**
- * Tags collection setter.
- *
- * @param Collection $tags
- *
- * @return ReportGeneratorInterface
- */
- public function setTags(Collection $tags): ReportGeneratorInterface
- {
- return $this;
+ return $result;
}
/**
@@ -185,7 +103,7 @@ class MonthReportGenerator implements ReportGeneratorInterface
* @SuppressWarnings(PHPMD.ExcessiveMethodLength) // not that long
* @throws FireflyException
*/
- private function getAuditReport(Account $account, Carbon $date): array
+ public function getAuditReport(Account $account, Carbon $date): array
{
/** @var CurrencyRepositoryInterface $currencyRepos */
$currencyRepos = app(CurrencyRepositoryInterface::class);
@@ -194,10 +112,10 @@ class MonthReportGenerator implements ReportGeneratorInterface
$accountRepository = app(AccountRepositoryInterface::class);
$accountRepository->setUser($account->user);
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAccounts(new Collection([$account]))->setRange($this->start, $this->end);
- $journals = $collector->getJournals();
+ $journals = $collector->getTransactions();
$journals = $journals->reverse();
$dayBeforeBalance = app('steam')->balance($account, $date);
$startBalance = $dayBeforeBalance;
@@ -232,4 +150,101 @@ class MonthReportGenerator implements ReportGeneratorInterface
return $return;
}
+
+ /**
+ * Account collection setter.
+ *
+ * @param Collection $accounts
+ *
+ * @return ReportGeneratorInterface
+ * @codeCoverageIgnore
+ */
+ public function setAccounts(Collection $accounts): ReportGeneratorInterface
+ {
+ $this->accounts = $accounts;
+
+ return $this;
+ }
+
+ /**
+ * Budget collection setter.
+ *
+ * @param Collection $budgets
+ *
+ * @return ReportGeneratorInterface
+ * @codeCoverageIgnore
+ */
+ public function setBudgets(Collection $budgets): ReportGeneratorInterface
+ {
+ return $this;
+ }
+
+ /**
+ * Category collection setter.
+ *
+ * @param Collection $categories
+ *
+ * @return ReportGeneratorInterface
+ * @codeCoverageIgnore
+ */
+ public function setCategories(Collection $categories): ReportGeneratorInterface
+ {
+ return $this;
+ }
+
+ /**
+ * End date setter.
+ *
+ * @param Carbon $date
+ *
+ * @return ReportGeneratorInterface
+ * @codeCoverageIgnore
+ */
+ public function setEndDate(Carbon $date): ReportGeneratorInterface
+ {
+ $this->end = $date;
+
+ return $this;
+ }
+
+ /**
+ * Expenses collection setter.
+ *
+ * @param Collection $expense
+ *
+ * @return ReportGeneratorInterface
+ * @codeCoverageIgnore
+ */
+ public function setExpense(Collection $expense): ReportGeneratorInterface
+ {
+ return $this;
+ }
+
+ /**
+ * Start date collection setter.
+ *
+ * @param Carbon $date
+ *
+ * @return ReportGeneratorInterface
+ * @codeCoverageIgnore
+ */
+ public function setStartDate(Carbon $date): ReportGeneratorInterface
+ {
+ $this->start = $date;
+
+ return $this;
+ }
+
+ /**
+ * Tags collection setter.
+ *
+ * @param Collection $tags
+ *
+ * @return ReportGeneratorInterface
+ * @codeCoverageIgnore
+ */
+ public function setTags(Collection $tags): ReportGeneratorInterface
+ {
+ return $this;
+ }
}
diff --git a/app/Generator/Report/Audit/MultiYearReportGenerator.php b/app/Generator/Report/Audit/MultiYearReportGenerator.php
index 0bd3b5697f..ade7d0b84b 100644
--- a/app/Generator/Report/Audit/MultiYearReportGenerator.php
+++ b/app/Generator/Report/Audit/MultiYearReportGenerator.php
@@ -24,6 +24,7 @@ namespace FireflyIII\Generator\Report\Audit;
/**
* Class MultiYearReportGenerator.
+ * @codeCoverageIgnore
*/
class MultiYearReportGenerator extends MonthReportGenerator
{
diff --git a/app/Generator/Report/Audit/YearReportGenerator.php b/app/Generator/Report/Audit/YearReportGenerator.php
index e341718728..4ff82713f4 100644
--- a/app/Generator/Report/Audit/YearReportGenerator.php
+++ b/app/Generator/Report/Audit/YearReportGenerator.php
@@ -24,6 +24,7 @@ namespace FireflyIII\Generator\Report\Audit;
/**
* Class YearReportGenerator.
+ * @codeCoverageIgnore
*/
class YearReportGenerator extends MonthReportGenerator
{
diff --git a/app/Generator/Report/Budget/MonthReportGenerator.php b/app/Generator/Report/Budget/MonthReportGenerator.php
index ff4229ab26..6b7539c0d4 100644
--- a/app/Generator/Report/Budget/MonthReportGenerator.php
+++ b/app/Generator/Report/Budget/MonthReportGenerator.php
@@ -27,7 +27,7 @@ namespace FireflyIII\Generator\Report\Budget;
use Carbon\Carbon;
use FireflyIII\Generator\Report\ReportGeneratorInterface;
use FireflyIII\Generator\Report\Support;
-use FireflyIII\Helpers\Collector\JournalCollectorInterface;
+use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Helpers\Filter\OpposingAccountFilter;
use FireflyIII\Helpers\Filter\PositiveAmountFilter;
use FireflyIII\Helpers\Filter\TransferFilter;
@@ -35,9 +35,12 @@ use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionType;
use Illuminate\Support\Collection;
use Log;
+use Throwable;
/**
* Class MonthReportGenerator.
+ *
+ * @codeCoverageIgnore
*/
class MonthReportGenerator extends Support implements ReportGeneratorInterface
{
@@ -64,7 +67,6 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface
* Generates the report.
*
* @return string
- * @throws \Throwable
*/
public function generate(): string
{
@@ -77,11 +79,18 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface
$topExpenses = $this->getTopExpenses();
// render!
- return view('reports.budget.month', compact('accountIds', 'budgetIds', 'accountSummary', 'budgetSummary', 'averageExpenses', 'topExpenses'))
- ->with('start', $this->start)->with('end', $this->end)
- ->with('budgets', $this->budgets)
- ->with('accounts', $this->accounts)
- ->render();
+ try {
+ $result = view('reports.budget.month', compact('accountIds', 'budgetIds', 'accountSummary', 'budgetSummary', 'averageExpenses', 'topExpenses'))
+ ->with('start', $this->start)->with('end', $this->end)
+ ->with('budgets', $this->budgets)
+ ->with('accounts', $this->accounts)
+ ->render();
+ } catch (Throwable $e) {
+ Log::error(sprintf('Cannot render reports.account.report: %s', $e->getMessage()));
+ $result = 'Could not render report view.';
+ }
+
+ return $result;
}
/**
@@ -189,8 +198,8 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface
return $this->expenses;
}
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAccounts($this->accounts)->setRange($this->start, $this->end)
->setTypes([TransactionType::WITHDRAWAL])
->setBudgets($this->budgets)->withOpposingAccount();
@@ -199,7 +208,7 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface
$collector->addFilter(OpposingAccountFilter::class);
$collector->addFilter(PositiveAmountFilter::class);
- $transactions = $collector->getJournals();
+ $transactions = $collector->getTransactions();
$this->expenses = $transactions;
return $transactions;
diff --git a/app/Generator/Report/Budget/MultiYearReportGenerator.php b/app/Generator/Report/Budget/MultiYearReportGenerator.php
index 1e32b8d8ce..ecfad642be 100644
--- a/app/Generator/Report/Budget/MultiYearReportGenerator.php
+++ b/app/Generator/Report/Budget/MultiYearReportGenerator.php
@@ -24,6 +24,7 @@ namespace FireflyIII\Generator\Report\Budget;
/**
* Class MultiYearReportGenerator.
+ * @codeCoverageIgnore
*/
class MultiYearReportGenerator extends MonthReportGenerator
{
diff --git a/app/Generator/Report/Budget/YearReportGenerator.php b/app/Generator/Report/Budget/YearReportGenerator.php
index a025513451..4323ce2ddd 100644
--- a/app/Generator/Report/Budget/YearReportGenerator.php
+++ b/app/Generator/Report/Budget/YearReportGenerator.php
@@ -24,6 +24,7 @@ namespace FireflyIII\Generator\Report\Budget;
/**
* Class YearReportGenerator.
+ * @codeCoverageIgnore
*/
class YearReportGenerator extends MonthReportGenerator
{
diff --git a/app/Generator/Report/Category/MonthReportGenerator.php b/app/Generator/Report/Category/MonthReportGenerator.php
index ef187a575b..ad477f15f1 100644
--- a/app/Generator/Report/Category/MonthReportGenerator.php
+++ b/app/Generator/Report/Category/MonthReportGenerator.php
@@ -27,7 +27,7 @@ namespace FireflyIII\Generator\Report\Category;
use Carbon\Carbon;
use FireflyIII\Generator\Report\ReportGeneratorInterface;
use FireflyIII\Generator\Report\Support;
-use FireflyIII\Helpers\Collector\JournalCollectorInterface;
+use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Helpers\Filter\NegativeAmountFilter;
use FireflyIII\Helpers\Filter\OpposingAccountFilter;
use FireflyIII\Helpers\Filter\PositiveAmountFilter;
@@ -36,9 +36,11 @@ use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionType;
use Illuminate\Support\Collection;
use Log;
+use Throwable;
/**
* Class MonthReportGenerator.
+ * @codeCoverageIgnore
*/
class MonthReportGenerator extends Support implements ReportGeneratorInterface
{
@@ -68,7 +70,6 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface
* Generates the report.
*
* @return string
- * @throws \Throwable
*/
public function generate(): string
{
@@ -85,24 +86,23 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface
$topIncome = $this->getTopIncome();
// render!
- return view(
- 'reports.category.month',
- compact(
- 'accountIds',
- 'categoryIds',
- 'topIncome',
- 'reportType',
- 'accountSummary',
- 'categorySummary',
- 'averageExpenses',
- 'averageIncome',
- 'topExpenses'
+ try {
+ return view(
+ 'reports.category.month', compact(
+ 'accountIds', 'categoryIds', 'topIncome', 'reportType', 'accountSummary', 'categorySummary', 'averageExpenses',
+ 'averageIncome', 'topExpenses'
+ )
)
- )
- ->with('start', $this->start)->with('end', $this->end)
- ->with('categories', $this->categories)
- ->with('accounts', $this->accounts)
- ->render();
+ ->with('start', $this->start)->with('end', $this->end)
+ ->with('categories', $this->categories)
+ ->with('accounts', $this->accounts)
+ ->render();
+ } catch (Throwable $e) {
+ Log::error(sprintf('Cannot render reports.category.month: %s', $e->getMessage()));
+ $result = 'Could not render report view.';
+ }
+
+ return $result;
}
/**
@@ -210,8 +210,8 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface
return $this->expenses;
}
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAccounts($this->accounts)->setRange($this->start, $this->end)
->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER])
->setCategories($this->categories)->withOpposingAccount();
@@ -220,7 +220,7 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface
$collector->addFilter(OpposingAccountFilter::class);
$collector->addFilter(PositiveAmountFilter::class);
- $transactions = $collector->getJournals();
+ $transactions = $collector->getTransactions();
$this->expenses = $transactions;
return $transactions;
@@ -237,8 +237,8 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface
return $this->income;
}
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAccounts($this->accounts)->setRange($this->start, $this->end)
->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER])
->setCategories($this->categories)->withOpposingAccount();
@@ -246,7 +246,7 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface
$collector->addFilter(OpposingAccountFilter::class);
$collector->addFilter(NegativeAmountFilter::class);
- $transactions = $collector->getJournals();
+ $transactions = $collector->getTransactions();
$this->income = $transactions;
return $transactions;
diff --git a/app/Generator/Report/Category/MultiYearReportGenerator.php b/app/Generator/Report/Category/MultiYearReportGenerator.php
index 2fe96eb9cd..39a881178e 100644
--- a/app/Generator/Report/Category/MultiYearReportGenerator.php
+++ b/app/Generator/Report/Category/MultiYearReportGenerator.php
@@ -24,6 +24,7 @@ namespace FireflyIII\Generator\Report\Category;
/**
* Class MultiYearReportGenerator.
+ * @codeCoverageIgnore
*/
class MultiYearReportGenerator extends MonthReportGenerator
{
diff --git a/app/Generator/Report/Category/YearReportGenerator.php b/app/Generator/Report/Category/YearReportGenerator.php
index 920ea366e7..57f07fd975 100644
--- a/app/Generator/Report/Category/YearReportGenerator.php
+++ b/app/Generator/Report/Category/YearReportGenerator.php
@@ -24,6 +24,7 @@ namespace FireflyIII\Generator\Report\Category;
/**
* Class YearReportGenerator.
+ * @codeCoverageIgnore
*/
class YearReportGenerator extends MonthReportGenerator
{
diff --git a/app/Generator/Report/ReportGeneratorFactory.php b/app/Generator/Report/ReportGeneratorFactory.php
index 6e06480a33..04e264bb3d 100644
--- a/app/Generator/Report/ReportGeneratorFactory.php
+++ b/app/Generator/Report/ReportGeneratorFactory.php
@@ -27,6 +27,7 @@ use FireflyIII\Exceptions\FireflyException;
/**
* Class ReportGeneratorFactory.
+ * @codeCoverageIgnore
*/
class ReportGeneratorFactory
{
diff --git a/app/Generator/Report/Standard/MonthReportGenerator.php b/app/Generator/Report/Standard/MonthReportGenerator.php
index e27f4d79ed..ba6fc749a3 100644
--- a/app/Generator/Report/Standard/MonthReportGenerator.php
+++ b/app/Generator/Report/Standard/MonthReportGenerator.php
@@ -26,9 +26,12 @@ use Carbon\Carbon;
use FireflyIII\Generator\Report\ReportGeneratorInterface;
use FireflyIII\Helpers\Report\ReportHelperInterface;
use Illuminate\Support\Collection;
+use Log;
+use Throwable;
/**
* Class MonthReportGenerator.
+ * @codeCoverageIgnore
*/
class MonthReportGenerator implements ReportGeneratorInterface
{
@@ -43,7 +46,6 @@ class MonthReportGenerator implements ReportGeneratorInterface
* Generates the report.
*
* @return string
- * @throws \Throwable
*/
public function generate(): string
{
@@ -53,11 +55,17 @@ class MonthReportGenerator implements ReportGeneratorInterface
$accountIds = implode(',', $this->accounts->pluck('id')->toArray());
$reportType = 'default';
- // continue!
- return view(
- 'reports.default.month',
- compact('bills', 'accountIds', 'reportType')
- )->with('start', $this->start)->with('end', $this->end)->render();
+ try {
+ return view(
+ 'reports.default.month',
+ compact('bills', 'accountIds', 'reportType')
+ )->with('start', $this->start)->with('end', $this->end)->render();
+ } catch (Throwable $e) {
+ Log::error(sprintf('Cannot render reports.default.month: %s', $e->getMessage()));
+ $result = 'Could not render report view.';
+ }
+
+ return $result;
}
/**
diff --git a/app/Generator/Report/Standard/MultiYearReportGenerator.php b/app/Generator/Report/Standard/MultiYearReportGenerator.php
index ec507b9386..e9ba9d40f5 100644
--- a/app/Generator/Report/Standard/MultiYearReportGenerator.php
+++ b/app/Generator/Report/Standard/MultiYearReportGenerator.php
@@ -25,9 +25,12 @@ namespace FireflyIII\Generator\Report\Standard;
use Carbon\Carbon;
use FireflyIII\Generator\Report\ReportGeneratorInterface;
use Illuminate\Support\Collection;
+use Log;
+use Throwable;
/**
* Class MonthReportGenerator.
+ * @codeCoverageIgnore
*/
class MultiYearReportGenerator implements ReportGeneratorInterface
{
@@ -42,7 +45,6 @@ class MultiYearReportGenerator implements ReportGeneratorInterface
* Generates the report.
*
* @return string
- * @throws \Throwable
*/
public function generate(): string
{
@@ -50,11 +52,17 @@ class MultiYearReportGenerator implements ReportGeneratorInterface
$accountIds = implode(',', $this->accounts->pluck('id')->toArray());
$reportType = 'default';
- // continue!
- return view(
- 'reports.default.multi-year',
- compact('accountIds', 'reportType')
- )->with('start', $this->start)->with('end', $this->end)->render();
+ try {
+ return view(
+ 'reports.default.multi-year',
+ compact('accountIds', 'reportType')
+ )->with('start', $this->start)->with('end', $this->end)->render();
+ } catch (Throwable $e) {
+ Log::error(sprintf('Cannot render reports.default.multi-year: %s', $e->getMessage()));
+ $result = 'Could not render report view.';
+ }
+
+ return $result;
}
/**
diff --git a/app/Generator/Report/Standard/YearReportGenerator.php b/app/Generator/Report/Standard/YearReportGenerator.php
index 47f92f47ad..7479aa7024 100644
--- a/app/Generator/Report/Standard/YearReportGenerator.php
+++ b/app/Generator/Report/Standard/YearReportGenerator.php
@@ -25,9 +25,12 @@ namespace FireflyIII\Generator\Report\Standard;
use Carbon\Carbon;
use FireflyIII\Generator\Report\ReportGeneratorInterface;
use Illuminate\Support\Collection;
+use Log;
+use Throwable;
/**
* Class MonthReportGenerator.
+ * @codeCoverageIgnore
*/
class YearReportGenerator implements ReportGeneratorInterface
{
@@ -42,7 +45,6 @@ class YearReportGenerator implements ReportGeneratorInterface
* Generates the report.
*
* @return string
- * @throws \Throwable
*/
public function generate(): string
{
@@ -50,13 +52,20 @@ class YearReportGenerator implements ReportGeneratorInterface
$accountIds = implode(',', $this->accounts->pluck('id')->toArray());
$reportType = 'default';
- // continue!
- return view(
- 'reports.default.year',
- compact('accountIds', 'reportType')
- )->with('start', $this->start)->with('end', $this->end)->render();
+ try {
+ $result = view(
+ 'reports.default.year',
+ compact('accountIds', 'reportType')
+ )->with('start', $this->start)->with('end', $this->end)->render();
+ } catch (Throwable $e) {
+ Log::error(sprintf('Cannot render reports.account.report: %s', $e->getMessage()));
+ $result = 'Could not render report view.';
+ }
+
+ return $result;
}
+
/**
* Set the accounts.
*
diff --git a/app/Generator/Report/Support.php b/app/Generator/Report/Support.php
index 0d5e42f4f2..d2b6c76f32 100644
--- a/app/Generator/Report/Support.php
+++ b/app/Generator/Report/Support.php
@@ -31,6 +31,7 @@ use Illuminate\Support\Collection;
* Class Support.
* @method Collection getExpenses()
* @method Collection getIncome()
+ * @codeCoverageIgnore
*/
class Support
{
diff --git a/app/Generator/Report/Tag/MonthReportGenerator.php b/app/Generator/Report/Tag/MonthReportGenerator.php
index e367283624..975a1080aa 100644
--- a/app/Generator/Report/Tag/MonthReportGenerator.php
+++ b/app/Generator/Report/Tag/MonthReportGenerator.php
@@ -28,7 +28,7 @@ namespace FireflyIII\Generator\Report\Tag;
use Carbon\Carbon;
use FireflyIII\Generator\Report\ReportGeneratorInterface;
use FireflyIII\Generator\Report\Support;
-use FireflyIII\Helpers\Collector\JournalCollectorInterface;
+use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Helpers\Filter\NegativeAmountFilter;
use FireflyIII\Helpers\Filter\OpposingAccountFilter;
use FireflyIII\Helpers\Filter\PositiveAmountFilter;
@@ -38,9 +38,11 @@ use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionType;
use Illuminate\Support\Collection;
use Log;
+use Throwable;
/**
* Class MonthReportGenerator.
+ * @codeCoverageIgnore
*/
class MonthReportGenerator extends Support implements ReportGeneratorInterface
{
@@ -70,7 +72,6 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface
* Generate the report.
*
* @return string
- * @throws \Throwable
*/
public function generate(): string
{
@@ -87,20 +88,19 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface
$topIncome = $this->getTopIncome();
// render!
- return view(
- 'reports.tag.month',
- compact(
- 'accountIds',
- 'tagTags',
- 'reportType',
- 'accountSummary',
- 'tagSummary',
- 'averageExpenses',
- 'averageIncome',
- 'topIncome',
- 'topExpenses'
- )
- )->with('start', $this->start)->with('end', $this->end)->with('tags', $this->tags)->with('accounts', $this->accounts)->render();
+ try {
+ $result = view(
+ 'reports.tag.month', compact(
+ 'accountIds', 'tagTags', 'reportType', 'accountSummary', 'tagSummary', 'averageExpenses', 'averageIncome', 'topIncome',
+ 'topExpenses'
+ )
+ )->with('start', $this->start)->with('end', $this->end)->with('tags', $this->tags)->with('accounts', $this->accounts)->render();
+ } catch (Throwable $e) {
+ Log::error(sprintf('Cannot render reports.tag.month: %s', $e->getMessage()));
+ $result = 'Could not render report view.';
+ }
+
+ return $result;
}
/**
@@ -208,8 +208,8 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface
return $this->expenses;
}
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAccounts($this->accounts)->setRange($this->start, $this->end)
->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER])
->setTags($this->tags)->withOpposingAccount();
@@ -218,7 +218,7 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface
$collector->addFilter(OpposingAccountFilter::class);
$collector->addFilter(PositiveAmountFilter::class);
- $transactions = $collector->getJournals();
+ $transactions = $collector->getTransactions();
$this->expenses = $transactions;
@@ -236,8 +236,8 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface
return $this->income;
}
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAccounts($this->accounts)->setRange($this->start, $this->end)
->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER])
->setTags($this->tags)->withOpposingAccount();
@@ -245,7 +245,7 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface
$collector->addFilter(OpposingAccountFilter::class);
$collector->addFilter(NegativeAmountFilter::class);
- $transactions = $collector->getJournals();
+ $transactions = $collector->getTransactions();
$this->income = $transactions;
return $transactions;
diff --git a/app/Generator/Report/Tag/MultiYearReportGenerator.php b/app/Generator/Report/Tag/MultiYearReportGenerator.php
index 0d6380416b..c34136289a 100644
--- a/app/Generator/Report/Tag/MultiYearReportGenerator.php
+++ b/app/Generator/Report/Tag/MultiYearReportGenerator.php
@@ -24,6 +24,7 @@ namespace FireflyIII\Generator\Report\Tag;
/**
* Class MultiYearReportGenerator.
+ * @codeCoverageIgnore
*/
class MultiYearReportGenerator extends MonthReportGenerator
{
diff --git a/app/Generator/Report/Tag/YearReportGenerator.php b/app/Generator/Report/Tag/YearReportGenerator.php
index 0cb3f216a0..087485a166 100644
--- a/app/Generator/Report/Tag/YearReportGenerator.php
+++ b/app/Generator/Report/Tag/YearReportGenerator.php
@@ -24,6 +24,7 @@ namespace FireflyIII\Generator\Report\Tag;
/**
* Class YearReportGenerator.
+ * @codeCoverageIgnore
*/
class YearReportGenerator extends MonthReportGenerator
{
diff --git a/app/Handlers/Events/APIEventHandler.php b/app/Handlers/Events/APIEventHandler.php
index b35130f0d0..0b93895ee1 100644
--- a/app/Handlers/Events/APIEventHandler.php
+++ b/app/Handlers/Events/APIEventHandler.php
@@ -65,10 +65,10 @@ class APIEventHandler
Log::error($e->getTraceAsString());
Session::flash('error', 'Possible email error: ' . $e->getMessage());
}
+ // @codeCoverageIgnoreEnd
Log::debug('If no error above this line, message was sent.');
}
- // @codeCoverageIgnoreEnd
return true;
diff --git a/app/Handlers/Events/AdminEventHandler.php b/app/Handlers/Events/AdminEventHandler.php
index 1bc55af2ba..6d3c09a62b 100644
--- a/app/Handlers/Events/AdminEventHandler.php
+++ b/app/Handlers/Events/AdminEventHandler.php
@@ -57,16 +57,17 @@ class AdminEventHandler
Log::debug('Trying to send message...');
Mail::to($email)->send(new AdminTestMail($email, $ipAddress));
// @codeCoverageIgnoreStart
+ // Laravel cannot pretend this process failed during testing.
} catch (Exception $e) {
Log::debug('Send message failed! :(');
Log::error($e->getMessage());
Log::error($e->getTraceAsString());
Session::flash('error', 'Possible email error: ' . $e->getMessage());
}
+ // @codeCoverageIgnoreEnd
Log::debug('If no error above this line, message was sent.');
}
- // @codeCoverageIgnoreEnd
return true;
}
}
diff --git a/app/Handlers/Events/AutomationHandler.php b/app/Handlers/Events/AutomationHandler.php
index f7549ae84b..2b1464010c 100644
--- a/app/Handlers/Events/AutomationHandler.php
+++ b/app/Handlers/Events/AutomationHandler.php
@@ -55,12 +55,14 @@ class AutomationHandler
Mail::to($user->email)->send(new ReportNewJournalsMail($user->email, '127.0.0.1', $event->journals));
// @codeCoverageIgnoreStart
} catch (Exception $e) {
+ Log::debug('Send message failed! :(');
Log::error($e->getMessage());
+ Log::error($e->getTraceAsString());
}
+ // @codeCoverageIgnoreEnd
Log::debug('Done!');
}
- // @codeCoverageIgnoreEnd
return true;
}
}
diff --git a/app/Handlers/Events/StoredJournalEventHandler.php b/app/Handlers/Events/StoredJournalEventHandler.php
index db3fcb41f2..f42d814b49 100644
--- a/app/Handlers/Events/StoredJournalEventHandler.php
+++ b/app/Handlers/Events/StoredJournalEventHandler.php
@@ -25,8 +25,6 @@ namespace FireflyIII\Handlers\Events;
use FireflyIII\Events\StoredTransactionJournal;
use FireflyIII\Models\Rule;
use FireflyIII\Models\RuleGroup;
-use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
-use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface;
use FireflyIII\TransactionRules\Processor;
@@ -35,28 +33,6 @@ use FireflyIII\TransactionRules\Processor;
*/
class StoredJournalEventHandler
{
- /** @var JournalRepositoryInterface The journal repository. */
- public $journalRepository;
- /** @var PiggyBankRepositoryInterface The Piggy bank repository */
- public $repository;
- /** @var RuleGroupRepositoryInterface The rule group repository */
- public $ruleGroupRepository;
-
- /**
- * StoredJournalEventHandler constructor.
- *
- * @param PiggyBankRepositoryInterface $repository
- * @param JournalRepositoryInterface $journalRepository
- * @param RuleGroupRepositoryInterface $ruleGroupRepository
- */
- public function __construct(
- PiggyBankRepositoryInterface $repository, JournalRepositoryInterface $journalRepository, RuleGroupRepositoryInterface $ruleGroupRepository
- ) {
- $this->repository = $repository;
- $this->journalRepository = $journalRepository;
- $this->ruleGroupRepository = $ruleGroupRepository;
- }
-
/**
* This method grabs all the users rules and processes them.
*
@@ -67,16 +43,22 @@ class StoredJournalEventHandler
*/
public function processRules(StoredTransactionJournal $storedJournalEvent): bool
{
- // get all the user's rule groups, with the rules, order by 'order'.
$journal = $storedJournalEvent->journal;
- $groups = $this->ruleGroupRepository->getActiveGroups($journal->user);
+
+ // create objects:
+ /** @var RuleGroupRepositoryInterface $ruleGroupRepos */
+ $ruleGroupRepos = app(RuleGroupRepositoryInterface::class);
+ $ruleGroupRepos->setUser($journal->user);
+ $groups = $ruleGroupRepos->getActiveGroups($journal->user);
/** @var RuleGroup $group */
foreach ($groups as $group) {
- $rules = $this->ruleGroupRepository->getActiveStoreRules($group);
+ $rules = $ruleGroupRepos->getActiveStoreRules($group);
/** @var Rule $rule */
foreach ($rules as $rule) {
- $processor = Processor::make($rule);
+ /** @var Processor $processor */
+ $processor = app(Processor::class);
+ $processor->make($rule);
$processor->handleTransactionJournal($journal);
if ($rule->stop_processing) {
diff --git a/app/Handlers/Events/UpdatedJournalEventHandler.php b/app/Handlers/Events/UpdatedJournalEventHandler.php
index f6308e08e3..30ffeb83ab 100644
--- a/app/Handlers/Events/UpdatedJournalEventHandler.php
+++ b/app/Handlers/Events/UpdatedJournalEventHandler.php
@@ -33,19 +33,6 @@ use FireflyIII\TransactionRules\Processor;
*/
class UpdatedJournalEventHandler
{
- /** @var RuleGroupRepositoryInterface The rule group repository */
- public $repository;
-
- /**
- * StoredJournalEventHandler constructor.
- *
- * @param RuleGroupRepositoryInterface $ruleGroupRepository
- */
- public function __construct(RuleGroupRepositoryInterface $ruleGroupRepository)
- {
- $this->repository = $ruleGroupRepository;
- }
-
/**
* This method will check all the rules when a journal is updated.
*
@@ -58,14 +45,21 @@ class UpdatedJournalEventHandler
{
// get all the user's rule groups, with the rules, order by 'order'.
$journal = $updatedJournalEvent->journal;
- $groups = $this->repository->getActiveGroups($journal->user);
+
+ /** @var RuleGroupRepositoryInterface $ruleGroupRepos */
+ $ruleGroupRepos = app(RuleGroupRepositoryInterface::class);
+ $ruleGroupRepos->setUser($journal->user);
+
+ $groups = $ruleGroupRepos->getActiveGroups($journal->user);
/** @var RuleGroup $group */
foreach ($groups as $group) {
- $rules = $this->repository->getActiveUpdateRules($group);
+ $rules = $ruleGroupRepos->getActiveUpdateRules($group);
/** @var Rule $rule */
foreach ($rules as $rule) {
- $processor = Processor::make($rule);
+ /** @var Processor $processor */
+ $processor = app(Processor::class);
+ $processor->make($rule);
$processor->handleTransactionJournal($journal);
if ($rule->stop_processing) {
diff --git a/app/Handlers/Events/UserEventHandler.php b/app/Handlers/Events/UserEventHandler.php
index e40ff67b55..52ef3c34e0 100644
--- a/app/Handlers/Events/UserEventHandler.php
+++ b/app/Handlers/Events/UserEventHandler.php
@@ -61,6 +61,7 @@ class UserEventHandler
// first user ever?
if (1 === $repository->count()) {
+ Log::debug('User count is one, attach role.');
$repository->attachRole($event->user, 'owner');
}
@@ -145,8 +146,8 @@ class UserEventHandler
} catch (Exception $e) {
Log::error($e->getMessage());
}
-
// @codeCoverageIgnoreEnd
+
return true;
}
@@ -171,8 +172,8 @@ class UserEventHandler
} catch (Exception $e) {
Log::error($e->getMessage());
}
-
// @codeCoverageIgnoreEnd
+
return true;
}
@@ -198,7 +199,6 @@ class UserEventHandler
} catch (Exception $e) {
Log::error($e->getMessage());
}
-
// @codeCoverageIgnoreEnd
return true;
diff --git a/app/Handlers/Events/VersionCheckEventHandler.php b/app/Handlers/Events/VersionCheckEventHandler.php
index 46b371772e..5b7efb4e45 100644
--- a/app/Handlers/Events/VersionCheckEventHandler.php
+++ b/app/Handlers/Events/VersionCheckEventHandler.php
@@ -27,14 +27,13 @@ namespace FireflyIII\Handlers\Events;
use FireflyConfig;
use FireflyIII\Events\RequestedVersionCheckStatus;
-
use FireflyIII\Helpers\Update\UpdateTrait;
+use FireflyIII\Models\Configuration;
use FireflyIII\Repositories\User\UserRepositoryInterface;
-
-
use FireflyIII\User;
use Log;
+
/**
* Class VersionCheckEventHandler
*/
@@ -52,10 +51,13 @@ class VersionCheckEventHandler
*/
public function checkForUpdates(RequestedVersionCheckStatus $event): void
{
+ Log::debug('Now in checkForUpdates()');
// in Sandstorm, cannot check for updates:
$sandstorm = 1 === (int)getenv('SANDSTORM');
if (true === $sandstorm) {
- return; // @codeCoverageIgnore
+ Log::debug('This is Sandstorm instance, done.');
+
+ return;
}
/** @var UserRepositoryInterface $repository */
@@ -63,25 +65,27 @@ class VersionCheckEventHandler
/** @var User $user */
$user = $event->user;
if (!$repository->hasRole($user, 'owner')) {
+ Log::debug('User is not admin, done.');
+
return;
}
+ /** @var Configuration $lastCheckTime */
$lastCheckTime = FireflyConfig::get('last_update_check', time());
$now = time();
$diff = $now - $lastCheckTime->data;
- Log::debug(sprintf('Difference is %d seconds.', $diff));
+ Log::debug(sprintf('Last check time is %d, current time is %d, difference is %d', $lastCheckTime->data, $now, $diff));
if ($diff < 604800) {
Log::debug(sprintf('Checked for updates less than a week ago (on %s).', date('Y-m-d H:i:s', $lastCheckTime->data)));
- //return;
-
+ return;
}
// last check time was more than a week ago.
Log::debug('Have not checked for a new version in a week!');
$latestRelease = $this->getLatestRelease();
$versionCheck = $this->versionCheck($latestRelease);
- $resultString = $this->parseResult($versionCheck, $latestRelease);
+ $resultString = $this->parseResult($versionCheck, $latestRelease);
if (0 !== $versionCheck && '' !== $resultString) {
// flash info
session()->flash('info', $resultString);
diff --git a/app/Helpers/Attachments/AttachmentHelper.php b/app/Helpers/Attachments/AttachmentHelper.php
index d840a2a812..9c9c7bf55e 100644
--- a/app/Helpers/Attachments/AttachmentHelper.php
+++ b/app/Helpers/Attachments/AttachmentHelper.php
@@ -25,7 +25,6 @@ namespace FireflyIII\Helpers\Attachments;
use Crypt;
use FireflyIII\Models\Attachment;
use Illuminate\Contracts\Encryption\DecryptException;
-use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\MessageBag;
@@ -80,7 +79,7 @@ class AttachmentHelper implements AttachmentHelperInterface
try {
$content = Crypt::decrypt($this->uploadDisk->get(sprintf('at-%d.data', $attachment->id)));
- } catch (DecryptException|FileNotFoundException $e) {
+ } catch (DecryptException $e) {
Log::error(sprintf('Could not decrypt data of attachment #%d: %s', $attachment->id, $e->getMessage()));
$content = '';
}
@@ -145,9 +144,11 @@ class AttachmentHelper implements AttachmentHelperInterface
{
$resource = tmpfile();
if (false === $resource) {
+ // @codeCoverageIgnoreStart
Log::error('Cannot create temp-file for file upload.');
return false;
+ // @codeCoverageIgnoreEnd
}
$path = stream_get_meta_data($resource)['uri'];
fwrite($resource, $content);
@@ -342,9 +343,12 @@ class AttachmentHelper implements AttachmentHelperInterface
if (!$this->validMime($file)) {
$result = false;
}
+ // @codeCoverageIgnoreStart
+ // can't seem to reach this point.
if (true === $result && !$this->validSize($file)) {
$result = false;
}
+ // @codeCoverageIgnoreEnd
if (true === $result && $this->hasFile($file, $model)) {
$result = false;
}
diff --git a/app/Helpers/Chart/MetaPieChart.php b/app/Helpers/Chart/MetaPieChart.php
index f1082cda43..a6c97ef32d 100644
--- a/app/Helpers/Chart/MetaPieChart.php
+++ b/app/Helpers/Chart/MetaPieChart.php
@@ -24,7 +24,7 @@ declare(strict_types=1);
namespace FireflyIII\Helpers\Chart;
use Carbon\Carbon;
-use FireflyIII\Helpers\Collector\JournalCollectorInterface;
+use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Helpers\Filter\NegativeAmountFilter;
use FireflyIII\Helpers\Filter\OpposingAccountFilter;
use FireflyIII\Helpers\Filter\PositiveAmountFilter;
@@ -108,12 +108,12 @@ class MetaPieChart implements MetaPieChartInterface
// also collect all other transactions
if ($this->collectOtherObjects && 'expense' === $direction) {
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setUser($this->user);
$collector->setAccounts($this->accounts)->setRange($this->start, $this->end)->setTypes([TransactionType::WITHDRAWAL]);
- $journals = $collector->getJournals();
+ $journals = $collector->getTransactions();
$sum = (string)$journals->sum('transaction_amount');
$sum = bcmul($sum, '-1');
$sum = bcsub($sum, $this->total);
@@ -121,11 +121,11 @@ class MetaPieChart implements MetaPieChartInterface
}
if ($this->collectOtherObjects && 'income' === $direction) {
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setUser($this->user);
$collector->setAccounts($this->accounts)->setRange($this->start, $this->end)->setTypes([TransactionType::DEPOSIT]);
- $journals = $collector->getJournals();
+ $journals = $collector->getTransactions();
$sum = (string)$journals->sum('transaction_amount');
$sum = bcsub($sum, $this->total);
$chartData[$key] = $sum;
@@ -271,8 +271,8 @@ class MetaPieChart implements MetaPieChartInterface
*/
protected function getTransactions(string $direction): Collection
{
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$types = [TransactionType::DEPOSIT, TransactionType::TRANSFER];
$collector->addFilter(NegativeAmountFilter::class);
if ('expense' === $direction) {
@@ -304,7 +304,7 @@ class MetaPieChart implements MetaPieChartInterface
// @codeCoverageIgnoreEnd
- return $collector->getJournals();
+ return $collector->getTransactions();
}
/**
@@ -360,7 +360,8 @@ class MetaPieChart implements MetaPieChartInterface
foreach ($array as $objectId => $amount) {
if (!isset($names[$objectId])) {
$object = $repository->findNull((int)$objectId);
- $names[$objectId] = $object->name ?? $object->tag;
+ $name = null === $object ? '(no name)' : $object->name;
+ $names[$objectId] = $name ?? $object->tag;
}
$amount = app('steam')->positive($amount);
$this->total = bcadd($this->total, $amount);
diff --git a/app/Helpers/Collection/Balance.php b/app/Helpers/Collection/Balance.php
index b10674cefd..71573853ea 100644
--- a/app/Helpers/Collection/Balance.php
+++ b/app/Helpers/Collection/Balance.php
@@ -26,6 +26,8 @@ use Illuminate\Support\Collection;
/**
* Class Balance.
+ *
+ * @codeCoverageIgnore
*/
class Balance
{
diff --git a/app/Helpers/Collection/BalanceEntry.php b/app/Helpers/Collection/BalanceEntry.php
index 8f2e4fb195..86eddf607b 100644
--- a/app/Helpers/Collection/BalanceEntry.php
+++ b/app/Helpers/Collection/BalanceEntry.php
@@ -26,6 +26,8 @@ use FireflyIII\Models\Account as AccountModel;
/**
* Class BalanceEntry.
+ *
+ * @codeCoverageIgnore
*/
class BalanceEntry
{
diff --git a/app/Helpers/Collection/BalanceHeader.php b/app/Helpers/Collection/BalanceHeader.php
index 46c3091333..3769b0f5c0 100644
--- a/app/Helpers/Collection/BalanceHeader.php
+++ b/app/Helpers/Collection/BalanceHeader.php
@@ -27,6 +27,8 @@ use Illuminate\Support\Collection;
/**
* Class BalanceHeader.
+ *
+ * @codeCoverageIgnore
*/
class BalanceHeader
{
diff --git a/app/Helpers/Collection/BalanceLine.php b/app/Helpers/Collection/BalanceLine.php
index a679ef66ff..89668f0963 100644
--- a/app/Helpers/Collection/BalanceLine.php
+++ b/app/Helpers/Collection/BalanceLine.php
@@ -29,6 +29,8 @@ use Illuminate\Support\Collection;
/**
* Class BalanceLine.
+ *
+ * @codeCoverageIgnore
*/
class BalanceLine
{
diff --git a/app/Helpers/Collection/Bill.php b/app/Helpers/Collection/Bill.php
index 9ce761345d..f950a93914 100644
--- a/app/Helpers/Collection/Bill.php
+++ b/app/Helpers/Collection/Bill.php
@@ -29,6 +29,8 @@ use Log;
/**
* Class Bill.
+ *
+ * @codeCoverageIgnore
*/
class Bill
{
diff --git a/app/Helpers/Collection/BillLine.php b/app/Helpers/Collection/BillLine.php
index 4be29cb07a..eb1e4c8fee 100644
--- a/app/Helpers/Collection/BillLine.php
+++ b/app/Helpers/Collection/BillLine.php
@@ -27,6 +27,8 @@ use FireflyIII\Models\Bill as BillModel;
/**
* Class BillLine.
+ *
+ * @codeCoverageIgnore
*/
class BillLine
{
diff --git a/app/Helpers/Collection/Category.php b/app/Helpers/Collection/Category.php
index 9868bdb945..ce774387ae 100644
--- a/app/Helpers/Collection/Category.php
+++ b/app/Helpers/Collection/Category.php
@@ -27,6 +27,8 @@ use Illuminate\Support\Collection;
/**
* Class Category.
+ *
+ * @codeCoverageIgnore
*/
class Category
{
diff --git a/app/Helpers/Collector/JournalCollector.php b/app/Helpers/Collector/TransactionCollector.php
similarity index 85%
rename from app/Helpers/Collector/JournalCollector.php
rename to app/Helpers/Collector/TransactionCollector.php
index 26f481bdce..680ff6c944 100644
--- a/app/Helpers/Collector/JournalCollector.php
+++ b/app/Helpers/Collector/TransactionCollector.php
@@ -1,7 +1,7 @@
.
*/
-/** @noinspection PhpDynamicAsStaticMethodCallInspection */
-/** @noinspection PropertyCanBeStaticInspection */
+
declare(strict_types=1);
namespace FireflyIII\Helpers\Collector;
+
use Carbon\Carbon;
use DB;
use FireflyIII\Exceptions\FireflyException;
@@ -51,15 +51,11 @@ use Illuminate\Support\Collection;
use Log;
/**
- * Maybe this is a good idea after all...
- *
- * Class JournalCollector
- * @SuppressWarnings(PHPMD.TooManyPublicMethods)
- * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
- * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
+ * Class TransactionCollector
*
+ * @codeCoverageIgnore
*/
-class JournalCollector implements JournalCollectorInterface
+class TransactionCollector implements TransactionCollectorInterface
{
/** @var array */
@@ -91,6 +87,7 @@ class JournalCollector implements JournalCollectorInterface
'transactions.amount as transaction_amount',
'transactions.transaction_currency_id as transaction_currency_id',
+ 'transaction_currencies.name as transaction_currency_name',
'transaction_currencies.code as transaction_currency_code',
'transaction_currencies.symbol as transaction_currency_symbol',
'transaction_currencies.decimal_places as transaction_currency_dp',
@@ -135,9 +132,9 @@ class JournalCollector implements JournalCollectorInterface
/**
* @param string $filter
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function addFilter(string $filter): JournalCollectorInterface
+ public function addFilter(string $filter): TransactionCollectorInterface
{
$interfaces = class_implements($filter);
if (\in_array(FilterInterface::class, $interfaces, true) && !\in_array($filter, $this->filters, true)) {
@@ -151,9 +148,9 @@ class JournalCollector implements JournalCollectorInterface
/**
* @param string $amount
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function amountIs(string $amount): JournalCollectorInterface
+ public function amountIs(string $amount): TransactionCollectorInterface
{
$this->query->where(
function (EloquentBuilder $q) use ($amount) {
@@ -168,9 +165,9 @@ class JournalCollector implements JournalCollectorInterface
/**
* @param string $amount
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function amountLess(string $amount): JournalCollectorInterface
+ public function amountLess(string $amount): TransactionCollectorInterface
{
$this->query->where(
function (EloquentBuilder $q1) use ($amount) {
@@ -196,9 +193,9 @@ class JournalCollector implements JournalCollectorInterface
/**
* @param string $amount
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function amountMore(string $amount): JournalCollectorInterface
+ public function amountMore(string $amount): TransactionCollectorInterface
{
$this->query->where(
function (EloquentBuilder $q1) use ($amount) {
@@ -229,7 +226,7 @@ class JournalCollector implements JournalCollectorInterface
public function count(): int
{
if (true === $this->run) {
- throw new FireflyException('Cannot count after run in JournalCollector.');
+ throw new FireflyException('Cannot count after run in TransactionCollector.');
}
$countQuery = clone $this->query;
@@ -246,13 +243,10 @@ class JournalCollector implements JournalCollectorInterface
return $this->count;
}
- /** @noinspection MultipleReturnStatementsInspection */
/**
* @return Collection
- * @SuppressWarnings(PHPMD.CyclomaticComplexity)
- * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
- public function getJournals(): Collection
+ public function getTransactions(): Collection
{
$this->run = true;
@@ -307,16 +301,15 @@ class JournalCollector implements JournalCollectorInterface
/**
* @return LengthAwarePaginator
- *
* @throws FireflyException
*/
- public function getPaginatedJournals(): LengthAwarePaginator
+ public function getPaginatedTransactions(): LengthAwarePaginator
{
if (true === $this->run) {
- throw new FireflyException('Cannot getPaginatedJournals after run in JournalCollector.');
+ throw new FireflyException('Cannot getPaginatedTransactions after run in TransactionCollector.');
}
$this->count();
- $set = $this->getJournals();
+ $set = $this->getTransactions();
$journals = new LengthAwarePaginator($set, $this->count, $this->limit, $this->page);
return $journals;
@@ -331,9 +324,9 @@ class JournalCollector implements JournalCollectorInterface
}
/**
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function ignoreCache(): JournalCollectorInterface
+ public function ignoreCache(): TransactionCollectorInterface
{
$this->ignoreCache = true;
@@ -343,9 +336,9 @@ class JournalCollector implements JournalCollectorInterface
/**
* @param string $filter
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function removeFilter(string $filter): JournalCollectorInterface
+ public function removeFilter(string $filter): TransactionCollectorInterface
{
$key = array_search($filter, $this->filters, true);
if (!(false === $key)) {
@@ -359,9 +352,9 @@ class JournalCollector implements JournalCollectorInterface
/**
* @param Collection $accounts
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setAccounts(Collection $accounts): JournalCollectorInterface
+ public function setAccounts(Collection $accounts): TransactionCollectorInterface
{
if ($accounts->count() > 0) {
$accountIds = $accounts->pluck('id')->toArray();
@@ -380,21 +373,21 @@ class JournalCollector implements JournalCollectorInterface
/**
* @param Carbon $after
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setAfter(Carbon $after): JournalCollectorInterface
+ public function setAfter(Carbon $after): TransactionCollectorInterface
{
$afterStr = $after->format('Y-m-d 00:00:00');
$this->query->where('transaction_journals.date', '>=', $afterStr);
- Log::debug(sprintf('JournalCollector range is now after %s (inclusive)', $afterStr));
+ Log::debug(sprintf('TransactionCollector range is now after %s (inclusive)', $afterStr));
return $this;
}
/**
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setAllAssetAccounts(): JournalCollectorInterface
+ public function setAllAssetAccounts(): TransactionCollectorInterface
{
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
@@ -416,13 +409,13 @@ class JournalCollector implements JournalCollectorInterface
/**
* @param Carbon $before
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setBefore(Carbon $before): JournalCollectorInterface
+ public function setBefore(Carbon $before): TransactionCollectorInterface
{
$beforeStr = $before->format('Y-m-d 00:00:00');
$this->query->where('transaction_journals.date', '<=', $beforeStr);
- Log::debug(sprintf('JournalCollector range is now before %s (inclusive)', $beforeStr));
+ Log::debug(sprintf('TransactionCollector range is now before %s (inclusive)', $beforeStr));
return $this;
}
@@ -430,9 +423,9 @@ class JournalCollector implements JournalCollectorInterface
/**
* @param Collection $bills
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setBills(Collection $bills): JournalCollectorInterface
+ public function setBills(Collection $bills): TransactionCollectorInterface
{
if ($bills->count() > 0) {
$billIds = $bills->pluck('id')->toArray();
@@ -445,9 +438,9 @@ class JournalCollector implements JournalCollectorInterface
/**
* @param Budget $budget
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setBudget(Budget $budget): JournalCollectorInterface
+ public function setBudget(Budget $budget): TransactionCollectorInterface
{
$this->joinBudgetTables();
@@ -464,9 +457,9 @@ class JournalCollector implements JournalCollectorInterface
/**
* @param Collection $budgets
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setBudgets(Collection $budgets): JournalCollectorInterface
+ public function setBudgets(Collection $budgets): TransactionCollectorInterface
{
$budgetIds = $budgets->pluck('id')->toArray();
if (0 !== \count($budgetIds)) {
@@ -487,9 +480,9 @@ class JournalCollector implements JournalCollectorInterface
/**
* @param Collection $categories
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setCategories(Collection $categories): JournalCollectorInterface
+ public function setCategories(Collection $categories): TransactionCollectorInterface
{
$categoryIds = $categories->pluck('id')->toArray();
if (0 !== \count($categoryIds)) {
@@ -509,9 +502,9 @@ class JournalCollector implements JournalCollectorInterface
/**
* @param Category $category
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setCategory(Category $category): JournalCollectorInterface
+ public function setCategory(Category $category): TransactionCollectorInterface
{
$this->joinCategoryTables();
@@ -528,9 +521,9 @@ class JournalCollector implements JournalCollectorInterface
/**
* @param Collection $journals
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setJournals(Collection $journals): JournalCollectorInterface
+ public function setJournals(Collection $journals): TransactionCollectorInterface
{
$ids = $journals->pluck('id')->toArray();
$this->query->where(
@@ -545,9 +538,9 @@ class JournalCollector implements JournalCollectorInterface
/**
* @param int $limit
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setLimit(int $limit): JournalCollectorInterface
+ public function setLimit(int $limit): TransactionCollectorInterface
{
$this->limit = $limit;
$this->query->limit($limit);
@@ -559,9 +552,9 @@ class JournalCollector implements JournalCollectorInterface
/**
* @param int $offset
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setOffset(int $offset): JournalCollectorInterface
+ public function setOffset(int $offset): TransactionCollectorInterface
{
$this->offset = $offset;
@@ -571,9 +564,9 @@ class JournalCollector implements JournalCollectorInterface
/**
* @param Collection $accounts
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setOpposingAccounts(Collection $accounts): JournalCollectorInterface
+ public function setOpposingAccounts(Collection $accounts): TransactionCollectorInterface
{
$this->withOpposingAccount();
@@ -585,9 +578,9 @@ class JournalCollector implements JournalCollectorInterface
/**
* @param int $page
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setPage(int $page): JournalCollectorInterface
+ public function setPage(int $page): TransactionCollectorInterface
{
if ($page < 1) {
$page = 1;
@@ -614,16 +607,16 @@ class JournalCollector implements JournalCollectorInterface
* @param Carbon $start
* @param Carbon $end
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setRange(Carbon $start, Carbon $end): JournalCollectorInterface
+ public function setRange(Carbon $start, Carbon $end): TransactionCollectorInterface
{
if ($start <= $end) {
$startStr = $start->format('Y-m-d 00:00:00');
$endStr = $end->format('Y-m-d 23:59:59');
$this->query->where('transaction_journals.date', '>=', $startStr);
$this->query->where('transaction_journals.date', '<=', $endStr);
- Log::debug(sprintf('JournalCollector range is now %s - %s (inclusive)', $startStr, $endStr));
+ Log::debug(sprintf('TransactionCollector range is now %s - %s (inclusive)', $startStr, $endStr));
}
return $this;
@@ -632,9 +625,9 @@ class JournalCollector implements JournalCollectorInterface
/**
* @param Tag $tag
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setTag(Tag $tag): JournalCollectorInterface
+ public function setTag(Tag $tag): TransactionCollectorInterface
{
$this->joinTagTables();
$this->query->where('tag_transaction_journal.tag_id', $tag->id);
@@ -645,9 +638,9 @@ class JournalCollector implements JournalCollectorInterface
/**
* @param Collection $tags
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setTags(Collection $tags): JournalCollectorInterface
+ public function setTags(Collection $tags): TransactionCollectorInterface
{
$this->joinTagTables();
$tagIds = $tags->pluck('id')->toArray();
@@ -659,9 +652,9 @@ class JournalCollector implements JournalCollectorInterface
/**
* @param array $types
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setTypes(array $types): JournalCollectorInterface
+ public function setTypes(array $types): TransactionCollectorInterface
{
if (\count($types) > 0) {
Log::debug('Set query collector types', $types);
@@ -686,7 +679,7 @@ class JournalCollector implements JournalCollectorInterface
*/
public function startQuery(): void
{
- Log::debug('journalCollector::startQuery');
+ Log::debug('TransactionCollector::startQuery');
/** @var EloquentBuilder $query */
$query = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->leftJoin('transaction_types', 'transaction_types.id', 'transaction_journals.transaction_type_id')
@@ -709,9 +702,9 @@ class JournalCollector implements JournalCollectorInterface
}
/**
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function withBudgetInformation(): JournalCollectorInterface
+ public function withBudgetInformation(): TransactionCollectorInterface
{
$this->joinBudgetTables();
@@ -719,9 +712,9 @@ class JournalCollector implements JournalCollectorInterface
}
/**
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function withCategoryInformation(): JournalCollectorInterface
+ public function withCategoryInformation(): TransactionCollectorInterface
{
$this->joinCategoryTables();
@@ -729,9 +722,9 @@ class JournalCollector implements JournalCollectorInterface
}
/**
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function withOpposingAccount(): JournalCollectorInterface
+ public function withOpposingAccount(): TransactionCollectorInterface
{
$this->joinOpposingTables();
@@ -739,9 +732,9 @@ class JournalCollector implements JournalCollectorInterface
}
/**
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function withoutBudget(): JournalCollectorInterface
+ public function withoutBudget(): TransactionCollectorInterface
{
$this->joinBudgetTables();
@@ -756,9 +749,9 @@ class JournalCollector implements JournalCollectorInterface
}
/**
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function withoutCategory(): JournalCollectorInterface
+ public function withoutCategory(): TransactionCollectorInterface
{
$this->joinCategoryTables();
@@ -901,4 +894,4 @@ class JournalCollector implements JournalCollectorInterface
$this->query->leftJoin('tag_transaction_journal', 'tag_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id');
}
}
-}
+}
\ No newline at end of file
diff --git a/app/Helpers/Collector/JournalCollectorInterface.php b/app/Helpers/Collector/TransactionCollectorInterface.php
similarity index 52%
rename from app/Helpers/Collector/JournalCollectorInterface.php
rename to app/Helpers/Collector/TransactionCollectorInterface.php
index f3d3dbbb7b..ef42ffbc9c 100644
--- a/app/Helpers/Collector/JournalCollectorInterface.php
+++ b/app/Helpers/Collector/TransactionCollectorInterface.php
@@ -1,7 +1,7 @@
.
*/
+
declare(strict_types=1);
namespace FireflyIII\Helpers\Collector;
@@ -32,9 +33,10 @@ use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
/**
- * Interface JournalCollectorInterface.
+ * Interface TransactionCollectorInterface
+ *
*/
-interface JournalCollectorInterface
+interface TransactionCollectorInterface
{
/**
@@ -42,36 +44,36 @@ interface JournalCollectorInterface
*
* @param string $filter
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function addFilter(string $filter): JournalCollectorInterface;
+ public function addFilter(string $filter): TransactionCollectorInterface;
/**
* Get transactions with a specific amount.
*
* @param string $amount
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function amountIs(string $amount): JournalCollectorInterface;
+ public function amountIs(string $amount): TransactionCollectorInterface;
/**
* Get transactions where the amount is less than.
*
* @param string $amount
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function amountLess(string $amount): JournalCollectorInterface;
+ public function amountLess(string $amount): TransactionCollectorInterface;
/**
* Get transactions where the amount is more than.
*
* @param string $amount
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function amountMore(string $amount): JournalCollectorInterface;
+ public function amountMore(string $amount): TransactionCollectorInterface;
/**
* Count the result.
@@ -81,18 +83,18 @@ interface JournalCollectorInterface
public function count(): int;
/**
- * Get all journals.
+ * Get all transactions.
*
* @return Collection
*/
- public function getJournals(): Collection;
+ public function getTransactions(): Collection;
/**
* Get a paginated result.
*
* @return LengthAwarePaginator
*/
- public function getPaginatedJournals(): LengthAwarePaginator;
+ public function getPaginatedTransactions(): LengthAwarePaginator;
/**
* Get the query.
@@ -104,142 +106,142 @@ interface JournalCollectorInterface
/**
* Set to ignore the cache.
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function ignoreCache(): JournalCollectorInterface;
+ public function ignoreCache(): TransactionCollectorInterface;
/**
* Remove a filter.
*
* @param string $filter
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function removeFilter(string $filter): JournalCollectorInterface;
+ public function removeFilter(string $filter): TransactionCollectorInterface;
/**
* Set the accounts to collect from.
*
* @param Collection $accounts
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setAccounts(Collection $accounts): JournalCollectorInterface;
+ public function setAccounts(Collection $accounts): TransactionCollectorInterface;
/**
- * Collect journals after a specific date.
+ * Collect transactions after a specific date.
*
* @param Carbon $after
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setAfter(Carbon $after): JournalCollectorInterface;
+ public function setAfter(Carbon $after): TransactionCollectorInterface;
/**
* Include all asset accounts.
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setAllAssetAccounts(): JournalCollectorInterface;
+ public function setAllAssetAccounts(): TransactionCollectorInterface;
/**
- * Collect journals before a specific date.
+ * Collect transactions before a specific date.
*
* @param Carbon $before
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setBefore(Carbon $before): JournalCollectorInterface;
+ public function setBefore(Carbon $before): TransactionCollectorInterface;
/**
* Set the bills to filter on.
*
* @param Collection $bills
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setBills(Collection $bills): JournalCollectorInterface;
+ public function setBills(Collection $bills): TransactionCollectorInterface;
/**
* Set the budget to filter on.
*
* @param Budget $budget
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setBudget(Budget $budget): JournalCollectorInterface;
+ public function setBudget(Budget $budget): TransactionCollectorInterface;
/**
* Set the budgets to filter on.
*
* @param Collection $budgets
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setBudgets(Collection $budgets): JournalCollectorInterface;
+ public function setBudgets(Collection $budgets): TransactionCollectorInterface;
/**
* Set the categories to filter on.
*
* @param Collection $categories
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setCategories(Collection $categories): JournalCollectorInterface;
+ public function setCategories(Collection $categories): TransactionCollectorInterface;
/**
* Set the category to filter on.
*
* @param Category $category
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setCategory(Category $category): JournalCollectorInterface;
+ public function setCategory(Category $category): TransactionCollectorInterface;
/**
* Set the journals to filter on.
*
* @param Collection $journals
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setJournals(Collection $journals): JournalCollectorInterface;
+ public function setJournals(Collection $journals): TransactionCollectorInterface;
/**
* Set the page limit.
*
* @param int $limit
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setLimit(int $limit): JournalCollectorInterface;
+ public function setLimit(int $limit): TransactionCollectorInterface;
/**
* Set the offset.
*
* @param int $offset
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setOffset(int $offset): JournalCollectorInterface;
+ public function setOffset(int $offset): TransactionCollectorInterface;
/**
* Set the opposing accounts to collect from.
*
* @param Collection $accounts
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setOpposingAccounts(Collection $accounts): JournalCollectorInterface;
+ public function setOpposingAccounts(Collection $accounts): TransactionCollectorInterface;
/**
* Set the page to get.
*
* @param int $page
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setPage(int $page): JournalCollectorInterface;
+ public function setPage(int $page): TransactionCollectorInterface;
/**
* Set the date range.
@@ -247,36 +249,36 @@ interface JournalCollectorInterface
* @param Carbon $start
* @param Carbon $end
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setRange(Carbon $start, Carbon $end): JournalCollectorInterface;
+ public function setRange(Carbon $start, Carbon $end): TransactionCollectorInterface;
/**
* Set the tag to collect from.
*
* @param Tag $tag
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setTag(Tag $tag): JournalCollectorInterface;
+ public function setTag(Tag $tag): TransactionCollectorInterface;
/**
* Set the tags to collect from.
*
* @param Collection $tags
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setTags(Collection $tags): JournalCollectorInterface;
+ public function setTags(Collection $tags): TransactionCollectorInterface;
/**
* Set the types to collect.
*
* @param array $types
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function setTypes(array $types): JournalCollectorInterface;
+ public function setTypes(array $types): TransactionCollectorInterface;
/**
* Set the user.
@@ -295,35 +297,35 @@ interface JournalCollectorInterface
/**
* Include budget information.
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function withBudgetInformation(): JournalCollectorInterface;
+ public function withBudgetInformation(): TransactionCollectorInterface;
/**
* Include category information.
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function withCategoryInformation(): JournalCollectorInterface;
+ public function withCategoryInformation(): TransactionCollectorInterface;
/**
* Include opposing account information.
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function withOpposingAccount(): JournalCollectorInterface;
+ public function withOpposingAccount(): TransactionCollectorInterface;
/**
* Include tranactions without a budget.
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function withoutBudget(): JournalCollectorInterface;
+ public function withoutBudget(): TransactionCollectorInterface;
/**
* Include tranactions without a category.
*
- * @return JournalCollectorInterface
+ * @return TransactionCollectorInterface
*/
- public function withoutCategory(): JournalCollectorInterface;
-}
+ public function withoutCategory(): TransactionCollectorInterface;
+}
\ No newline at end of file
diff --git a/app/Helpers/Filter/CountAttachmentsFilter.php b/app/Helpers/Filter/CountAttachmentsFilter.php
index bcb61fa3c2..c47cb838d9 100644
--- a/app/Helpers/Filter/CountAttachmentsFilter.php
+++ b/app/Helpers/Filter/CountAttachmentsFilter.php
@@ -31,6 +31,7 @@ use Illuminate\Support\Collection;
/**
* Class CountAttachmentsFilter
+ * @codeCoverageIgnore
*/
class CountAttachmentsFilter implements FilterInterface
{
diff --git a/app/Helpers/Filter/EmptyFilter.php b/app/Helpers/Filter/EmptyFilter.php
index 2235fe2bfa..716b3d9cdb 100644
--- a/app/Helpers/Filter/EmptyFilter.php
+++ b/app/Helpers/Filter/EmptyFilter.php
@@ -26,6 +26,8 @@ use Illuminate\Support\Collection;
/**
* Class EmptyFilter.
+ *
+ * @codeCoverageIgnore
*/
class EmptyFilter implements FilterInterface
{
diff --git a/app/Helpers/Filter/InternalTransferFilter.php b/app/Helpers/Filter/InternalTransferFilter.php
index 1442db3d20..a72474c4e0 100644
--- a/app/Helpers/Filter/InternalTransferFilter.php
+++ b/app/Helpers/Filter/InternalTransferFilter.php
@@ -32,6 +32,8 @@ use Log;
* This filter removes any filters that are from A to B or from B to A given a set of
* account id's (in $parameters) where A and B are mentioned. So transfers between the mentioned
* accounts will be removed.
+ *
+ * @codeCoverageIgnore
*/
class InternalTransferFilter implements FilterInterface
{
diff --git a/app/Helpers/Filter/NegativeAmountFilter.php b/app/Helpers/Filter/NegativeAmountFilter.php
index b6b0f10cf6..58ebc6ed3b 100644
--- a/app/Helpers/Filter/NegativeAmountFilter.php
+++ b/app/Helpers/Filter/NegativeAmountFilter.php
@@ -30,6 +30,8 @@ use Log;
* Class NegativeAmountFilter.
*
* This filter removes entries with a negative amount (the original modifier is -1).
+ *
+ * @codeCoverageIgnore
*/
class NegativeAmountFilter implements FilterInterface
{
diff --git a/app/Helpers/Filter/OpposingAccountFilter.php b/app/Helpers/Filter/OpposingAccountFilter.php
index 5df2170ce6..681a4c26b2 100644
--- a/app/Helpers/Filter/OpposingAccountFilter.php
+++ b/app/Helpers/Filter/OpposingAccountFilter.php
@@ -31,6 +31,8 @@ use Log;
*
* This filter is similar to the internal transfer filter but only removes transactions when the opposing account is
* amongst $parameters (list of account ID's).
+ *
+ * @codeCoverageIgnore
*/
class OpposingAccountFilter implements FilterInterface
{
diff --git a/app/Helpers/Filter/PositiveAmountFilter.php b/app/Helpers/Filter/PositiveAmountFilter.php
index 155c6c3f33..257bdf43b4 100644
--- a/app/Helpers/Filter/PositiveAmountFilter.php
+++ b/app/Helpers/Filter/PositiveAmountFilter.php
@@ -33,6 +33,8 @@ use Log;
*
* This filter removes transactions with either a positive amount ($parameters = 1) or a negative amount
* ($parameter = -1). This is helpful when a Collection has you with both transactions in a journal.
+ *
+ * @codeCoverageIgnore
*/
class PositiveAmountFilter implements FilterInterface
{
diff --git a/app/Helpers/Filter/SplitIndicatorFilter.php b/app/Helpers/Filter/SplitIndicatorFilter.php
index afc42c30c5..609c3a424e 100644
--- a/app/Helpers/Filter/SplitIndicatorFilter.php
+++ b/app/Helpers/Filter/SplitIndicatorFilter.php
@@ -30,6 +30,8 @@ use Illuminate\Support\Collection;
/**
* Class SplitIndicatorFilter
+ *
+ * @codeCoverageIgnore
*/
class SplitIndicatorFilter implements FilterInterface
{
diff --git a/app/Helpers/Filter/TransactionViewFilter.php b/app/Helpers/Filter/TransactionViewFilter.php
index 1c8dc2938a..baae12173d 100644
--- a/app/Helpers/Filter/TransactionViewFilter.php
+++ b/app/Helpers/Filter/TransactionViewFilter.php
@@ -35,6 +35,8 @@ use Log;
*
* This is used in the mass-edit routine.
*
+ * @codeCoverageIgnore
+ *
*/
class TransactionViewFilter implements FilterInterface
{
diff --git a/app/Helpers/Filter/TransferFilter.php b/app/Helpers/Filter/TransferFilter.php
index 40f2037a3b..26037700a8 100644
--- a/app/Helpers/Filter/TransferFilter.php
+++ b/app/Helpers/Filter/TransferFilter.php
@@ -30,6 +30,8 @@ use Illuminate\Support\Collection;
* Class TransferFilter.
*
* This filter removes any transfers that are in the collection twice (from A to B and from B to A).
+ *
+ * @codeCoverageIgnore
*/
class TransferFilter implements FilterInterface
{
diff --git a/app/Helpers/Help/Help.php b/app/Helpers/Help/Help.php
index 787d4710fe..a07818d6cd 100644
--- a/app/Helpers/Help/Help.php
+++ b/app/Helpers/Help/Help.php
@@ -43,6 +43,8 @@ class Help implements HelpInterface
/**
* Get from cache.
*
+ * @codeCoverageIgnore
+ *
* @param string $route
* @param string $language
*
@@ -70,14 +72,14 @@ class Help implements HelpInterface
$opt = ['headers' => ['User-Agent' => $this->userAgent]];
$content = '';
$statusCode = 500;
- $client = new Client;
+ $client = app(Client::class);
try {
$res = $client->request('GET', $uri, $opt);
$statusCode = $res->getStatusCode();
$content = trim($res->getBody()->getContents());
} catch (GuzzleException|Exception $e) {
- Log::error($e->getMessage());
- Log::error($e->getTraceAsString());
+ Log::info($e->getMessage());
+ Log::info($e->getTraceAsString());
}
Log::debug(sprintf('Status code is %d', $statusCode));
@@ -94,6 +96,8 @@ class Help implements HelpInterface
/**
* Do we have the route?
*
+ * @codeCoverageIgnore
+ *
* @param string $route
*
* @return bool
@@ -106,6 +110,8 @@ class Help implements HelpInterface
/**
* Is in cache?
*
+ * @codeCoverageIgnore
+ *
* @param string $route
* @param string $language
*
@@ -128,6 +134,8 @@ class Help implements HelpInterface
/**
* Put help text in cache.
*
+ * @codeCoverageIgnore
+ *
* @param string $route
* @param string $language
* @param string $content
diff --git a/app/Helpers/Report/BalanceReportHelper.php b/app/Helpers/Report/BalanceReportHelper.php
index 1daec53c1c..ae1404c83f 100644
--- a/app/Helpers/Report/BalanceReportHelper.php
+++ b/app/Helpers/Report/BalanceReportHelper.php
@@ -34,6 +34,8 @@ use Log;
/**
* Class BalanceReportHelper.
+ *
+ * @codeCoverageIgnore
*/
class BalanceReportHelper implements BalanceReportHelperInterface
{
@@ -160,6 +162,7 @@ class BalanceReportHelper implements BalanceReportHelperInterface
{
$set = $balance->getBalanceLines();
$newSet = new Collection;
+ /** @var BalanceLine $entry */
foreach ($set as $entry) {
if (null !== $entry->getBudget()->id) {
$sum = '0';
diff --git a/app/Helpers/Report/BudgetReportHelper.php b/app/Helpers/Report/BudgetReportHelper.php
index 5d33e5ca71..dde31c246c 100644
--- a/app/Helpers/Report/BudgetReportHelper.php
+++ b/app/Helpers/Report/BudgetReportHelper.php
@@ -30,6 +30,8 @@ use Illuminate\Support\Collection;
/**
* Class BudgetReportHelper.
+ *
+ * @codeCoverageIgnore
*/
class BudgetReportHelper implements BudgetReportHelperInterface
{
diff --git a/app/Helpers/Report/NetWorth.php b/app/Helpers/Report/NetWorth.php
new file mode 100644
index 0000000000..8e27cfc519
--- /dev/null
+++ b/app/Helpers/Report/NetWorth.php
@@ -0,0 +1,148 @@
+.
+ */
+
+declare(strict_types=1);
+
+namespace FireflyIII\Helpers\Report;
+
+use Carbon\Carbon;
+use FireflyIII\Models\Account;
+use FireflyIII\Models\AccountType;
+use FireflyIII\Repositories\Account\AccountRepositoryInterface;
+use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
+use FireflyIII\Support\CacheProperties;
+use FireflyIII\User;
+use Illuminate\Support\Collection;
+use Log;
+
+/**
+ *
+ * Class NetWorth
+ */
+class NetWorth implements NetWorthInterface
+{
+
+ /** @var AccountRepositoryInterface */
+ private $accountRepository;
+
+ /** @var CurrencyRepositoryInterface */
+ private $currencyRepos;
+ /** @var User */
+ private $user;
+
+ /**
+ * Returns the user's net worth in an array with the following layout:
+ *
+ * -
+ * - currency: TransactionCurrency object
+ * - date: the current date
+ * - amount: the user's net worth in that currency.
+ *
+ * This repeats for each currency the user has transactions in.
+ * Result of this method is cached.
+ *
+ * @param Collection $accounts
+ * @param Carbon $date
+ *
+ * @return array
+ */
+ public function getNetWorthByCurrency(Collection $accounts, Carbon $date): array
+ {
+
+ // start in the past, end in the future? use $date
+ $cache = new CacheProperties;
+ $cache->addProperty($date);
+ $cache->addProperty('net-worth-by-currency');
+ $cache->addProperty(implode(',', $accounts->pluck('id')->toArray()));
+ if ($cache->has()) {
+ return $cache->get(); // @codeCoverageIgnore
+ }
+
+ $netWorth = [];
+ $result = [];
+ Log::debug(sprintf('Now in getNetWorthByCurrency(%s)', $date->format('Y-m-d')));
+
+ // get default currency
+ $default = app('amount')->getDefaultCurrencyByUser($this->user);
+
+ // get all balances:
+ $balances = app('steam')->balancesByAccounts($accounts, $date);
+
+ // get the preferred currency for this account
+ /** @var Account $account */
+ foreach ($accounts as $account) {
+ Log::debug(sprintf('Now at account #%d: "%s"', $account->id, $account->name));
+ $currencyId = (int)$this->accountRepository->getMetaValue($account, 'currency_id');
+ $currencyId = 0 === $currencyId ? $default->id : $currencyId;
+
+ Log::debug(sprintf('Currency ID is #%d', $currencyId));
+
+ // balance in array:
+ $balance = $balances[$account->id] ?? '0';
+
+ Log::debug(sprintf('Balance is %s', $balance));
+
+ // if the account is a credit card, subtract the virtual balance from the balance,
+ // to better reflect that this is not money that is actually "yours".
+ $role = (string)$this->accountRepository->getMetaValue($account, 'accountRole');
+ $virtualBalance = (string)$account->virtual_balance;
+ if ('ccAsset' === $role && '' !== $virtualBalance && (float)$virtualBalance > 0) {
+ $balance = bcsub($balance, $virtualBalance);
+ }
+
+ Log::debug(sprintf('Balance corrected to %s', $balance));
+
+ if (!isset($netWorth[$currencyId])) {
+ $netWorth[$currencyId] = '0';
+ }
+ $netWorth[$currencyId] = bcadd($balance, $netWorth[$currencyId]);
+
+ Log::debug(sprintf('Total net worth for currency #%d is %s', $currencyId, $netWorth[$currencyId]));
+ }
+ ksort($netWorth);
+
+ // loop results and add currency information:
+ foreach ($netWorth as $currencyId => $balance) {
+ $result[] = [
+ 'currency' => $this->currencyRepos->findNull($currencyId),
+ 'balance' => $balance,
+ ];
+ }
+ $cache->store($result);
+
+ return $result;
+ }
+
+ /**
+ * @param User $user
+ */
+ public function setUser(User $user): void
+ {
+ $this->user = $user;
+
+ // make repository:
+ $this->accountRepository = app(AccountRepositoryInterface::class);
+ $this->accountRepository->setUser($this->user);
+
+ $this->currencyRepos = app(CurrencyRepositoryInterface::class);
+ $this->currencyRepos->setUser($this->user);
+ }
+}
\ No newline at end of file
diff --git a/app/Helpers/Report/NetWorthInterface.php b/app/Helpers/Report/NetWorthInterface.php
new file mode 100644
index 0000000000..8e7e532211
--- /dev/null
+++ b/app/Helpers/Report/NetWorthInterface.php
@@ -0,0 +1,60 @@
+.
+ */
+
+declare(strict_types=1);
+
+namespace FireflyIII\Helpers\Report;
+
+use Carbon\Carbon;
+use FireflyIII\User;
+use Illuminate\Support\Collection;
+
+/**
+ * Interface NetWorthInterface
+ *
+ * @package FireflyIII\Helpers\Report
+ */
+interface NetWorthInterface
+{
+ /**
+ * Returns the user's net worth in an array with the following layout:
+ *
+ * -
+ * - currency: TransactionCurrency object
+ * - date: the current date
+ * - amount: the user's net worth in that currency.
+ *
+ * This repeats for each currency the user has transactions in.
+ * Result of this method is cached.
+ *
+ * @param Collection $accounts
+ * @param Carbon $date
+ *
+ * @return array
+ */
+ public function getNetWorthByCurrency(Collection $accounts, Carbon $date): array;
+
+ /**
+ * @param User $user
+ */
+ public function setUser(User $user): void;
+
+}
\ No newline at end of file
diff --git a/app/Helpers/Report/PopupReport.php b/app/Helpers/Report/PopupReport.php
index c0dac93458..a50b248377 100644
--- a/app/Helpers/Report/PopupReport.php
+++ b/app/Helpers/Report/PopupReport.php
@@ -22,7 +22,7 @@ declare(strict_types=1);
namespace FireflyIII\Helpers\Report;
-use FireflyIII\Helpers\Collector\JournalCollectorInterface;
+use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Models\Account;
use FireflyIII\Models\Budget;
use FireflyIII\Models\Category;
@@ -33,6 +33,8 @@ use Illuminate\Support\Collection;
/**
* Class PopupReport.
+ *
+ * @codeCoverageIgnore
*/
class PopupReport implements PopupReportInterface
{
@@ -47,11 +49,11 @@ class PopupReport implements PopupReportInterface
*/
public function balanceForBudget(Budget $budget, Account $account, array $attributes): Collection
{
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAccounts(new Collection([$account]))->setRange($attributes['startDate'], $attributes['endDate'])->setBudget($budget);
- return $collector->getJournals();
+ return $collector->getTransactions();
}
/**
@@ -64,15 +66,15 @@ class PopupReport implements PopupReportInterface
*/
public function balanceForNoBudget(Account $account, array $attributes): Collection
{
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector
->setAccounts(new Collection([$account]))
->setTypes([TransactionType::WITHDRAWAL])
->setRange($attributes['startDate'], $attributes['endDate'])
->withoutBudget();
- return $collector->getJournals();
+ return $collector->getTransactions();
}
/**
@@ -85,8 +87,8 @@ class PopupReport implements PopupReportInterface
*/
public function byBudget(Budget $budget, array $attributes): Collection
{
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAccounts($attributes['accounts'])->setRange($attributes['startDate'], $attributes['endDate']);
@@ -97,7 +99,7 @@ class PopupReport implements PopupReportInterface
$collector->setBudget($budget);
}
- return $collector->getJournals();
+ return $collector->getTransactions();
}
/**
@@ -110,13 +112,13 @@ class PopupReport implements PopupReportInterface
*/
public function byCategory(Category $category, array $attributes): Collection
{
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAccounts($attributes['accounts'])->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER])
->setRange($attributes['startDate'], $attributes['endDate'])->withOpposingAccount()
->setCategory($category);
- return $collector->getJournals();
+ return $collector->getTransactions();
}
/**
@@ -133,17 +135,17 @@ class PopupReport implements PopupReportInterface
$repository = app(JournalRepositoryInterface::class);
$repository->setUser($account->user);
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAccounts(new Collection([$account]))->setRange($attributes['startDate'], $attributes['endDate'])
->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER]);
- $journals = $collector->getJournals();
+ $transactions = $collector->getTransactions();
$report = $attributes['accounts']->pluck('id')->toArray(); // accounts used in this report
// filter for transfers and withdrawals TO the given $account
- $journals = $journals->filter(
+ $transactions = $transactions->filter(
function (Transaction $transaction) use ($report, $repository) {
// get the destinations:
$sources = $repository->getJournalSourceAccounts($transaction->transactionJournal)->pluck('id')->toArray();
@@ -153,7 +155,7 @@ class PopupReport implements PopupReportInterface
}
);
- return $journals;
+ return $transactions;
}
/**
@@ -169,15 +171,15 @@ class PopupReport implements PopupReportInterface
/** @var JournalRepositoryInterface $repository */
$repository = app(JournalRepositoryInterface::class);
$repository->setUser($account->user);
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAccounts(new Collection([$account]))->setRange($attributes['startDate'], $attributes['endDate'])
->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER]);
- $journals = $collector->getJournals();
+ $transactions = $collector->getTransactions();
$report = $attributes['accounts']->pluck('id')->toArray(); // accounts used in this report
// filter the set so the destinations outside of $attributes['accounts'] are not included.
- $journals = $journals->filter(
+ $transactions = $transactions->filter(
function (Transaction $transaction) use ($report, $repository) {
// get the destinations:
$journal = $transaction->transactionJournal;
@@ -188,6 +190,6 @@ class PopupReport implements PopupReportInterface
}
);
- return $journals;
+ return $transactions;
}
}
diff --git a/app/Helpers/Report/ReportHelper.php b/app/Helpers/Report/ReportHelper.php
index 8f1cad60b1..bd8c5af15a 100644
--- a/app/Helpers/Report/ReportHelper.php
+++ b/app/Helpers/Report/ReportHelper.php
@@ -25,7 +25,7 @@ namespace FireflyIII\Helpers\Report;
use Carbon\Carbon;
use FireflyIII\Helpers\Collection\Bill as BillCollection;
use FireflyIII\Helpers\Collection\BillLine;
-use FireflyIII\Helpers\Collector\JournalCollectorInterface;
+use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Helpers\FiscalHelperInterface;
use FireflyIII\Models\Bill;
use FireflyIII\Models\Transaction;
@@ -35,6 +35,8 @@ use Illuminate\Support\Collection;
/**
* Class ReportHelper.
+ *
+ * @codeCoverageIgnore
*/
class ReportHelper implements ReportHelperInterface
{
@@ -83,10 +85,10 @@ class ReportHelper implements ReportHelperInterface
foreach ($expectedDates as $payDate) {
$endOfPayPeriod = app('navigation')->endOfX($payDate, $bill->repeat_freq, null);
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAccounts($accounts)->setRange($payDate, $endOfPayPeriod)->setBills($bills);
- $journals = $collector->getJournals();
+ $transactions = $collector->getTransactions();
$billLine = new BillLine;
$billLine->setBill($bill);
@@ -95,7 +97,7 @@ class ReportHelper implements ReportHelperInterface
$billLine->setMin((string)$bill->amount_min);
$billLine->setMax((string)$bill->amount_max);
$billLine->setHit(false);
- $entry = $journals->filter(
+ $entry = $transactions->filter(
function (Transaction $transaction) use ($bill) {
return $transaction->bill_id === $bill->id;
}
diff --git a/app/Helpers/Update/UpdateTrait.php b/app/Helpers/Update/UpdateTrait.php
index 775c44e186..e01d1b4891 100644
--- a/app/Helpers/Update/UpdateTrait.php
+++ b/app/Helpers/Update/UpdateTrait.php
@@ -42,6 +42,7 @@ trait UpdateTrait
*/
public function getLatestRelease(): ?Release
{
+ Log::debug('Now in getLatestRelease()');
$return = null;
/** @var UpdateRequest $request */
$request = app(UpdateRequest::class);
@@ -53,11 +54,15 @@ trait UpdateTrait
// get releases from array.
$releases = $request->getReleases();
+
+ Log::debug(sprintf('Found %d releases', \count($releases)));
+
if (\count($releases) > 0) {
// first entry should be the latest entry:
/** @var Release $first */
$first = reset($releases);
$return = $first;
+ Log::debug(sprintf('Number of releases found is larger than zero. Return %s ', $first->getTitle()));
}
return $return;
@@ -73,17 +78,21 @@ trait UpdateTrait
*/
public function parseResult(int $versionCheck, Release $release = null): string
{
+ Log::debug(sprintf('Now in parseResult(%d)', $versionCheck));
$current = (string)config('firefly.version');
$return = '';
if ($versionCheck === -2) {
+ Log::debug('-2, so give error.');
$return = (string)trans('firefly.update_check_error');
}
if ($versionCheck === -1 && null !== $release) {
+ Log::debug('New version!');
// there is a new FF version!
// has it been released for at least three days?
$today = new Carbon;
$releaseDate = $release->getUpdated();
if ($today->diffInDays($releaseDate, true) > 3) {
+ Log::debug('New version is older than 3 days!');
$monthAndDayFormat = (string)trans('config.month_and_day');
$return = (string)trans(
'firefly.update_new_version_alert',
@@ -97,10 +106,12 @@ trait UpdateTrait
}
if (0 === $versionCheck) {
+ Log::debug('User is running current version.');
// you are running the current version!
$return = (string)trans('firefly.update_current_version_alert', ['version' => $current]);
}
if (1 === $versionCheck && null !== $release) {
+ Log::debug('User is running NEWER version.');
// you are running a newer version!
$return = (string)trans('firefly.update_newer_version_alert', ['your_version' => $current, 'new_version' => $release->getTitle()]);
}
@@ -117,7 +128,9 @@ trait UpdateTrait
*/
public function versionCheck(Release $release = null): int
{
+ Log::debug('Now in versionCheck()');
if (null === $release) {
+ Log::debug('Release is null, return -2.');
return -2;
}
$current = (string)config('firefly.version');
diff --git a/app/Http/Controllers/Account/CreateController.php b/app/Http/Controllers/Account/CreateController.php
index 10da6e9c81..11848ca120 100644
--- a/app/Http/Controllers/Account/CreateController.php
+++ b/app/Http/Controllers/Account/CreateController.php
@@ -78,8 +78,34 @@ class CreateController extends Controller
$roles[$role] = (string)trans('firefly.account_role_' . $role);
}
+ // types of liability:
+ $debt = $this->repository->getAccountTypeByType(AccountType::DEBT);
+ $loan = $this->repository->getAccountTypeByType(AccountType::LOAN);
+ $mortgage = $this->repository->getAccountTypeByType(AccountType::MORTGAGE);
+ $creditCard = $this->repository->getAccountTypeByType(AccountType::CREDITCARD);
+ $liabilityTypes = [
+ $debt->id => (string)trans('firefly.account_type_' . AccountType::DEBT),
+ $loan->id => (string)trans('firefly.account_type_' . AccountType::LOAN),
+ $mortgage->id => (string)trans('firefly.account_type_' . AccountType::MORTGAGE),
+ $creditCard->id => (string)trans('firefly.account_type_' . AccountType::CREDITCARD),
+ ];
+ asort($liabilityTypes);
+
+ // interest calculation periods:
+ $interestPeriods = [
+ 'daily' => (string)trans('firefly.interest_calc_daily'),
+ 'monthly' => (string)trans('firefly.interest_calc_monthly'),
+ 'yearly' => (string)trans('firefly.interest_calc_yearly'),
+ ];
+
// pre fill some data
- $request->session()->flash('preFilled', ['currency_id' => $defaultCurrency->id]);
+ $hasOldInput = null !== $request->old('_token');
+ $request->session()->flash(
+ 'preFilled', [
+ 'currency_id' => $defaultCurrency->id,
+ 'include_net_worth' => $hasOldInput ? (bool)$request->old('include_net_worth') : true,
+ ]
+ );
// put previous url in session if not redirect from store (not "create another").
if (true !== session('accounts.create.fromStore')) {
@@ -87,7 +113,7 @@ class CreateController extends Controller
}
$request->session()->forget('accounts.create.fromStore');
- return view('accounts.create', compact('subTitleIcon', 'what', 'subTitle', 'roles'));
+ return view('accounts.create', compact('subTitleIcon', 'what', 'interestPeriods', 'subTitle', 'roles', 'liabilityTypes'));
}
@@ -100,6 +126,7 @@ class CreateController extends Controller
*/
public function store(AccountFormRequest $request)
{
+
$data = $request->getAccountData();
$account = $this->repository->store($data);
$request->session()->flash('success', (string)trans('firefly.stored_new_account', ['name' => $account->name]));
diff --git a/app/Http/Controllers/Account/DeleteController.php b/app/Http/Controllers/Account/DeleteController.php
index 700a0660c7..3ec08fe917 100644
--- a/app/Http/Controllers/Account/DeleteController.php
+++ b/app/Http/Controllers/Account/DeleteController.php
@@ -69,12 +69,13 @@ class DeleteController extends Controller
$typeName = config('firefly.shortNamesByFullName.' . $account->accountType->type);
$subTitle = (string)trans('firefly.delete_' . $typeName . '_account', ['name' => $account->name]);
$accountList = app('expandedform')->makeSelectListWithEmpty($this->repository->getAccountsByType([$account->accountType->type]));
+ $what = $typeName;
unset($accountList[$account->id]);
// put previous url in session
$this->rememberPreviousUri('accounts.delete.uri');
- return view('accounts.delete', compact('account', 'subTitle', 'accountList'));
+ return view('accounts.delete', compact('account', 'subTitle', 'accountList', 'what'));
}
/**
diff --git a/app/Http/Controllers/Account/EditController.php b/app/Http/Controllers/Account/EditController.php
index 3bab623ed8..2ce4d98aa7 100644
--- a/app/Http/Controllers/Account/EditController.php
+++ b/app/Http/Controllers/Account/EditController.php
@@ -27,6 +27,7 @@ namespace FireflyIII\Http\Controllers\Account;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Requests\AccountFormRequest;
use FireflyIII\Models\Account;
+use FireflyIII\Models\AccountType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use Illuminate\Http\Request;
@@ -85,6 +86,26 @@ class EditController extends Controller
$roles[$role] = (string)trans('firefly.account_role_' . $role);
}
+ // types of liability:
+ $debt = $this->repository->getAccountTypeByType(AccountType::DEBT);
+ $loan = $this->repository->getAccountTypeByType(AccountType::LOAN);
+ $mortgage = $this->repository->getAccountTypeByType(AccountType::MORTGAGE);
+ $creditCard = $this->repository->getAccountTypeByType(AccountType::CREDITCARD);
+ $liabilityTypes = [
+ $debt->id => (string)trans('firefly.account_type_' . AccountType::DEBT),
+ $loan->id => (string)trans('firefly.account_type_' . AccountType::LOAN),
+ $mortgage->id => (string)trans('firefly.account_type_' . AccountType::MORTGAGE),
+ $creditCard->id => (string)trans('firefly.account_type_' . AccountType::CREDITCARD),
+ ];
+ asort($liabilityTypes);
+
+ // interest calculation periods:
+ $interestPeriods = [
+ 'daily' => (string)trans('firefly.interest_calc_daily'),
+ 'monthly' => (string)trans('firefly.interest_calc_monthly'),
+ 'yearly' => (string)trans('firefly.interest_calc_yearly'),
+ ];
+
// put previous url in session if not redirect from store (not "return_to_edit").
if (true !== session('accounts.edit.fromUpdate')) {
$this->rememberPreviousUri('accounts.edit.uri');
@@ -95,6 +116,11 @@ class EditController extends Controller
$openingBalanceDate = $repository->getOpeningBalanceDate($account);
$default = app('amount')->getDefaultCurrency();
$currency = $this->currencyRepos->findNull((int)$repository->getMetaValue($account, 'currency_id'));
+
+ // include this account in net-worth charts?
+ $includeNetWorth = $repository->getMetaValue($account, 'include_net_worth');
+ $includeNetWorth = null === $includeNetWorth ? true : '1' === $includeNetWorth;
+
if (null === $currency) {
$currency = $default;
}
@@ -108,16 +134,25 @@ class EditController extends Controller
'ccMonthlyPaymentDate' => $repository->getMetaValue($account, 'ccMonthlyPaymentDate'),
'BIC' => $repository->getMetaValue($account, 'BIC'),
'openingBalanceDate' => $openingBalanceDate,
+ 'liability_type_id' => $account->account_type_id,
'openingBalance' => $openingBalanceAmount,
'virtualBalance' => $account->virtual_balance,
'currency_id' => $currency->id,
+ 'include_net_worth' => $includeNetWorth,
+ 'interest' => $repository->getMetaValue($account, 'interest'),
+ 'interest_period' => $repository->getMetaValue($account, 'interest_period'),
'notes' => $this->repository->getNoteText($account),
'active' => $hasOldInput ? (bool)$request->old('active') : $account->active,
];
+ if ('liabilities' === $what) {
+ $preFilled['openingBalance'] = bcmul($preFilled['openingBalance'], '-1');
+ }
$request->session()->flash('preFilled', $preFilled);
- return view('accounts.edit', compact('account', 'currency', 'subTitle', 'subTitleIcon', 'what', 'roles', 'preFilled'));
+ return view(
+ 'accounts.edit', compact('account', 'currency', 'subTitle', 'subTitleIcon', 'what', 'roles', 'preFilled', 'liabilityTypes', 'interestPeriods')
+ );
}
diff --git a/app/Http/Controllers/Account/IndexController.php b/app/Http/Controllers/Account/IndexController.php
index 363a818cdc..6c9e8a19eb 100644
--- a/app/Http/Controllers/Account/IndexController.php
+++ b/app/Http/Controllers/Account/IndexController.php
@@ -27,6 +27,7 @@ use Carbon\Carbon;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\Account;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
+use FireflyIII\Support\Http\Controllers\BasicDataSupport;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
@@ -36,6 +37,7 @@ use Illuminate\Pagination\LengthAwarePaginator;
*/
class IndexController extends Controller
{
+ use BasicDataSupport;
/** @var AccountRepositoryInterface The account repository */
private $repository;
@@ -75,9 +77,18 @@ class IndexController extends Controller
$types = config('firefly.accountTypesByIdentifier.' . $what);
$collection = $this->repository->getAccountsByType($types);
$total = $collection->count();
- $page = 0 === (int)$request->get('page') ? 1 : (int)$request->get('page');
- $pageSize = (int)app('preferences')->get('listPageSize', 50)->data;
- $accounts = $collection->slice(($page - 1) * $pageSize, $pageSize);
+
+ // sort collection:
+ $collection = $collection->sortBy(
+ function (Account $account) {
+ return ($account->active ? '0' : '1') . $account->name;
+ }
+ );
+
+
+ $page = 0 === (int)$request->get('page') ? 1 : (int)$request->get('page');
+ $pageSize = (int)app('preferences')->get('listPageSize', 50)->data;
+ $accounts = $collection->slice(($page - 1) * $pageSize, $pageSize);
unset($collection);
/** @var Carbon $start */
$start = clone session('start', Carbon::now()->startOfMonth());
@@ -92,10 +103,13 @@ class IndexController extends Controller
$accounts->each(
function (Account $account) use ($activities, $startBalances, $endBalances) {
- $account->lastActivityDate = $this->isInArray($activities, $account->id);
- $account->startBalance = $this->isInArray($startBalances, $account->id);
- $account->endBalance = $this->isInArray($endBalances, $account->id);
- $account->difference = bcsub($account->endBalance, $account->startBalance);
+ $account->lastActivityDate = $this->isInArray($activities, $account->id);
+ $account->startBalance = $this->isInArray($startBalances, $account->id);
+ $account->endBalance = $this->isInArray($endBalances, $account->id);
+ $account->difference = bcsub($account->endBalance, $account->startBalance);
+ $account->interest = round($this->repository->getMetaValue($account, 'interest'), 6);
+ $account->interestPeriod = (string)trans('firefly.interest_calc_' . $this->repository->getMetaValue($account, 'interest_period'));
+ $account->accountTypeString = (string)trans('firefly.account_type_' . $account->accountType->type);
}
);
@@ -107,23 +121,4 @@ class IndexController extends Controller
}
- /**
- * Find the ID in a given array. Return '0' of not there (amount).
- *
- * @param array $array
- * @param int $entryId
- *
- * @return null|mixed
- */
- protected function isInArray(array $array, int $entryId)
- {
- $result = '0';
- if (isset($array[$entryId])) {
- $result = $array[$entryId];
- }
-
- return $result;
- }
-
-
}
diff --git a/app/Http/Controllers/Account/ReconcileController.php b/app/Http/Controllers/Account/ReconcileController.php
index 99c3ab623c..1283863a73 100644
--- a/app/Http/Controllers/Account/ReconcileController.php
+++ b/app/Http/Controllers/Account/ReconcileController.php
@@ -36,6 +36,7 @@ use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
+use FireflyIII\Support\Http\Controllers\UserNavigation;
use Log;
/**
@@ -45,6 +46,7 @@ use Log;
*/
class ReconcileController extends Controller
{
+ use UserNavigation;
/** @var AccountRepositoryInterface The account repository */
private $accountRepos;
/** @var CurrencyRepositoryInterface The currency repository */
@@ -147,7 +149,7 @@ class ReconcileController extends Controller
/** @var Carbon $start */
$start = clone session('start', app('navigation')->startOfPeriod(new Carbon, $range));
/** @var Carbon $end */
- $end = clone session('end', app('navigation')->endOfPeriod(new Carbon, $range));
+ $end = clone session('end', app('navigation')->endOfPeriod(new Carbon, $range));
}
if (null === $end) {
/** @var Carbon $end */
@@ -370,32 +372,4 @@ class ReconcileController extends Controller
// redirect to previous URL.
return redirect($this->getPreviousUri('reconcile.edit.uri'));
}
-
- /**
- * Redirect user to the original asset account.
- *
- * @param Account $account
- *
- * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
- *
- * @throws FireflyException
- */
- private function redirectToOriginalAccount(Account $account)
- {
- /** @var Transaction $transaction */
- $transaction = $account->transactions()->first();
- if (null === $transaction) {
- throw new FireflyException(sprintf('Expected a transaction. Account #%d has none. BEEP, error.', $account->id)); // @codeCoverageIgnore
- }
-
- $journal = $transaction->transactionJournal;
- /** @var Transaction $opposingTransaction */
- $opposingTransaction = $journal->transactions()->where('transactions.id', '!=', $transaction->id)->first();
-
- if (null === $opposingTransaction) {
- throw new FireflyException('Expected an opposing transaction. This account has none. BEEP, error.'); // @codeCoverageIgnore
- }
-
- return redirect(route('accounts.show', [$opposingTransaction->account_id]));
- }
}
diff --git a/app/Http/Controllers/Account/ShowController.php b/app/Http/Controllers/Account/ShowController.php
index a5273323c4..6032d1fb78 100644
--- a/app/Http/Controllers/Account/ShowController.php
+++ b/app/Http/Controllers/Account/ShowController.php
@@ -25,15 +25,14 @@ namespace FireflyIII\Http\Controllers\Account;
use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException;
-use FireflyIII\Helpers\Collector\JournalCollectorInterface;
+use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
-use FireflyIII\Models\Transaction;
-use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
-use FireflyIII\Support\CacheProperties;
+use FireflyIII\Support\Http\Controllers\PeriodOverview;
+use FireflyIII\Support\Http\Controllers\UserNavigation;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use View;
@@ -45,6 +44,8 @@ use View;
*/
class ShowController extends Controller
{
+ use UserNavigation, PeriodOverview;
+
/** @var CurrencyRepositoryInterface The currency repository */
private $currencyRepos;
/** @var AccountRepositoryInterface The account repository */
@@ -90,10 +91,15 @@ class ShowController extends Controller
if (AccountType::INITIAL_BALANCE === $account->accountType->type) {
return $this->redirectToOriginalAccount($account);
}
+ // a basic thing to determin if this account is a liability:
+ if ($this->repository->isLiability($account)) {
+ return redirect(route('accounts.show.all', [$account->id]));
+ }
+
/** @var Carbon $start */
$start = $start ?? session('start');
/** @var Carbon $end */
- $end = $end ?? session('end');
+ $end = $end ?? session('end');
if ($end < $start) {
throw new FireflyException('End is after start!'); // @codeCoverageIgnore
}
@@ -112,18 +118,22 @@ class ShowController extends Controller
$fEnd = $end->formatLocalized($this->monthAndDayFormat);
$subTitle = (string)trans('firefly.journals_in_period_for_account', ['name' => $account->name, 'start' => $fStart, 'end' => $fEnd]);
$chartUri = route('chart.account.period', [$account->id, $start->format('Y-m-d'), $end->format('Y-m-d')]);
- $periods = $this->getPeriodOverview($account, $end);
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ $periods = $this->getAccountPeriodOverview($account, $end);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAccounts(new Collection([$account]))->setLimit($pageSize)->setPage($page);
$collector->setRange($start, $end);
- $transactions = $collector->getPaginatedJournals();
+ $transactions = $collector->getPaginatedTransactions();
$transactions->setPath(route('accounts.show', [$account->id, $start->format('Y-m-d'), $end->format('Y-m-d')]));
$showAll = false;
+
return view(
'accounts.show',
- compact('account', 'showAll', 'what', 'currency', 'today', 'periods', 'subTitleIcon', 'transactions', 'subTitle', 'start', 'end', 'chartUri')
+ compact(
+ 'account', 'showAll', 'what', 'currency', 'today', 'periods', 'subTitleIcon', 'transactions', 'subTitle', 'start', 'end',
+ 'chartUri'
+ )
);
}
@@ -143,9 +153,10 @@ class ShowController extends Controller
if (AccountType::INITIAL_BALANCE === $account->accountType->type) {
return $this->redirectToOriginalAccount($account); // @codeCoverageIgnore
}
+ $isLiability = $this->repository->isLiability($account);
$end = new Carbon;
$today = new Carbon;
- $start = $this->repository->oldestJournalDate($account);
+ $start = $this->repository->oldestJournalDate($account) ?? Carbon::now()->startOfMonth();
$subTitleIcon = config('firefly.subIconsByIdentifier.' . $account->accountType->type);
$page = (int)$request->get('page');
$pageSize = (int)app('preferences')->get('listPageSize', 50)->data;
@@ -156,118 +167,18 @@ class ShowController extends Controller
}
$subTitle = (string)trans('firefly.all_journals_for_account', ['name' => $account->name]);
$periods = new Collection;
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAccounts(new Collection([$account]))->setLimit($pageSize)->setPage($page);
- $transactions = $collector->getPaginatedJournals();
+ $transactions = $collector->getPaginatedTransactions();
$transactions->setPath(route('accounts.show.all', [$account->id]));
$chartUri = route('chart.account.period', [$account->id, $start->format('Y-m-d'), $end->format('Y-m-d')]);
$showAll = true;
return view(
'accounts.show',
- compact('account', 'showAll', 'currency', 'today', 'chartUri', 'periods', 'subTitleIcon', 'transactions', 'subTitle', 'start', 'end')
+ compact('account', 'showAll','isLiability', 'currency', 'today', 'chartUri', 'periods', 'subTitleIcon', 'transactions', 'subTitle', 'start', 'end')
);
}
-
-
- /** @noinspection MoreThanThreeArgumentsInspection */
-
- /**
- * This method returns "period entries", so nov-2015, dec-2015, etc etc (this depends on the users session range)
- * and for each period, the amount of money spent and earned. This is a complex operation which is cached for
- * performance reasons.
- *
- * @param Account $account the account involved
- *
- * @param Carbon|null $date
- *
- * @return Collection
- *
- * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
- */
- private function getPeriodOverview(Account $account, ?Carbon $date): Collection
- {
- $range = app('preferences')->get('viewRange', '1M')->data;
- $start = $this->repository->oldestJournalDate($account);
- $end = $date ?? new Carbon;
- if ($end < $start) {
- [$start, $end] = [$end, $start]; // @codeCoverageIgnore
- }
-
- // properties for cache
- $cache = new CacheProperties;
- $cache->addProperty($start);
- $cache->addProperty($end);
- $cache->addProperty('account-show-period-entries');
- $cache->addProperty($account->id);
- if ($cache->has()) {
- return $cache->get(); // @codeCoverageIgnore
- }
- /** @var array $dates */
- $dates = app('navigation')->blockPeriods($start, $end, $range);
- $entries = new Collection;
- // loop dates
- foreach ($dates as $currentDate) {
-
- // try a collector for income:
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
- $collector->setAccounts(new Collection([$account]))->setRange($currentDate['start'], $currentDate['end'])->setTypes([TransactionType::DEPOSIT])
- ->withOpposingAccount();
- $earned = (string)$collector->getJournals()->sum('transaction_amount');
-
- // try a collector for expenses:
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
- $collector->setAccounts(new Collection([$account]))->setRange($currentDate['start'], $currentDate['end'])->setTypes([TransactionType::WITHDRAWAL])
- ->withOpposingAccount();
- $spent = (string)$collector->getJournals()->sum('transaction_amount');
-
- $dateName = app('navigation')->periodShow($currentDate['start'], $currentDate['period']);
- /** @noinspection PhpUndefinedMethodInspection */
- $entries->push(
- [
- 'name' => $dateName,
- 'spent' => $spent,
- 'earned' => $earned,
- 'start' => $currentDate['start']->format('Y-m-d'),
- 'end' => $currentDate['end']->format('Y-m-d'),
- ]
- );
- }
-
- $cache->store($entries);
-
- return $entries;
- }
-
- /**
- * Redirect to the original account.
- *
- * @param Account $account
- *
- * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
- *
- * @throws FireflyException
- */
- private function redirectToOriginalAccount(Account $account)
- {
- /** @var Transaction $transaction */
- $transaction = $account->transactions()->first();
- if (null === $transaction) {
- throw new FireflyException('Expected a transaction. This account has none. BEEP, error.');
- }
-
- $journal = $transaction->transactionJournal;
- /** @var Transaction $opposingTransaction */
- $opposingTransaction = $journal->transactions()->where('transactions.id', '!=', $transaction->id)->first();
-
- if (null === $opposingTransaction) {
- throw new FireflyException('Expected an opposing transaction. This account has none. BEEP, error.'); // @codeCoverageIgnore
- }
-
- return redirect(route('accounts.show', [$opposingTransaction->account_id]));
- }
}
diff --git a/app/Http/Controllers/Admin/LinkController.php b/app/Http/Controllers/Admin/LinkController.php
index fd877c1e5d..b16db6a4de 100644
--- a/app/Http/Controllers/Admin/LinkController.php
+++ b/app/Http/Controllers/Admin/LinkController.php
@@ -55,6 +55,7 @@ class LinkController extends Controller
/**
* Make a new link form.
+ *
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function create()
diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php
index 273cc4c18e..2ece879132 100644
--- a/app/Http/Controllers/Auth/RegisterController.php
+++ b/app/Http/Controllers/Auth/RegisterController.php
@@ -25,12 +25,12 @@ namespace FireflyIII\Http\Controllers\Auth;
use FireflyConfig;
use FireflyIII\Http\Controllers\Controller;
+use FireflyIII\Support\Http\Controllers\CreateStuff;
+use FireflyIII\Support\Http\Controllers\RequestInformation;
use FireflyIII\User;
use Illuminate\Auth\Events\Registered;
-use Illuminate\Contracts\Validation\Validator as ValidatorContract;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Http\Request;
-use Illuminate\Support\Facades\Validator;
/**
* Class RegisterController
@@ -43,7 +43,7 @@ use Illuminate\Support\Facades\Validator;
*/
class RegisterController extends Controller
{
- use RegistersUsers;
+ use RegistersUsers, RequestInformation, CreateStuff;
/**
* Where to redirect users after registration.
@@ -82,7 +82,7 @@ class RegisterController extends Controller
/** @noinspection PhpUndefinedMethodInspection */
$this->validator($request->all())->validate();
- event(new Registered($user = $this->create($request->all())));
+ event(new Registered($user = $this->createUser($request->all())));
$this->guard()->login($user);
@@ -119,38 +119,4 @@ class RegisterController extends Controller
return view('auth.register', compact('isDemoSite', 'email'));
}
- /**
- * Create a new user instance after a valid registration.
- *
- * @param array $data
- *
- * @return \FireflyIII\User
- */
- protected function create(array $data): User
- {
- return User::create(
- [
- 'email' => $data['email'],
- 'password' => bcrypt($data['password']),
- ]
- );
- }
-
- /**
- * Get a validator for an incoming registration request.
- *
- * @param array $data
- *
- * @return ValidatorContract
- */
- protected function validator(array $data): ValidatorContract
- {
- return Validator::make(
- $data,
- [
- 'email' => 'required|string|email|max:255|unique:users',
- 'password' => 'required|string|secure_password|confirmed',
- ]
- );
- }
}
diff --git a/app/Http/Controllers/BillController.php b/app/Http/Controllers/BillController.php
index 5e3e12f362..2990b32e35 100644
--- a/app/Http/Controllers/BillController.php
+++ b/app/Http/Controllers/BillController.php
@@ -24,11 +24,10 @@ namespace FireflyIII\Http\Controllers;
use Carbon\Carbon;
use FireflyIII\Helpers\Attachments\AttachmentHelperInterface;
-use FireflyIII\Helpers\Collector\JournalCollectorInterface;
+use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Http\Requests\BillFormRequest;
use FireflyIII\Models\Bill;
use FireflyIII\Repositories\Bill\BillRepositoryInterface;
-use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface;
use FireflyIII\TransactionRules\TransactionMatcher;
use FireflyIII\Transformers\BillTransformer;
use Illuminate\Http\RedirectResponse;
@@ -51,8 +50,6 @@ class BillController extends Controller
private $attachments;
/** @var BillRepositoryInterface Bill repository */
private $billRepository;
- /** @var RuleGroupRepositoryInterface Rule group repository */
- private $ruleGroupRepos;
/**
* BillController constructor.
@@ -72,7 +69,6 @@ class BillController extends Controller
app('view')->share('mainTitleIcon', 'fa-calendar-o');
$this->attachments = app(AttachmentHelperInterface::class);
$this->billRepository = app(BillRepositoryInterface::class);
- $this->ruleGroupRepos = app(RuleGroupRepositoryInterface::class);
return $next($request);
}
@@ -204,7 +200,10 @@ class BillController extends Controller
/** @var Collection $bills */
$bills = $paginator->getCollection()->map(
function (Bill $bill) use ($transformer) {
- return $transformer->transform($bill);
+ $return = $transformer->transform($bill);
+ $return['currency'] = $bill->transactionCurrency;
+
+ return $return;
}
);
$bills = $bills->sortBy(
@@ -276,10 +275,10 @@ class BillController extends Controller
public function show(Request $request, Bill $bill)
{
// add info about rules:
- $rules = $this->billRepository->getRulesForBill($bill);
- $subTitle = $bill->name;
+ $rules = $this->billRepository->getRulesForBill($bill);
+ $subTitle = $bill->name;
/** @var Carbon $start */
- $start = session('start');
+ $start = session('start');
/** @var Carbon $end */
$end = session('end');
$year = $start->year;
@@ -295,15 +294,16 @@ class BillController extends Controller
$parameters = new ParameterBag();
$parameters->set('start', $start);
$parameters->set('end', $end);
- $resource = new Item($bill, new BillTransformer($parameters), 'bill');
- $object = $manager->createData($resource)->toArray();
+ $resource = new Item($bill, new BillTransformer($parameters), 'bill');
+ $object = $manager->createData($resource)->toArray();
+ $object['data']['currency'] = $bill->transactionCurrency;
// use collector:
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAllAssetAccounts()->setBills(new Collection([$bill]))->setLimit($pageSize)->setPage($page)->withBudgetInformation()
->withCategoryInformation();
- $transactions = $collector->getPaginatedJournals();
+ $transactions = $collector->getPaginatedTransactions();
$transactions->setPath(route('bills.show', [$bill->id]));
@@ -342,30 +342,7 @@ class BillController extends Controller
$request->session()->flash('info', $this->attachments->getMessages()->get('attachments')); // @codeCoverageIgnore
}
- // do return to original bill form?
- $return = 'false';
- if (1 === (int)$request->get('create_another')) {
- $return = 'true';
- }
-
- $group = null;
- // find first rule group, or create one:
- $count = $this->ruleGroupRepos->count();
- if (0 === $count) {
- $data = [
- 'title' => (string)trans('firefly.rulegroup_for_bills_title'),
- 'description' => (string)trans('firefly.rulegroup_for_bills_description'),
- ];
- $group = $this->ruleGroupRepos->store($data);
- }
- if ($count > 0) {
- $group = $this->ruleGroupRepos->getActiveGroups($bill->user)->first();
- }
-
- // redirect to page that will create a new rule.
- $params = http_build_query(['fromBill' => $bill->id, 'return' => $return]);
-
- return redirect(route('rules.create', [$group->id]) . '?' . $params);
+ return redirect(route('rules.create-from-bill', [$bill->id]));
}
/**
diff --git a/app/Http/Controllers/Budget/AmountController.php b/app/Http/Controllers/Budget/AmountController.php
index 3753265b1d..f6d7a52ba0 100644
--- a/app/Http/Controllers/Budget/AmountController.php
+++ b/app/Http/Controllers/Budget/AmountController.php
@@ -25,7 +25,7 @@ namespace FireflyIII\Http\Controllers\Budget;
use Carbon\Carbon;
-use FireflyIII\Helpers\Collector\JournalCollectorInterface;
+use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Requests\BudgetIncomeRequest;
use FireflyIII\Models\Budget;
@@ -36,6 +36,7 @@ use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
+use Log;
/**
* Class AmountController
@@ -80,24 +81,34 @@ class AmountController extends Controller
*/
public function amount(Request $request, BudgetRepositoryInterface $repository, Budget $budget): JsonResponse
{
- $amount = (string)$request->get('amount');
- $start = Carbon::createFromFormat('Y-m-d', $request->get('start'));
- $end = Carbon::createFromFormat('Y-m-d', $request->get('end'));
- $budgetLimit = $this->repository->updateLimitAmount($budget, $start, $end, $amount);
- $spent = $repository->spentInPeriod(new Collection([$budget]), new Collection, $start, $end);
- $currency = app('amount')->getDefaultCurrency();
- $left = app('amount')->formatAnything($currency, bcadd($amount, $spent), true);
- $largeDiff = false;
- $warnText = '';
- $leftPerDay = null;
- $periodLength = $start->diffInDays($end);
- $dayDifference = $this->getDayDifference($start, $end);
+ // grab vars from URI
+ $amount = (string)$request->get('amount');
+ $start = Carbon::createFromFormat('Y-m-d', $request->get('start'));
+ $end = Carbon::createFromFormat('Y-m-d', $request->get('end'));
+
+ // grab other useful vars
+ $currency = app('amount')->getDefaultCurrency();
+ $activeDaysLeft = $this->activeDaysLeft($start, $end);
+ $periodLength = $start->diffInDays($end) + 1; // absolute period length.
+
+ // update limit amount:
+ $budgetLimit = $this->repository->updateLimitAmount($budget, $start, $end, $amount);
+
+ // calculate what the user has spent in current period.
+ $spent = $repository->spentInPeriod(new Collection([$budget]), new Collection, $start, $end);
+
+ // given the new budget, this is what they have left (and left per day?)
+ $left = app('amount')->formatAnything($currency, bcadd($amount, $spent), true);
+ $leftPerDay = null; //
// If the user budgets ANY amount per day for this budget (anything but zero) Firefly III calculates how much he could spend per day.
if (1 === bccomp(bcadd($amount, $spent), '0')) {
- $leftPerDay = app('amount')->formatAnything($currency, bcdiv(bcadd($amount, $spent), (string)$dayDifference), true);
+ $leftPerDay = app('amount')->formatAnything($currency, bcdiv(bcadd($amount, $spent), (string)$activeDaysLeft), true);
}
+ $largeDiff = false;
+ $warnText = '';
+
// Get the average amount of money the user budgets for this budget. And calculate the same for the current amount.
// If the difference is very large, give the user a notification.
$average = $this->repository->budgetedPerDay($budget);
@@ -116,8 +127,17 @@ class AmountController extends Controller
app('preferences')->mark();
return response()->json(
- ['left' => $left, 'name' => $budget->name, 'limit' => $budgetLimit ? $budgetLimit->id : 0, 'amount' => $amount, 'current' => $current,
- 'average' => $average, 'large_diff' => $largeDiff, 'left_per_day' => $leftPerDay, 'warn_text' => $warnText,]
+ [
+ 'left' => $left,
+ 'name' => $budget->name,
+ 'limit' => $budgetLimit ? $budgetLimit->id : 0,
+ 'amount' => $amount,
+ 'current' => $current,
+ 'average' => $average,
+ 'large_diff' => $largeDiff,
+ 'left_per_day' => $leftPerDay,
+ 'warn_text' => $warnText,
+ ]
);
}
@@ -141,27 +161,36 @@ class AmountController extends Controller
$average = $this->repository->getAverageAvailable($start, $end);
$available = bcmul($average, (string)$daysInPeriod);
+ Log::debug(sprintf('Average is %s, so total available is %s because days is %d.', $average, $available, $daysInPeriod));
+
// amount earned in this period:
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($searchBegin, $searchEnd)->setTypes([TransactionType::DEPOSIT])->withOpposingAccount();
- $earned = (string)$collector->getJournals()->sum('transaction_amount');
+ $earned = (string)$collector->getTransactions()->sum('transaction_amount');
// Total amount earned divided by the number of days in the whole search period is the average amount earned per day.
// This is multiplied by the number of days in the current period, showing you the average.
$earnedAverage = bcmul(bcdiv($earned, (string)$daysInSearchPeriod), (string)$daysInPeriod);
+ Log::debug(sprintf('Earned is %s, earned average is %s', $earned, $earnedAverage));
+
// amount spent in period
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($searchBegin, $searchEnd)->setTypes([TransactionType::WITHDRAWAL])->withOpposingAccount();
- $spent = (string)$collector->getJournals()->sum('transaction_amount');
+ $spent = (string)$collector->getTransactions()->sum('transaction_amount');
$spentAverage = app('steam')->positive(bcmul(bcdiv($spent, (string)$daysInSearchPeriod), (string)$daysInPeriod));
+ Log::debug(sprintf('Spent is %s, spent average is %s', $earned, $earnedAverage));
+
// the default suggestion is the money the user has spent, on average, over this period.
$suggested = $spentAverage;
+ Log::debug(sprintf('Suggested is now %s (spent average)',$suggested));
+
// if the user makes less per period, suggest that amount instead.
if (1 === bccomp($spentAverage, $earnedAverage)) {
+ Log::debug(sprintf('Because earned average (%s) is less than spent average (%s) will suggest earned average instead.', $earnedAverage, $spentAverage));
$suggested = $earnedAverage;
}
diff --git a/app/Http/Controllers/Budget/IndexController.php b/app/Http/Controllers/Budget/IndexController.php
index f23cd4f159..78bd66d719 100644
--- a/app/Http/Controllers/Budget/IndexController.php
+++ b/app/Http/Controllers/Budget/IndexController.php
@@ -25,13 +25,11 @@ namespace FireflyIII\Http\Controllers\Budget;
use Carbon\Carbon;
-use Exception;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Support\Http\Controllers\DateCalculation;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
-use Log;
/**
*
@@ -58,6 +56,7 @@ class IndexController extends Controller
app('view')->share('title', (string)trans('firefly.budgets'));
app('view')->share('mainTitleIcon', 'fa-tasks');
$this->repository = app(BudgetRepositoryInterface::class);
+ $this->repository->cleanupBudgets();
return $next($request);
}
@@ -69,74 +68,67 @@ class IndexController extends Controller
* Show all budgets.
*
* @param Request $request
- * @param string|null $moment
+ *
+ * @param Carbon|null $start
+ * @param Carbon|null $end
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
- public function index(Request $request, string $moment = null)
+ public function index(Request $request, Carbon $start = null, Carbon $end = null)
{
- /** @var string $range */
- $range = app('preferences')->get('viewRange', '1M')->data;
- /** @var Carbon $start */
- $start = session('start', new Carbon);
- /** @var Carbon $end */
- $end = session('end', new Carbon);
- $page = 0 === (int)$request->get('page') ? 1 : (int)$request->get('page');
- $pageSize = (int)app('preferences')->get('listPageSize', 50)->data;
- $moment = $moment ?? '';
-
- // make date if the data is given.
- if ('' !== (string)$moment) {
- try {
- $start = new Carbon($moment);
- /** @var Carbon $end */
- $end = app('navigation')->endOfPeriod($start, $range);
- } catch (Exception $e) {
- // start and end are already defined.
- Log::debug(sprintf('start and end are already defined: %s', $e->getMessage()));
- }
- }
-
- // if today is between start and end, use the diff in days between end and today (days left)
- // otherwise, use diff between start and end.
- $dayDifference = $this->getDayDifference($start, $end);
+ // collect some basic vars:
+ $range = app('preferences')->get('viewRange', '1M')->data;
+ $start = $start ?? session('start', Carbon::now()->startOfMonth());
+ $end = $end ?? app('navigation')->endOfPeriod($start, $range);
+ $page = 0 === (int)$request->get('page') ? 1 : (int)$request->get('page');
+ $pageSize = (int)app('preferences')->get('listPageSize', 50)->data;
+ $defaultCurrency = app('amount')->getDefaultCurrency();
+ // make the next and previous period, and calculate the periods used for period navigation
$next = clone $end;
$next->addDay();
$prev = clone $start;
$prev->subDay();
- $prev = app('navigation')->startOfPeriod($prev, $range);
- $this->repository->cleanupBudgets();
- $daysPassed = $this->getDaysPassedInPeriod($start, $end);
- $allBudgets = $this->repository->getActiveBudgets();
- $total = $allBudgets->count();
- $budgets = $allBudgets->slice(($page - 1) * $pageSize, $pageSize);
- $inactive = $this->repository->getInactiveBudgets();
- $periodStart = $start->formatLocalized($this->monthAndDayFormat);
- $periodEnd = $end->formatLocalized($this->monthAndDayFormat);
- $budgetInformation = $this->repository->collectBudgetInformation($allBudgets, $start, $end);
- $defaultCurrency = app('amount')->getDefaultCurrency();
- $available = $this->repository->getAvailableBudget($defaultCurrency, $start, $end);
- $spent = array_sum(array_column($budgetInformation, 'spent'));
- $budgeted = array_sum(array_column($budgetInformation, 'budgeted'));
- $previousLoop = $this->getPreviousPeriods($start, $range);
- $nextLoop = $this->getNextPeriods($end, $range);
-
- // paginate budgets
- $budgets = new LengthAwarePaginator($budgets, $total, $pageSize, $page);
- $budgets->setPath(route('budgets.index'));
- // display info
+ $prev = app('navigation')->startOfPeriod($prev, $range);
+ $previousLoop = $this->getPreviousPeriods($start, $range);
+ $nextLoop = $this->getNextPeriods($start, $range);
$currentMonth = app('navigation')->periodShow($start, $range);
$nextText = app('navigation')->periodShow($next, $range);
$prevText = app('navigation')->periodShow($prev, $range);
+ // number of days for consistent budgeting.
+ $activeDaysPassed = $this->activeDaysPassed($start, $end); // see method description.
+ $activeDaysLeft = $this->activeDaysLeft($start, $end); // see method description.
+
+ // get all budgets, and paginate them into $budgets.
+ $collection = $this->repository->getActiveBudgets();
+ $total = $collection->count();
+ $budgets = $collection->slice(($page - 1) * $pageSize, $pageSize);
+
+ // get all inactive budgets, and simply list them:
+ $inactive = $this->repository->getInactiveBudgets();
+
+ // collect budget info to fill bars and so on.
+ $budgetInformation = $this->repository->collectBudgetInformation($collection, $start, $end);
+
+ // to display available budget:
+ $available = $this->repository->getAvailableBudget($defaultCurrency, $start, $end);
+ $spent = array_sum(array_column($budgetInformation, 'spent'));
+ $budgeted = array_sum(array_column($budgetInformation, 'budgeted'));
+
+
+ // paginate budgets
+ $paginator = new LengthAwarePaginator($budgets, $total, $pageSize, $page);
+ $paginator->setPath(route('budgets.index'));
+
return view(
'budgets.index', compact(
- 'available', 'currentMonth', 'next', 'nextText', 'prev', 'allBudgets', 'prevText', 'periodStart', 'periodEnd', 'dayDifference',
- 'page',
- 'budgetInformation', 'daysPassed',
+ 'available', 'currentMonth', 'next', 'nextText', 'prev', 'paginator',
+ 'prevText',
+ 'page', 'activeDaysPassed', 'activeDaysLeft',
+ 'budgetInformation',
'inactive', 'budgets', 'spent', 'budgeted', 'previousLoop', 'nextLoop', 'start', 'end'
)
);
diff --git a/app/Http/Controllers/Budget/ShowController.php b/app/Http/Controllers/Budget/ShowController.php
index 0b48280422..7b0e62bffd 100644
--- a/app/Http/Controllers/Budget/ShowController.php
+++ b/app/Http/Controllers/Budget/ShowController.php
@@ -26,7 +26,7 @@ namespace FireflyIII\Http\Controllers\Budget;
use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException;
-use FireflyIII\Helpers\Collector\JournalCollectorInterface;
+use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\Budget;
use FireflyIII\Models\BudgetLimit;
@@ -34,6 +34,7 @@ use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Support\CacheProperties;
+use FireflyIII\Support\Http\Controllers\PeriodOverview;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
@@ -43,6 +44,7 @@ use Illuminate\Support\Collection;
*/
class ShowController extends Controller
{
+ use PeriodOverview;
/** @var BudgetRepositoryInterface The budget repository */
private $repository;
@@ -86,15 +88,15 @@ class ShowController extends Controller
'firefly.without_budget_between',
['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)]
);
- $periods = $this->getPeriodOverview();
+ $periods = $this->getBudgetPeriodOverview();
$page = (int)$request->get('page');
$pageSize = (int)app('preferences')->get('listPageSize', 50)->data;
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setLimit($pageSize)->setPage($page)
->withoutBudget()->withOpposingAccount();
- $transactions = $collector->getPaginatedJournals();
+ $transactions = $collector->getPaginatedTransactions();
$transactions->setPath(route('budgets.no-budget'));
return view('budgets.no-budget', compact('transactions', 'subTitle', 'periods', 'start', 'end'));
@@ -118,16 +120,15 @@ class ShowController extends Controller
$end = new Carbon;
$page = (int)$request->get('page');
$pageSize = (int)app('preferences')->get('listPageSize', 50)->data;
- $moment = 'all';
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setLimit($pageSize)->setPage($page)
->withoutBudget()->withOpposingAccount();
- $transactions = $collector->getPaginatedJournals();
+ $transactions = $collector->getPaginatedTransactions();
$transactions->setPath(route('budgets.no-budget'));
- return view('budgets.no-budget', compact('transactions', 'subTitle', 'moment', 'start', 'end'));
+ return view('budgets.no-budget', compact('transactions', 'subTitle', 'start', 'end'));
}
@@ -142,7 +143,7 @@ class ShowController extends Controller
public function show(Request $request, Budget $budget)
{
/** @var Carbon $start */
- $start = session('first', Carbon::create()->startOfYear());
+ $start = session('first', Carbon::now()->startOfYear());
$end = new Carbon;
$page = (int)$request->get('page');
$pageSize = (int)app('preferences')->get('listPageSize', 50)->data;
@@ -150,10 +151,10 @@ class ShowController extends Controller
$repetition = null;
// collector:
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($start, $end)->setBudget($budget)->setLimit($pageSize)->setPage($page)->withBudgetInformation();
- $transactions = $collector->getPaginatedJournals();
+ $transactions = $collector->getPaginatedTransactions();
$transactions->setPath(route('budgets.show', [$budget->id]));
$subTitle = (string)trans('firefly.all_journals_for_budget', ['name' => $budget->name]);
@@ -189,14 +190,14 @@ class ShowController extends Controller
);
// collector:
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($budgetLimit->start_date, $budgetLimit->end_date)
->setBudget($budget)->setLimit($pageSize)->setPage($page)->withBudgetInformation();
- $transactions = $collector->getPaginatedJournals();
+ $transactions = $collector->getPaginatedTransactions();
$transactions->setPath(route('budgets.show', [$budget->id, $budgetLimit->id]));
/** @var Carbon $start */
- $start = session('first', Carbon::create()->startOfYear());
+ $start = session('first', Carbon::now()->startOfYear());
$end = new Carbon;
$limits = $this->getLimits($budget, $start, $end);
@@ -212,7 +213,7 @@ class ShowController extends Controller
*
* @return Collection
*/
- private function getLimits(Budget $budget, Carbon $start, Carbon $end): Collection
+ protected function getLimits(Budget $budget, Carbon $start, Carbon $end): Collection // get data + augment with info
{
// properties for cache
$cache = new CacheProperties;
@@ -237,57 +238,4 @@ class ShowController extends Controller
return $set;
}
-
-
- /**
- * Gets period overview used for budgets.
- *
- * @return Collection
- *
- * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
- */
- private function getPeriodOverview(): Collection
- {
- /** @var JournalRepositoryInterface $repository */
- $repository = app(JournalRepositoryInterface::class);
- $first = $repository->firstNull();
- $start = null === $first ? new Carbon : $first->date;
- $range = app('preferences')->get('viewRange', '1M')->data;
- $start = app('navigation')->startOfPeriod($start, $range);
- $end = app('navigation')->endOfX(new Carbon, $range, null);
- $entries = new Collection;
- $cache = new CacheProperties;
- $cache->addProperty($start);
- $cache->addProperty($end);
- $cache->addProperty('no-budget-period-entries');
-
- if ($cache->has()) {
- return $cache->get(); // @codeCoverageIgnore
- }
- $dates = app('navigation')->blockPeriods($start, $end, $range);
- foreach ($dates as $date) {
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
- $collector->setAllAssetAccounts()->setRange($date['start'], $date['end'])->withoutBudget()->withOpposingAccount()->setTypes(
- [TransactionType::WITHDRAWAL]
- );
- $set = $collector->getJournals();
- $sum = (string)($set->sum('transaction_amount') ?? '0');
- $journals = $set->count();
- /** @noinspection PhpUndefinedMethodInspection */
- $dateStr = $date['end']->format('Y-m-d');
- $dateName = app('navigation')->periodShow($date['end'], $date['period']);
- $entries->push(
- ['string' => $dateStr, 'name' => $dateName, 'count' => $journals, 'sum' => $sum, 'date' => clone $date['end'],
- 'start' => $date['start'],
- 'end' => $date['end'],
-
- ]
- );
- }
- $cache->store($entries);
-
- return $entries;
- }
-
}
diff --git a/app/Http/Controllers/Category/NoCategoryController.php b/app/Http/Controllers/Category/NoCategoryController.php
index 59b03f4a1f..1f438869e2 100644
--- a/app/Http/Controllers/Category/NoCategoryController.php
+++ b/app/Http/Controllers/Category/NoCategoryController.php
@@ -25,11 +25,10 @@ namespace FireflyIII\Http\Controllers\Category;
use Carbon\Carbon;
-use FireflyIII\Helpers\Collector\JournalCollectorInterface;
+use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Helpers\Filter\InternalTransferFilter;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\TransactionType;
-
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Support\CacheProperties;
use Illuminate\Http\Request;
@@ -80,7 +79,6 @@ class NoCategoryController extends Controller
$start = $start ?? session('start');
/** @var Carbon $end */
$end = $end ?? session('end');
- $moment = '';
$page = (int)$request->get('page');
$pageSize = (int)app('preferences')->get('listPageSize', 50)->data;
$subTitle = trans(
@@ -92,30 +90,28 @@ class NoCategoryController extends Controller
Log::debug(sprintf('Start for noCategory() is %s', $start->format('Y-m-d')));
Log::debug(sprintf('End for noCategory() is %s', $end->format('Y-m-d')));
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($start, $end)->setLimit($pageSize)->setPage($page)->withoutCategory()->withOpposingAccount()
->setTypes([TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::TRANSFER]);
$collector->removeFilter(InternalTransferFilter::class);
- $transactions = $collector->getPaginatedJournals();
+ $transactions = $collector->getPaginatedTransactions();
$transactions->setPath(route('categories.no-category'));
- return view('categories.no-category', compact('transactions', 'subTitle', 'moment', 'periods', 'start', 'end'));
+ return view('categories.no-category', compact('transactions', 'subTitle', 'periods', 'start', 'end'));
}
/**
* Show all transactions without a category.
*
- * @param Request $request
- * @param string|null $moment
+ * @param Request $request
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
- public function showAll(Request $request, string $moment = null)
+ public function showAll(Request $request)
{
// default values:
- $moment = $moment ?? '';
$start = null;
$end = null;
$periods = new Collection;
@@ -129,15 +125,15 @@ class NoCategoryController extends Controller
Log::debug(sprintf('Start for noCategory() is %s', $start->format('Y-m-d')));
Log::debug(sprintf('End for noCategory() is %s', $end->format('Y-m-d')));
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($start, $end)->setLimit($pageSize)->setPage($page)->withoutCategory()->withOpposingAccount()
->setTypes([TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::TRANSFER]);
$collector->removeFilter(InternalTransferFilter::class);
- $transactions = $collector->getPaginatedJournals();
+ $transactions = $collector->getPaginatedTransactions();
$transactions->setPath(route('categories.no-category'));
- return view('categories.no-category', compact('transactions', 'subTitle', 'moment', 'periods', 'start', 'end'));
+ return view('categories.no-category', compact('transactions', 'subTitle', 'periods', 'start', 'end'));
}
@@ -149,7 +145,7 @@ class NoCategoryController extends Controller
* @return Collection
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
*/
- private function getNoCategoryPeriodOverview(Carbon $theDate): Collection
+ protected function getNoCategoryPeriodOverview(Carbon $theDate): Collection // period overview method.
{
Log::debug(sprintf('Now in getNoCategoryPeriodOverview(%s)', $theDate->format('Y-m-d')));
$range = app('preferences')->get('viewRange', '1M')->data;
@@ -176,36 +172,36 @@ class NoCategoryController extends Controller
foreach ($dates as $date) {
// count journals without category in this period:
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($date['start'], $date['end'])->withoutCategory()
->withOpposingAccount()->setTypes([TransactionType::WITHDRAWAL, TransactionType::DEPOSIT, TransactionType::TRANSFER]);
$collector->removeFilter(InternalTransferFilter::class);
- $count = $collector->getJournals()->count();
+ $count = $collector->getTransactions()->count();
// amount transferred
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($date['start'], $date['end'])->withoutCategory()
->withOpposingAccount()->setTypes([TransactionType::TRANSFER]);
$collector->removeFilter(InternalTransferFilter::class);
- $transferred = app('steam')->positive((string)$collector->getJournals()->sum('transaction_amount'));
+ $transferred = app('steam')->positive((string)$collector->getTransactions()->sum('transaction_amount'));
// amount spent
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($date['start'], $date['end'])->withoutCategory()->withOpposingAccount()->setTypes(
[TransactionType::WITHDRAWAL]
);
- $spent = $collector->getJournals()->sum('transaction_amount');
+ $spent = $collector->getTransactions()->sum('transaction_amount');
// amount earned
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($date['start'], $date['end'])->withoutCategory()->withOpposingAccount()->setTypes(
[TransactionType::DEPOSIT]
);
- $earned = $collector->getJournals()->sum('transaction_amount');
+ $earned = $collector->getTransactions()->sum('transaction_amount');
/** @noinspection PhpUndefinedMethodInspection */
$dateStr = $date['end']->format('Y-m-d');
$dateName = app('navigation')->periodShow($date['end'], $date['period']);
diff --git a/app/Http/Controllers/Category/ShowController.php b/app/Http/Controllers/Category/ShowController.php
index eb0e54edfd..42826046e4 100644
--- a/app/Http/Controllers/Category/ShowController.php
+++ b/app/Http/Controllers/Category/ShowController.php
@@ -24,16 +24,14 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Category;
use Carbon\Carbon;
-use FireflyIII\Helpers\Collector\JournalCollectorInterface;
+use FireflyIII\Helpers\Collector\TransactionCollectorInterface;
use FireflyIII\Helpers\Filter\InternalTransferFilter;
use FireflyIII\Http\Controllers\Controller;
-use FireflyIII\Models\AccountType;
use FireflyIII\Models\Category;
-use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
-use FireflyIII\Support\CacheProperties;
+use FireflyIII\Support\Http\Controllers\PeriodOverview;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Log;
@@ -46,7 +44,7 @@ use Log;
*/
class ShowController extends Controller
{
-
+ use PeriodOverview;
/** @var AccountRepositoryInterface The account repository */
private $accountRepos;
/** @var JournalRepositoryInterface Journals and transactions overview */
@@ -90,14 +88,13 @@ class ShowController extends Controller
{
Log::debug('Now in show()');
/** @var Carbon $start */
- $start = $start ?? session('start', Carbon::create()->startOfMonth());
+ $start = $start ?? session('start', Carbon::now()->startOfMonth());
/** @var Carbon $end */
- $end = $end ?? session('end', Carbon::create()->startOfMonth());
+ $end = $end ?? session('end', Carbon::now()->endOfMonth());
$subTitleIcon = 'fa-bar-chart';
- $moment = '';
$page = (int)$request->get('page');
$pageSize = (int)app('preferences')->get('listPageSize', 50)->data;
- $periods = $this->getPeriodOverview($category, $start);
+ $periods = $this->getCategoryPeriodOverview($category, $start);
$path = route('categories.show', [$category->id, $start->format('Y-m-d'), $end->format('Y-m-d')]);
$subTitle = trans(
'firefly.journals_in_period_for_category',
@@ -105,17 +102,17 @@ class ShowController extends Controller
'end' => $end->formatLocalized($this->monthAndDayFormat),]
);
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($start, $end)->setLimit($pageSize)->setPage($page)->withOpposingAccount()
->setCategory($category)->withBudgetInformation()->withCategoryInformation();
$collector->removeFilter(InternalTransferFilter::class);
- $transactions = $collector->getPaginatedJournals();
+ $transactions = $collector->getPaginatedTransactions();
$transactions->setPath($path);
Log::debug('End of show()');
- return view('categories.show', compact('category', 'transactions', 'moment', 'periods', 'subTitle', 'subTitleIcon', 'start', 'end'));
+ return view('categories.show', compact('category', 'transactions', 'periods', 'subTitle', 'subTitleIcon', 'start', 'end'));
}
/**
@@ -135,92 +132,23 @@ class ShowController extends Controller
$start = null;
$end = null;
$periods = new Collection;
- $moment = 'all';
$subTitle = (string)trans('firefly.all_journals_for_category', ['name' => $category->name]);
$first = $this->repository->firstUseDate($category);
/** @var Carbon $start */
$start = $first ?? new Carbon;
$end = new Carbon;
- $path = route('categories.show-all', [$category->id]);
+ $path = route('categories.show.all', [$category->id]);
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
+ /** @var TransactionCollectorInterface $collector */
+ $collector = app(TransactionCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($start, $end)->setLimit($pageSize)->setPage($page)->withOpposingAccount()
->setCategory($category)->withBudgetInformation()->withCategoryInformation();
$collector->removeFilter(InternalTransferFilter::class);
- $transactions = $collector->getPaginatedJournals();
+ $transactions = $collector->getPaginatedTransactions();
$transactions->setPath($path);
- return view('categories.show', compact('category', 'moment', 'transactions', 'periods', 'subTitle', 'subTitleIcon', 'start', 'end'));
+ return view('categories.show', compact('category', 'transactions', 'periods', 'subTitle', 'subTitleIcon', 'start', 'end'));
}
-
- /**
- * Get a period overview for category.
- *
- * @param Category $category
- *
- * @param Carbon $date
- *
- * @return Collection
- *
- * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
- */
- private function getPeriodOverview(Category $category, Carbon $date): Collection
- {
- $range = app('preferences')->get('viewRange', '1M')->data;
- $first = $this->journalRepos->firstNull();
- $start = null === $first ? new Carbon : $first->date;
- $end = $date ?? new Carbon;
- $accounts = $this->accountRepos->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]);
-
- // properties for entries with their amounts.
- $cache = new CacheProperties();
- $cache->addProperty($start);
- $cache->addProperty($end);
- $cache->addProperty($range);
- $cache->addProperty('categories.entries');
- $cache->addProperty($category->id);
-
- if ($cache->has()) {
- return $cache->get(); // @codeCoverageIgnore
- }
- /** @var array $dates */
- $dates = app('navigation')->blockPeriods($start, $end, $range);
- $entries = new Collection;
-
- foreach ($dates as $currentDate) {
- $spent = $this->repository->spentInPeriod(new Collection([$category]), $accounts, $currentDate['start'], $currentDate['end']);
- $earned = $this->repository->earnedInPeriod(new Collection([$category]), $accounts, $currentDate['start'], $currentDate['end']);
- /** @noinspection PhpUndefinedMethodInspection */
- $dateStr = $currentDate['end']->format('Y-m-d');
- $dateName = app('navigation')->periodShow($currentDate['end'], $currentDate['period']);
-
- // amount transferred
- /** @var JournalCollectorInterface $collector */
- $collector = app(JournalCollectorInterface::class);
- $collector->setAllAssetAccounts()->setRange($currentDate['start'], $currentDate['end'])->setCategory($category)
- ->withOpposingAccount()->setTypes([TransactionType::TRANSFER]);
- $collector->removeFilter(InternalTransferFilter::class);
- $transferred = app('steam')->positive((string)$collector->getJournals()->sum('transaction_amount'));
-
- $entries->push(
- [
- 'string' => $dateStr,
- 'name' => $dateName,
- 'spent' => $spent,
- 'earned' => $earned,
- 'sum' => bcadd($earned, $spent),
- 'transferred' => $transferred,
- 'start' => clone $currentDate['start'],
- 'end' => clone $currentDate['end'],
- ]
- );
- }
- $cache->store($entries);
-
- return $entries;
- }
-
}
diff --git a/app/Http/Controllers/Chart/AccountController.php b/app/Http/Controllers/Chart/AccountController.php
index 124cc00da6..ff75bc676c 100644
--- a/app/Http/Controllers/Chart/AccountController.php
+++ b/app/Http/Controllers/Chart/AccountController.php
@@ -18,24 +18,23 @@
* You should have received a copy of the GNU General Public License
* along with Firefly III. If not, see
' . trans('firefly.route_has_no_help') . '
'; - - // if no such route, log error and return default text. - if (!$this->help->hasRoute($route)) { - Log::error('No such route: ' . $route); - - return $content; - } - - // help content may be cached: - if ($this->help->inCache($route, $language)) { - $content = $this->help->getFromCache($route, $language); - Log::debug(sprintf('Help text %s was in cache.', $language)); - - return $content; - } - - // get help content from Github: - $content = $this->help->getFromGitHub($route, $language); - - // content will have 0 length when Github failed. Try en_US when it does: - if ('' === $content) { - $language = 'en_US'; - - // also check cache first: - if ($this->help->inCache($route, $language)) { - Log::debug(sprintf('Help text %s was in cache.', $language)); - $content = $this->help->getFromCache($route, $language); - - return $content; - } - - $content = $this->help->getFromGitHub($route, $language); - } - - // help still empty? - if ('' !== $content) { - $this->help->putInCache($route, $language, $content); - - return $content; - } - - return '' . trans('firefly.route_has_no_help') . '
'; - } } diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index cf2b749385..804deca82b 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -24,7 +24,7 @@ namespace FireflyIII\Http\Controllers; use Carbon\Carbon; use FireflyIII\Events\RequestedVersionCheckStatus; -use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Http\Middleware\Installer; use FireflyIII\Models\AccountType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; @@ -108,7 +108,9 @@ class HomeController extends Controller } $subTitle = (string)trans('firefly.welcomeBack'); $transactions = []; - $frontPage = app('preferences')->get('frontPageAccounts', $repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET])->pluck('id')->toArray()); + $frontPage = app('preferences')->get( + 'frontPageAccounts', $repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET])->pluck('id')->toArray() + ); /** @var Carbon $start */ $start = session('start', Carbon::now()->startOfMonth()); /** @var Carbon $end */ @@ -122,9 +124,9 @@ class HomeController extends Controller $billCount = $billRepository->getBills()->count(); foreach ($accounts as $account) { - $collector = app(JournalCollectorInterface::class); + $collector = app(TransactionCollectorInterface::class); $collector->setAccounts(new Collection([$account]))->setRange($start, $end)->setLimit(10)->setPage(1); - $set = $collector->getJournals(); + $set = $collector->getTransactions(); $transactions[] = [$set, $account]; } diff --git a/app/Http/Controllers/Import/CallbackController.php b/app/Http/Controllers/Import/CallbackController.php new file mode 100644 index 0000000000..f85915ac19 --- /dev/null +++ b/app/Http/Controllers/Import/CallbackController.php @@ -0,0 +1,75 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Http\Controllers\Import; + + +use FireflyIII\Http\Controllers\Controller; +use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; +use Illuminate\Http\Request; +use Log; + +/** + * Class CallbackController + */ +class CallbackController extends Controller +{ + + /** + * Callback specifically for YNAB logins. + * + * @param Request $request + * + * @param ImportJobRepositoryInterface $repository + * + * @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\View\View + */ + public function ynab(Request $request, ImportJobRepositoryInterface $repository) + { + $code = (string)$request->get('code'); + $jobKey = (string)$request->get('state'); + + if ('' === $code) { + return view('error')->with('message', 'You Need A Budget did not reply with a valid authorization code. Firefly III cannot continue.'); + } + + $importJob = $repository->findByKey($jobKey); + + if ('' === $jobKey || null === $importJob) { + return view('error')->with('message', 'You Need A Budget did not reply with the correct state identifier. Firefly III cannot continue.'); + } + Log::debug(sprintf('Got a code from YNAB: %s', $code)); + + // we have a code. Make the job ready for the next step, and then redirect the user. + $configuration = $repository->getConfiguration($importJob); + $configuration['auth_code'] = $code; + $repository->setConfiguration($importJob, $configuration); + + // set stage to make the import routine take the correct action: + $repository->setStatus($importJob, 'ready_to_run'); + $repository->setStage($importJob, 'get_access_token'); + + return redirect(route('import.job.status.index', [$importJob->key])); + } + +} \ No newline at end of file diff --git a/app/Http/Controllers/Import/IndexController.php b/app/Http/Controllers/Import/IndexController.php index 799904d0c2..fd8817913b 100644 --- a/app/Http/Controllers/Import/IndexController.php +++ b/app/Http/Controllers/Import/IndexController.php @@ -37,7 +37,7 @@ use Log; */ class IndexController extends Controller { - /** @var array */ + /** @var array All available providers */ public $providers; /** @var ImportJobRepositoryInterface The import job repository */ public $repository; @@ -76,23 +76,29 @@ class IndexController extends Controller */ public function create(string $importProvider) { - Log::debug(sprintf('Will create job for provider "%s"', $importProvider)); - $importJob = $this->repository->create($importProvider); - $hasPreReq = (bool)config(sprintf('import.has_prereq.%s', $importProvider)); - $hasConfig = (bool)config(sprintf('import.has_job_config.%s', $importProvider)); + + $hasPreReq = (bool)config(sprintf('import.has_prereq.%s', $importProvider)); + $hasConfig = (bool)config(sprintf('import.has_job_config.%s', $importProvider)); + $allowedForDemo = (bool)config(sprintf('import.allowed_for_demo.%s', $importProvider)); + $isDemoUser = $this->userRepository->hasRole(auth()->user(), 'demo'); + + Log::debug(sprintf('Will create job for provider "%s"', $importProvider)); + Log::debug(sprintf('Is demo user? %s',var_export($isDemoUser, true))); + Log::debug(sprintf('Is allowed for user? %s',var_export($allowedForDemo, true))); + Log::debug(sprintf('Has prerequisites? %s',var_export($hasPreReq, true))); + Log::debug(sprintf('Has config? %s',var_export($hasConfig, true))); + + + if ($isDemoUser && !$allowedForDemo) { + Log::debug('User is demo and this provider doesnt work for demo users.'); + return redirect(route('import.index')); + } + + $importJob = $this->repository->create($importProvider); Log::debug(sprintf('Created job #%d for provider %s', $importJob->id, $importProvider)); - // no prerequisites and no config: - if (false === $hasPreReq && false === $hasConfig) { - Log::debug('Provider needs no configuration for job. Job is ready to start.'); - $this->repository->updateStatus($importJob, 'ready_to_run'); - Log::debug('Redirect to status-page.'); - - return redirect(route('import.job.status.index', [$importJob->key])); - } - // no prerequisites but job has config: if (false === $hasPreReq && false !== $hasConfig) { Log::debug('Provider has no prerequisites. Continue.'); @@ -124,7 +130,7 @@ class IndexController extends Controller if (false === $hasConfig) { // @codeCoverageIgnoreStart Log::debug('Provider has no configuration. Job is ready to start.'); - $this->repository->updateStatus($importJob, 'ready_to_run'); + $this->repository->setStatus($importJob, 'ready_to_run'); Log::debug('Redirect to status-page.'); return redirect(route('import.job.status.index', [$importJob->key])); @@ -180,7 +186,8 @@ class IndexController extends Controller $providers = $this->providers; $subTitle = (string)trans('import.index_breadcrumb'); $subTitleIcon = 'fa-home'; + $isDemoUser = $this->userRepository->hasRole(auth()->user(), 'demo'); - return view('import.index', compact('subTitle', 'subTitleIcon', 'providers')); + return view('import.index', compact('subTitle', 'subTitleIcon', 'providers', 'isDemoUser')); } } diff --git a/app/Http/Controllers/Import/JobConfigurationController.php b/app/Http/Controllers/Import/JobConfigurationController.php index caaeee9825..0a2d9e96d5 100644 --- a/app/Http/Controllers/Import/JobConfigurationController.php +++ b/app/Http/Controllers/Import/JobConfigurationController.php @@ -24,9 +24,9 @@ namespace FireflyIII\Http\Controllers\Import; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Http\Controllers\Controller; -use FireflyIII\Import\JobConfiguration\JobConfigurationInterface; use FireflyIII\Models\ImportJob; use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; +use FireflyIII\Support\Http\Controllers\CreateStuff; use Illuminate\Http\Request; use Illuminate\Http\UploadedFile; use Illuminate\Support\MessageBag; @@ -37,6 +37,7 @@ use Log; */ class JobConfigurationController extends Controller { + use CreateStuff; /** @var ImportJobRepositoryInterface The import job repository */ public $repository; @@ -87,7 +88,7 @@ class JobConfigurationController extends Controller if (!(bool)config(sprintf('import.has_job_config.%s', $importProvider))) { // @codeCoverageIgnoreStart Log::debug('Job needs no config, is ready to run!'); - $this->repository->updateStatus($importJob, 'ready_to_run'); + $this->repository->setStatus($importJob, 'ready_to_run'); return redirect(route('import.job.status.index', [$importJob->key])); // @codeCoverageIgnoreEnd @@ -96,7 +97,7 @@ class JobConfigurationController extends Controller $configurator = $this->makeConfigurator($importJob); if ($configurator->configurationComplete()) { Log::debug('Config is complete, set status to ready_to_run.'); - $this->repository->updateStatus($importJob, 'ready_to_run'); + $this->repository->setStatus($importJob, 'ready_to_run'); return redirect(route('import.job.status.index', [$importJob->key])); } @@ -136,7 +137,7 @@ class JobConfigurationController extends Controller // is the job already configured? if ($configurator->configurationComplete()) { - $this->repository->updateStatus($importJob, 'ready_to_run'); + $this->repository->setStatus($importJob, 'ready_to_run'); return redirect(route('import.job.status.index', [$importJob->key])); } @@ -161,27 +162,5 @@ class JobConfigurationController extends Controller return redirect(route('import.job.configuration.index', [$importJob->key])); } - /** - * Make a configurator object. - * - * @param ImportJob $importJob - * - * @return JobConfigurationInterface - * - * @throws FireflyException - */ - private function makeConfigurator(ImportJob $importJob): JobConfigurationInterface - { - $key = sprintf('import.configuration.%s', $importJob->provider); - $className = (string)config($key); - if (null === $className || !class_exists($className)) { - throw new FireflyException(sprintf('Cannot find configurator class for job with provider "%s".', $importJob->provider)); // @codeCoverageIgnore - } - Log::debug(sprintf('Going to create class "%s"', $className)); - /** @var JobConfigurationInterface $configurator */ - $configurator = app($className); - $configurator->setImportJob($importJob); - return $configurator; - } } diff --git a/app/Http/Controllers/Import/JobStatusController.php b/app/Http/Controllers/Import/JobStatusController.php index 62adfeb7eb..949f15d156 100644 --- a/app/Http/Controllers/Import/JobStatusController.php +++ b/app/Http/Controllers/Import/JobStatusController.php @@ -26,9 +26,9 @@ use Exception; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Import\Routine\RoutineInterface; -use FireflyIII\Import\Storage\ImportArrayStorage; use FireflyIII\Models\ImportJob; use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; +use FireflyIII\Support\Http\Controllers\CreateStuff; use Illuminate\Http\JsonResponse; use Log; @@ -37,6 +37,7 @@ use Log; */ class JobStatusController extends Controller { + use CreateStuff; /** @var ImportJobRepositoryInterface The import job repository */ private $repository; @@ -132,11 +133,12 @@ class JobStatusController extends Controller */ public function start(ImportJob $importJob): JsonResponse { + Log::debug('Now in JobStatusController::start'); // catch impossible status: $allowed = ['ready_to_run', 'need_job_config']; if (null !== $importJob && !\in_array($importJob->status, $allowed, true)) { - Log::error('Job is not ready.'); + Log::error(sprintf('Job is not ready. Status should be in array, but is %s', $importJob->status), $allowed); $this->repository->setStatus($importJob, 'error'); return response()->json( @@ -157,7 +159,11 @@ class JobStatusController extends Controller /** @var RoutineInterface $routine */ $routine = app($className); $routine->setImportJob($importJob); + + Log::debug(sprintf('Created class of type %s', $className)); + try { + Log::debug(sprintf('Try to call %s:run()', $className)); $routine->run(); } catch (FireflyException|Exception $e) { $message = 'The import routine crashed: ' . $e->getMessage(); @@ -189,7 +195,7 @@ class JobStatusController extends Controller // catch impossible status: $allowed = ['provider_finished', 'storing_data']; if (null !== $importJob && !\in_array($importJob->status, $allowed, true)) { - Log::error('Job is not ready.'); + Log::error(sprintf('Job is not ready. Status should be in array, but is %s', $importJob->status), $allowed); return response()->json( ['status' => 'NOK', 'message' => sprintf('JobStatusController::start expects status "provider_finished" instead of "%s".', $importJob->status)] @@ -219,22 +225,5 @@ class JobStatusController extends Controller return response()->json(['status' => 'OK', 'message' => 'storage_finished']); } - /** - * Store the transactions. - * - * @param ImportJob $importJob - * - * @throws FireflyException - */ - private function storeTransactions(ImportJob $importJob): void - { - /** @var ImportArrayStorage $storage */ - $storage = app(ImportArrayStorage::class); - $storage->setImportJob($importJob); - try { - $storage->store(); - } catch (FireflyException|Exception $e) { - throw new FireflyException($e->getMessage()); - } - } + } diff --git a/app/Http/Controllers/JavascriptController.php b/app/Http/Controllers/JavascriptController.php index d9432135bf..d5527846a6 100644 --- a/app/Http/Controllers/JavascriptController.php +++ b/app/Http/Controllers/JavascriptController.php @@ -22,21 +22,22 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers; -use Carbon\Carbon; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; +use FireflyIII\Support\Http\Controllers\GetConfigurationData; use Illuminate\Http\Request; use Illuminate\Http\Response; -use Log; /** * Class JavascriptController. */ class JavascriptController extends Controller { + use GetConfigurationData; + /** * Show info about accounts. * @@ -47,7 +48,7 @@ class JavascriptController extends Controller */ public function accounts(AccountRepositoryInterface $repository, CurrencyRepositoryInterface $currencyRepository): Response { - $accounts = $repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]); + $accounts = $repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET, AccountType::DEBT,AccountType::LOAN,AccountType::MORTGAGE, AccountType::CREDITCARD]); $preference = app('preferences')->get('currencyPreference', config('firefly.default_currency', 'EUR')); /** @noinspection NullPointerExceptionInspection */ $default = $currencyRepository->findByCodeNull($preference->data); @@ -140,79 +141,4 @@ class JavascriptController extends Controller ->header('Content-Type', 'text/javascript'); } - /** - * Get config for date range. - * - * @return array - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - private function getDateRangeConfig(): array - { - $viewRange = app('preferences')->get('viewRange', '1M')->data; - /** @var Carbon $start */ - $start = session('start'); - /** @var Carbon $end */ - $end = session('end'); - /** @var Carbon $first */ - $first = session('first'); - $title = sprintf('%s - %s', $start->formatLocalized($this->monthAndDayFormat), $end->formatLocalized($this->monthAndDayFormat)); - $isCustom = true === session('is_custom_range', false); - $today = new Carbon; - $ranges = [ - // first range is the current range: - $title => [$start, $end], - ]; - Log::debug(sprintf('viewRange is %s', $viewRange)); - Log::debug(sprintf('isCustom is %s', var_export($isCustom, true))); - - // when current range is a custom range, add the current period as the next range. - if ($isCustom) { - Log::debug('Custom is true.'); - $index = app('navigation')->periodShow($start, $viewRange); - $customPeriodStart = app('navigation')->startOfPeriod($start, $viewRange); - $customPeriodEnd = app('navigation')->endOfPeriod($customPeriodStart, $viewRange); - $ranges[$index] = [$customPeriodStart, $customPeriodEnd]; - } - // then add previous range and next range - $previousDate = app('navigation')->subtractPeriod($start, $viewRange); - $index = app('navigation')->periodShow($previousDate, $viewRange); - $previousStart = app('navigation')->startOfPeriod($previousDate, $viewRange); - $previousEnd = app('navigation')->endOfPeriod($previousStart, $viewRange); - $ranges[$index] = [$previousStart, $previousEnd]; - - $nextDate = app('navigation')->addPeriod($start, $viewRange, 0); - $index = app('navigation')->periodShow($nextDate, $viewRange); - $nextStart = app('navigation')->startOfPeriod($nextDate, $viewRange); - $nextEnd = app('navigation')->endOfPeriod($nextStart, $viewRange); - $ranges[$index] = [$nextStart, $nextEnd]; - - // today: - /** @var Carbon $todayStart */ - $todayStart = app('navigation')->startOfPeriod($today, $viewRange); - /** @var Carbon $todayEnd */ - $todayEnd = app('navigation')->endOfPeriod($todayStart, $viewRange); - if ($todayStart->ne($start) || $todayEnd->ne($end)) { - $ranges[ucfirst((string)trans('firefly.today'))] = [$todayStart, $todayEnd]; - } - - // everything - $index = (string)trans('firefly.everything'); - $ranges[$index] = [$first, new Carbon]; - - $return = [ - 'title' => $title, - 'configuration' => [ - 'apply' => (string)trans('firefly.apply'), - 'cancel' => (string)trans('firefly.cancel'), - 'from' => (string)trans('firefly.from'), - 'to' => (string)trans('firefly.to'), - 'customRange' => (string)trans('firefly.customRange'), - 'start' => $start->format('Y-m-d'), - 'end' => $end->format('Y-m-d'), - 'ranges' => $ranges, - ], - ]; - - return $return; - } } diff --git a/app/Http/Controllers/Json/AutoCompleteController.php b/app/Http/Controllers/Json/AutoCompleteController.php index a2b1aba242..d6c758bb0d 100644 --- a/app/Http/Controllers/Json/AutoCompleteController.php +++ b/app/Http/Controllers/Json/AutoCompleteController.php @@ -22,7 +22,7 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Json; -use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; @@ -68,14 +68,14 @@ class AutoCompleteController extends Controller /** * List of all journals. * - * @param JournalCollectorInterface $collector + * @param TransactionCollectorInterface $collector * * @return JsonResponse */ - public function allTransactionJournals(JournalCollectorInterface $collector): JsonResponse + public function allTransactionJournals(TransactionCollectorInterface $collector): JsonResponse { $collector->setLimit(250)->setPage(1); - $return = array_unique($collector->getJournals()->pluck('description')->toArray()); + $return = array_unique($collector->getTransactions()->pluck('description')->toArray()); sort($return); return response()->json($return); @@ -173,12 +173,12 @@ class AutoCompleteController extends Controller /** * List of journals with their ID. * - * @param JournalCollectorInterface $collector + * @param TransactionCollectorInterface $collector * @param TransactionJournal $except * * @return JsonResponse */ - public function journalsWithId(JournalCollectorInterface $collector, TransactionJournal $except): JsonResponse + public function journalsWithId(TransactionCollectorInterface $collector, TransactionJournal $except): JsonResponse { $cache = new CacheProperties; $cache->addProperty('recent-journals-id'); @@ -188,7 +188,7 @@ class AutoCompleteController extends Controller } $collector->setLimit(400)->setPage(1); - $set = $collector->getJournals()->pluck('description', 'journal_id')->toArray(); + $set = $collector->getTransactions()->pluck('description', 'journal_id')->toArray(); $return = []; foreach ($set as $id => $description) { $id = (int)$id; @@ -248,18 +248,18 @@ class AutoCompleteController extends Controller /** * List of journals by type. * - * @param JournalCollectorInterface $collector + * @param TransactionCollectorInterface $collector * @param string $what * * @return JsonResponse */ - public function transactionJournals(JournalCollectorInterface $collector, string $what): JsonResponse + public function transactionJournals(TransactionCollectorInterface $collector, string $what): JsonResponse { $type = config('firefly.transactionTypesByWhat.' . $what); $types = [$type]; $collector->setTypes($types)->setLimit(250)->setPage(1); - $return = array_unique($collector->getJournals()->pluck('description')->toArray()); + $return = array_unique($collector->getTransactions()->pluck('description')->toArray()); sort($return); return response()->json($return); diff --git a/app/Http/Controllers/Json/BoxController.php b/app/Http/Controllers/Json/BoxController.php index f059036100..60027f0a83 100644 --- a/app/Http/Controllers/Json/BoxController.php +++ b/app/Http/Controllers/Json/BoxController.php @@ -23,7 +23,8 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Json; use Carbon\Carbon; -use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Helpers\Collector\TransactionCollectorInterface; +use FireflyIII\Helpers\Report\NetWorthInterface; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; @@ -35,13 +36,16 @@ use FireflyIII\Repositories\Bill\BillRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Support\CacheProperties; +use FireflyIII\Support\Http\Controllers\RequestInformation; use Illuminate\Http\JsonResponse; +use Log; /** * Class BoxController. */ class BoxController extends Controller { + use RequestInformation; /** * How much money user has available. @@ -130,12 +134,12 @@ class BoxController extends Controller $sums = []; // collect income of user: - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); $collector->setAllAssetAccounts()->setRange($start, $end) ->setTypes([TransactionType::DEPOSIT]) ->withOpposingAccount(); - $set = $collector->getJournals(); + $set = $collector->getTransactions(); /** @var Transaction $transaction */ foreach ($set as $transaction) { $currencyId = (int)$transaction->transaction_currency_id; @@ -146,12 +150,12 @@ class BoxController extends Controller } // collect expenses - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); $collector->setAllAssetAccounts()->setRange($start, $end) ->setTypes([TransactionType::WITHDRAWAL]) ->withOpposingAccount(); - $set = $collector->getJournals(); + $set = $collector->getTransactions(); /** @var Transaction $transaction */ foreach ($set as $transaction) { $currencyId = (int)$transaction->transaction_currency_id; @@ -202,7 +206,7 @@ class BoxController extends Controller /** @var Carbon $start */ $start = session('start', Carbon::now()->startOfMonth()); /** @var Carbon $end */ - $end = session('end', Carbon::now()->endOfMonth()); + $end = session('end', Carbon::now()->endOfMonth()); $cache = new CacheProperties; $cache->addProperty($start); @@ -233,16 +237,13 @@ class BoxController extends Controller /** * Total user net worth. * - * @param AccountRepositoryInterface $repository - * * @return JsonResponse - * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - public function netWorth(AccountRepositoryInterface $repository): JsonResponse + public function netWorth(): JsonResponse { - $date = new Carbon(date('Y-m-d')); // needed so its per day. + $date = Carbon::create()->startOfDay(); // start and end in the future? use $end if ($this->notInSessionRange($date)) { @@ -250,101 +251,44 @@ class BoxController extends Controller $date = session('end', Carbon::now()->endOfMonth()); } - // start in the past, end in the future? use $date - $cache = new CacheProperties; - $cache->addProperty($date); - $cache->addProperty('box-net-worth'); - if ($cache->has()) { - return response()->json($cache->get()); // @codeCoverageIgnore - } - $netWorth = []; - $accounts = $repository->getActiveAccountsByType([AccountType::DEFAULT, AccountType::ASSET]); + /** @var NetWorthInterface $netWorthHelper */ + $netWorthHelper = app(NetWorthInterface::class); + $netWorthHelper->setUser(auth()->user()); - $balances = app('steam')->balancesByAccounts($accounts, $date); + /** @var AccountRepositoryInterface $accountRepository */ + $accountRepository = app(AccountRepositoryInterface::class); + $allAccounts = $accountRepository->getActiveAccountsByType( + [AccountType::DEFAULT, AccountType::ASSET, AccountType::DEBT, AccountType::LOAN, AccountType::MORTGAGE, AccountType::CREDITCARD] + ); + Log::debug(sprintf('Found %d accounts.', $allAccounts->count())); - /** @var Account $account */ - foreach ($accounts as $account) { - $accountCurrency = $this->getCurrencyOrDefault($account); - $balance = $balances[$account->id] ?? '0'; + // filter list on preference of being included. + $filtered = $allAccounts->filter( + function (Account $account) use ($accountRepository) { + $includeNetWorth = $accountRepository->getMetaValue($account, 'include_net_worth'); + $result = null === $includeNetWorth ? true : '1' === $includeNetWorth; + if (false === $result) { + Log::debug(sprintf('Will not include "%s" in net worth charts.', $account->name)); + } - // if the account is a credit card, subtract the virtual balance from the balance, - // to better reflect that this is not money that is actually "yours". - $role = (string)$repository->getMetaValue($account, 'accountRole'); - $virtualBalance = (string)$account->virtual_balance; - if ('ccAsset' === $role && '' !== $virtualBalance && (float)$virtualBalance > 0) { - $balance = bcsub($balance, $virtualBalance); + return $result; } + ); + + $netWorthSet = $netWorthHelper->getNetWorthByCurrency($filtered, $date); - if (!isset($netWorth[$accountCurrency->id])) { - $netWorth[$accountCurrency->id]['currency'] = $accountCurrency; - $netWorth[$accountCurrency->id]['sum'] = '0'; - } - $netWorth[$accountCurrency->id]['sum'] = bcadd($netWorth[$accountCurrency->id]['sum'], $balance); - } $return = []; - foreach ($netWorth as $currencyId => $data) { - $return[$currencyId] = app('amount')->formatAnything($data['currency'], $data['sum'], false); + foreach ($netWorthSet as $index => $data) { + /** @var TransactionCurrency $currency */ + $currency = $data['currency']; + $return[$currency->id] = app('amount')->formatAnything($currency, $data['balance'], false); } $return = [ 'net_worths' => array_values($return), ]; - $cache->store($return); - return response()->json($return); } - /** - * Get a currency or return default currency. - * - * @param Account $account - * - * @return TransactionCurrency - */ - private function getCurrencyOrDefault(Account $account): TransactionCurrency - { - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - /** @var CurrencyRepositoryInterface $currencyRepos */ - $currencyRepos = app(CurrencyRepositoryInterface::class); - - $currency = app('amount')->getDefaultCurrency(); - $accountCurrency = null; - $currencyId = (int)$repository->getMetaValue($account, 'currency_id'); - if (0 !== $currencyId) { - $accountCurrency = $currencyRepos->findNull($currencyId); - } - if (null === $accountCurrency) { - $accountCurrency = $currency; - } - - return $accountCurrency; - } - - /** - * Check if date is outside session range. - * - * @param Carbon $date - * - * @return bool - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - private function notInSessionRange(Carbon $date): bool - { - /** @var Carbon $start */ - $start = session('start', Carbon::now()->startOfMonth()); - /** @var Carbon $end */ - $end = session('end', Carbon::now()->endOfMonth()); - $result = false; - if ($start->greaterThanOrEqualTo($date) && $end->greaterThanOrEqualTo($date)) { - $result = true; - } - // start and end in the past? use $end - if ($start->lessThanOrEqualTo($date) && $end->lessThanOrEqualTo($date)) { - $result = true; - } - - return $result; - } } diff --git a/app/Http/Controllers/Json/FrontpageController.php b/app/Http/Controllers/Json/FrontpageController.php index b27dec9585..1303ba1109 100644 --- a/app/Http/Controllers/Json/FrontpageController.php +++ b/app/Http/Controllers/Json/FrontpageController.php @@ -26,6 +26,8 @@ use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\PiggyBank; use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; use Illuminate\Http\JsonResponse; +use Log; +use Throwable; /** * Class FrontpageController. @@ -38,7 +40,6 @@ class FrontpageController extends Controller * @param PiggyBankRepositoryInterface $repository * * @return JsonResponse - * @throws \Throwable */ public function piggyBanks(PiggyBankRepositoryInterface $repository): JsonResponse { @@ -64,7 +65,14 @@ class FrontpageController extends Controller } $html = ''; if (\count($info) > 0) { - $html = view('json.piggy-banks', compact('info'))->render(); + try { + $html = view('json.piggy-banks', compact('info'))->render(); + // @codeCoverageIgnoreStart + } catch (Throwable $e) { + Log::error(sprintf('Cannot render json.piggy-banks: %s', $e->getMessage())); + $html = 'Could not render view.'; + } + // @codeCoverageIgnoreEnd } return response()->json(['html' => $html]); diff --git a/app/Http/Controllers/Json/IntroController.php b/app/Http/Controllers/Json/IntroController.php index f146e06091..9ad233c2b1 100644 --- a/app/Http/Controllers/Json/IntroController.php +++ b/app/Http/Controllers/Json/IntroController.php @@ -22,6 +22,7 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Json; +use FireflyIII\Support\Http\Controllers\GetConfigurationData; use Illuminate\Http\JsonResponse; use Log; @@ -30,6 +31,8 @@ use Log; */ class IntroController { + use GetConfigurationData; + /** * Returns the introduction wizard for a page. * @@ -135,66 +138,4 @@ class IntroController return response()->json(['result' => sprintf('Reported demo watched for route "%s".', $route)]); } - /** - * Get the basic steps from config. - * - * @param string $route - * - * @return array - */ - private function getBasicSteps(string $route): array - { - $routeKey = str_replace('.', '_', $route); - $elements = config(sprintf('intro.%s', $routeKey)); - $steps = []; - if (\is_array($elements) && \count($elements) > 0) { - foreach ($elements as $key => $options) { - $currentStep = $options; - - // get the text: - $currentStep['intro'] = (string)trans('intro.' . $route . '_' . $key); - - // save in array: - $steps[] = $currentStep; - } - } - Log::debug(sprintf('Total basic steps for %s is %d', $routeKey, \count($steps))); - - return $steps; - } - - /** - * Get specific info for special routes. - * - * @param string $route - * @param string $specificPage - * - * @return array - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - private function getSpecificSteps(string $route, string $specificPage): array - { - $steps = []; - $routeKey = ''; - - // user is on page with specific instructions: - if (\strlen($specificPage) > 0) { - $routeKey = str_replace('.', '_', $route); - $elements = config(sprintf('intro.%s', $routeKey . '_' . $specificPage)); - if (\is_array($elements) && \count($elements) > 0) { - foreach ($elements as $key => $options) { - $currentStep = $options; - - // get the text: - $currentStep['intro'] = (string)trans('intro.' . $route . '_' . $specificPage . '_' . $key); - - // save in array: - $steps[] = $currentStep; - } - } - } - Log::debug(sprintf('Total specific steps for route "%s" and page "%s" (routeKey is "%s") is %d', $route, $specificPage, $routeKey, \count($steps))); - - return $steps; - } } diff --git a/app/Http/Controllers/Json/ReconcileController.php b/app/Http/Controllers/Json/ReconcileController.php index dc36328675..0739102c7c 100644 --- a/app/Http/Controllers/Json/ReconcileController.php +++ b/app/Http/Controllers/Json/ReconcileController.php @@ -26,7 +26,7 @@ namespace FireflyIII\Http\Controllers\Json; use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; -use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; @@ -34,9 +34,12 @@ use FireflyIII\Models\Transaction; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; +use FireflyIII\Support\Http\Controllers\UserNavigation; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Collection; +use Log; +use Throwable; /** * @@ -46,7 +49,7 @@ use Illuminate\Support\Collection; */ class ReconcileController extends Controller { - + use UserNavigation; /** @var AccountRepositoryInterface The account repository */ private $accountRepos; /** @var CurrencyRepositoryInterface The currency repository */ @@ -90,7 +93,6 @@ class ReconcileController extends Controller * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - * @throws \Throwable */ public function overview(Request $request, Account $account, Carbon $start, Carbon $end): JsonResponse { @@ -117,21 +119,32 @@ class ReconcileController extends Controller /** @var Transaction $transaction */ foreach ($cleared as $transaction) { if ($transaction->transactionJournal->date <= $end) { - $clearedAmount = bcadd($clearedAmount, $transaction->amount); + $clearedAmount = bcadd($clearedAmount, $transaction->amount); // @codeCoverageIgnore ++$countCleared; } } $difference = bcadd(bcadd(bcsub($startBalance, $endBalance), $clearedAmount), $amount); $diffCompare = bccomp($difference, '0'); - $return = [ - 'post_uri' => $route, - 'html' => view( + + try { + $view = view( 'accounts.reconcile.overview', compact( 'account', 'start', 'diffCompare', 'difference', 'end', 'clearedIds', 'transactionIds', 'clearedAmount', 'startBalance', 'endBalance', 'amount', 'route', 'countCleared' ) - )->render(), + )->render(); + // @codeCoverageIgnoreStart + } catch (Throwable $e) { + Log::debug(sprintf('View error: %s', $e->getMessage())); + $view = 'Could not render accounts.reconcile.overview'; + } + // @codeCoverageIgnoreEnd + + + $return = [ + 'post_uri' => $route, + 'html' => $view, ]; return response()->json($return); @@ -148,7 +161,6 @@ class ReconcileController extends Controller * @return mixed * * @throws FireflyException - * @throws \Throwable */ public function transactions(Account $account, Carbon $start, Carbon $end) { @@ -175,43 +187,22 @@ class ReconcileController extends Controller $selectionEnd->addDays(3); // grab transactions: - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); $collector->setAccounts(new Collection([$account])) ->setRange($selectionStart, $selectionEnd)->withBudgetInformation()->withOpposingAccount()->withCategoryInformation(); - $transactions = $collector->getJournals(); - $html = view( - 'accounts.reconcile.transactions', compact('account', 'transactions', 'currency', 'start', 'end', 'selectionStart', 'selectionEnd') - )->render(); + $transactions = $collector->getTransactions(); + try { + $html = view( + 'accounts.reconcile.transactions', compact('account', 'transactions', 'currency', 'start', 'end', 'selectionStart', 'selectionEnd') + )->render(); + // @codeCoverageIgnoreStart + } catch (Throwable $e) { + Log::debug(sprintf('Could not render: %s', $e->getMessage())); + $html = 'Could not render accounts.reconcile.transactions'; + } + // @codeCoverageIgnoreEnd return response()->json(['html' => $html, 'startBalance' => $startBalance, 'endBalance' => $endBalance]); } - - /** - * Redirect to actual account. - * - * @param Account $account - * - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector - * - * @throws FireflyException - */ - private function redirectToOriginalAccount(Account $account) - { - /** @var Transaction $transaction */ - $transaction = $account->transactions()->first(); - if (null === $transaction) { - throw new FireflyException(sprintf('Expected a transaction. Account #%d has none. BEEP, error.', $account->id)); // @codeCoverageIgnore - } - - $journal = $transaction->transactionJournal; - /** @var Transaction $opposingTransaction */ - $opposingTransaction = $journal->transactions()->where('transactions.id', '!=', $transaction->id)->first(); - - if (null === $opposingTransaction) { - throw new FireflyException('Expected an opposing transaction. This account has none. BEEP, error.'); // @codeCoverageIgnore - } - - return redirect(route('accounts.show', [$opposingTransaction->account_id])); - } } diff --git a/app/Http/Controllers/Json/RecurrenceController.php b/app/Http/Controllers/Json/RecurrenceController.php index b983022876..37f2e58b81 100644 --- a/app/Http/Controllers/Json/RecurrenceController.php +++ b/app/Http/Controllers/Json/RecurrenceController.php @@ -103,10 +103,8 @@ class RecurrenceController extends Controller $repetition->repetition_skip = (int)$request->get('skip'); $repetition->weekend = (int)$request->get('weekend'); $actualEnd = clone $end; - + $occurrences = []; switch ($endsAt) { - default: - throw new FireflyException(sprintf('Cannot generate events for type that ends at "%s".', $endsAt)); case 'forever': // simply generate up until $end. No change from default behavior. $occurrences = $this->recurring->getOccurrencesInRange($repetition, $actualStart, $actualEnd); diff --git a/app/Http/Controllers/JsonController.php b/app/Http/Controllers/JsonController.php index a18561b929..a3f36b4ad0 100644 --- a/app/Http/Controllers/JsonController.php +++ b/app/Http/Controllers/JsonController.php @@ -24,6 +24,8 @@ namespace FireflyIII\Http\Controllers; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Log; +use Throwable; /** * Class JsonController. @@ -36,7 +38,6 @@ class JsonController extends Controller * @param Request $request * * @return JsonResponse - * @throws \Throwable */ public function action(Request $request): JsonResponse { @@ -46,7 +47,14 @@ class JsonController extends Controller foreach ($keys as $key) { $actions[$key] = (string)trans('firefly.rule_action_' . $key . '_choice'); } - $view = view('rules.partials.action', compact('actions', 'count'))->render(); + try { + $view = view('rules.partials.action', compact('actions', 'count'))->render(); + // @codeCoverageIgnoreStart + } catch (Throwable $e) { + Log::error(sprintf('Cannot render rules.partials.action: %s', $e->getMessage())); + $view = 'Could not render view.'; + } + // @codeCoverageIgnoreEnd return response()->json(['html' => $view]); } @@ -57,7 +65,6 @@ class JsonController extends Controller * @param Request $request * * @return JsonResponse - * @throws \Throwable */ public function trigger(Request $request): JsonResponse { @@ -71,7 +78,14 @@ class JsonController extends Controller } asort($triggers); - $view = view('rules.partials.trigger', compact('triggers', 'count'))->render(); + try { + $view = view('rules.partials.trigger', compact('triggers', 'count'))->render(); + // @codeCoverageIgnoreStart + } catch (Throwable $e) { + Log::error(sprintf('Cannot render rules.partials.trigger: %s', $e->getMessage())); + $view = 'Could not render view.'; + } + // @codeCoverageIgnoreEnd return response()->json(['html' => $view]); } diff --git a/app/Http/Controllers/NewUserController.php b/app/Http/Controllers/NewUserController.php index 0b67b0b609..8ad8aee7b1 100644 --- a/app/Http/Controllers/NewUserController.php +++ b/app/Http/Controllers/NewUserController.php @@ -22,11 +22,10 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers; -use Carbon\Carbon; use FireflyIII\Http\Requests\NewUserFormRequest; -use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; +use FireflyIII\Support\Http\Controllers\CreateStuff; use View; /** @@ -34,6 +33,7 @@ use View; */ class NewUserController extends Controller { + use CreateStuff; /** @var AccountRepositoryInterface The account repository */ private $repository; @@ -120,87 +120,4 @@ class NewUserController extends Controller return redirect(route('index')); } - /** - * Creates an asset account. - * - * @param NewUserFormRequest $request - * @param TransactionCurrency $currency - * - * @return bool - */ - private function createAssetAccount(NewUserFormRequest $request, TransactionCurrency $currency): bool - { - $assetAccount = [ - 'name' => $request->get('bank_name'), - 'iban' => null, - 'accountType' => 'asset', - 'virtualBalance' => 0, - 'account_type_id' => null, - 'active' => true, - 'accountRole' => 'defaultAsset', - 'openingBalance' => $request->input('bank_balance'), - 'openingBalanceDate' => new Carbon, - 'currency_id' => $currency->id, - ]; - - $this->repository->store($assetAccount); - - return true; - } - - /** - * Creates a cash wallet. - * - * @param TransactionCurrency $currency - * @param string $language - * - * @return bool - */ - private function createCashWalletAccount(TransactionCurrency $currency, string $language): bool - { - $assetAccount = [ - 'name' => (string)trans('firefly.cash_wallet', [], $language), - 'iban' => null, - 'accountType' => 'asset', - 'virtualBalance' => 0, - 'account_type_id' => null, - 'active' => true, - 'accountRole' => 'cashWalletAsset', - 'openingBalance' => null, - 'openingBalanceDate' => null, - 'currency_id' => $currency->id, - ]; - - $this->repository->store($assetAccount); - - return true; - } - - /** - * Create a savings account. - * - * @param NewUserFormRequest $request - * @param TransactionCurrency $currency - * @param string $language - * - * @return bool - */ - private function createSavingsAccount(NewUserFormRequest $request, TransactionCurrency $currency, string $language): bool - { - $savingsAccount = [ - 'name' => (string)trans('firefly.new_savings_account', ['bank_name' => $request->get('bank_name')], $language), - 'iban' => null, - 'accountType' => 'asset', - 'account_type_id' => null, - 'virtualBalance' => 0, - 'active' => true, - 'accountRole' => 'savingAsset', - 'openingBalance' => $request->input('savings_balance'), - 'openingBalanceDate' => new Carbon, - 'currency_id' => $currency->id, - ]; - $this->repository->store($savingsAccount); - - return true; - } } diff --git a/app/Http/Controllers/PiggyBankController.php b/app/Http/Controllers/PiggyBankController.php index 7eb87a3f17..bc23632b74 100644 --- a/app/Http/Controllers/PiggyBankController.php +++ b/app/Http/Controllers/PiggyBankController.php @@ -208,7 +208,7 @@ class PiggyBankController extends Controller 'targetamount' => $piggyBank->targetamount, 'targetdate' => $targetDate, 'startdate' => $startDate, - 'note' => null === $note ? '' : $note->text, + 'notes' => null === $note ? '' : $note->text, ]; session()->flash('preFilled', $preFilled); diff --git a/app/Http/Controllers/Popup/ReportController.php b/app/Http/Controllers/Popup/ReportController.php index d07a09ab63..25dfe38cb4 100644 --- a/app/Http/Controllers/Popup/ReportController.php +++ b/app/Http/Controllers/Popup/ReportController.php @@ -22,20 +22,11 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Popup; -use Carbon\Carbon; -use FireflyIII\Helpers\Collection\BalanceLine; -use FireflyIII\Helpers\Report\PopupReportInterface; use FireflyIII\Http\Controllers\Controller; -use FireflyIII\Repositories\Account\AccountRepositoryInterface; -use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; -use FireflyIII\Repositories\Category\CategoryRepositoryInterface; -use FireflyIII\Support\Binder\AccountList; +use FireflyIII\Support\Http\Controllers\RenderPartialViews; +use FireflyIII\Support\Http\Controllers\RequestInformation; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Illuminate\Routing\Route; -use InvalidArgumentException; -use Log; -use Throwable; /** * Class ReportController. @@ -44,42 +35,11 @@ use Throwable; */ class ReportController extends Controller { - /** @var AccountRepositoryInterface The account repository */ - private $accountRepository; - /** @var BudgetRepositoryInterface The budget repository */ - private $budgetRepository; - /** @var CategoryRepositoryInterface The category repository */ - private $categoryRepository; - /** @var PopupReportInterface Various helper functions. */ - private $popupHelper; - - /** - * ReportController constructor. - */ - public function __construct() - { - parent::__construct(); - $this->middleware( - function ($request, $next) { - /** @var AccountRepositoryInterface accountRepository */ - $this->accountRepository = app(AccountRepositoryInterface::class); - - /** @var BudgetRepositoryInterface budgetRepository */ - $this->budgetRepository = app(BudgetRepositoryInterface::class); - - /** @var CategoryRepositoryInterface categoryRepository */ - $this->categoryRepository = app(CategoryRepositoryInterface::class); - - /** @var PopupReportInterface popupHelper */ - $this->popupHelper = app(PopupReportInterface::class); - - return $next($request); - } - ); - } + use RequestInformation, RenderPartialViews; /** * Generate popup view. + * * @param Request $request * * @return JsonResponse @@ -117,175 +77,5 @@ class ReportController extends Controller return response()->json(['html' => $html]); } - /** - * View for balance row. - * - * @param array $attributes - * - * @return string - * - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - */ - private function balanceAmount(array $attributes): string - { - $role = (int)$attributes['role']; - $budget = $this->budgetRepository->findNull((int)$attributes['budgetId']); - $account = $this->accountRepository->findNull((int)$attributes['accountId']); - - switch (true) { - case BalanceLine::ROLE_DEFAULTROLE === $role && null !== $budget && null !== $account: - // normal row with a budget: - $journals = $this->popupHelper->balanceForBudget($budget, $account, $attributes); - break; - case BalanceLine::ROLE_DEFAULTROLE === $role && null === $budget && null !== $account: - // normal row without a budget: - $journals = $this->popupHelper->balanceForNoBudget($account, $attributes); - $budget->name = (string)trans('firefly.no_budget'); - break; - case BalanceLine::ROLE_TAGROLE === $role: - // row with tag info. - return 'Firefly cannot handle this type of info-button (BalanceLine::TagRole)'; - } - try { - $view = view('popup.report.balance-amount', compact('journals', 'budget', 'account'))->render(); - } catch (Throwable $e) { - Log::error(sprintf('Could not render: %s', $e->getMessage())); - $view = 'Firefly III could not render the view. Please see the log files.'; - } - - return $view; - } - - /** - * View for spent in a single budget. - * - * @param array $attributes - * - * @return string - */ - private function budgetSpentAmount(array $attributes): string - { - $budget = $this->budgetRepository->findNull((int)$attributes['budgetId']); - if (null === $budget) { - return 'This is an unknown budget. Apologies.'; - } - $journals = $this->popupHelper->byBudget($budget, $attributes); - try { - $view = view('popup.report.budget-spent-amount', compact('journals', 'budget'))->render(); - } catch (Throwable $e) { - Log::error(sprintf('Could not render: %s', $e->getMessage())); - $view = 'Firefly III could not render the view. Please see the log files.'; - } - - return $view; - } - - /** - * View for transactions in a category. - * - * @param array $attributes - * - * @return string - */ - private function categoryEntry(array $attributes): string - { - $category = $this->categoryRepository->findNull((int)$attributes['categoryId']); - - if (null === $category) { - return 'This is an unknown category. Apologies.'; - } - - $journals = $this->popupHelper->byCategory($category, $attributes); - try { - $view = view('popup.report.category-entry', compact('journals', 'category'))->render(); - } catch (Throwable $e) { - Log::error(sprintf('Could not render: %s', $e->getMessage())); - $view = 'Firefly III could not render the view. Please see the log files.'; - } - - return $view; - } - - /** - * Returns all the expenses that went to the given expense account. - * - * @param array $attributes - * - * @return string - */ - private function expenseEntry(array $attributes): string - { - $account = $this->accountRepository->findNull((int)$attributes['accountId']); - - if (null === $account) { - return 'This is an unknown account. Apologies.'; - } - - $journals = $this->popupHelper->byExpenses($account, $attributes); - try { - $view = view('popup.report.expense-entry', compact('journals', 'account'))->render(); - } catch (Throwable $e) { - Log::error(sprintf('Could not render: %s', $e->getMessage())); - $view = 'Firefly III could not render the view. Please see the log files.'; - } - - return $view; - } - - /** - * Returns all the incomes that went to the given asset account. - * - * @param array $attributes - * - * @return string - */ - private function incomeEntry(array $attributes): string - { - $account = $this->accountRepository->findNull((int)$attributes['accountId']); - - if (null === $account) { - return 'This is an unknown category. Apologies.'; - } - - $journals = $this->popupHelper->byIncome($account, $attributes); - try { - $view = view('popup.report.income-entry', compact('journals', 'account'))->render(); - } catch (Throwable $e) { - Log::error(sprintf('Could not render: %s', $e->getMessage())); - $view = 'Firefly III could not render the view. Please see the log files.'; - } - - return $view; - } - - /** - * Parses attributes from URI. - * - * @param array $attributes - * - * @return array - */ - private function parseAttributes(array $attributes): array - { - $attributes['location'] = $attributes['location'] ?? ''; - $attributes['accounts'] = AccountList::routeBinder($attributes['accounts'] ?? '', new Route('get', '', [])); - try { - $attributes['startDate'] = Carbon::createFromFormat('Ymd', $attributes['startDate']); - } catch (InvalidArgumentException $e) { - Log::debug(sprintf('Not important error message: %s', $e->getMessage())); - $date = Carbon::create()->startOfMonth(); - $attributes['startDate'] = $date; - } - - try { - $attributes['endDate'] = Carbon::createFromFormat('Ymd', $attributes['endDate']); - } catch (InvalidArgumentException $e) { - Log::debug(sprintf('Not important error message: %s', $e->getMessage())); - $date = Carbon::create()->startOfMonth(); - $attributes['endDate'] = $date; - } - - return $attributes; - } } diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index 944a221915..a22196b051 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -35,15 +35,15 @@ use FireflyIII\Http\Requests\ProfileFormRequest; use FireflyIII\Http\Requests\TokenFormRequest; use FireflyIII\Models\Preference; use FireflyIII\Repositories\User\UserRepositoryInterface; +use FireflyIII\Support\Http\Controllers\CreateStuff; +use FireflyIII\Support\Http\Controllers\RequestInformation; use FireflyIII\User; use Google2FA; use Hash; use Illuminate\Contracts\Auth\Guard; use Illuminate\Support\Collection; use Laravel\Passport\ClientRepository; -use Laravel\Passport\Passport; use Log; -use phpseclib\Crypt\RSA; /** * Class ProfileController. @@ -54,6 +54,8 @@ use phpseclib\Crypt\RSA; */ class ProfileController extends Controller { + use RequestInformation, CreateStuff; + /** * ProfileController constructor. */ @@ -188,17 +190,10 @@ class ProfileController extends Controller /** * Enable 2FA screen. * - * @param UserRepositoryInterface $repository - * * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ - public function enable2FA(UserRepositoryInterface $repository) + public function enable2FA() { - /** @var User $user */ - $user = auth()->user(); - if ($repository->hasRole($user, 'demo')) { - return redirect(route('profile.index')); - } $hasSecret = (null !== app('preferences')->get('twoFactorAuthSecret')); // if we don't have a valid secret yet, redirect to the code page to get one. @@ -340,6 +335,7 @@ class ProfileController extends Controller session()->flash('success', (string)trans('firefly.saved_preferences')); app('preferences')->mark(); + return redirect(route('profile.index')); } @@ -371,6 +367,7 @@ class ProfileController extends Controller /** * Regenerate access token. + * * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ public function regenerate() @@ -437,64 +434,5 @@ class ProfileController extends Controller return redirect(route('login')); } - /** - * Validate users new password. - * - * @param User $user - * @param string $current - * @param string $new - * - * @return bool - * - * @throws ValidationException - */ - protected function validatePassword(User $user, string $current, string $new): bool - { - if (!Hash::check($current, $user->password)) { - throw new ValidationException((string)trans('firefly.invalid_current_password')); - } - if ($current === $new) { - throw new ValidationException((string)trans('firefly.should_change')); - } - - return true; - } - - /** - * Create new RSA keys. - */ - private function createOAuthKeys(): void - { - $rsa = new RSA(); - $keys = $rsa->createKey(4096); - - [$publicKey, $privateKey] = [ - Passport::keyPath('oauth-public.key'), - Passport::keyPath('oauth-private.key'), - ]; - - if (file_exists($publicKey) || file_exists($privateKey)) { - return; - } - // @codeCoverageIgnoreStart - Log::alert('NO OAuth keys were found. They have been created.'); - - file_put_contents($publicKey, array_get($keys, 'publickey')); - file_put_contents($privateKey, array_get($keys, 'privatekey')); - } - // @codeCoverageIgnoreEnd - - /** - * Get the domain of FF system. - * - * @return string - */ - private function getDomain(): string - { - $url = url()->to('/'); - $parts = parse_url($url); - - return $parts['host']; - } } diff --git a/app/Http/Controllers/Recurring/CreateController.php b/app/Http/Controllers/Recurring/CreateController.php index 38bde82fe2..88e5abd5a4 100644 --- a/app/Http/Controllers/Recurring/CreateController.php +++ b/app/Http/Controllers/Recurring/CreateController.php @@ -86,7 +86,7 @@ class CreateController extends Controller $this->rememberPreviousUri('recurring.create.uri'); } $request->session()->forget('recurring.create.fromStore'); - $repetitionEnds = [ + $repetitionEnds = [ 'forever' => (string)trans('firefly.repeat_forever'), 'until_date' => (string)trans('firefly.repeat_until_date'), 'times' => (string)trans('firefly.repeat_times'), diff --git a/app/Http/Controllers/Recurring/EditController.php b/app/Http/Controllers/Recurring/EditController.php index 400bd07a5b..ebae84cf4d 100644 --- a/app/Http/Controllers/Recurring/EditController.php +++ b/app/Http/Controllers/Recurring/EditController.php @@ -86,7 +86,7 @@ class EditController extends Controller $budgets = app('expandedform')->makeSelectListWithEmpty($this->budgets->getActiveBudgets()); /** @var RecurrenceRepetition $repetition */ - $repetition = $recurrence->recurrenceRepetitions()->first(); + $repetition = $recurrence->recurrenceRepetitions()->first(); $currentRepType = $repetition->repetition_type; if ('' !== $repetition->repetition_moment) { $currentRepType .= ',' . $repetition->repetition_moment; diff --git a/app/Http/Controllers/Recurring/IndexController.php b/app/Http/Controllers/Recurring/IndexController.php index 4aceec0e57..e26a5507fe 100644 --- a/app/Http/Controllers/Recurring/IndexController.php +++ b/app/Http/Controllers/Recurring/IndexController.php @@ -29,8 +29,11 @@ use FireflyIII\Exceptions\FireflyException; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\Recurrence; use FireflyIII\Repositories\Recurring\RecurringRepositoryInterface; +use FireflyIII\Support\Http\Controllers\GetConfigurationData; use FireflyIII\Transformers\RecurrenceTransformer; use Illuminate\Http\Request; +use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Support\Collection; use Symfony\Component\HttpFoundation\ParameterBag; /** @@ -39,6 +42,7 @@ use Symfony\Component\HttpFoundation\ParameterBag; */ class IndexController extends Controller { + use GetConfigurationData; /** @var RecurringRepositoryInterface Recurring repository */ private $recurring; @@ -77,19 +81,27 @@ class IndexController extends Controller $pageSize = (int)app('preferences')->get('listPageSize', 50)->data; $collection = $this->recurring->get(); + // split collection + $total = $collection->count(); + /** @var Collection $recurrences */ + $recurrences = $collection->slice(($page - 1) * $pageSize, $pageSize); $transformer = new RecurrenceTransformer(new ParameterBag); $recurring = []; /** @var Recurrence $recurrence */ - foreach ($collection as $recurrence) { + foreach ($recurrences as $recurrence) { $array = $transformer->transform($recurrence); $array['first_date'] = new Carbon($array['first_date']); $array['repeat_until'] = null === $array['repeat_until'] ? null : new Carbon($array['repeat_until']); $array['latest_date'] = null === $array['latest_date'] ? null : new Carbon($array['latest_date']); $recurring[] = $array; } + $paginator = new LengthAwarePaginator($recurring, $total, $pageSize, $page); + $paginator->setPath(route('recurring.index')); - return view('recurring.index', compact('recurring', 'page', 'pageSize')); + $this->verifyRecurringCronJob(); + + return view('recurring.index', compact('paginator', 'page', 'pageSize', 'total')); } /** diff --git a/app/Http/Controllers/Report/AccountController.php b/app/Http/Controllers/Report/AccountController.php index 68c0b078ef..01b67723af 100644 --- a/app/Http/Controllers/Report/AccountController.php +++ b/app/Http/Controllers/Report/AccountController.php @@ -27,6 +27,8 @@ use FireflyIII\Http\Controllers\Controller; use FireflyIII\Repositories\Account\AccountTaskerInterface; use FireflyIII\Support\CacheProperties; use Illuminate\Support\Collection; +use Log; +use Throwable; /** * Class AccountController. @@ -43,7 +45,6 @@ class AccountController extends Controller * * @return mixed|string * - * @throws \Throwable */ public function general(Collection $accounts, Carbon $start, Carbon $end) { @@ -60,8 +61,12 @@ class AccountController extends Controller /** @var AccountTaskerInterface $accountTasker */ $accountTasker = app(AccountTaskerInterface::class); $accountReport = $accountTasker->getAccountReport($accounts, $start, $end); - - $result = view('reports.partials.accounts', compact('accountReport'))->render(); + try { + $result = view('reports.partials.accounts', compact('accountReport'))->render(); + } catch (Throwable $e) { + Log::debug(sprintf('Could not render reports.partials.accounts: %s', $e->getMessage())); + $result = 'Could not render view.'; + } $cache->store($result); return $result; diff --git a/app/Http/Controllers/Report/BalanceController.php b/app/Http/Controllers/Report/BalanceController.php index 6ac7642c85..1c08c89f27 100644 --- a/app/Http/Controllers/Report/BalanceController.php +++ b/app/Http/Controllers/Report/BalanceController.php @@ -27,6 +27,8 @@ use FireflyIII\Helpers\Report\BalanceReportHelperInterface; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Support\CacheProperties; use Illuminate\Support\Collection; +use Log; +use Throwable; /** * Class BalanceController. @@ -42,7 +44,6 @@ class BalanceController extends Controller * @param Carbon $end * * @return mixed|string - * @throws \Throwable */ public function general(Collection $accounts, Carbon $start, Carbon $end) { @@ -58,8 +59,12 @@ class BalanceController extends Controller } $helper = app(BalanceReportHelperInterface::class); $balance = $helper->getBalanceReport($accounts, $start, $end); - - $result = view('reports.partials.balance', compact('balance'))->render(); + try { + $result = view('reports.partials.balance', compact('balance'))->render(); + } catch (Throwable $e) { + Log::debug(sprintf('Could not render reports.partials.balance: %s', $e->getMessage())); + $result = 'Could not render view.'; + } $cache->store($result); return $result; diff --git a/app/Http/Controllers/Report/BudgetController.php b/app/Http/Controllers/Report/BudgetController.php index 420fd485c7..3eddbc2839 100644 --- a/app/Http/Controllers/Report/BudgetController.php +++ b/app/Http/Controllers/Report/BudgetController.php @@ -27,13 +27,17 @@ use FireflyIII\Helpers\Report\BudgetReportHelperInterface; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Support\CacheProperties; +use FireflyIII\Support\Http\Controllers\BasicDataSupport; use Illuminate\Support\Collection; +use Log; +use Throwable; /** * Class BudgetController. */ class BudgetController extends Controller { + use BasicDataSupport; /** * Show partial overview of budgets. @@ -43,7 +47,6 @@ class BudgetController extends Controller * @param Carbon $end * * @return mixed|string - * @throws \Throwable */ public function general(Collection $accounts, Carbon $start, Carbon $end) { @@ -58,8 +61,12 @@ class BudgetController extends Controller } $helper = app(BudgetReportHelperInterface::class); $budgets = $helper->getBudgetReport($start, $end, $accounts); - - $result = view('reports.partials.budgets', compact('budgets'))->render(); + try { + $result = view('reports.partials.budgets', compact('budgets'))->render(); + } catch (Throwable $e) { + Log::debug(sprintf('Could not render reports.partials.budgets: %s', $e->getMessage())); + $result = 'Could not render view.'; + } $cache->store($result); return $result; @@ -74,7 +81,6 @@ class BudgetController extends Controller * @param Carbon $end * * @return mixed|string - * @throws \Throwable */ public function period(Collection $accounts, Carbon $start, Carbon $end) { @@ -93,39 +99,17 @@ class BudgetController extends Controller $budgets = $repository->getBudgets(); $data = $repository->getBudgetPeriodReport($budgets, $accounts, $start, $end); $data[0] = $repository->getNoBudgetPeriodReport($accounts, $start, $end); // append report data for "no budget" - $report = $this->filterBudgetPeriodReport($data); + $report = $this->filterPeriodReport($data); $periods = app('navigation')->listOfPeriods($start, $end); - - $result = view('reports.partials.budget-period', compact('report', 'periods'))->render(); + try { + $result = view('reports.partials.budget-period', compact('report', 'periods'))->render(); + } catch (Throwable $e) { + Log::debug(sprintf('Could not render reports.partials.budget-period: %s', $e->getMessage())); + $result = 'Could not render view.'; + } $cache->store($result); return $result; } - /** - * Filters empty results from getBudgetPeriodReport. - * - * @param array $data - * - * @return array - */ - private function filterBudgetPeriodReport(array $data): array - { - /** - * @var int - * @var array $set - */ - foreach ($data as $budgetId => $set) { - $sum = '0'; - foreach ($set['entries'] as $amount) { - $sum = bcadd($amount, $sum); - } - $data[$budgetId]['sum'] = $sum; - if (0 === bccomp('0', $sum)) { - unset($data[$budgetId]); - } - } - - return $data; - } } diff --git a/app/Http/Controllers/Report/CategoryController.php b/app/Http/Controllers/Report/CategoryController.php index 34693bf846..7214d556cb 100644 --- a/app/Http/Controllers/Report/CategoryController.php +++ b/app/Http/Controllers/Report/CategoryController.php @@ -27,6 +27,7 @@ use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\Category; use FireflyIII\Repositories\Category\CategoryRepositoryInterface; use FireflyIII\Support\CacheProperties; +use FireflyIII\Support\Http\Controllers\BasicDataSupport; use Illuminate\Support\Collection; use Log; use Throwable; @@ -36,6 +37,7 @@ use Throwable; */ class CategoryController extends Controller { + use BasicDataSupport; /** * Show overview of expenses in category. @@ -61,7 +63,7 @@ class CategoryController extends Controller $categories = $repository->getCategories(); $data = $repository->periodExpenses($categories, $accounts, $start, $end); $data[0] = $repository->periodExpensesNoCategory($accounts, $start, $end); - $report = $this->filterReport($data); + $report = $this->filterPeriodReport($data); $periods = app('navigation')->listOfPeriods($start, $end); try { $result = view('reports.partials.category-period', compact('report', 'periods'))->render(); @@ -101,7 +103,7 @@ class CategoryController extends Controller $categories = $repository->getCategories(); $data = $repository->periodIncome($categories, $accounts, $start, $end); $data[0] = $repository->periodIncomeNoCategory($accounts, $start, $end); - $report = $this->filterReport($data); + $report = $this->filterPeriodReport($data); $periods = app('navigation')->listOfPeriods($start, $end); try { $result = view('reports.partials.category-period', compact('report', 'periods'))->render(); @@ -166,26 +168,5 @@ class CategoryController extends Controller return $result; } - /** - * Filters empty results from category period report. - * - * @param array $data - * - * @return array - */ - private function filterReport(array $data): array - { - foreach ($data as $categoryId => $set) { - $sum = '0'; - foreach ($set['entries'] as $amount) { - $sum = bcadd($amount, $sum); - } - $data[$categoryId]['sum'] = $sum; - if (0 === bccomp('0', $sum)) { - unset($data[$categoryId]); - } - } - return $data; - } } diff --git a/app/Http/Controllers/Report/ExpenseController.php b/app/Http/Controllers/Report/ExpenseController.php index 7749607f7f..2e2b3a9429 100644 --- a/app/Http/Controllers/Report/ExpenseController.php +++ b/app/Http/Controllers/Report/ExpenseController.php @@ -23,14 +23,13 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Report; use Carbon\Carbon; -use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Http\Controllers\Controller; -use FireflyIII\Models\Account; -use FireflyIII\Models\AccountType; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Support\CacheProperties; +use FireflyIII\Support\Http\Controllers\AugumentData; use Illuminate\Support\Collection; use Log; use Throwable; @@ -42,6 +41,8 @@ use Throwable; */ class ExpenseController extends Controller { + use AugumentData; + /** @var AccountRepositoryInterface The account repository */ protected $accountRepository; @@ -258,11 +259,11 @@ class ExpenseController extends Controller $all = $all->merge($combi); } // get all expenses in period: - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($accounts); $collector->setOpposingAccounts($all); - $set = $collector->getJournals(); + $set = $collector->getTransactions(); $sorted = $set->sortBy( function (Transaction $transaction) { return (float)$transaction->transaction_amount; @@ -307,11 +308,11 @@ class ExpenseController extends Controller $all = $all->merge($combi); } // get all expenses in period: - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAccounts($accounts); $collector->setOpposingAccounts($all); - $set = $collector->getJournals(); + $set = $collector->getTransactions(); $sorted = $set->sortByDesc( function (Transaction $transaction) { return (float)$transaction->transaction_amount; @@ -328,30 +329,7 @@ class ExpenseController extends Controller return $result; } - /** - * Combine accounts into a single list. - * - * @param Collection $accounts - * - * @return array - */ - protected function combineAccounts(Collection $accounts): array - { - $combined = []; - /** @var Account $expenseAccount */ - foreach ($accounts as $expenseAccount) { - $collection = new Collection; - $collection->push($expenseAccount); - $revenue = $this->accountRepository->findByName($expenseAccount->name, [AccountType::REVENUE]); - if (null !== $revenue) { - $collection->push($revenue); - } - $combined[$expenseAccount->name] = $collection; - } - - return $combined; - } /** @noinspection MoreThanThreeArgumentsInspection */ /** @@ -367,13 +345,13 @@ class ExpenseController extends Controller * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - protected function earnedByCategory(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array + protected function earnedByCategory(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array // get data + augment with info { - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAccounts($assets); $collector->setOpposingAccounts($opposing)->withCategoryInformation(); - $set = $collector->getJournals(); + $set = $collector->getTransactions(); $sum = []; // loop to support multi currency foreach ($set as $transaction) { @@ -431,13 +409,13 @@ class ExpenseController extends Controller * * @return array */ - protected function earnedInPeriod(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array + protected function earnedInPeriod(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array // get data + augment with info { - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAccounts($assets); $collector->setOpposingAccounts($opposing); - $set = $collector->getJournals(); + $set = $collector->getTransactions(); $sum = [ 'grand_sum' => '0', 'per_currency' => [], @@ -478,13 +456,13 @@ class ExpenseController extends Controller * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - protected function spentByBudget(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array + protected function spentByBudget(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array // get data + augment with info { - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($assets); $collector->setOpposingAccounts($opposing)->withBudgetInformation(); - $set = $collector->getJournals(); + $set = $collector->getTransactions(); $sum = []; // loop to support multi currency foreach ($set as $transaction) { @@ -545,13 +523,13 @@ class ExpenseController extends Controller * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ - protected function spentByCategory(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array + protected function spentByCategory(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array // get data + augment with info { - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($assets); $collector->setOpposingAccounts($opposing)->withCategoryInformation(); - $set = $collector->getJournals(); + $set = $collector->getTransactions(); $sum = []; // loop to support multi currency foreach ($set as $transaction) { @@ -609,13 +587,13 @@ class ExpenseController extends Controller * * @return array */ - protected function spentInPeriod(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array + protected function spentInPeriod(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array // get data + augment with info { - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($assets); $collector->setOpposingAccounts($opposing); - $set = $collector->getJournals(); + $set = $collector->getTransactions(); $sum = [ 'grand_sum' => '0', 'per_currency' => [], diff --git a/app/Http/Controllers/Report/OperationsController.php b/app/Http/Controllers/Report/OperationsController.php index 04a5307c0b..383629446b 100644 --- a/app/Http/Controllers/Report/OperationsController.php +++ b/app/Http/Controllers/Report/OperationsController.php @@ -27,6 +27,8 @@ use FireflyIII\Http\Controllers\Controller; use FireflyIII\Repositories\Account\AccountTaskerInterface; use FireflyIII\Support\CacheProperties; use Illuminate\Support\Collection; +use Log; +use Throwable; /** * Class OperationsController. @@ -38,7 +40,7 @@ class OperationsController extends Controller private $tasker; /** - * + * OperationsController constructor. */ public function __construct() { @@ -63,7 +65,6 @@ class OperationsController extends Controller * @param Carbon $end * * @return mixed|string - * @throws \Throwable */ public function expenses(Collection $accounts, Carbon $start, Carbon $end) { @@ -78,7 +79,12 @@ class OperationsController extends Controller } $entries = $this->tasker->getExpenseReport($start, $end, $accounts); $type = 'expense-entry'; - $result = view('reports.partials.income-expenses', compact('entries', 'type'))->render(); + try { + $result = view('reports.partials.income-expenses', compact('entries', 'type'))->render(); + } catch (Throwable $e) { + Log::debug(sprintf('Could not render reports.partials.income-expense: %s', $e->getMessage())); + $result = 'Could not render view.'; + } $cache->store($result); return $result; @@ -92,7 +98,6 @@ class OperationsController extends Controller * @param Carbon $end * * @return string - * @throws \Throwable */ public function income(Collection $accounts, Carbon $start, Carbon $end): string { @@ -107,7 +112,12 @@ class OperationsController extends Controller } $entries = $this->tasker->getIncomeReport($start, $end, $accounts); $type = 'income-entry'; - $result = view('reports.partials.income-expenses', compact('entries', 'type'))->render(); + try { + $result = view('reports.partials.income-expenses', compact('entries', 'type'))->render(); + } catch (Throwable $e) { + Log::debug(sprintf('Could not render reports.partials.income-expenses: %s', $e->getMessage())); + $result = 'Could not render view.'; + } $cache->store($result); @@ -122,7 +132,6 @@ class OperationsController extends Controller * @param Carbon $end * * @return mixed|string - * @throws \Throwable */ public function operations(Collection $accounts, Carbon $start, Carbon $end) { @@ -155,8 +164,12 @@ class OperationsController extends Controller $expenses ) ); - - $result = view('reports.partials.operations', compact('incomeSum', 'expensesSum'))->render(); + try { + $result = view('reports.partials.operations', compact('incomeSum', 'expensesSum'))->render(); + } catch (Throwable $e) { + Log::debug(sprintf('Could not render reports.partials.operations: %s', $e->getMessage())); + $result = 'Could not render view.'; + } $cache->store($result); return $result; diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index ff77965966..82339668ff 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -28,11 +28,9 @@ use FireflyIII\Generator\Report\ReportGeneratorFactory; use FireflyIII\Helpers\Report\ReportHelperInterface; use FireflyIII\Http\Requests\ReportFormRequest; use FireflyIII\Models\AccountType; -use FireflyIII\Models\Tag; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; -use FireflyIII\Repositories\Category\CategoryRepositoryInterface; -use FireflyIII\Repositories\Tag\TagRepositoryInterface; +use FireflyIII\Support\Http\Controllers\RenderPartialViews; use Illuminate\Http\RedirectResponse; use Illuminate\Support\Collection; use Log; @@ -44,6 +42,8 @@ use Log; */ class ReportController extends Controller { + use RenderPartialViews; + /** @var ReportHelperInterface Helper interface. */ protected $helper; @@ -277,7 +277,6 @@ class ReportController extends Controller * * @return mixed * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @throws \Throwable */ public function options(string $reportType) { @@ -324,7 +323,7 @@ class ReportController extends Controller $accounts = implode(',', $request->getAccountList()->pluck('id')->toArray()); $categories = implode(',', $request->getCategoryList()->pluck('id')->toArray()); $budgets = implode(',', $request->getBudgetList()->pluck('id')->toArray()); - $tags = implode(',', $request->getTagList()->pluck('tag')->toArray()); + $tags = implode(',', $request->getTagList()->pluck('id')->toArray()); $expense = implode(',', $request->getExpenseList()->pluck('id')->toArray()); $uri = route('reports.index'); @@ -391,7 +390,6 @@ class ReportController extends Controller * @param Carbon $end * * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View|string - * * @throws \FireflyIII\Exceptions\FireflyException */ public function tagReport(Collection $accounts, Collection $tags, Carbon $start, Carbon $end) @@ -419,82 +417,5 @@ class ReportController extends Controller return $generator->generate(); } - /** - * Get options for account report. - * - * @return string - * @throws \Throwable - */ - private function accountReportOptions(): string - { - /** @var AccountRepositoryInterface $repository */ - $repository = app(AccountRepositoryInterface::class); - $expense = $repository->getActiveAccountsByType([AccountType::EXPENSE]); - $revenue = $repository->getActiveAccountsByType([AccountType::REVENUE]); - $set = new Collection; - $names = $revenue->pluck('name')->toArray(); - foreach ($expense as $exp) { - if (\in_array($exp->name, $names, true)) { - $set->push($exp); - } - } - return view('reports.options.account', compact('set'))->render(); - } - - /** - * Get options for budget report. - * @return string - * @throws \Throwable - */ - private function budgetReportOptions(): string - { - /** @var BudgetRepositoryInterface $repository */ - $repository = app(BudgetRepositoryInterface::class); - $budgets = $repository->getBudgets(); - - return view('reports.options.budget', compact('budgets'))->render(); - } - - /** - * Get options for category report. - * @return string - * @throws \Throwable - */ - private function categoryReportOptions(): string - { - /** @var CategoryRepositoryInterface $repository */ - $repository = app(CategoryRepositoryInterface::class); - $categories = $repository->getCategories(); - - return view('reports.options.category', compact('categories'))->render(); - } - - /** - * Get options for default report. - * @return string - * @throws \Throwable - */ - private function noReportOptions(): string - { - return view('reports.options.no-options')->render(); - } - - /** - * Get options for tag report. - * @return string - * @throws \Throwable - */ - private function tagReportOptions(): string - { - /** @var TagRepositoryInterface $repository */ - $repository = app(TagRepositoryInterface::class); - $tags = $repository->get()->sortBy( - function (Tag $tag) { - return $tag->tag; - } - ); - - return view('reports.options.tag', compact('tags'))->render(); - } } diff --git a/app/Http/Controllers/Rule/CreateController.php b/app/Http/Controllers/Rule/CreateController.php index e35b243640..2cc5c1eb06 100644 --- a/app/Http/Controllers/Rule/CreateController.php +++ b/app/Http/Controllers/Rule/CreateController.php @@ -28,9 +28,7 @@ use FireflyIII\Http\Controllers\Controller; use FireflyIII\Http\Requests\RuleFormRequest; use FireflyIII\Models\Bill; use FireflyIII\Models\RuleGroup; -use FireflyIII\Repositories\Bill\BillRepositoryInterface; use FireflyIII\Repositories\Rule\RuleRepositoryInterface; - use FireflyIII\Support\Http\Controllers\RuleManagement; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -43,8 +41,6 @@ use Throwable; class CreateController extends Controller { use RuleManagement; - /** @var BillRepositoryInterface Bill repository */ - private $billRepos; /** @var RuleRepositoryInterface Rule repository */ private $ruleRepos; @@ -60,7 +56,6 @@ class CreateController extends Controller app('view')->share('title', (string)trans('firefly.rules')); app('view')->share('mainTitleIcon', 'fa-random'); - $this->billRepos = app(BillRepositoryInterface::class); $this->ruleRepos = app(RuleRepositoryInterface::class); return $next($request); @@ -78,51 +73,33 @@ class CreateController extends Controller * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - public function create(Request $request, RuleGroup $ruleGroup) + public function create(Request $request, RuleGroup $ruleGroup = null) { $this->createDefaultRuleGroup(); $this->createDefaultRule(); - $bill = null; - $billId = (int)$request->get('fromBill'); - $preFilled = [ + $preFilled = [ 'strict' => true, ]; - $oldTriggers = []; - $oldActions = []; - $returnToBill = false; + $oldTriggers = []; + $oldActions = []; - if ('true' === $request->get('return')) { - $returnToBill = true; - } - - // has bill? - if ($billId > 0) { - $bill = $this->billRepos->find($billId); - } - - // has old input? + // restore actions and triggers from old input: if ($request->old()) { $oldTriggers = $this->getPreviousTriggers($request); $oldActions = $this->getPreviousActions($request); } - // has existing bill refered to in URI? - if (null !== $bill && !$request->old()) { - - // create some sensible defaults: - $preFilled['title'] = (string)trans('firefly.new_rule_for_bill_title', ['name' => $bill->name]); - $preFilled['description'] = (string)trans('firefly.new_rule_for_bill_description', ['name' => $bill->name]); - - - // get triggers and actions for bill: - $oldTriggers = $this->getTriggersForBill($bill); - $oldActions = $this->getActionsForBill($bill); - } $triggerCount = \count($oldTriggers); $actionCount = \count($oldActions); $subTitleIcon = 'fa-clone'; - $subTitle = (string)trans('firefly.make_new_rule', ['title' => $ruleGroup->title]); + // title depends on whether or not there is a rule group: + $subTitle = (string)trans('firefly.make_new_rule_no_group'); + if (null !== $ruleGroup) { + $subTitle = (string)trans('firefly.make_new_rule', ['title' => $ruleGroup->title]); + } + + // flash old data $request->session()->flash('preFilled', $preFilled); // put previous url in session if not redirect from store (not "create another"). @@ -132,11 +109,56 @@ class CreateController extends Controller session()->forget('rules.create.fromStore'); return view( - 'rules.rule.create', - compact( - 'subTitleIcon', 'oldTriggers', 'returnToBill', 'preFilled', 'bill', 'oldActions', 'triggerCount', 'actionCount', 'ruleGroup', - 'subTitle' - ) + 'rules.rule.create', compact('subTitleIcon', 'oldTriggers', 'preFilled', 'oldActions', 'triggerCount', 'actionCount', 'ruleGroup', 'subTitle') + ); + } + + /** + * Create a new rule. It will be stored under the given $ruleGroup. + * + * @param Request $request + * @param Bill $bill + * + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + public function createFromBill(Request $request, Bill $bill) + { + $request->session()->flash('info', (string)trans('firefly.instructions_rule_from_bill', ['name' => $bill->name])); + + $this->createDefaultRuleGroup(); + $this->createDefaultRule(); + $preFilled = [ + 'strict' => true, + 'title' => (string)trans('firefly.new_rule_for_bill_title', ['name' => $bill->name]), + 'description' => (string)trans('firefly.new_rule_for_bill_description', ['name' => $bill->name]), + ]; + + // make triggers and actions from the bill itself. + + // get triggers and actions for bill: + $oldTriggers = $this->getTriggersForBill($bill); + $oldActions = $this->getActionsForBill($bill); + + $triggerCount = \count($oldTriggers); + $actionCount = \count($oldActions); + $subTitleIcon = 'fa-clone'; + + // title depends on whether or not there is a rule group: + $subTitle = (string)trans('firefly.make_new_rule_no_group'); + + // flash old data + $request->session()->flash('preFilled', $preFilled); + + // put previous url in session if not redirect from store (not "create another"). + if (true !== session('rules.create.fromStore')) { + $this->rememberPreviousUri('rules.create.uri'); + } + session()->forget('rules.create.fromStore'); + + return view( + 'rules.rule.create', compact('subTitleIcon', 'oldTriggers', 'preFilled', 'oldActions', 'triggerCount', 'actionCount', 'subTitle') ); } @@ -184,7 +206,7 @@ class CreateController extends Controller * * @return array */ - private function getActionsForBill(Bill $bill): array + protected function getActionsForBill(Bill $bill): array // get info and augument { try { $result = view( @@ -215,7 +237,7 @@ class CreateController extends Controller * * @return array */ - private function getTriggersForBill(Bill $bill): array + protected function getTriggersForBill(Bill $bill): array // get info and augument { $result = []; $triggers = ['currency_is', 'amount_more', 'amount_less', 'description_contains']; diff --git a/app/Http/Controllers/Rule/EditController.php b/app/Http/Controllers/Rule/EditController.php index edd265844a..c4c4623e68 100644 --- a/app/Http/Controllers/Rule/EditController.php +++ b/app/Http/Controllers/Rule/EditController.php @@ -154,7 +154,7 @@ class EditController extends Controller * * @return array */ - private function getCurrentActions(Rule $rule): array + protected function getCurrentActions(Rule $rule): array // get info from object and present. { $index = 0; $actions = []; @@ -192,7 +192,7 @@ class EditController extends Controller * @return array * */ - private function getCurrentTriggers(Rule $rule): array + protected function getCurrentTriggers(Rule $rule): array // get info from object and present. { $index = 0; $triggers = []; diff --git a/app/Http/Controllers/Rule/IndexController.php b/app/Http/Controllers/Rule/IndexController.php index 5de05fc9a8..13a6d21978 100644 --- a/app/Http/Controllers/Rule/IndexController.php +++ b/app/Http/Controllers/Rule/IndexController.php @@ -130,7 +130,7 @@ class IndexController extends Controller /** * Move rule ip. - * + * * @param Rule $rule * * @return RedirectResponse|\Illuminate\Routing\Redirector diff --git a/app/Http/Controllers/Rule/SelectController.php b/app/Http/Controllers/Rule/SelectController.php index 6381fbf21c..04e30eb455 100644 --- a/app/Http/Controllers/Rule/SelectController.php +++ b/app/Http/Controllers/Rule/SelectController.php @@ -32,6 +32,7 @@ use FireflyIII\Http\Requests\TestRuleFormRequest; use FireflyIII\Jobs\ExecuteRuleOnExistingTransactions; use FireflyIII\Models\Rule; use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Support\Http\Controllers\RequestInformation; use FireflyIII\Support\Http\Controllers\RuleManagement; use FireflyIII\TransactionRules\TransactionMatcher; use FireflyIII\User; @@ -48,7 +49,7 @@ use Throwable; */ class SelectController extends Controller { - use RuleManagement; + use RuleManagement, RequestInformation; /** @var AccountRepositoryInterface The account repository */ private $accountRepos; @@ -118,13 +119,12 @@ class SelectController extends Controller { // does the user have shared accounts? $first = session('first')->format('Y-m-d'); - $today = Carbon::create()->format('Y-m-d'); + $today = Carbon::now()->format('Y-m-d'); $subTitle = (string)trans('firefly.apply_rule_selection', ['title' => $rule->title]); return view('rules.rule.select-transactions', compact('first', 'today', 'rule', 'subTitle')); } - /** * This method allows the user to test a certain set of rule triggers. The rule triggers are passed along * using the URL parameters (GET), and are usually put there using a Javascript thing. @@ -152,11 +152,13 @@ class SelectController extends Controller $limit = (int)config('firefly.test-triggers.limit'); $range = (int)config('firefly.test-triggers.range'); $matchingTransactions = new Collection; + $strict = $request->get('strict') === '1'; /** @var TransactionMatcher $matcher */ $matcher = app(TransactionMatcher::class); $matcher->setLimit($limit); $matcher->setRange($range); $matcher->setTriggers($triggers); + $matcher->setStrict($strict); try { $matchingTransactions = $matcher->findTransactionsByTriggers(); // @codeCoverageIgnoreStart @@ -257,32 +259,4 @@ class SelectController extends Controller } - /** - * Get a list of triggers. - * - * @param TestRuleFormRequest $request - * - * @return array - */ - private function getValidTriggerList(TestRuleFormRequest $request): array - { - $triggers = []; - $data = [ - 'rule-triggers' => $request->get('rule-trigger'), - 'rule-trigger-values' => $request->get('rule-trigger-value'), - 'rule-trigger-stop' => $request->get('rule-trigger-stop'), - ]; - if (\is_array($data['rule-triggers'])) { - foreach ($data['rule-triggers'] as $index => $triggerType) { - $data['rule-trigger-stop'][$index] = (int)($data['rule-trigger-stop'][$index] ?? 0.0); - $triggers[] = [ - 'type' => $triggerType, - 'value' => $data['rule-trigger-values'][$index], - 'stopProcessing' => 1 === (int)$data['rule-trigger-stop'][$index], - ]; - } - } - - return $triggers; - } } diff --git a/app/Http/Controllers/RuleGroupController.php b/app/Http/Controllers/RuleGroupController.php index c814776eb8..8a99cd2ab7 100644 --- a/app/Http/Controllers/RuleGroupController.php +++ b/app/Http/Controllers/RuleGroupController.php @@ -208,7 +208,7 @@ class RuleGroupController extends Controller public function selectTransactions(RuleGroup $ruleGroup) { $first = session('first')->format('Y-m-d'); - $today = Carbon::create()->format('Y-m-d'); + $today = Carbon::now()->format('Y-m-d'); $subTitle = (string)trans('firefly.apply_rule_group_selection', ['title' => $ruleGroup->title]); return view('rules.rule-group.select-transactions', compact('first', 'today', 'ruleGroup', 'subTitle')); diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index 6308bd1770..3799299e90 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -27,6 +27,8 @@ use FireflyIII\Support\Search\SearchInterface; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Collection; +use Log; +use Throwable; /** * Class SearchController. @@ -77,7 +79,6 @@ class SearchController extends Controller * @param SearchInterface $searcher * * @return \Illuminate\Http\JsonResponse - * @throws \Throwable */ public function search(Request $request, SearchInterface $searcher): JsonResponse { @@ -99,8 +100,14 @@ class SearchController extends Controller $transactions = $searcher->searchTransactions(); $cache->store($transactions); } - - $html = view('search.search', compact('transactions'))->render(); + try { + $html = view('search.search', compact('transactions'))->render(); + // @codeCoverageIgnoreStart + } catch (Throwable $e) { + Log::error(sprintf('Cannot render search.search: %s', $e->getMessage())); + $html = 'Could not render view.'; + } + // @codeCoverageIgnoreEnd return response()->json(['count' => $transactions->count(), 'html' => $html]); } diff --git a/app/Http/Controllers/System/CronController.php b/app/Http/Controllers/System/CronController.php new file mode 100644 index 0000000000..46a50c8384 --- /dev/null +++ b/app/Http/Controllers/System/CronController.php @@ -0,0 +1,65 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Http\Controllers\System; + +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Support\Cronjobs\RecurringCronjob; + +/** + * Class CronController + */ +class CronController +{ + /** + * @param string $token + * + * @return string + */ + public function cron(string $token): string + { + $results = []; + $results[] = $this->runRecurring(); + + return implode("' . trans('firefly.route_has_no_help') . '
'; + + // if no such route, log error and return default text. + if (!$help->hasRoute($route)) { + Log::error('No such route: ' . $route); + + return $content; + } + + // help content may be cached: + if ($help->inCache($route, $language)) { + $content = $help->getFromCache($route, $language); + Log::debug(sprintf('Help text %s was in cache.', $language)); + + return $content; + } + + // get help content from Github: + $content = $help->getFromGitHub($route, $language); + $originalLanguage = $language; + // content will have 0 length when Github failed. Try en_US when it does: + if ('' === $content) { + $language = 'en_US'; + + // also check cache first: + if ($help->inCache($route, $language)) { + Log::debug(sprintf('Help text %s was in cache.', $language)); + $content = $help->getFromCache($route, $language); + + return $content; + } + $baseHref = route('index'); + $helpString = sprintf( + '
%s
' . trans('firefly.route_has_no_help') . '
'; + } + + /** + * Get user's language. + * + * @return string + */ + protected function getLanguage(): string // get preference + { + /** @var string $language */ + $language = app('preferences')->get('language', config('firefly.default_language', 'en_US'))->data; + + return $language; + } + + /** + * @return string + */ + protected function getPageName(): string // get request info + { + return str_replace('.', '_', RouteFacade::currentRouteName()); + } + + /** + * Get the specific name of a page for intro. + * + * @return string + */ + protected function getSpecificPageName(): string // get request info + { + return null === RouteFacade::current()->parameter('what') ? '' : '_' . RouteFacade::current()->parameter('what'); + } + + /** + * Get transaction overview from journal. + * + * @param TransactionJournal $journal + * + * @return array + * @throws FireflyException + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + protected function getTransactionDataFromJournal(TransactionJournal $journal): array // convert object + { + // use collector to collect transactions. + $collector = app(TransactionCollectorInterface::class); + $collector->setUser(auth()->user()); + $collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation(); + // filter on specific journals. + $collector->setJournals(new Collection([$journal])); + $set = $collector->getTransactions(); + $transactions = []; + $transformer = new TransactionTransformer(new ParameterBag); + /** @var Transaction $transaction */ + foreach ($set as $transaction) { + $res = []; + if ((float)$transaction->transaction_amount > 0 && $journal->transactionType->type === TransactionType::DEPOSIT) { + $res = $transformer->transform($transaction); + } + if ((float)$transaction->transaction_amount < 0 && $journal->transactionType->type !== TransactionType::DEPOSIT) { + $res = $transformer->transform($transaction); + } + + if (\count($res) > 0) { + $res['amount'] = app('steam')->positive((string)$res['amount']); + $res['foreign_amount'] = app('steam')->positive((string)$res['foreign_amount']); + $transactions[] = $res; + } + } + + return $transactions; + } + + /** + * Get a list of triggers. + * + * @param TestRuleFormRequest $request + * + * @return array + */ + protected function getValidTriggerList(TestRuleFormRequest $request): array // process input + { + $triggers = []; + $data = $request->get('rule_triggers'); + if (\is_array($data)) { + foreach ($data as $index => $triggerInfo) { + $triggers[] = [ + 'type' => $triggerInfo['name'] ?? '', + 'value' => $triggerInfo['value'] ?? '', + 'stop_processing' => 1 === (int)($triggerInfo['stop_processing'] ?? '0'), + ]; + } + } + + return $triggers; + } + + /** + * Returns if user has seen demo. + * + * @return bool + */ + protected function hasSeenDemo(): bool // get request info + get preference + { + $page = $this->getPageName(); + $specificPage = $this->getSpecificPageName(); + + // indicator if user has seen the help for this page ( + special page): + $key = 'shown_demo_' . $page . $specificPage; + // is there an intro for this route? + $intro = config('intro.' . $page) ?? []; + $specialIntro = config('intro.' . $page . $specificPage) ?? []; + // some routes have a "what" parameter, which indicates a special page: + + $shownDemo = true; + // both must be array and either must be > 0 + if (\count($intro) > 0 || \count($specialIntro) > 0) { + $shownDemo = app('preferences')->get($key, false)->data; + Log::debug(sprintf('Check if user has already seen intro with key "%s". Result is %d', $key, $shownDemo)); + } + + return $shownDemo; + } + + /** + * Check if date is outside session range. + * + * @param Carbon $date + * + * @return bool + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ + protected function notInSessionRange(Carbon $date): bool // Validate a preference + { + /** @var Carbon $start */ + $start = session('start', Carbon::now()->startOfMonth()); + /** @var Carbon $end */ + $end = session('end', Carbon::now()->endOfMonth()); + $result = false; + if ($start->greaterThanOrEqualTo($date) && $end->greaterThanOrEqualTo($date)) { + $result = true; + } + // start and end in the past? use $end + if ($start->lessThanOrEqualTo($date) && $end->lessThanOrEqualTo($date)) { + $result = true; + } + + return $result; + } + + /** + * Parses attributes from URI. + * + * @param array $attributes + * + * @return array + */ + protected function parseAttributes(array $attributes): array // parse input + return result + { + $attributes['location'] = $attributes['location'] ?? ''; + $attributes['accounts'] = AccountList::routeBinder($attributes['accounts'] ?? '', new Route('get', '', [])); + try { + $attributes['startDate'] = Carbon::createFromFormat('Ymd', $attributes['startDate']); + } catch (InvalidArgumentException $e) { + Log::debug(sprintf('Not important error message: %s', $e->getMessage())); + $date = Carbon::now()->startOfMonth(); + $attributes['startDate'] = $date; + } + + try { + $attributes['endDate'] = Carbon::createFromFormat('Ymd', $attributes['endDate']); + } catch (InvalidArgumentException $e) { + Log::debug(sprintf('Not important error message: %s', $e->getMessage())); + $date = Carbon::now()->startOfMonth(); + $attributes['endDate'] = $date; + } + + return $attributes; + } + + /** + * Validate users new password. + * + * @param User $user + * @param string $current + * @param string $new + * + * @return bool + * + * @throws ValidationException + */ + protected function validatePassword(User $user, string $current, string $new): bool //get request info + { + if (!Hash::check($current, $user->password)) { + throw new ValidationException((string)trans('firefly.invalid_current_password')); + } + + if ($current === $new) { + throw new ValidationException((string)trans('firefly.should_change')); + } + + return true; + } + + /** + * Get a validator for an incoming registration request. + * + * @param array $data + * + * @return ValidatorContract + */ + protected function validator(array $data): ValidatorContract + { + return Validator::make( + $data, + [ + 'email' => 'required|string|email|max:255|unique:users', + 'password' => 'required|string|secure_password|confirmed', + ] + ); + } + +} \ No newline at end of file diff --git a/app/Support/Http/Controllers/RuleManagement.php b/app/Support/Http/Controllers/RuleManagement.php index 7033ae5e25..ee55a39d7d 100644 --- a/app/Support/Http/Controllers/RuleManagement.php +++ b/app/Support/Http/Controllers/RuleManagement.php @@ -32,7 +32,6 @@ use Throwable; /** * Trait RuleManagement * - * @package FireflyIII\Support\Http\Controllers */ trait RuleManagement { @@ -47,36 +46,36 @@ trait RuleManagement if (0 === $ruleRepository->count()) { $data = [ 'rule_group_id' => $ruleRepository->getFirstRuleGroup()->id, - 'stop-processing' => 0, + 'stop_processing' => 0, 'title' => (string)trans('firefly.default_rule_name'), 'description' => (string)trans('firefly.default_rule_description'), 'trigger' => 'store-journal', 'strict' => true, - 'rule-triggers' => [ + 'rule_triggers' => [ [ 'name' => 'description_is', 'value' => (string)trans('firefly.default_rule_trigger_description'), - 'stop-processing' => false, + 'stop_processing' => false, ], [ 'name' => 'from_account_is', 'value' => (string)trans('firefly.default_rule_trigger_from_account'), - 'stop-processing' => false, + 'stop_processing' => false, ], ], - 'rule-actions' => [ + 'rule_actions' => [ [ 'name' => 'prepend_description', 'value' => (string)trans('firefly.default_rule_action_prepend'), - 'stop-processing' => false, + 'stop_processing' => false, ], [ 'name' => 'set_category', 'value' => (string)trans('firefly.default_rule_action_set_category'), - 'stop-processing' => false, + 'stop_processing' => false, ], ], ]; @@ -93,33 +92,30 @@ trait RuleManagement */ protected function getPreviousActions(Request $request): array { - $newIndex = 0; - $actions = []; - /** @var array $oldActions */ - $oldActions = \is_array($request->old('rule-action')) ? $request->old('rule-action') : []; - foreach ($oldActions as $index => $entry) { - $count = ($newIndex + 1); - $checked = isset($request->old('rule-action-stop')[$index]) ? true : false; - try { - $actions[] = view( - 'rules.partials.action', - [ - 'oldAction' => $entry, - 'oldValue' => $request->old('rule-action-value')[$index], - 'oldChecked' => $checked, - 'count' => $count, - ] - )->render(); - // @codeCoverageIgnoreStart - } catch (Throwable $e) { - Log::debug(sprintf('Throwable was thrown in getPreviousActions(): %s', $e->getMessage())); - Log::error($e->getTraceAsString()); + $index = 0; + $triggers = []; + $oldInput = $request->old('rule_actions'); + if (\is_array($oldInput)) { + foreach ($oldInput as $oldAction) { + try { + $triggers[] = view( + 'rules.partials.action', + [ + 'oldAction' => $oldAction['name'], + 'oldValue' => $oldAction['value'], + 'oldChecked' => 1 === (int)($oldAction['stop_processing'] ?? '0'), + 'count' => $index + 1, + ] + )->render(); + } catch (Throwable $e) { + Log::debug(sprintf('Throwable was thrown in getPreviousActions(): %s', $e->getMessage())); + Log::error($e->getTraceAsString()); + } + $index++; } - // @codeCoverageIgnoreEnd - ++$newIndex; } - return $actions; + return $triggers; } /** @@ -129,30 +125,27 @@ trait RuleManagement */ protected function getPreviousTriggers(Request $request): array { - $newIndex = 0; + $index = 0; $triggers = []; - /** @var array $oldTriggers */ - $oldTriggers = \is_array($request->old('rule-trigger')) ? $request->old('rule-trigger') : []; - foreach ($oldTriggers as $index => $entry) { - $count = ($newIndex + 1); - $oldChecked = isset($request->old('rule-trigger-stop')[$index]) ? true : false; - try { - $triggers[] = view( - 'rules.partials.trigger', - [ - 'oldTrigger' => $entry, - 'oldValue' => $request->old('rule-trigger-value')[$index], - 'oldChecked' => $oldChecked, - 'count' => $count, - ] - )->render(); - // @codeCoverageIgnoreStart - } catch (Throwable $e) { - Log::debug(sprintf('Throwable was thrown in getPreviousTriggers(): %s', $e->getMessage())); - Log::error($e->getTraceAsString()); + $oldInput = $request->old('rule_triggers'); + if (\is_array($oldInput)) { + foreach ($oldInput as $oldTrigger) { + try { + $triggers[] = view( + 'rules.partials.trigger', + [ + 'oldTrigger' => $oldTrigger['name'], + 'oldValue' => $oldTrigger['value'], + 'oldChecked' => 1 === (int)($oldTrigger['stop_processing'] ?? '0'), + 'count' => $index + 1, + ] + )->render(); + } catch (Throwable $e) { + Log::debug(sprintf('Throwable was thrown in getPreviousTriggers(): %s', $e->getMessage())); + Log::error($e->getTraceAsString()); + } + $index++; } - // @codeCoverageIgnoreEnd - ++$newIndex; } return $triggers; diff --git a/app/Support/Http/Controllers/TransactionCalculation.php b/app/Support/Http/Controllers/TransactionCalculation.php new file mode 100644 index 0000000000..6c318fa90b --- /dev/null +++ b/app/Support/Http/Controllers/TransactionCalculation.php @@ -0,0 +1,202 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Support\Http\Controllers; + +use Carbon\Carbon; +use FireflyIII\Helpers\Collector\TransactionCollectorInterface; +use FireflyIII\Helpers\Filter\NegativeAmountFilter; +use FireflyIII\Helpers\Filter\OpposingAccountFilter; +use FireflyIII\Helpers\Filter\PositiveAmountFilter; +use FireflyIII\Helpers\Filter\TransferFilter; +use FireflyIII\Models\TransactionType; +use Illuminate\Support\Collection; + +/** + * Trait TransactionCalculation + * + */ +trait TransactionCalculation +{ + /** + * Get all expenses for a set of accounts. + * + * @param Collection $accounts + * @param Collection $opposing + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + protected function getExpensesForOpposing(Collection $accounts, Collection $opposing, Carbon $start, Carbon $end): Collection // get data + augument + { + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); + $collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setOpposingAccounts($opposing); + + return $collector->getTransactions(); + } + + /** + * Get all expenses by tags. + * + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + * + */ + protected function getExpensesForTags(Collection $accounts, Collection $tags, Carbon $start, Carbon $end): Collection // get data + augument + { + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); + $collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER]) + ->setTags($tags)->withOpposingAccount(); + $collector->removeFilter(TransferFilter::class); + + $collector->addFilter(OpposingAccountFilter::class); + $collector->addFilter(PositiveAmountFilter::class); + + return $collector->getTransactions(); + } + + /** + * Helper function that collects expenses for the given budgets. + * + * @param Collection $accounts + * @param Collection $budgets + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + protected function getExpensesInBudgets(Collection $accounts, Collection $budgets, Carbon $start, Carbon $end): Collection // get data + augment with info + { + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); + $collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER]) + ->setBudgets($budgets)->withOpposingAccount(); + $collector->removeFilter(TransferFilter::class); + + $collector->addFilter(OpposingAccountFilter::class); + $collector->addFilter(PositiveAmountFilter::class); + + return $collector->getTransactions(); + } + + /** + * Get all expenses in a period for categories. + * + * @param Collection $accounts + * @param Collection $categories + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + * + * + */ + protected function getExpensesInCategories(Collection $accounts, Collection $categories, Carbon $start, Carbon $end): Collection // get data + augument + { + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); + $collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER]) + ->setCategories($categories)->withOpposingAccount(); + $collector->removeFilter(TransferFilter::class); + + $collector->addFilter(OpposingAccountFilter::class); + $collector->addFilter(PositiveAmountFilter::class); + + return $collector->getTransactions(); + } + + /** + * Get all income for a period and a bunch of categories. + * + * @param Collection $accounts + * @param Collection $categories + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + protected function getIncomeForCategories(Collection $accounts, Collection $categories, Carbon $start, Carbon $end): Collection // get data + augument + { + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); + $collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER]) + ->setCategories($categories)->withOpposingAccount(); + + $collector->addFilter(OpposingAccountFilter::class); + $collector->addFilter(NegativeAmountFilter::class); + + return $collector->getTransactions(); + } + + /** + * Get the income for a set of accounts. + * + * @param Collection $accounts + * @param Collection $opposing + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + protected function getIncomeForOpposing(Collection $accounts, Collection $opposing, Carbon $start, Carbon $end): Collection // get data + augument + { + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); + $collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setOpposingAccounts($opposing); + + return $collector->getTransactions(); + } + + /** + * Get all income by tag. + * + * @param Collection $accounts + * @param Collection $tags + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + protected function getIncomeForTags(Collection $accounts, Collection $tags, Carbon $start, Carbon $end): Collection // get data + augument + { + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); + $collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER]) + ->setTags($tags)->withOpposingAccount(); + + $collector->addFilter(OpposingAccountFilter::class); + $collector->addFilter(NegativeAmountFilter::class); + + return $collector->getTransactions(); + } + +} \ No newline at end of file diff --git a/app/Support/Http/Controllers/UserNavigation.php b/app/Support/Http/Controllers/UserNavigation.php new file mode 100644 index 0000000000..3d55d27f95 --- /dev/null +++ b/app/Support/Http/Controllers/UserNavigation.php @@ -0,0 +1,126 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Support\Http\Controllers; + +use FireflyIII\Models\Account; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionJournal; +use Illuminate\Http\RedirectResponse; +use Log; +use URL; + +/** + * Trait UserNavigation + * + */ +trait UserNavigation +{ + /** + * Functionality:. + * + * - If the $identifier contains the word "delete" then a remembered uri with the text "/show/" in it will not be returned but instead the index (/) + * will be returned. + * - If the remembered uri contains "javascript/" the remembered uri will not be returned but instead the index (/) will be returned. + * + * @param string $identifier + * + * @return string + */ + protected function getPreviousUri(string $identifier): string + { + $uri = (string)session($identifier); + if (!(false === strpos($identifier, 'delete')) && !(false === strpos($uri, '/show/'))) { + $uri = $this->redirectUri; + } + if (!(false === strpos($uri, 'jscript'))) { + $uri = $this->redirectUri; // @codeCoverageIgnore + } + + return $uri; + } + + /** + * Redirect to asset account that transaction belongs to. + * + * @param TransactionJournal $journal + * + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + */ + protected function redirectToAccount(TransactionJournal $journal) + { + $valid = [AccountType::DEFAULT, AccountType::ASSET]; + $transactions = $journal->transactions; + /** @var Transaction $transaction */ + foreach ($transactions as $transaction) { + $account = $transaction->account; + if (\in_array($account->accountType->type, $valid, true)) { + return redirect(route('accounts.show', [$account->id])); + } + } + // @codeCoverageIgnoreStart + session()->flash('error', (string)trans('firefly.cannot_redirect_to_account')); + + return redirect(route('index')); + // @codeCoverageIgnoreEnd + } + + /** + * @param Account $account + * + * @return RedirectResponse|\Illuminate\Routing\Redirector + */ + protected function redirectToOriginalAccount(Account $account) + { + /** @var Transaction $transaction */ + $transaction = $account->transactions()->first(); + if (null === $transaction) { + app('session')->flash('error', trans('firefly.account_missing_transaction', ['name' => $account->name, 'id' => $account->id])); + Log::error(sprintf('Expected a transaction. Account #%d has none. BEEP, error.', $account->id)); + + return redirect(route('index')); + } + + $journal = $transaction->transactionJournal; + /** @var Transaction $opposingTransaction */ + $opposingTransaction = $journal->transactions()->where('transactions.id', '!=', $transaction->id)->first(); + + if (null === $opposingTransaction) { + app('session')->flash('error', trans('firefly.account_missing_transaction', ['name' => $account->name, 'id' => $account->id])); + Log::error(sprintf('Expected an opposing transaction. Account #%d has none. BEEP, error.', $account->id)); + } + + return redirect(route('accounts.show', [$opposingTransaction->account_id])); + } + + /** + * Remember previous URL. + * + * @param string $identifier + */ + protected function rememberPreviousUri(string $identifier): void + { + session()->put($identifier, URL::previous()); + } +} \ No newline at end of file diff --git a/app/Support/Import/JobConfiguration/Bunq/ChooseAccountsHandler.php b/app/Support/Import/JobConfiguration/Bunq/ChooseAccountsHandler.php index 6a801deccc..4a357846e0 100644 --- a/app/Support/Import/JobConfiguration/Bunq/ChooseAccountsHandler.php +++ b/app/Support/Import/JobConfiguration/Bunq/ChooseAccountsHandler.php @@ -81,6 +81,17 @@ class ChooseAccountsHandler implements BunqJobConfigurationInterface $mapping = $data['account_mapping'] ?? []; $applyRules = 1 === (int)$data['apply_rules']; $final = []; + + /* + * $ibanToAsset is used to map bunq IBAN's to Firefly III asset accounts. The array is structured like this: + * 12BUNQ123456.. => 1, + * 12BUNQ928811.. => 4, + * + * And contains the bunq asset account iban (left) and the FF3 asset ID (right). + * + * This is used to properly map transfers. + */ + $ibanToAsset = []; if (0 === \count($accounts)) { throw new FireflyException('No bunq accounts found. Import cannot continue.'); // @codeCoverageIgnore } @@ -95,11 +106,16 @@ class ChooseAccountsHandler implements BunqJobConfigurationInterface $localId = (int)$localId; // validate each - $bunqId = $this->validBunqAccount($bunqId); - $accountId = $this->validLocalAccount($localId); + $bunqId = $this->validBunqAccount($bunqId); + $accountId = $this->validLocalAccount($localId); + $bunqIban = $this->getBunqIban($bunqId); + if (null !== $bunqIban) { + $ibanToAsset[$bunqIban] = $accountId; + } $final[$bunqId] = $accountId; } $config['mapping'] = $final; + $config['bunq-iban'] = $ibanToAsset; $config['apply-rules'] = $applyRules; $this->repository->setConfiguration($this->importJob, $config); @@ -169,6 +185,25 @@ class ChooseAccountsHandler implements BunqJobConfigurationInterface $this->accountRepository->setUser($importJob->user); } + /** + * @param int $bunqId + * + * @return null|string + */ + private function getBunqIban(int $bunqId): ?string + { + $config = $this->repository->getConfiguration($this->importJob); + $accounts = $config['accounts'] ?? []; + /** @var array $bunqAccount */ + foreach ($accounts as $bunqAccount) { + if ((int)$bunqAccount['id'] === $bunqId) { + return $bunqAccount['iban'] ?? null; + } + } + + return null; + } + /** * @param int $currencyId * diff --git a/app/Support/Import/JobConfiguration/File/ConfigureMappingHandler.php b/app/Support/Import/JobConfiguration/File/ConfigureMappingHandler.php index 568ff9d384..e8803b31b0 100644 --- a/app/Support/Import/JobConfiguration/File/ConfigureMappingHandler.php +++ b/app/Support/Import/JobConfiguration/File/ConfigureMappingHandler.php @@ -267,6 +267,7 @@ class ConfigureMappingHandler implements FileConfigurationInterface * - Add the value to the list of "values" that the user must map. * * @param Reader $reader + * @param array $config * @param array $columnConfig * * @return array diff --git a/app/Support/Import/JobConfiguration/File/NewFileJobHandler.php b/app/Support/Import/JobConfiguration/File/NewFileJobHandler.php index 047f8c5d39..6ce648d5aa 100644 --- a/app/Support/Import/JobConfiguration/File/NewFileJobHandler.php +++ b/app/Support/Import/JobConfiguration/File/NewFileJobHandler.php @@ -101,7 +101,7 @@ class NewFileJobHandler implements FileConfigurationInterface } /** - * @param ImportJob $job + * @param ImportJob $importJob */ public function setImportJob(ImportJob $importJob): void { @@ -114,7 +114,6 @@ class NewFileJobHandler implements FileConfigurationInterface /** * Store config from job. * - * @throws FireflyException */ public function storeConfiguration(): void { diff --git a/app/Support/Import/JobConfiguration/Ynab/NewYnabJobHandler.php b/app/Support/Import/JobConfiguration/Ynab/NewYnabJobHandler.php new file mode 100644 index 0000000000..5a9ddaccda --- /dev/null +++ b/app/Support/Import/JobConfiguration/Ynab/NewYnabJobHandler.php @@ -0,0 +1,258 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Support\Import\JobConfiguration\Ynab; + +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\ImportJob; +use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; +use GuzzleHttp\Client; +use GuzzleHttp\Exception\GuzzleException; +use Illuminate\Support\MessageBag; +use Log; +use RuntimeException; + +/** + * Class NewYnabJobHandler + */ +class NewYnabJobHandler implements YnabJobConfigurationInterface +{ + /** @var ImportJob */ + private $importJob; + /** @var ImportJobRepositoryInterface */ + private $repository; + + /** + * Return true when this stage is complete. + * + * @return bool + * @throws FireflyException + */ + public function configurationComplete(): bool + { + if (!$this->hasRefreshToken()) { + Log::debug('YNAB NewYnabJobHandler configurationComplete: stage is new, no refresh token, return false'); + + return false; + } + if ($this->hasRefreshToken() && $this->hasClientId() && $this->hasClientSecret()) { + Log::debug('YNAB NewYnabJobHandler configurationComplete: stage is new, has a refresh token, return true'); + // need to grab access token using refresh token + $this->getAccessToken(); + $this->repository->setStatus($this->importJob, 'ready_to_run'); + $this->repository->setStage($this->importJob, 'get_budgets'); + + return true; + } + Log::error('YNAB NewYnabJobHandler configurationComplete: something broke, return true'); + + return true; + } + + /** + * Store the job configuration. There is never anything to store for this stage. + * + * @param array $data + * + * @return MessageBag + */ + public function configureJob(array $data): MessageBag + { + Log::debug('YNAB NewYnabJobHandler configureJob: nothing to do.'); + + return new MessageBag; + } + + /** + * Get data for config view. + * + * @return array + * @throws \Psr\Container\NotFoundExceptionInterface + * @throws \Psr\Container\ContainerExceptionInterface + */ + public function getNextData(): array + { + $data = []; + // here we update the job so it can redirect properly to YNAB + if (!$this->hasRefreshToken() && $this->hasClientSecret() && $this->hasClientId()) { + // update stage to make sure we catch the token. + $this->repository->setStage($this->importJob, 'catch-auth-code'); + $clientId = app('preferences')->get('ynab_client_id')->data; + $callBackUri = route('import.callback.ynab'); + $uri = sprintf( + 'https://app.youneedabudget.com/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code&state=%s', $clientId, $callBackUri, + $this->importJob->key + ); + $data['token-url'] = $uri; + Log::debug(sprintf('YNAB getNextData: URI to redirect to is %s', $uri)); + } + + return $data; + } + + /** + * Get the view for this stage. + * + * @return string + */ + public function getNextView(): string + { + Log::debug('Return YNAB redirect view.'); + + return 'import.ynab.redirect'; + } + + /** + * Set the import job. + * + * @param ImportJob $importJob + */ + public function setImportJob(ImportJob $importJob): void + { + $this->importJob = $importJob; + $this->repository = app(ImportJobRepositoryInterface::class); + $this->repository->setUser($importJob->user); + } + + + /** + * @throws \Psr\Container\NotFoundExceptionInterface + * @throws \Psr\Container\ContainerExceptionInterface + * @throws FireflyException + */ + private function getAccessToken(): void + { + $clientId = app('preferences')->get('ynab_client_id')->data; + $clientSecret = app('preferences')->get('ynab_client_secret')->data; + $refreshToken = app('preferences')->get('ynab_refresh_token')->data; + $uri = sprintf( + 'https://app.youneedabudget.com/oauth/token?client_id=%s&client_secret=%s&grant_type=refresh_token&refresh_token=%s', $clientId, $clientSecret, + $refreshToken + ); + + $client = new Client(); + try { + $res = $client->request('post', $uri, []); + } catch (GuzzleException $e) { + Log::error($e->getMessage()); + Log::error($e->getTraceAsString()); + throw new FireflyException($e->getMessage()); + } + $statusCode = $res->getStatusCode(); + try { + $content = trim($res->getBody()->getContents()); + } catch (RuntimeException $e) { + Log::error($e->getMessage()); + Log::error($e->getTraceAsString()); + throw new FireflyException($e->getMessage()); + } + $json = json_decode($content, true) ?? []; + Log::debug(sprintf('Status code from YNAB is %d', $statusCode)); + Log::debug(sprintf('Body of result is %s', $content), $json); + + // store refresh token (if present?) as preference + // store token in job: + $configuration = $this->repository->getConfiguration($this->importJob); + $configuration['access_token'] = $json['access_token']; + $configuration['access_token_expires'] = (int)$json['created_at'] + (int)$json['expires_in']; + $this->repository->setConfiguration($this->importJob, $configuration); + + // also store new refresh token: + $refreshToken = (string)($json['refresh_token'] ?? ''); + if ('' !== $refreshToken) { + app('preferences')->set('ynab_refresh_token', $refreshToken); + } + + + Log::debug('end of NewYnabJobHandler::getAccessToken()'); + } + + /** + * Check if we have the client ID. + * + * @return bool + */ + private function hasClientId(): bool + { + $clientId = app('preferences')->getForUser($this->importJob->user, 'ynab_client_id', null); + if (null === $clientId) { + Log::debug('user has no YNAB client ID'); + + return false; + } + if ('' === (string)$clientId->data) { + Log::debug('user has no YNAB client ID (empty)'); + + return false; + } + Log::debug('user has a YNAB client ID'); + + return true; + } + + /** + * Check if we have the client secret + * + * @return bool + */ + private function hasClientSecret(): bool + { + $clientSecret = app('preferences')->getForUser($this->importJob->user, 'ynab_client_secret', null); + if (null === $clientSecret) { + Log::debug('user has no YNAB client secret'); + + return false; + } + if ('' === (string)$clientSecret->data) { + Log::debug('user has no YNAB client secret (empty)'); + + return false; + } + Log::debug('user has a YNAB client secret'); + + return true; + } + + /** + * @return bool + * @throws \Psr\Container\NotFoundExceptionInterface + * @throws \Psr\Container\ContainerExceptionInterface + */ + private function hasRefreshToken(): bool + { + $preference = app('preferences')->get('ynab_refresh_token'); + if (null === $preference) { + Log::debug('user has no YNAB refresh token.'); + + return false; + } + if ('' === (string)$preference->data) { + Log::debug('user has no YNAB refresh token (empty).'); + + return false; + } + Log::debug(sprintf('user has YNAB refresh token: %s', $preference->data)); + + return true; + } +} \ No newline at end of file diff --git a/app/Support/Import/JobConfiguration/Ynab/SelectAccountsHandler.php b/app/Support/Import/JobConfiguration/Ynab/SelectAccountsHandler.php new file mode 100644 index 0000000000..4dbf8fa49b --- /dev/null +++ b/app/Support/Import/JobConfiguration/Ynab/SelectAccountsHandler.php @@ -0,0 +1,237 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Support\Import\JobConfiguration\Ynab; + + +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\Account; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\ImportJob; +use FireflyIII\Models\TransactionCurrency; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; +use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; +use Illuminate\Support\MessageBag; +use Log; + +/** + * Class SelectAccountsHandler + */ +class SelectAccountsHandler implements YnabJobConfigurationInterface +{ + /** @var AccountRepositoryInterface */ + private $accountRepository; + /** @var CurrencyRepositoryInterface */ + private $currencyRepository; + /** @var ImportJob */ + private $importJob; + /** @var ImportJobRepositoryInterface */ + private $repository; + + /** + * Return true when this stage is complete. + * + * @return bool + */ + public function configurationComplete(): bool + { + Log::debug('Now in SelectAccountsHandler::configurationComplete()'); + $config = $this->importJob->configuration; + $mapping = $config['mapping'] ?? []; + if (\count($mapping) > 0) { + // mapping is complete. + Log::debug('Looks like user has mapped YNAB accounts to Firefly III accounts', $mapping); + $this->repository->setStage($this->importJob, 'go-for-import'); + + return true; + } + + return false; + } + + /** + * Store the job configuration. + * + * @param array $data + * + * @return MessageBag + */ + public function configureJob(array $data): MessageBag + { + Log::debug('Now in SelectAccountsHandler::configureJob()', $data); + $config = $this->importJob->configuration; + $mapping = $data['account_mapping'] ?? []; + $final = []; + $applyRules = 1 === (int)($data['apply_rules'] ?? 0); + foreach ($mapping as $ynabId => $localId) { + // validate each + $ynabId = $this->validYnabAccount($ynabId); + $accountId = $this->validLocalAccount((int)$localId); + $final[$ynabId] = $accountId; + + } + Log::debug('Final mapping is:', $final); + $messages = new MessageBag; + $config['mapping'] = $final; + $config['apply-rules'] = $applyRules; + $this->repository->setConfiguration($this->importJob, $config); + if ($final === ['' => 0] || 0 === \count($final)) { + $messages->add('count', (string)trans('import.ynab_no_mapping')); + } + + return $messages; + } + + /** + * Get data for config view. + * + * @return array + * @throws FireflyException + */ + public function getNextData(): array + { + + Log::debug('Now in ChooseAccountsHandler::getnextData()'); + $config = $this->importJob->configuration; + $ynabAccounts = $config['accounts'] ?? []; + $budget = $this->getSelectedBudget(); + if (0 === \count($ynabAccounts)) { + throw new FireflyException('It seems you have no accounts with this budget. The import cannot continue.'); // @codeCoverageIgnore + } + // list the users accounts: + $ffAccounts = $this->accountRepository->getAccountsByType([AccountType::ASSET]); + + $array = []; + /** @var Account $account */ + foreach ($ffAccounts as $account) { + $accountId = $account->id; + $currencyId = (int)$this->accountRepository->getMetaValue($account, 'currency_id'); + $currency = $this->getCurrency($currencyId); + $array[$accountId] = [ + 'name' => $account->name, + 'iban' => $account->iban, + 'code' => $currency->code, + ]; + } + + return [ + 'budget' => $budget, + 'ynab_accounts' => $ynabAccounts, + 'ff_accounts' => $array, + ]; + } + + /** + * Get the view for this stage. + * + * @return string + */ + public function getNextView(): string + { + return 'import.ynab.accounts'; + } + + /** + * @codeCoverageIgnore + * Set the import job. + * + * @param ImportJob $importJob + */ + public function setImportJob(ImportJob $importJob): void + { + $this->importJob = $importJob; + $this->repository = app(ImportJobRepositoryInterface::class); + $this->accountRepository = app(AccountRepositoryInterface::class); + $this->currencyRepository = app(CurrencyRepositoryInterface::class); + $this->repository->setUser($importJob->user); + $this->currencyRepository->setUser($importJob->user); + $this->accountRepository->setUser($importJob->user); + } + + /** + * @param int $currencyId + * + * @return TransactionCurrency + */ + private function getCurrency(int $currencyId): TransactionCurrency + { + $currency = $this->currencyRepository->findNull($currencyId); + if (null === $currency) { + return app('amount')->getDefaultCurrencyByUser($this->importJob->user); + } + + return $currency; + + } + + /** + * @return array + */ + private function getSelectedBudget(): array + { + $config = $this->repository->getConfiguration($this->importJob); + $budgets = $config['budgets'] ?? []; + $selected = $config['selected_budget'] ?? ''; + foreach ($budgets as $budget) { + if ($budget['id'] === $selected) { + return $budget; + } + } + + return $budgets[0] ?? []; + } + + /** + * @param int $accountId + * + * @return int + */ + private function validLocalAccount(int $accountId): int + { + $account = $this->accountRepository->findNull($accountId); + if (null === $account) { + return 0; + } + + return $accountId; + } + + /** + * @param string $accountId + * + * @return string + */ + private function validYnabAccount(string $accountId): string + { + $config = $this->importJob->configuration; + $accounts = $config['accounts'] ?? []; + foreach ($accounts as $account) { + if ($account['id'] === $accountId) { + return $accountId; + } + } + + return ''; + } +} \ No newline at end of file diff --git a/app/Support/Import/JobConfiguration/Ynab/SelectBudgetHandler.php b/app/Support/Import/JobConfiguration/Ynab/SelectBudgetHandler.php new file mode 100644 index 0000000000..92a14d57c1 --- /dev/null +++ b/app/Support/Import/JobConfiguration/Ynab/SelectBudgetHandler.php @@ -0,0 +1,185 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Support\Import\JobConfiguration\Ynab; + +use FireflyIII\Models\Account; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\ImportJob; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; +use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; +use Illuminate\Support\Collection; +use Illuminate\Support\MessageBag; +use Log; + +/** + * Class SelectBudgetHandler + */ +class SelectBudgetHandler implements YnabJobConfigurationInterface +{ + /** @var AccountRepositoryInterface */ + private $accountRepository; + /** @var Collection */ + private $accounts; + /** @var CurrencyRepositoryInterface */ + private $currencyRepository; + /** @var ImportJob */ + private $importJob; + /** @var ImportJobRepositoryInterface */ + private $repository; + + /** + * Return true when this stage is complete. + * + * @return bool + */ + public function configurationComplete(): bool + { + Log::debug('Now in SelectBudgetHandler::configComplete'); + $configuration = $this->repository->getConfiguration($this->importJob); + $selectedBudget = $configuration['selected_budget'] ?? ''; + if ('' !== $selectedBudget) { + Log::debug(sprintf('Selected budget is %s, config is complete. Return true.', $selectedBudget)); + $this->repository->setStage($this->importJob, 'get_accounts'); + + return true; + } + Log::debug('User has not selected a budget yet, config is not yet complete.'); + + return false; + } + + /** + * Store the job configuration. + * + * @param array $data + * + * @return MessageBag + */ + public function configureJob(array $data): MessageBag + { + Log::debug('Now in SelectBudgetHandler::configureJob'); + $configuration = $this->repository->getConfiguration($this->importJob); + $configuration['selected_budget'] = $data['budget_id']; + + Log::debug(sprintf('Set selected budget to %s', $data['budget_id'])); + Log::debug('Mark job as ready for next stage.'); + + + $this->repository->setConfiguration($this->importJob, $configuration); + + return new MessageBag; + } + + /** + * Get data for config view. + * + * @return array + */ + public function getNextData(): array + { + Log::debug('Now in SelectBudgetHandler::getNextData'); + $configuration = $this->repository->getConfiguration($this->importJob); + $budgets = $configuration['budgets'] ?? []; + $available = []; + $notAvailable = []; + $total = \count($budgets); + foreach ($budgets as $budget) { + if ($this->haveAssetWithCurrency($budget['currency_code'])) { + Log::debug('Add budget to available list.'); + $available[$budget['id']] = $budget['name'] . ' (' . $budget['currency_code'] . ')'; + continue; + } + Log::debug('Add budget to notAvailable list.'); + $notAvailable[$budget['id']] = $budget['name'] . ' (' . $budget['currency_code'] . ')'; + + } + + return [ + 'available' => $available, + 'not_available' => $notAvailable, + 'total' => $total, + ]; + } + + /** + * Get the view for this stage. + * + * @return string + */ + public function getNextView(): string + { + Log::debug('Now in SelectBudgetHandler::getNextView'); + + return 'import.ynab.select-budgets'; + } + + /** + * Set the import job. + * + * @param ImportJob $importJob + */ + public function setImportJob(ImportJob $importJob): void + { + $this->importJob = $importJob; + $this->repository = app(ImportJobRepositoryInterface::class); + $this->currencyRepository = app(CurrencyRepositoryInterface::class); + $this->accountRepository = app(AccountRepositoryInterface::class); + + $this->repository->setUser($importJob->user); + $this->currencyRepository->setUser($importJob->user); + $this->accountRepository->setUser($importJob->user); + $this->accountRepository->setUser($importJob->user); + + $this->accounts = $this->accountRepository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT]); + } + + /** + * @param string $code + * + * @return bool + */ + private function haveAssetWithCurrency(string $code): bool + { + $currency = $this->currencyRepository->findByCodeNull($code); + if (null === $currency) { + Log::debug(sprintf('No currency found with code "%s"', $code)); + + return false; + } + /** @var Account $account */ + foreach ($this->accounts as $account) { + $currencyId = (int)$this->accountRepository->getMetaValue($account, 'currency_id'); + Log::debug(sprintf('Currency of %s is %d (looking for %d).', $account->name, $currencyId, $currency->id)); + if ($currencyId === $currency->id) { + Log::debug('Return true!'); + + return true; + } + } + Log::debug('Found nothing, return false.'); + + return false; + } +} \ No newline at end of file diff --git a/app/Support/Import/JobConfiguration/Ynab/YnabJobConfigurationInterface.php b/app/Support/Import/JobConfiguration/Ynab/YnabJobConfigurationInterface.php new file mode 100644 index 0000000000..539f26dc44 --- /dev/null +++ b/app/Support/Import/JobConfiguration/Ynab/YnabJobConfigurationInterface.php @@ -0,0 +1,72 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Support\Import\JobConfiguration\Ynab; + +use FireflyIII\Models\ImportJob; +use Illuminate\Support\MessageBag; + +/** + * Interface YnabJobConfigurationInterface + * + */ +interface YnabJobConfigurationInterface +{ + /** + * Return true when this stage is complete. + * + * @return bool + */ + public function configurationComplete(): bool; + + + /** + * Store the job configuration. + * + * @param array $data + * + * @return MessageBag + */ + public function configureJob(array $data): MessageBag; + + /** + * Get data for config view. + * + * @return array + */ + public function getNextData(): array; + + /** + * Get the view for this stage. + * + * @return string + */ + public function getNextView(): string; + + /** + * Set the import job. + * + * @param ImportJob $importJob + */ + public function setImportJob(ImportJob $importJob): void; +} \ No newline at end of file diff --git a/app/Support/Import/Placeholder/ImportTransaction.php b/app/Support/Import/Placeholder/ImportTransaction.php index 3847c7553c..0d397561f7 100644 --- a/app/Support/Import/Placeholder/ImportTransaction.php +++ b/app/Support/Import/Placeholder/ImportTransaction.php @@ -183,7 +183,9 @@ class ImportTransaction $meta = ['sepa-ct-id', 'sepa-ct-op', 'sepa-db', 'sepa-cc', 'sepa-country', 'sepa-batch-id', 'sepa-ep', 'sepa-ci', 'internal-reference', 'date-interest', 'date-invoice', 'date-book', 'date-payment', 'date-process', 'date-due',]; + Log::debug(sprintf('Now going to check role "%s".', $role)); if (\in_array($role, $meta, true)) { + Log::debug(sprintf('Role "%s" is in allowed meta roles, so store its value %s.', $role, $columnValue->getValue())); $this->meta[$role] = $columnValue->getValue(); return; diff --git a/app/Support/Import/Routine/Bunq/StageImportDataHandler.php b/app/Support/Import/Routine/Bunq/StageImportDataHandler.php index f0067b57dd..3920d988e4 100644 --- a/app/Support/Import/Routine/Bunq/StageImportDataHandler.php +++ b/app/Support/Import/Routine/Bunq/StageImportDataHandler.php @@ -44,17 +44,33 @@ use Log; */ class StageImportDataHandler { + private const DOWNLOAD_BACKWARDS = 1; + private const DOWNLOAD_FORWARDS = 2; + + /** @var bool */ + public $stillRunning; /** @var AccountFactory */ private $accountFactory; /** @var AccountRepositoryInterface */ private $accountRepository; /** @var ImportJob */ private $importJob; + /** @var array */ + private $jobConfiguration; /** @var ImportJobRepositoryInterface */ private $repository; + /** @var float */ + private $timeStart; /** @var array */ private $transactions; + public function __construct() + { + $this->stillRunning = true; + $this->timeStart = microtime(true); + + } + /** * @codeCoverageIgnore * @return array @@ -71,10 +87,11 @@ class StageImportDataHandler public function run(): void { $this->getContext(); - $config = $this->repository->getConfiguration($this->importJob); - $accounts = $config['accounts'] ?? []; - $mapping = $config['mapping'] ?? []; - $collection = [[]]; + $config = $this->repository->getConfiguration($this->importJob); + $accounts = $config['accounts'] ?? []; + $mapping = $config['mapping'] ?? []; + $collection = [[]]; + $this->jobConfiguration = $config; /** @var array $bunqAccount */ foreach ($accounts as $bunqAccount) { $bunqAccountId = $bunqAccount['id'] ?? 0; @@ -111,8 +128,9 @@ class StageImportDataHandler * @param LocalAccount $source * * @return array + * @throws FireflyException */ - private function convertPayment(BunqPayment $payment, LocalAccount $source): array + private function convertPayment(BunqPayment $payment, int $bunqAccountId, LocalAccount $source): array { Log::debug(sprintf('Now at payment with ID #%d', $payment->getId())); $type = TransactionType::WITHDRAWAL; @@ -191,9 +209,24 @@ class StageImportDataHandler */ private function convertToAccount(LabelMonetaryAccount $party, string $expectedType): LocalAccount { - Log::debug('in convertToAccount()'); + + Log::debug(sprintf('in convertToAccount() with LabelMonetaryAccount: %s', '')); if (null !== $party->getIban()) { - // find opposing party by IBAN first. + // find account in 'bunq-iban' array first. + $bunqIbans = $this->jobConfiguration['bunq-iban'] ?? []; + if (isset($bunqIbans[$party->getIban()])) { + $accountId = (int)$bunqIbans[$party->getIban()]; + $result = $this->accountRepository->findNull($accountId); + if (null !== $result) { + Log::debug( + sprintf('Search for #%s (based on IBAN %s) resulted in account %s (#%d)', $accountId, $party->getIban(), $result->name, $result->id) + ); + + return $result; + } + } + + // find opposing party by IBAN second. $result = $this->accountRepository->findByIbanNull($party->getIban(), [$expectedType]); if (null !== $result) { Log::debug(sprintf('Search for %s resulted in account %s (#%d)', $party->getIban(), $result->name, $result->id)); @@ -253,6 +286,40 @@ class StageImportDataHandler throw new FireflyException('The bunq API context is unexpectedly empty.'); // @codeCoverageIgnore } + /** + * Get the direction in which we must download. + * + * @param int $bunqAccountId + * + * @return int + */ + private function getDirection(int $bunqAccountId): int + { + Log::debug(sprintf('Now in getDirection(%d)', $bunqAccountId)); + + // if oldest transaction ID is 0, AND the newest transaction is 0 + // we don't know about this account, so we must go backward in time. + $oldest = \Preferences::getForUser($this->importJob->user, sprintf('bunq-oldest-transaction-%d', $bunqAccountId), 0); + $newest = \Preferences::getForUser($this->importJob->user, sprintf('bunq-newest-transaction-%d', $bunqAccountId), 0); + + if (0 === $oldest->data && 0 === $newest->data) { + Log::debug(sprintf('Oldest tranaction ID is %d and newest tranasction ID is %d, so go backwards.', $oldest->data, $newest->data)); + + return self::DOWNLOAD_BACKWARDS; + } + + // if newest is not zero but oldest is zero, go forward. + if (0 === $oldest->data && 0 !== $newest->data) { + Log::debug(sprintf('Oldest tranaction ID is %d and newest tranasction ID is %d, so go forwards.', $oldest->data, $newest->data)); + + return self::DOWNLOAD_FORWARDS; + } + + Log::debug(sprintf('Oldest tranaction ID is %d and newest tranasction ID is %d, so go backwards.', $oldest->data, $newest->data)); + + return self::DOWNLOAD_BACKWARDS; + } + /** * @param int $accountId * @@ -281,17 +348,232 @@ class StageImportDataHandler */ private function getTransactionsFromBunq(int $bunqAccountId, LocalAccount $localAccount): array { - $return = []; - // make request: - /** @var Payment $paymentRequest */ - $paymentRequest = app(Payment::class); - $result = $paymentRequest->listing($bunqAccountId, ['count' => 100]); - // loop result: - /** @var BunqPayment $payment */ - foreach ($result->getValue() as $payment) { - $return[] = $this->convertPayment($payment, $localAccount); + Log::debug(sprintf('Now in getTransactionsFromBunq(%d).', $bunqAccountId)); + + $direction = $this->getDirection($bunqAccountId); + $return = []; + if (self::DOWNLOAD_BACKWARDS === $direction) { + // go back either from NULL or from ID. + // ID is the very last transaction downloaded from bunq. + $preference = \Preferences::getForUser($this->importJob->user, sprintf('bunq-oldest-transaction-%d', $bunqAccountId), 0); + $transactionId = 0 === $preference->data ? null : $preference->data; + $return = $this->goBackInTime($bunqAccountId, $localAccount, $transactionId); + } + if (self::DOWNLOAD_FORWARDS === $direction) { + // go forward from ID. There is no NULL, young padawan + $return = $this->goForwardInTime($bunqAccountId, $localAccount); } return $return; } + + /** + * This method downloads the transactions from bunq going back in time. Assuming bunq + * is fairly consistent with the transactions it provides through the API, the method + * will store both the highest and the lowest transaction ID downloaded in this manner. + * + * The highest transaction ID is used to continue forward in time. The lowest is used to continue + * even further back in time. + * + * The lowest transaction ID can also be given to this method as a parameter (as $startTransaction). + * + * @param int $bunqAccountId + * @param LocalAccount $localAccount + * @param int $startTransaction + * + * @return array + * @throws FireflyException + */ + private function goBackInTime(int $bunqAccountId, LocalAccount $localAccount, int $startTransaction = null): array + { + Log::debug(sprintf('Now in goBackInTime(#%d, #%s, #%s).', $bunqAccountId, $localAccount->id, $startTransaction)); + $hasMoreTransactions = true; + $olderId = $startTransaction; + $oldestTransaction = null; + $newestTransaction = null; + $count = 0; + $return = []; + + /* + * Do a loop during which we run: + */ + while ($hasMoreTransactions && $this->timeRunning() < 25) { + Log::debug(sprintf('Now in loop #%d', $count)); + Log::debug(sprintf('Now running for %s seconds.', $this->timeRunning())); + + /* + * Send request to bunq. + */ + /** @var Payment $paymentRequest */ + $paymentRequest = app(Payment::class); + $params = ['count' => 53, 'older_id' => $olderId]; + $response = $paymentRequest->listing($bunqAccountId, $params); + $pagination = $response->getPagination(); + Log::debug('Params for the request to bunq are: ', $params); + + /* + * If pagination is not null, we can go back even further. + */ + if (null !== $pagination) { + $olderId = $pagination->getOlderId(); + Log::debug(sprintf('Pagination object is not null, new olderID is "%s"', $olderId)); + } + + /* + * Loop the results from bunq + */ + Log::debug('Now looping results from bunq...'); + /** @var BunqPayment $payment */ + foreach ($response->getValue() as $index => $payment) { + $return[] = $this->convertPayment($payment, $bunqAccountId, $localAccount); + $paymentId = $payment->getId(); + /* + * If oldest and newest transaction are null, they have to be set: + */ + $oldestTransaction = $oldestTransaction ?? $paymentId; + $newestTransaction = $newestTransaction ?? $paymentId; + + /* + * Then, overwrite if appropriate + */ + $oldestTransaction = $paymentId < $oldestTransaction ? $paymentId : $oldestTransaction; + $newestTransaction = $paymentId > $newestTransaction ? $paymentId : $newestTransaction; + } + + /* + * After the loop, check if Firefly III must loop again. + */ + Log::debug(sprintf('Count of result is now %d', \count($return))); + $count++; + if (null === $olderId) { + Log::debug('Older ID is NULL, so stop looping cause we are done!'); + $hasMoreTransactions = false; + $this->stillRunning = false; + /* + * We no longer care about the oldest transaction ID: + */ + $oldestTransaction = 0; + } + if (null === $pagination) { + Log::debug('No pagination object, stop looping.'); + $hasMoreTransactions = false; + $this->stillRunning = false; + /* + * We no longer care about the oldest transaction ID: + */ + $oldestTransaction = 0; + } + sleep(1); + + + } + // store newest and oldest tranasction ID to be used later: + \Preferences::setForUser($this->importJob->user, sprintf('bunq-oldest-transaction-%d', $bunqAccountId), $oldestTransaction); + \Preferences::setForUser($this->importJob->user, sprintf('bunq-newest-transaction-%d', $bunqAccountId), $newestTransaction); + + return $return; + } + + /** + * @param int $bunqAccountId + * @param LocalAccount $localAccount + * + * @return array + * @throws FireflyException + */ + private function goForwardInTime(int $bunqAccountId, LocalAccount $localAccount): array + { + Log::debug(sprintf('Now in goForwardInTime(%d).', $bunqAccountId)); + $hasMoreTransactions = true; + $count = 0; + $return = []; + $newestTransaction = null; + + /* + * Go forward from the newest transaction we know about: + */ + $preferenceName = sprintf('bunq-newest-transaction-%d', $bunqAccountId); + $transactionPref = \Preferences::getForUser($this->importJob->user, $preferenceName, 0); + $newerId = (int)$transactionPref->data; + + /* + * Run a loop. + */ + while ($hasMoreTransactions && $this->timeRunning() < 25) { + /* + * Debug information: + */ + Log::debug(sprintf('Now in loop #%d', $count)); + Log::debug(sprintf('Now running for %s seconds.', $this->timeRunning())); + + /* + * Send a request to bunq. + */ + /** @var Payment $paymentRequest */ + $paymentRequest = app(Payment::class); + $params = ['count' => 53, 'newer_id' => $newerId]; + $response = $paymentRequest->listing($bunqAccountId, $params); + $pagination = $response->getPagination(); + Log::debug('Submit payment request with params', $params); + + /* + * If pagination is not null, we can go forward even further. + */ + if (null !== $pagination) { + $newerId = $pagination->getNewerId(); + Log::debug(sprintf('Pagination object is not null, newerID is "%s"', $newerId)); + } + Log::debug('Now looping results...'); + /* + * Process the bunq loop. + */ + /** @var BunqPayment $payment */ + foreach ($response->getValue() as $payment) { + $return[] = $this->convertPayment($payment, $bunqAccountId, $localAccount); + $paymentId = $payment->getId(); + + /* + * If oldest and newest transaction are null, they have to be set: + */ + $newestTransaction = $newestTransaction ?? $paymentId; + + /* + * Then, overwrite if appropriate + */ + $newestTransaction = $paymentId > $newestTransaction ? $paymentId : $newestTransaction; + } + + /* + * After the loop, check if Firefly III must loop again. + */ + Log::debug(sprintf('Count of result is now %d', \count($return))); + $count++; + if (null === $newerId) { + Log::debug('Newer ID is NULL, so stop looping cause we are done!'); + $hasMoreTransactions = false; + $this->stillRunning = false; + } + if (null === $pagination) { + Log::debug('No pagination object, stop looping.'); + $hasMoreTransactions = false; + $this->stillRunning = false; + } + sleep(1); + } + + // store newest tranasction ID to be used later: + \Preferences::setForUser($this->importJob->user, sprintf('bunq-newest-transaction-%d', $bunqAccountId), $newestTransaction); + + return $return; + } + + /** + * @return float + */ + private function timeRunning(): float + { + $time_end = microtime(true); + + return $time_end - $this->timeStart; + } } diff --git a/app/Support/Import/Routine/Bunq/StageNewHandler.php b/app/Support/Import/Routine/Bunq/StageNewHandler.php index b4e9a30743..05debeba55 100644 --- a/app/Support/Import/Routine/Bunq/StageNewHandler.php +++ b/app/Support/Import/Routine/Bunq/StageNewHandler.php @@ -68,7 +68,6 @@ class StageNewHandler $config = $this->repository->getConfiguration($this->importJob); $config['accounts'] = $accounts; $this->repository->setConfiguration($this->importJob, $config); - return; } throw new FireflyException('The bunq API context is unexpectedly empty.'); // @codeCoverageIgnore @@ -150,6 +149,7 @@ class StageNewHandler 'balance' => $mab->getBalance(), 'status' => $mab->getStatus(), 'type' => 'MonetaryAccountBank', + 'iban' => null, 'aliases' => [], ]; @@ -168,6 +168,11 @@ class StageNewHandler 'name' => $alias->getName(), 'value' => $alias->getValue(), ]; + + // store IBAN alias separately: + if ('IBAN' === $alias->getType()) { + $return['iban'] = $alias->getValue(); + } } } diff --git a/app/Support/Import/Routine/Fake/StageFinalHandler.php b/app/Support/Import/Routine/Fake/StageFinalHandler.php index 23d02676ca..bb93636f90 100644 --- a/app/Support/Import/Routine/Fake/StageFinalHandler.php +++ b/app/Support/Import/Routine/Fake/StageFinalHandler.php @@ -47,7 +47,7 @@ class StageFinalHandler for ($i = 0; $i < 5; $i++) { $transaction = [ 'type' => 'withdrawal', - 'date' => Carbon::create()->format('Y-m-d'), + 'date' => Carbon::now()->format('Y-m-d'), 'tags' => '', 'user' => $this->importJob->user_id, diff --git a/app/Support/Import/Routine/File/CurrencyMapper.php b/app/Support/Import/Routine/File/CurrencyMapper.php index 21362912b5..83821c019f 100644 --- a/app/Support/Import/Routine/File/CurrencyMapper.php +++ b/app/Support/Import/Routine/File/CurrencyMapper.php @@ -71,7 +71,7 @@ class CurrencyMapper return $result; } } - if (!isset($data['code']) || null === $data['code']) { + if (!isset($data['code'])) { return null; } diff --git a/app/Support/Import/Routine/File/ImportableConverter.php b/app/Support/Import/Routine/File/ImportableConverter.php index 17fcfe2b46..b12cb52216 100644 --- a/app/Support/Import/Routine/File/ImportableConverter.php +++ b/app/Support/Import/Routine/File/ImportableConverter.php @@ -133,9 +133,9 @@ class ImportableConverter /** * @param string|null $date * - * @return string + * @return string|null */ - private function convertDateValue(string $date = null): string + private function convertDateValue(string $date = null): ?string { $result = null; if (null !== $date) { @@ -147,10 +147,6 @@ class ImportableConverter Log::error($e->getTraceAsString()); } } - if (null === $result) { - $object = new Carbon; - $result = $object->format('Y-m-d'); - } return $result; } @@ -167,6 +163,8 @@ class ImportableConverter $foreignAmount = $importable->calculateForeignAmount(); $amount = $importable->calculateAmount(); + Log::debug('All meta data: ', $importable->meta); + if ('' === $amount) { $amount = $foreignAmount; } @@ -215,7 +213,7 @@ class ImportableConverter return [ 'type' => $transactionType, - 'date' => $this->convertDateValue($importable->date), + 'date' => $this->convertDateValue($importable->date) ?? Carbon::now()->format('Y-m-d'), 'tags' => $importable->tags, 'user' => $this->importJob->user_id, 'notes' => $importable->note, diff --git a/app/Support/Import/Routine/Ynab/GetAccountsHandler.php b/app/Support/Import/Routine/Ynab/GetAccountsHandler.php new file mode 100644 index 0000000000..a0c05fd195 --- /dev/null +++ b/app/Support/Import/Routine/Ynab/GetAccountsHandler.php @@ -0,0 +1,80 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Support\Import\Routine\Ynab; + +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\ImportJob; +use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; +use FireflyIII\Services\Ynab\Request\GetAccountsRequest; + +/** + * Class GetAccountsHandler + */ +class GetAccountsHandler +{ + /** @var ImportJob */ + private $importJob; + /** @var ImportJobRepositoryInterface */ + private $repository; + + /** + * Get list of accounts for the selected budget. + * + * @throws FireflyException + */ + public function run(): void + { + + $config = $this->repository->getConfiguration($this->importJob); + $selectedBudget = $config['selected_budget'] ?? ''; + if ('' === $selectedBudget) { + $firstBudget = $config['budgets'][0] ?? false; + if (false === $firstBudget) { + throw new FireflyException('The configuration contains no budget. Erroring out.'); + } + $selectedBudget = $firstBudget['id']; + $config['selected_budget'] = $selectedBudget; + } + $token = $config['access_token']; + $request = new GetAccountsRequest; + $request->budgetId = $selectedBudget; + $request->setAccessToken($token); + $request->call(); + $config['accounts'] = $request->accounts; + $this->repository->setConfiguration($this->importJob, $config); + if (0 === \count($config['accounts'])) { + throw new FireflyException('This budget contains zero accounts.'); + } + } + + /** + * @param ImportJob $importJob + */ + public function setImportJob(ImportJob $importJob): void + { + $this->importJob = $importJob; + $this->repository = app(ImportJobRepositoryInterface::class); + $this->repository->setUser($importJob->user); + } +} \ No newline at end of file diff --git a/app/Support/Import/Routine/Ynab/ImportDataHandler.php b/app/Support/Import/Routine/Ynab/ImportDataHandler.php new file mode 100644 index 0000000000..0e97b2f5ea --- /dev/null +++ b/app/Support/Import/Routine/Ynab/ImportDataHandler.php @@ -0,0 +1,273 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Support\Import\Routine\Ynab; + + +use Carbon\Carbon; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\Account; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\ImportJob; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; +use FireflyIII\Services\Ynab\Request\GetTransactionsRequest; +use FireflyIII\Support\Import\Routine\File\OpposingAccountMapper; +use Log; + +/** + * Class ImportDataHandler + */ +class ImportDataHandler +{ + /** @var AccountRepositoryInterface */ + private $accountRepository; + /** @var ImportJob */ + private $importJob; + /** @var OpposingAccountMapper */ + private $mapper; + /** @var ImportJobRepositoryInterface */ + private $repository; + + /** + * Get list of accounts for the selected budget. + * + * @throws FireflyException + */ + public function run(): void + { + $config = $this->repository->getConfiguration($this->importJob); + $token = $config['access_token']; + // make request for each mapping: + $mapping = $config['mapping'] ?? []; + $total = [[]]; + + /** + * @var string $ynabId + * @var string $localId + */ + foreach ($mapping as $ynabId => $localId) { + $localAccount = $this->getLocalAccount((int)$localId); + $transactions = $this->getTransactions($token, $ynabId); + $converted = $this->convertToArray($transactions, $localAccount); + $total[] = $converted; + } + + $totalSet = array_merge(...$total); + Log::debug(sprintf('Found %d transactions in total.', \count($totalSet))); + $this->repository->setTransactions($this->importJob, $totalSet); + + // assuming this works, store today's date as a preference + // (combined with the budget from which FF3 imported) + $budgetId = $this->getSelectedBudget()['id'] ?? ''; + if ('' !== $budgetId) { + app('preferences')->set('ynab_' . $budgetId, Carbon::now()->format('Y-m-d')); + } + } + + /** + * @param ImportJob $importJob + */ + public function setImportJob(ImportJob $importJob): void + { + $this->importJob = $importJob; + $this->repository = app(ImportJobRepositoryInterface::class); + $this->accountRepository = app(AccountRepositoryInterface::class); + $this->mapper = app(OpposingAccountMapper::class); + $this->accountRepository->setUser($importJob->user); + $this->repository->setUser($importJob->user); + $this->mapper->setUser($importJob->user); + } + + /** + * @param array $transactions + * @param Account $localAccount + * + * @return array + * @throws FireflyException + */ + private function convertToArray(array $transactions, Account $localAccount): array + { + $config = $this->repository->getConfiguration($this->importJob); + $array = []; + $total = \count($transactions); + $budget = $this->getSelectedBudget(); + Log::debug(sprintf('Now in StageImportDataHandler::convertToArray() with count %d', \count($transactions))); + /** @var array $transaction */ + foreach ($transactions as $index => $transaction) { + $description = $transaction['memo'] ?? '(empty)'; + Log::debug(sprintf('Now creating array for transaction %d of %d ("%s")', $index + 1, $total, $description)); + $amount = (string)($transaction['amount'] ?? 0); + if ('0' === $amount) { + Log::debug(sprintf('Amount is zero (%s), skip this transaction.', $amount)); + continue; + } + Log::debug(sprintf('Amount detected is %s', $amount)); + $source = $localAccount; + $type = 'withdrawal'; + $tags = [ + $transaction['cleared'] ?? '', + $transaction['approved'] ? 'approved' : 'not-approved', + $transaction['flag_color'] ?? '', + ]; + $possibleDestinationId = null; + if (null !== $transaction['transfer_account_id']) { + // indication that it is a transfer. + $possibleDestinationId = $config['mapping'][$transaction['transfer_account_id']] ?? null; + Log::debug(sprintf('transfer_account_id has value %s', $transaction['transfer_account_id'])); + Log::debug(sprintf('Can map this to the following FF3 asset account: %d', $possibleDestinationId)); + $type = 'transfer'; + + } + + $destinationData = [ + 'name' => $transaction['payee_name'], + 'iban' => null, + 'number' => $transaction['payee_id'], + 'bic' => null, + ]; + + $destination = $this->mapper->map($possibleDestinationId, $amount, $destinationData); + if (1 === bccomp($amount, '0')) { + [$source, $destination] = [$destination, $source]; + $type = 'transfer' === $type ? 'transfer' : 'deposit'; + Log::debug(sprintf('Amount is %s, so switch source/dest and make this a %s', $amount, $type)); + } + + Log::debug(sprintf('Final source account: #%d ("%s")', $source->id, $source->name)); + Log::debug(sprintf('Final destination account: #%d ("%s")', $destination->id, $destination->name)); + + $entry = [ + 'type' => $type, + 'date' => $transaction['date'] ?? date('Y-m-d'), + 'tags' => $tags, + 'user' => $this->importJob->user_id, + 'notes' => null, + + // all custom fields: + 'external_id' => $transaction['id'] ?? '', + + // journal data: + 'description' => $description, + 'piggy_bank_id' => null, + 'piggy_bank_name' => null, + 'bill_id' => null, + 'bill_name' => null, + + // transaction data: + 'transactions' => [ + [ + 'currency_id' => null, + 'currency_code' => $budget['currency_code'], + 'description' => null, + 'amount' => bcdiv((string)$transaction['amount'], '1000'), + 'budget_id' => null, + 'budget_name' => null, + 'category_id' => null, + 'category_name' => $transaction['category_name'], + 'source_id' => $source->id, + 'source_name' => null, + 'destination_id' => $destination->id, + 'destination_name' => null, + 'foreign_currency_id' => null, + 'foreign_currency_code' => null, + 'foreign_amount' => null, + 'reconciled' => false, + 'identifier' => 0, + ], + ], + ]; + Log::debug(sprintf('Done with entry #%d', $index)); + $array[] = $entry; + } + + return $array; + } + + /** + * @param int $accountId + * + * @return Account + * @throws FireflyException + */ + private function getLocalAccount(int $accountId): Account + { + $account = $this->accountRepository->findNull($accountId); + if (null === $account) { + throw new FireflyException(sprintf('Cannot find Firefly III asset account with ID #%d. Job must stop now.', $accountId)); // @codeCoverageIgnore + } + if ($account->accountType->type !== AccountType::ASSET) { + throw new FireflyException(sprintf('Account with ID #%d is not an asset account. Job must stop now.', $accountId)); // @codeCoverageIgnore + } + + return $account; + } + + /** + * @return array + * @throws FireflyException + */ + private function getSelectedBudget(): array + { + $config = $this->repository->getConfiguration($this->importJob); + $budgets = $config['budgets'] ?? []; + $selected = $config['selected_budget'] ?? ''; + + if ('' === $selected) { + $firstBudget = $config['budgets'][0] ?? false; + if (false === $firstBudget) { + throw new FireflyException('The configuration contains no budget. Erroring out.'); + } + $selected = $firstBudget['id']; + } + + foreach ($budgets as $budget) { + if ($budget['id'] === $selected) { + return $budget; + } + } + + return $budgets[0] ?? []; + } + + /** + * @param string $token + * @param string $account + * + * @return array + * @throws FireflyException + */ + private function getTransactions(string $token, string $account): array + { + $budget = $this->getSelectedBudget(); + $request = new GetTransactionsRequest; + $request->budgetId = $budget['id']; + $request->accountId = $account; + + // todo grab latest date for $ynabId + $request->setAccessToken($token); + $request->call(); + + return $request->transactions; + } +} \ No newline at end of file diff --git a/app/Support/Import/Routine/Ynab/StageGetAccessHandler.php b/app/Support/Import/Routine/Ynab/StageGetAccessHandler.php new file mode 100644 index 0000000000..c155a4e0ca --- /dev/null +++ b/app/Support/Import/Routine/Ynab/StageGetAccessHandler.php @@ -0,0 +1,108 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Support\Import\Routine\Ynab; + +use Exception; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\ImportJob; +use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; +use GuzzleHttp\Client; +use GuzzleHttp\Exception\GuzzleException; +use Log; +use RuntimeException; + +/** + * Class StageGetAccessHandler + */ +class StageGetAccessHandler +{ + /** @var ImportJob */ + private $importJob; + /** @var ImportJobRepositoryInterface */ + private $repository; + + /** + * Send a token request to YNAB. Return with access token (if all goes well). + * + * @throws \Psr\Container\NotFoundExceptionInterface + * @throws \Psr\Container\ContainerExceptionInterface + * @throws FireflyException + */ + public function run(): void + { + $config = $this->repository->getConfiguration($this->importJob); + $clientId = app('preferences')->get('ynab_client_id', '')->data; + $clientSecret = app('preferences')->get('ynab_client_secret', '')->data; + $redirectUri = route('import.callback.ynab'); + $code = $config['auth_code']; + $uri = sprintf( + 'https://app.youneedabudget.com/oauth/token?client_id=%s&client_secret=%s&redirect_uri=%s&grant_type=authorization_code&code=%s', $clientId, + $clientSecret, $redirectUri, $code + ); + $client = new Client; + try { + $res = $client->request('POST', $uri); + } catch (GuzzleException|Exception $e) { + Log::error($e->getMessage()); + Log::error($e->getTraceAsString()); + throw new FireflyException($e->getMessage()); + } + $statusCode = $res->getStatusCode(); + try { + $content = trim($res->getBody()->getContents()); + } catch (RuntimeException $e) { + Log::error($e->getMessage()); + Log::error($e->getTraceAsString()); + throw new FireflyException($e->getMessage()); + } + + $json = json_decode($content, true) ?? []; + Log::debug(sprintf('Status code from YNAB is %d', $statusCode)); + Log::debug(sprintf('Body of result is %s', $content), $json); + + // store refresh token (if present?) as preference + // store token in job: + $configuration = $this->repository->getConfiguration($this->importJob); + $configuration['access_token'] = $json['access_token']; + $configuration['access_token_expires'] = (int)$json['created_at'] + (int)$json['expires_in']; + $this->repository->setConfiguration($this->importJob, $configuration); + + Log::debug('end of StageGetAccessHandler::run()'); + + $refreshToken = (string)($json['refresh_token'] ?? ''); + if ('' !== $refreshToken) { + app('preferences')->set('ynab_refresh_token', $refreshToken); + } + } + + /** + * @param ImportJob $importJob + */ + public function setImportJob(ImportJob $importJob): void + { + $this->importJob = $importJob; + $this->repository = app(ImportJobRepositoryInterface::class); + $this->repository->setUser($importJob->user); + } +} \ No newline at end of file diff --git a/app/Support/Import/Routine/Ynab/StageGetBudgetsHandler.php b/app/Support/Import/Routine/Ynab/StageGetBudgetsHandler.php new file mode 100644 index 0000000000..fd8c3dbeb6 --- /dev/null +++ b/app/Support/Import/Routine/Ynab/StageGetBudgetsHandler.php @@ -0,0 +1,75 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Support\Import\Routine\Ynab; + +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Models\ImportJob; +use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; +use FireflyIII\Services\Ynab\Request\GetBudgetsRequest; +use Log; + +/** + * Class StageGetBudgetsHandler + */ +class StageGetBudgetsHandler +{ + + /** @var ImportJob */ + private $importJob; + /** @var ImportJobRepositoryInterface */ + private $repository; + + /** + * + * @throws FireflyException + */ + public function run(): void + { + Log::debug('Now in StageGetBudgetsHandler::run()'); + // grab access token from job: + $configuration = $this->repository->getConfiguration($this->importJob); + $token = $configuration['access_token']; + $request = new GetBudgetsRequest; + $request->setAccessToken($token); + $request->call(); + + // store budgets in users preferences. + $configuration['budgets'] = $request->budgets; + $this->repository->setConfiguration($this->importJob, $configuration); + Log::debug(sprintf('Found %d budgets', \count($request->budgets))); + if (0 === \count($request->budgets)) { + throw new FireflyException('It seems this user has zero budgets or an error prevented Firefly III from reading them.'); + } + } + + /** + * @param ImportJob $importJob + */ + public function setImportJob(ImportJob $importJob): void + { + $this->importJob = $importJob; + $this->repository = app(ImportJobRepositoryInterface::class); + $this->repository->setUser($importJob->user); + } +} \ No newline at end of file diff --git a/app/Support/Import/Routine/Ynab/StageGetTransactionsHandler.php b/app/Support/Import/Routine/Ynab/StageGetTransactionsHandler.php new file mode 100644 index 0000000000..9c87cf78d5 --- /dev/null +++ b/app/Support/Import/Routine/Ynab/StageGetTransactionsHandler.php @@ -0,0 +1,55 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Support\Import\Routine\Ynab; + +use FireflyIII\Models\ImportJob; +use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; + +/** + * Class StageGetTransactionsHandler + */ +class StageGetTransactionsHandler +{ + /** @var ImportJob */ + private $importJob; + /** @var ImportJobRepositoryInterface */ + private $repository; + + /** + * + */ + public function run(): void + { + } + + /** + * @param ImportJob $importJob + */ + public function setImportJob(ImportJob $importJob): void + { + $this->importJob = $importJob; + $this->repository = app(ImportJobRepositoryInterface::class); + $this->repository->setUser($importJob->user); + } +} \ No newline at end of file diff --git a/app/Support/Import/Routine/Ynab/StageMatchAccountsHandler.php b/app/Support/Import/Routine/Ynab/StageMatchAccountsHandler.php new file mode 100644 index 0000000000..5944f8514c --- /dev/null +++ b/app/Support/Import/Routine/Ynab/StageMatchAccountsHandler.php @@ -0,0 +1,57 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Support\Import\Routine\Ynab; + +use FireflyIII\Models\ImportJob; +use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; + +/** + * Class StageMatchAccountsHandler + */ +class StageMatchAccountsHandler +{ + + + /** @var ImportJob */ + private $importJob; + /** @var ImportJobRepositoryInterface */ + private $repository; + + /** + * + */ + public function run(): void + { + } + + /** + * @param ImportJob $importJob + */ + public function setImportJob(ImportJob $importJob): void + { + $this->importJob = $importJob; + $this->repository = app(ImportJobRepositoryInterface::class); + $this->repository->setUser($importJob->user); + } +} \ No newline at end of file diff --git a/app/Support/Preferences.php b/app/Support/Preferences.php index a27ab81001..f86340ded9 100644 --- a/app/Support/Preferences.php +++ b/app/Support/Preferences.php @@ -62,7 +62,7 @@ class Preferences try { Preference::where('user_id', auth()->user()->id)->where('name', $name)->delete(); } catch (Exception $e) { - Log::debug(sprintf('Not interesting: %s', $e->getMessage())); + Log::debug(sprintf('Could not delete preference: %s', $e->getMessage())); // don't care. } @@ -171,10 +171,8 @@ class Preferences if (\is_array($lastActivity)) { $lastActivity = implode(',', $lastActivity); } - $hash = md5($lastActivity); - Log::debug(sprintf('Value of last activity is %s, hash is %s', $lastActivity, $hash)); - return $hash; + return md5($lastActivity); } /** diff --git a/app/Support/Search/Modifier.php b/app/Support/Search/Modifier.php index 5b6972c46b..9e29d24925 100644 --- a/app/Support/Search/Modifier.php +++ b/app/Support/Search/Modifier.php @@ -101,7 +101,7 @@ class Modifier try { $compareDate = new Carbon($compare); } catch (Exception $e) { - Log::debug(sprintf('Not interesting: %s', $e->getMessage())); + Log::debug(sprintf('Not interesting in Modifier:dateAfter(): %s', $e->getMessage())); return false; } @@ -120,7 +120,7 @@ class Modifier try { $compareDate = new Carbon($compare); } catch (Exception $e) { - Log::debug(sprintf('Not interesting: %s', $e->getMessage())); + Log::debug(sprintf('Not interesting in modifier:dateBefore(): %s', $e->getMessage())); return false; } @@ -139,7 +139,7 @@ class Modifier try { $compareDate = new Carbon($compare); } catch (Exception $e) { - Log::debug(sprintf('Not interesting: %s', $e->getMessage())); + Log::debug(sprintf('Not interesting in Modifier:sameDate(): %s', $e->getMessage())); return false; } diff --git a/app/Support/Search/Search.php b/app/Support/Search/Search.php index cc1e884954..f456c4be8a 100644 --- a/app/Support/Search/Search.php +++ b/app/Support/Search/Search.php @@ -23,7 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Support\Search; use Carbon\Carbon; -use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Helpers\Filter\InternalTransferFilter; use FireflyIII\Models\Transaction; use FireflyIII\User; @@ -111,8 +111,8 @@ class Search implements SearchInterface $result = new Collection(); $startTime = microtime(true); do { - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); $collector->setAllAssetAccounts()->setLimit($pageSize)->setPage($page)->withOpposingAccount(); if ($this->hasModifiers()) { $collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation(); @@ -122,7 +122,7 @@ class Search implements SearchInterface $collector = $this->applyModifiers($collector); $collector->removeFilter(InternalTransferFilter::class); - $set = $collector->getPaginatedJournals()->getCollection(); + $set = $collector->getPaginatedTransactions()->getCollection(); Log::debug(sprintf('Found %d journals to check. ', $set->count())); @@ -185,12 +185,12 @@ class Search implements SearchInterface } /** - * @param JournalCollectorInterface $collector + * @param TransactionCollectorInterface $collector * - * @return JournalCollectorInterface + * @return TransactionCollectorInterface * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - private function applyModifiers(JournalCollectorInterface $collector): JournalCollectorInterface + private function applyModifiers(TransactionCollectorInterface $collector): TransactionCollectorInterface { foreach ($this->modifiers as $modifier) { switch ($modifier['type']) { @@ -246,7 +246,7 @@ class Search implements SearchInterface private function extractModifier(string $string): void { $parts = explode(':', $string); - if (2 === \count($parts) && \strlen(trim((string)$parts[0])) > 0 && '' !== trim((string)$parts[1])) { + if (2 === \count($parts) && '' !== trim((string)$parts[1]) && \strlen(trim((string)$parts[0])) > 0) { $type = trim((string)$parts[0]); $value = trim((string)$parts[1]); if (\in_array($type, $this->validModifiers, true)) { diff --git a/app/Support/Steam.php b/app/Support/Steam.php index 3256f37f0a..1e1d135a73 100644 --- a/app/Support/Steam.php +++ b/app/Support/Steam.php @@ -31,6 +31,7 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface; use Illuminate\Contracts\Encryption\DecryptException; use Illuminate\Support\Collection; use Log; +use stdClass; /** * Class Steam. @@ -162,6 +163,13 @@ class Steam $balances[$formatted] = $startBalance; $currencyId = (int)$repository->getMetaValue($account, 'currency_id'); + + // use system default currency: + if (0 === $currencyId) { + $currency = app('amount')->getDefaultCurrencyByUser($account->user); + $currencyId = $currency->id; + } + $start->addDay(); // query! @@ -211,6 +219,38 @@ class Steam return $balances; } + /** + * @param \FireflyIII\Models\Account $account + * @param \Carbon\Carbon $date + * + * @return string + */ + public function balancePerCurrency(Account $account, Carbon $date): array + { + + // abuse chart properties: + $cache = new CacheProperties; + $cache->addProperty($account->id); + $cache->addProperty('balance-per-currency'); + $cache->addProperty($date); + if ($cache->has()) { + return $cache->get(); // @codeCoverageIgnore + } + $query = $account->transactions() + ->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') + ->where('transaction_journals.date', '<=', $date->format('Y-m-d 23:59:59')) + ->groupBy('transactions.transaction_currency_id'); + $balances = $query->get(['transactions.transaction_currency_id', DB::raw('SUM(transactions.amount) as sum_for_currency')]); + $return = []; + /** @var stdClass $entry */ + foreach ($balances as $entry) { + $return[(int)$entry->transaction_currency_id] = $entry->sum_for_currency; + } + $cache->store($return); + + return $return; + } + /** * This method always ignores the virtual balance. * @@ -243,6 +283,38 @@ class Steam return $result; } + /** + * Same as above, but also groups per currency. + * + * @param \Illuminate\Support\Collection $accounts + * @param \Carbon\Carbon $date + * + * @return array + */ + public function balancesPerCurrencyByAccounts(Collection $accounts, Carbon $date): array + { + $ids = $accounts->pluck('id')->toArray(); + // cache this property. + $cache = new CacheProperties; + $cache->addProperty($ids); + $cache->addProperty('balances-per-currency'); + $cache->addProperty($date); + if ($cache->has()) { + return $cache->get(); // @codeCoverageIgnore + } + + // need to do this per account. + $result = []; + /** @var Account $account */ + foreach ($accounts as $account) { + $result[$account->id] = $this->balancePerCurrency($account, $date); + } + + $cache->store($result); + + return $result; + } + /** * @param int $isEncrypted * @param $value @@ -367,7 +439,6 @@ class Steam $value = Crypt::decrypt($value); } catch (DecryptException $e) { // do not care. - Log::debug(sprintf('Not interesting: %s', $e->getMessage())); } return $value; diff --git a/app/Support/Twig/AmountFormat.php b/app/Support/Twig/AmountFormat.php index 52a7554885..9992740d8e 100644 --- a/app/Support/Twig/AmountFormat.php +++ b/app/Support/Twig/AmountFormat.php @@ -132,6 +132,7 @@ class AmountFormat extends Twig_Extension { return new Twig_SimpleFunction( 'formatAmountBySymbol', + /** @noinspection MoreThanThreeArgumentsInspection */ function (string $amount, string $symbol, int $decimalPlaces = null, bool $coloured = null): string { $decimalPlaces = $decimalPlaces ?? 2; $coloured = $coloured ?? true; diff --git a/app/Support/Twig/Extension/TransactionJournal.php b/app/Support/Twig/Extension/TransactionJournal.php index 32bfa25f3a..59e15cfbe6 100644 --- a/app/Support/Twig/Extension/TransactionJournal.php +++ b/app/Support/Twig/Extension/TransactionJournal.php @@ -136,7 +136,7 @@ class TransactionJournal extends Twig_Extension /** * @param JournalModel $journal * - * @return string + * @return array */ private function getTotalAmount(JournalModel $journal): array { @@ -157,6 +157,7 @@ class TransactionJournal extends Twig_Extension $totals[$currencyId]['amount'] = bcadd($transaction->amount, $totals[$currencyId]['amount']); if (null !== $transaction->foreign_currency_id) { + $foreignAmount = $transaction->foreign_amount ?? '0'; $foreignId = $transaction->foreign_currency_id; $foreign = $transaction->foreignCurrency; if (!isset($totals[$foreignId])) { @@ -166,7 +167,7 @@ class TransactionJournal extends Twig_Extension ]; } $totals[$foreignId]['amount'] = bcadd( - $transaction->foreign_amount, + $foreignAmount, $totals[$foreignId]['amount'] ); } diff --git a/app/Support/Twig/General.php b/app/Support/Twig/General.php index 633de8f1c3..e26f79374d 100644 --- a/app/Support/Twig/General.php +++ b/app/Support/Twig/General.php @@ -24,6 +24,7 @@ namespace FireflyIII\Support\Twig; use Carbon\Carbon; use FireflyIII\Models\Account; +use FireflyIII\Repositories\User\UserRepositoryInterface; use FireflyIII\Support\Twig\Extension\Account as AccountExtension; use League\CommonMark\CommonMarkConverter; use Route; @@ -64,6 +65,7 @@ class General extends Twig_Extension $this->activeRoutePartialWhat(), $this->formatDate(), new Twig_SimpleFunction('accountGetMetaField', [AccountExtension::class, 'getMetaField']), + $this->hasRole(), ]; } @@ -101,9 +103,7 @@ class General extends Twig_Extension return new Twig_SimpleFunction( 'activeRoutePartialWhat', function ($context): string { - $args = \func_get_args(); - $route = $args[1]; // name of the route. - $what = $args[2]; // name of the route. + [, $route, $what] = \func_get_args(); $activeWhat = $context['what'] ?? false; if ($what === $activeWhat && !(false === stripos(Route::getCurrentRoute()->getName(), $route))) { @@ -235,6 +235,26 @@ class General extends Twig_Extension ); } + /** + * Will return true if the user is of role X. + * + * @return Twig_SimpleFunction + */ + protected function hasRole(): Twig_SimpleFunction + { + return new Twig_SimpleFunction( + 'hasRole', + function (string $role): bool { + $repository = app(UserRepositoryInterface::class); + if ($repository->hasRole(auth()->user(), $role)) { + return true; + } + + return false; + } + ); + } + /** * @return Twig_SimpleFilter */ diff --git a/app/Support/Twig/Translation.php b/app/Support/Twig/Translation.php index 74198da564..6aa55ceec3 100644 --- a/app/Support/Twig/Translation.php +++ b/app/Support/Twig/Translation.php @@ -70,6 +70,7 @@ class Translation extends Twig_Extension function (string $direction, string $original) { $key = sprintf('firefly.%s_%s', $original, $direction); $translation = trans($key); + if ($key === $translation) { return $original; } diff --git a/app/TransactionRules/Factory/TriggerFactory.php b/app/TransactionRules/Factory/TriggerFactory.php index 96caf8d78e..3bced52e35 100644 --- a/app/TransactionRules/Factory/TriggerFactory.php +++ b/app/TransactionRules/Factory/TriggerFactory.php @@ -89,7 +89,7 @@ class TriggerFactory /** @var AbstractTrigger $class */ $class = self::getTriggerClass($triggerType); $obj = $class::makeFromStrings($triggerValue, $stopProcessing); - Log::debug('Created trigger from string', ['type' => $triggerType, 'value' => $triggerValue, 'stopProcessing' => $stopProcessing, 'class' => $class]); + Log::debug('Created trigger from string', ['type' => $triggerType, 'value' => $triggerValue, 'stop_processing' => $stopProcessing, 'class' => $class]); return $obj; } diff --git a/app/TransactionRules/Processor.php b/app/TransactionRules/Processor.php index 14d44d0fc7..8ff61c2b46 100644 --- a/app/TransactionRules/Processor.php +++ b/app/TransactionRules/Processor.php @@ -38,7 +38,7 @@ use Log; /** * Class Processor. */ -final class Processor +class Processor { /** @var Collection Actions to exectute */ public $actions; @@ -56,90 +56,26 @@ final class Processor /** * Processor constructor. */ - private function __construct() + public function __construct() { $this->triggers = new Collection; $this->actions = new Collection; } /** - * This method will make a Processor that will process each transaction journal using the triggers - * and actions found in the given Rule. - * - * @param Rule $rule - * @param bool $includeActions - * - * @return Processor - * @throws \FireflyIII\Exceptions\FireflyException + * @return bool */ - public static function make(Rule $rule, bool $includeActions = null): Processor + public function isStrict(): bool { - $includeActions = $includeActions ?? true; - Log::debug(sprintf('Making new rule from Rule %d', $rule->id)); - Log::debug(sprintf('Rule is strict: %s', var_export($rule->strict, true))); - $self = new self; - $self->rule = $rule; - $self->strict = $rule->strict; - $triggerSet = $rule->ruleTriggers()->orderBy('order', 'ASC')->get(); - /** @var RuleTrigger $trigger */ - foreach ($triggerSet as $trigger) { - Log::debug(sprintf('Push trigger %d', $trigger->id)); - $self->triggers->push(TriggerFactory::getTrigger($trigger)); - } - if ($includeActions === true) { - $self->actions = $rule->ruleActions()->orderBy('order', 'ASC')->get(); - } - - return $self; + return $this->strict; } /** - * This method will make a Processor that will process each transaction journal using the given - * trigger (singular!). It can only report if the transaction journal was hit by the given trigger - * and will not be able to act on it using actions. - * - * @param string $triggerName - * @param string $triggerValue - * - * @return Processor - * - * @throws \FireflyIII\Exceptions\FireflyException + * @param bool $strict */ - public static function makeFromString(string $triggerName, string $triggerValue): Processor + public function setStrict(bool $strict): void { - Log::debug(sprintf('Processor::makeFromString("%s", "%s")', $triggerName, $triggerValue)); - $self = new self; - $trigger = TriggerFactory::makeTriggerFromStrings($triggerName, $triggerValue, false); - $self->triggers->push($trigger); - - return $self; - } - - /** - * This method will make a Processor that will process each transaction journal using the given - * triggers. It can only report if the transaction journal was hit by the given triggers - * and will not be able to act on it using actions. - * - * The given triggers must be in the following format: - * - * [type => xx, value => yy, stopProcessing => bool], [type => xx, value => yy, stopProcessing => bool], - * - * @param array $triggers - * - * @return Processor - * - * @throws \FireflyIII\Exceptions\FireflyException - */ - public static function makeFromStringArray(array $triggers): Processor - { - $self = new self; - foreach ($triggers as $entry) { - $entry['value'] = $entry['value'] ?? ''; - $trigger = TriggerFactory::makeTriggerFromStrings($entry['type'], $entry['value'], $entry['stopProcessing']); - $self->triggers->push($trigger); - } - - return $self; + $this->strict = $strict; } /** @@ -234,6 +170,73 @@ final class Processor return false; } + /** + * This method will make a Processor that will process each transaction journal using the triggers + * and actions found in the given Rule. + * + * @param Rule $rule + * @param bool $includeActions + * + * @throws \FireflyIII\Exceptions\FireflyException + */ + public function make(Rule $rule, bool $includeActions = null): void + { + $includeActions = $includeActions ?? true; + Log::debug(sprintf('Making new rule from Rule %d', $rule->id)); + Log::debug(sprintf('Rule is strict: %s', var_export($rule->strict, true))); + $this->rule = $rule; + $this->strict = $rule->strict; + $triggerSet = $rule->ruleTriggers()->orderBy('order', 'ASC')->get(); + /** @var RuleTrigger $trigger */ + foreach ($triggerSet as $trigger) { + Log::debug(sprintf('Push trigger %d', $trigger->id)); + $this->triggers->push(TriggerFactory::getTrigger($trigger)); + } + if (true === $includeActions) { + $this->actions = $rule->ruleActions()->orderBy('order', 'ASC')->get(); + } + } + + /** + * This method will make a Processor that will process each transaction journal using the given + * trigger (singular!). It can only report if the transaction journal was hit by the given trigger + * and will not be able to act on it using actions. + * + * @param string $triggerName + * @param string $triggerValue + * + * @throws \FireflyIII\Exceptions\FireflyException + */ + public function makeFromString(string $triggerName, string $triggerValue): void + { + Log::debug(sprintf('Processor::makeFromString("%s", "%s")', $triggerName, $triggerValue)); + $trigger = TriggerFactory::makeTriggerFromStrings($triggerName, $triggerValue, false); + $this->triggers->push($trigger); + } + + /** + * This method will make a Processor that will process each transaction journal using the given + * triggers. It can only report if the transaction journal was hit by the given triggers + * and will not be able to act on it using actions. + * + * The given triggers must be in the following format: + * + * [type => xx, value => yy, stop_processing => bool], [type => xx, value => yy, stop_processing => bool], + * + * @param array $triggers + * + * @throws \FireflyIII\Exceptions\FireflyException + */ + public function makeFromStringArray(array $triggers): void + { + foreach ($triggers as $entry) { + $entry['value'] = $entry['value'] ?? ''; + $trigger = TriggerFactory::makeTriggerFromStrings($entry['type'], $entry['value'], $entry['stop_processing']); + $this->triggers->push($trigger); + } + + } + /** * Run the actions * diff --git a/app/TransactionRules/TransactionMatcher.php b/app/TransactionRules/TransactionMatcher.php index b417311aae..cd901335ae 100644 --- a/app/TransactionRules/TransactionMatcher.php +++ b/app/TransactionRules/TransactionMatcher.php @@ -22,7 +22,7 @@ declare(strict_types=1); namespace FireflyIII\TransactionRules; -use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Models\Rule; use FireflyIII\Models\RuleTrigger; use FireflyIII\Models\Transaction; @@ -48,11 +48,18 @@ class TransactionMatcher private $range = 200; /** @var Rule The rule to apply */ private $rule; + /** @var bool */ + private $strict; /** @var array Types that can be matched using this matcher */ private $transactionTypes = [TransactionType::DEPOSIT, TransactionType::WITHDRAWAL, TransactionType::TRANSFER]; /** @var array List of triggers to match */ private $triggers = []; + public function __construct() + { + $this->strict = false; + } + /** * This method will search the user's transaction journal (with an upper limit of $range) for * transaction journals matching the given rule. This is accomplished by trying to fire these @@ -68,8 +75,10 @@ class TransactionMatcher } // Variables used within the loop - $processor = Processor::make($this->rule, false); - $result = $this->runProcessor($processor); + /** @var Processor $processor */ + $processor = app(Processor::class); + $processor->make($this->rule, false); + $result = $this->runProcessor($processor); // If the list of matchingTransactions is larger than the maximum number of results // (e.g. if a large percentage of the transactions match), truncate the list @@ -93,8 +102,11 @@ class TransactionMatcher } // Variables used within the loop - $processor = Processor::makeFromStringArray($this->triggers); - $result = $this->runProcessor($processor); + /** @var Processor $processor */ + $processor = app(Processor::class); + $processor->makeFromStringArray($this->triggers); + $processor->setStrict($this->strict); + $result = $this->runProcessor($processor); // If the list of matchingTransactions is larger than the maximum number of results // (e.g. if a large percentage of the transactions match), truncate the list @@ -175,6 +187,22 @@ class TransactionMatcher return $this; } + /** + * @return bool + */ + public function isStrict(): bool + { + return $this->strict; + } + + /** + * @param bool $strict + */ + public function setStrict(bool $strict): void + { + $this->strict = $strict; + } + /** * Set rule * @@ -195,15 +223,15 @@ class TransactionMatcher $allTriggers = $this->rule->ruleTriggers()->whereIn('trigger_type', $valid)->get(); /** @var RuleTrigger $trigger */ foreach ($allTriggers as $trigger) { - if ($trigger->trigger_type === 'amount_less') { + if ('amount_less' === $trigger->trigger_type) { $this->maxAmount = $trigger->trigger_value; Log::debug(sprintf('Set max amount to be %s', $trigger->trigger_value)); } - if ($trigger->trigger_type === 'amount_more') { + if ('amount_more' === $trigger->trigger_type) { $this->minAmount = $trigger->trigger_value; Log::debug(sprintf('Set min amount to be %s', $trigger->trigger_value)); } - if ($trigger->trigger_type === 'amount_exactly') { + if ('amount_exactly' === $trigger->trigger_type) { $this->exactAmount = $trigger->trigger_value; Log::debug(sprintf('Set exact amount to be %s', $trigger->trigger_value)); } @@ -236,8 +264,8 @@ class TransactionMatcher $result = new Collection(); do { // Fetch a batch of transactions from the database - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); $collector->setUser(auth()->user()); $collector->setAllAssetAccounts()->setLimit($pageSize)->setPage($page)->setTypes($this->transactionTypes); if (null !== $this->maxAmount) { @@ -254,7 +282,7 @@ class TransactionMatcher } - $set = $collector->getPaginatedJournals(); + $set = $collector->getPaginatedTransactions(); Log::debug(sprintf('Found %d journals to check. ', $set->count())); // Filter transactions that match the given triggers. diff --git a/app/Transformers/AccountTransformer.php b/app/Transformers/AccountTransformer.php index 64ec626911..f22bbf6bf5 100644 --- a/app/Transformers/AccountTransformer.php +++ b/app/Transformers/AccountTransformer.php @@ -25,7 +25,7 @@ namespace FireflyIII\Transformers; use Carbon\Carbon; -use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; use FireflyIII\Models\TransactionCurrency; @@ -104,7 +104,7 @@ class AccountTransformer extends TransformerAbstract $pageSize = (int)app('preferences')->getForUser($account->user, 'listPageSize', 50)->data; // journals always use collector and limited using URL parameters. - $collector = app(JournalCollectorInterface::class); + $collector = app(TransactionCollectorInterface::class); $collector->setUser($account->user); $collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation(); if ($account->accountType->type === AccountType::ASSET) { @@ -116,9 +116,9 @@ class AccountTransformer extends TransformerAbstract $collector->setRange($this->parameters->get('start'), $this->parameters->get('end')); } $collector->setLimit($pageSize)->setPage($this->parameters->get('page')); - $journals = $collector->getJournals(); + $transactions = $collector->getTransactions(); - return $this->collection($journals, new TransactionTransformer($this->parameters), 'transactions'); + return $this->collection($transactions, new TransactionTransformer($this->parameters), 'transactions'); } /** @@ -148,7 +148,7 @@ class AccountTransformer extends TransformerAbstract $type = $account->accountType->type; $role = $this->repository->getMetaValue($account, 'accountRole'); - if ($type !== AccountType::ASSET || (string)$role === '') { + if ($type !== AccountType::ASSET || '' === (string)$role) { $role = null; } $currencyId = (int)$this->repository->getMetaValue($account, 'currency_id'); @@ -167,13 +167,13 @@ class AccountTransformer extends TransformerAbstract $date = $this->parameters->get('date'); } - if ($currencyId === 0) { + if (0 === $currencyId) { $currencyId = null; } $monthlyPaymentDate = null; $creditCardType = null; - if ($role === 'ccAsset' && $type === AccountType::ASSET) { + if ('ccAsset' === $role && $type === AccountType::ASSET) { $creditCardType = $this->repository->getMetaValue($account, 'ccType'); $monthlyPaymentDate = $this->repository->getMetaValue($account, 'ccMonthlyPaymentDate'); } @@ -194,7 +194,7 @@ class AccountTransformer extends TransformerAbstract 'updated_at' => $account->updated_at->toAtomString(), 'created_at' => $account->created_at->toAtomString(), 'name' => $account->name, - 'active' => (int)$account->active === 1, + 'active' => 1 === (int)$account->active, 'type' => $type, 'currency_id' => $currencyId, 'currency_code' => $currencyCode, diff --git a/app/Transformers/BillTransformer.php b/app/Transformers/BillTransformer.php index 2654d304d8..acc49db569 100644 --- a/app/Transformers/BillTransformer.php +++ b/app/Transformers/BillTransformer.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Transformers; use Carbon\Carbon; -use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Models\Bill; use FireflyIII\Repositories\Bill\BillRepositoryInterface; use Illuminate\Support\Collection; @@ -129,7 +129,7 @@ class BillTransformer extends TransformerAbstract $pageSize = (int)app('preferences')->getForUser($bill->user, 'listPageSize', 50)->data; // journals always use collector and limited using URL parameters. - $collector = app(JournalCollectorInterface::class); + $collector = app(TransactionCollectorInterface::class); $collector->setUser($bill->user); $collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation(); $collector->setAllAssetAccounts(); @@ -138,9 +138,9 @@ class BillTransformer extends TransformerAbstract $collector->setRange($this->parameters->get('start'), $this->parameters->get('end')); } $collector->setLimit($pageSize)->setPage($this->parameters->get('page')); - $journals = $collector->getJournals(); + $transactions = $collector->getTransactions(); - return $this->collection($journals, new TransactionTransformer($this->parameters), 'transactions'); + return $this->collection($transactions, new TransactionTransformer($this->parameters), 'transactions'); } /** @@ -174,6 +174,7 @@ class BillTransformer extends TransformerAbstract 'name' => $bill->name, 'currency_id' => $bill->transaction_currency_id, 'currency_code' => $bill->transactionCurrency->code, + 'currency_symbol' => $bill->transactionCurrency->symbol, 'amount_min' => round((float)$bill->amount_min, 2), 'amount_max' => round((float)$bill->amount_max, 2), 'date' => $bill->date->format('Y-m-d'), diff --git a/app/Transformers/BudgetTransformer.php b/app/Transformers/BudgetTransformer.php index 4b9562292a..09fe78c2d7 100644 --- a/app/Transformers/BudgetTransformer.php +++ b/app/Transformers/BudgetTransformer.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Transformers; -use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Models\Budget; use Illuminate\Support\Collection; use League\Fractal\Resource\Collection as FractalCollection; @@ -78,7 +78,7 @@ class BudgetTransformer extends TransformerAbstract $pageSize = (int)app('preferences')->getForUser($budget->user, 'listPageSize', 50)->data; // journals always use collector and limited using URL parameters. - $collector = app(JournalCollectorInterface::class); + $collector = app(TransactionCollectorInterface::class); $collector->setUser($budget->user); $collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation(); $collector->setAllAssetAccounts(); @@ -87,9 +87,9 @@ class BudgetTransformer extends TransformerAbstract $collector->setRange($this->parameters->get('start'), $this->parameters->get('end')); } $collector->setLimit($pageSize)->setPage($this->parameters->get('page')); - $journals = $collector->getJournals(); + $transactions = $collector->getTransactions(); - return $this->collection($journals, new TransactionTransformer($this->parameters), 'transactions'); + return $this->collection($transactions, new TransactionTransformer($this->parameters), 'transactions'); } /** @@ -118,7 +118,7 @@ class BudgetTransformer extends TransformerAbstract 'id' => (int)$budget->id, 'updated_at' => $budget->updated_at->toAtomString(), 'created_at' => $budget->created_at->toAtomString(), - 'active' => (int)$budget->active === 1, + 'active' => 1 === (int)$budget->active, 'name' => $budget->name, 'links' => [ [ diff --git a/app/Transformers/CategoryTransformer.php b/app/Transformers/CategoryTransformer.php index 421946e6b2..6282a1b48b 100644 --- a/app/Transformers/CategoryTransformer.php +++ b/app/Transformers/CategoryTransformer.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Transformers; -use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Models\Category; use Illuminate\Support\Collection; use League\Fractal\Resource\Collection as FractalCollection; @@ -78,7 +78,7 @@ class CategoryTransformer extends TransformerAbstract $pageSize = (int)app('preferences')->getForUser($category->user, 'listPageSize', 50)->data; // journals always use collector and limited using URL parameters. - $collector = app(JournalCollectorInterface::class); + $collector = app(TransactionCollectorInterface::class); $collector->setUser($category->user); $collector->withOpposingAccount()->withCategoryInformation()->withCategoryInformation(); $collector->setAllAssetAccounts(); @@ -87,9 +87,9 @@ class CategoryTransformer extends TransformerAbstract $collector->setRange($this->parameters->get('start'), $this->parameters->get('end')); } $collector->setLimit($pageSize)->setPage($this->parameters->get('page')); - $journals = $collector->getJournals(); + $transactions = $collector->getTransactions(); - return $this->collection($journals, new TransactionTransformer($this->parameters), 'transactions'); + return $this->collection($transactions, new TransactionTransformer($this->parameters), 'transactions'); } /** diff --git a/app/Transformers/CurrencyExchangeRateTransformer.php b/app/Transformers/CurrencyExchangeRateTransformer.php index e1c8ebc0e6..e9dbd007dc 100644 --- a/app/Transformers/CurrencyExchangeRateTransformer.php +++ b/app/Transformers/CurrencyExchangeRateTransformer.php @@ -29,6 +29,9 @@ use League\Fractal\Resource\Item; use League\Fractal\TransformerAbstract; use Symfony\Component\HttpFoundation\ParameterBag; +/** + * Class CurrencyExchangeRateTransformer + */ class CurrencyExchangeRateTransformer extends TransformerAbstract { /** @@ -79,6 +82,11 @@ class CurrencyExchangeRateTransformer extends TransformerAbstract return $this->item($rate->toCurrency, new CurrencyTransformer($this->parameters), 'transaction_currencies'); } + /** + * @param CurrencyExchangeRate $rate + * + * @return array + */ public function transform(CurrencyExchangeRate $rate): array { $data = [ diff --git a/app/Transformers/JournalLinkTransformer.php b/app/Transformers/JournalLinkTransformer.php index f05d6807b8..43e5f386dd 100644 --- a/app/Transformers/JournalLinkTransformer.php +++ b/app/Transformers/JournalLinkTransformer.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Transformers; -use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Models\Note; use FireflyIII\Models\TransactionJournalLink; use Illuminate\Support\Collection; @@ -75,12 +75,12 @@ class JournalLinkTransformer extends TransformerAbstract { // need to use the collector to get the transaction :( // journals always use collector and limited using URL parameters. - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); $collector->setUser($link->source->user); $collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation(); $collector->setJournals(new Collection([$link->source])); - $transactions = $collector->getJournals(); + $transactions = $collector->getTransactions(); return $this->item($transactions->first(), new TransactionTransformer($this->parameters), 'transactions'); } @@ -104,12 +104,12 @@ class JournalLinkTransformer extends TransformerAbstract { // need to use the collector to get the transaction :( // journals always use collector and limited using URL parameters. - /** @var JournalCollectorInterface $collector */ - $collector = app(JournalCollectorInterface::class); + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); $collector->setUser($link->source->user); $collector->withOpposingAccount()->withCategoryInformation()->withBudgetInformation(); $collector->setJournals(new Collection([$link->destination])); - $transactions = $collector->getJournals(); + $transactions = $collector->getTransactions(); return $this->item($transactions->first(), new TransactionTransformer($this->parameters), 'transactions'); } diff --git a/app/Transformers/JournalMetaTransformer.php b/app/Transformers/JournalMetaTransformer.php index 6f54be2e2e..125304b05b 100644 --- a/app/Transformers/JournalMetaTransformer.php +++ b/app/Transformers/JournalMetaTransformer.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Transformers; -use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Models\TransactionJournalMeta; use Illuminate\Support\Collection; use League\Fractal\Resource\Collection as FractalCollection; @@ -78,7 +78,7 @@ class JournalMetaTransformer extends TransformerAbstract $pageSize = (int)app('preferences')->getForUser($journal->user, 'listPageSize', 50)->data; // journals always use collector and limited using URL parameters. - $collector = app(JournalCollectorInterface::class); + $collector = app(TransactionCollectorInterface::class); $collector->setUser($journal->user); $collector->withOpposingAccount()->withCategoryInformation()->withCategoryInformation(); $collector->setAllAssetAccounts(); @@ -87,9 +87,9 @@ class JournalMetaTransformer extends TransformerAbstract $collector->setRange($this->parameters->get('start'), $this->parameters->get('end')); } $collector->setLimit($pageSize)->setPage($this->parameters->get('page')); - $journals = $collector->getJournals(); + $transactions= $collector->getTransactions(); - return $this->collection($journals, new TransactionTransformer($this->parameters), 'transactions'); + return $this->collection($transactions, new TransactionTransformer($this->parameters), 'transactions'); } /** diff --git a/app/Transformers/PiggyBankEventTransformer.php b/app/Transformers/PiggyBankEventTransformer.php index 44b60aad55..5999421b30 100644 --- a/app/Transformers/PiggyBankEventTransformer.php +++ b/app/Transformers/PiggyBankEventTransformer.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Transformers; -use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Models\PiggyBankEvent; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; @@ -95,7 +95,8 @@ class PiggyBankEventTransformer extends TransformerAbstract $pageSize = (int)app('preferences')->getForUser($journal->user, 'listPageSize', 50)->data; // journals always use collector and limited using URL parameters. - $collector = app(JournalCollectorInterface::class); + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); $collector->setUser($journal->user); $collector->withOpposingAccount()->withCategoryInformation()->withCategoryInformation(); $collector->setAllAssetAccounts(); @@ -104,9 +105,9 @@ class PiggyBankEventTransformer extends TransformerAbstract $collector->setRange($this->parameters->get('start'), $this->parameters->get('end')); } $collector->setLimit($pageSize)->setPage($this->parameters->get('page')); - $journals = $collector->getJournals(); + $transactions = $collector->getTransactions(); - return $this->item($journals->first(), new TransactionTransformer($this->parameters), 'transactions'); + return $this->item($transactions->first(), new TransactionTransformer($this->parameters), 'transactions'); } /** @@ -129,7 +130,8 @@ class PiggyBankEventTransformer extends TransformerAbstract /** @var CurrencyRepositoryInterface $repository */ $repository = app(CurrencyRepositoryInterface::class); $repository->setUser($account->user); - $currency = $repository->findNull($currencyId); + $currency = $repository->findNull($currencyId); + /** @noinspection NullPointerExceptionInspection */ $decimalPlaces = $currency->decimal_places; } diff --git a/app/Transformers/PiggyBankTransformer.php b/app/Transformers/PiggyBankTransformer.php index 897e969b7e..7490b89f02 100644 --- a/app/Transformers/PiggyBankTransformer.php +++ b/app/Transformers/PiggyBankTransformer.php @@ -170,7 +170,7 @@ class PiggyBankTransformer extends TransformerAbstract 'start_date' => $startDate, 'target_date' => $targetDate, 'order' => (int)$piggyBank->order, - 'active' => (int)$piggyBank->active === 1, + 'active' => true, 'notes' => null, 'links' => [ [ diff --git a/app/Transformers/PreferenceTransformer.php b/app/Transformers/PreferenceTransformer.php index 033b682050..254c11bf3c 100644 --- a/app/Transformers/PreferenceTransformer.php +++ b/app/Transformers/PreferenceTransformer.php @@ -28,6 +28,9 @@ use FireflyIII\Models\Preference; use League\Fractal\TransformerAbstract; use Symfony\Component\HttpFoundation\ParameterBag; +/** + * Class PreferenceTransformer + */ class PreferenceTransformer extends TransformerAbstract { /** @@ -45,6 +48,11 @@ class PreferenceTransformer extends TransformerAbstract /** @var ParameterBag */ protected $parameters; + /** + * PreferenceTransformer constructor. + * + * @param ParameterBag $parameters + */ public function __construct(ParameterBag $parameters) { $this->parameters = $parameters; diff --git a/app/Transformers/TagTransformer.php b/app/Transformers/TagTransformer.php index 19ce472453..09f1ac8264 100644 --- a/app/Transformers/TagTransformer.php +++ b/app/Transformers/TagTransformer.php @@ -24,7 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Transformers; -use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Helpers\Collector\TransactionCollectorInterface; use FireflyIII\Models\Tag; use League\Fractal\Resource\Collection as FractalCollection; use League\Fractal\Resource\Item; @@ -77,7 +77,7 @@ class TagTransformer extends TransformerAbstract $pageSize = (int)app('preferences')->getForUser($tag->user, 'listPageSize', 50)->data; // journals always use collector and limited using URL parameters. - $collector = app(JournalCollectorInterface::class); + $collector = app(TransactionCollectorInterface::class); $collector->setUser($tag->user); $collector->withOpposingAccount()->withCategoryInformation()->withCategoryInformation(); $collector->setAllAssetAccounts(); @@ -86,9 +86,9 @@ class TagTransformer extends TransformerAbstract $collector->setRange($this->parameters->get('start'), $this->parameters->get('end')); } $collector->setLimit($pageSize)->setPage($this->parameters->get('page')); - $journals = $collector->getJournals(); + $transactions = $collector->getTransactions(); - return $this->collection($journals, new TransactionTransformer($this->parameters), 'transactions'); + return $this->collection($transactions, new TransactionTransformer($this->parameters), 'transactions'); } /** @@ -121,7 +121,7 @@ class TagTransformer extends TransformerAbstract 'tag' => $tag->tag, 'tag_mode' => $tag->tagMode, 'date' => $date, - 'description' => $tag->description === '' ? null : $tag->description, + 'description' => '' === $tag->description ? null : $tag->description, 'latitude' => (float)$tag->latitude, 'longitude' => (float)$tag->longitude, 'zoom_level' => (int)$tag->zoomLevel, diff --git a/app/Transformers/UserTransformer.php b/app/Transformers/UserTransformer.php index dadb23fb94..cca96ecedd 100644 --- a/app/Transformers/UserTransformer.php +++ b/app/Transformers/UserTransformer.php @@ -193,7 +193,7 @@ class UserTransformer extends TransformerAbstract 'updated_at' => $user->updated_at->toAtomString(), 'created_at' => $user->created_at->toAtomString(), 'email' => $user->email, - 'blocked' => (int)$user->blocked === 1, + 'blocked' => 1 === (int)$user->blocked, 'blocked_code' => $user->blocked_code, 'role' => $role, 'links' => [ diff --git a/app/User.php b/app/User.php index c8ff6c43c6..17a64c29c9 100644 --- a/app/User.php +++ b/app/User.php @@ -61,7 +61,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; * @property bool $isAdmin used in admin user controller. * @property bool $has2FA used in admin user controller. * @property array $prefs used in admin user controller. - * @property string password + * @property string password * @property Collection roles * @property string blocked_code * @property bool blocked @@ -216,7 +216,7 @@ class User extends Authenticatable { $bytes = random_bytes(16); - return (string)bin2hex($bytes); + return bin2hex($bytes); } /** @@ -302,7 +302,7 @@ class User extends Authenticatable * * @param string $token */ - public function sendPasswordResetNotification($token) + public function sendPasswordResetNotification($token): void { $ipAddress = Request::ip(); diff --git a/app/Validation/FireflyValidator.php b/app/Validation/FireflyValidator.php index 1034b1ef63..c16c7430ba 100644 --- a/app/Validation/FireflyValidator.php +++ b/app/Validation/FireflyValidator.php @@ -38,7 +38,9 @@ use FireflyIII\TransactionRules\Triggers\TriggerInterface; use FireflyIII\User; use Google2FA; use Illuminate\Contracts\Encryption\DecryptException; +use Illuminate\Support\Collection; use Illuminate\Validation\Validator; +use Log; /** * Class FireflyValidator. @@ -195,12 +197,12 @@ class FireflyValidator extends Validator * * @return bool */ - public function validateMore($attribute, $value, $parameters): bool + public function validateLess($attribute, $value, $parameters): bool { /** @var mixed $compare */ $compare = $parameters[0] ?? '0'; - return bccomp((string)$value, (string)$compare) > 0; + return bccomp((string)$value, (string)$compare) < 0; } /** @@ -211,15 +213,14 @@ class FireflyValidator extends Validator * * @return bool */ - public function validateLess($attribute, $value, $parameters): bool + public function validateMore($attribute, $value, $parameters): bool { /** @var mixed $compare */ $compare = $parameters[0] ?? '0'; - return bccomp((string)$value, (string)$compare) < 0; + return bccomp((string)$value, (string)$compare) > 0; } - /** * @SuppressWarnings(PHPMD.UnusedFormalParameter) * @@ -242,152 +243,108 @@ class FireflyValidator extends Validator } /** - * @param $attribute + * @param string $attribute + * + * @param string $value * * @return bool */ - public function validateRuleActionValue($attribute): bool + public function validateRuleActionValue(string $attribute, string $value): bool { - // get the index from a string like "rule-action-value.2". + // first, get the index from this string: $parts = explode('.', $attribute); - $index = $parts[\count($parts) - 1]; - if ($index === 'value') { - // user is coming from API. - $index = $parts[\count($parts) - 2]; - } - $index = (int)$index; + $index = (int)($parts[1] ?? '0'); - // get actions from $this->data - $actions = []; - if (isset($this->data['rule-action']) && \is_array($this->data['rule-action'])) { - $actions = $this->data['rule-action']; - } - if (isset($this->data['rule-actions']) && \is_array($this->data['rule-actions'])) { - $actions = $this->data['rule-actions']; + // get the name of the trigger from the data array: + $actionType = $this->data['rule_actions'][$index]['name'] ?? 'invalid'; + + // if it's "invalid" return false. + if ('invalid' === $actionType) { + return false; } + // if it's set_budget, verify the budget name: + if ('set_budget' === $actionType) { + /** @var BudgetRepositoryInterface $repository */ + $repository = app(BudgetRepositoryInterface::class); + $budgets = $repository->getBudgets(); + // count budgets, should have at least one + $count = $budgets->filter( + function (Budget $budget) use ($value) { + return $budget->name === $value; + } + )->count(); - // loop all rule-actions. - // check if rule-action-value matches the thing. - if (\is_array($actions)) { - $name = $this->getRuleActionName($index); - $value = $this->getRuleActionValue($index); - switch ($name) { - default: - - return true; - case 'set_budget': - /** @var BudgetRepositoryInterface $repository */ - $repository = app(BudgetRepositoryInterface::class); - $budgets = $repository->getBudgets(); - // count budgets, should have at least one - $count = $budgets->filter( - function (Budget $budget) use ($value) { - return $budget->name === $value; - } - )->count(); - - return 1 === $count; - case 'link_to_bill': - /** @var BillRepositoryInterface $repository */ - $repository = app(BillRepositoryInterface::class); - $bill = $repository->findByName((string)$value); - - return null !== $bill; - case 'invalid': - return false; - } + return 1 === $count; } - return false; + // if it's link to bill, verify the name of the bill. + if ('link_to_bill' === $actionType) { + /** @var BillRepositoryInterface $repository */ + $repository = app(BillRepositoryInterface::class); + $bill = $repository->findByName($value); + + return null !== $bill; + } + + // return true for the rest. + return true; } /** - * @param $attribute + * $attribute has the format rule_triggers.%d.value. + * + * @param string $attribute + * @param string $value * * @return bool */ - public function validateRuleTriggerValue($attribute): bool + public function validateRuleTriggerValue(string $attribute, string $value): bool { - // get the index from a string like "rule-trigger-value.2". + // + + // first, get the index from this string: $parts = explode('.', $attribute); - $index = $parts[\count($parts) - 1]; - // if the index is not a number, then we might be dealing with an API $attribute - // which is formatted "rule-triggers.0.value" - if ($index === 'value') { - $index = $parts[\count($parts) - 2]; - } - $index = (int)$index; + $index = (int)($parts[1] ?? '0'); - // get triggers from $this->data - $triggers = []; - if (isset($this->data['rule-trigger']) && \is_array($this->data['rule-trigger'])) { - $triggers = $this->data['rule-trigger']; - } - if (isset($this->data['rule-triggers']) && \is_array($this->data['rule-triggers'])) { - $triggers = $this->data['rule-triggers']; + // get the name of the trigger from the data array: + $triggerType = $this->data['rule_triggers'][$index]['name'] ?? 'invalid'; + + // invalid always returns false: + if ('invalid' === $triggerType) { + return false; } - // loop all rule-triggers. - // check if rule-value matches the thing. - if (\is_array($triggers)) { - $name = $this->getRuleTriggerName($index); - $value = $this->getRuleTriggerValue($index); - - // break on some easy checks: - switch ($name) { - case 'amount_less': - case 'amount_more': - case 'amount_exactly': - $result = is_numeric($value); - if (false === $result) { - return false; - } - break; - case 'from_account_starts': - case 'from_account_ends': - case 'from_account_is': - case 'from_account_contains': - case 'to_account_starts': - case 'to_account_ends': - case 'to_account_is': - case 'to_account_contains': - case 'description_starts': - case 'description_ends': - case 'description_contains': - case 'description_is': - case 'category_is': - case 'budget_is': - case 'tag_is': - case 'currency_is': - case 'notes_contain': - case 'notes_start': - case 'notes_end': - case 'notes_are': - return \strlen($value) > 0; - - break; - case 'transaction_type': - $count = TransactionType::where('type', $value)->count(); - if (!(1 === $count)) { - return false; - } - break; - case 'invalid': - return false; - } - // still a special case where the trigger is - // triggered in such a way that it would trigger ANYTHING. We can check for such things - // with function willmatcheverything - // we know which class it is so dont bother checking that. - $classes = Config::get('firefly.rule-triggers'); - /** @var TriggerInterface $class */ - $class = $classes[$name]; - - return !$class::willMatchEverything($value); + // these trigger types need a numerical check: + $numerical = ['amount_less', 'amount_more', 'amount_exactly']; + if (\in_array($triggerType, $numerical, true)) { + return is_numeric($value); } - return false; + // these trigger types need a simple strlen check: + $length = ['from_account_starts', 'from_account_ends', 'from_account_is', 'from_account_contains', 'to_account_starts', 'to_account_ends', + 'to_account_is', 'to_account_contains', 'description_starts', 'description_ends', 'description_contains', 'description_is', 'category_is', + 'budget_is', 'tag_is', 'currency_is', 'notes_contain', 'notes_start', 'notes_end', 'notes_are',]; + if (\in_array($triggerType, $length, true)) { + return '' !== $value; + } + + // check transaction type. + if ('transaction_type' === $triggerType) { + $count = TransactionType::where('type', $value)->count(); + + return 1 !== $count; + } + + // and finally a "will match everything check": + $classes = app('config')->get('firefly.rule-triggers'); + /** @var TriggerInterface $class */ + $class = $classes[$triggerType] ?? false; + if (false === $class) { + return false; + } + + return !$class::willMatchEverything($value); } /** @@ -457,7 +414,7 @@ class FireflyValidator extends Validator public function validateUniqueAccountNumberForUser($attribute, $value, $parameters): bool { $accountId = (int)($this->data['id'] ?? 0.0); - if ($accountId === 0) { + if (0 === $accountId) { $accountId = (int)($parameters[0] ?? 0.0); } @@ -466,9 +423,9 @@ class FireflyValidator extends Validator ->where('accounts.user_id', auth()->user()->id) ->where('account_meta.name', 'accountNumber'); - if ((int)$accountId > 0) { + if ($accountId > 0) { // exclude current account from check. - $query->where('account_meta.account_id', '!=', (int)$accountId); + $query->where('account_meta.account_id', '!=', $accountId); } $set = $query->get(['account_meta.*']); @@ -499,9 +456,7 @@ class FireflyValidator extends Validator public function validateUniqueObjectForUser($attribute, $value, $parameters): bool { $value = $this->tryDecrypt($value); - // exclude? - $table = $parameters[0]; - $field = $parameters[1]; + [$table, $field] = $parameters; $exclude = (int)($parameters[2] ?? 0.0); /* @@ -560,66 +515,6 @@ class FireflyValidator extends Validator return true; } - /** - * @param int $index - * - * @return string - */ - private function getRuleActionName(int $index): string - { - $name = $this->data['rule-action'][$index] ?? 'invalid'; - if (!isset($this->data['rule-action'][$index])) { - $name = $this->data['rule-actions'][$index]['name'] ?? 'invalid'; - } - - return $name; - } - - /** - * @param int $index - * - * @return string - */ - private function getRuleActionValue(int $index): string - { - $value = $this->data['rule-action-value'][$index] ?? ''; - if (!isset($this->data['rule-action-value'][$index])) { - $value = $this->data['rule-actions'][$index]['value'] ?? ''; - } - - return $value; - } - - /** - * @param int $index - * - * @return string - */ - private function getRuleTriggerName(int $index): string - { - $name = $this->data['rule-trigger'][$index] ?? 'invalid'; - if (!isset($this->data['rule-trigger'][$index])) { - $name = $this->data['rule-triggers'][$index]['name'] ?? 'invalid'; - } - - return $name; - } - - /** - * @param int $index - * - * @return string - */ - private function getRuleTriggerValue(int $index): string - { - $value = $this->data['rule-trigger-value'][$index] ?? ''; - if (!isset($this->data['rule-trigger-value'][$index])) { - $value = $this->data['rule-triggers'][$index]['value'] ?? ''; - } - - return $value; - } - /** * @param $value * @@ -630,7 +525,7 @@ class FireflyValidator extends Validator try { $value = Crypt::decrypt($value); } catch (DecryptException $e) { - // do not care. + Log::debug(sprintf('Could not decrypt. %s', $e->getMessage())); } return $value; @@ -674,6 +569,7 @@ class FireflyValidator extends Validator $ignore = $existingAccount->id; $value = $this->tryDecrypt($value); + /** @var Collection $set */ $set = auth()->user()->accounts()->where('account_type_id', $type->id)->where('id', '!=', $ignore)->get(); /** @var Account $entry */ foreach ($set as $entry) { @@ -697,6 +593,7 @@ class FireflyValidator extends Validator $ignore = (int)($parameters[0] ?? 0.0); $value = $this->tryDecrypt($value); + /** @var Collection $set */ $set = auth()->user()->accounts()->where('account_type_id', $type->id)->where('id', '!=', $ignore)->get(); /** @var Account $entry */ foreach ($set as $entry) { @@ -717,11 +614,14 @@ class FireflyValidator extends Validator */ private function validateByAccountTypeString(string $value, array $parameters, string $type): bool { - $search = Config::get('firefly.accountTypeByIdentifier.' . $type); - $accountType = AccountType::whereType($search)->first(); - $ignore = (int)($parameters[0] ?? 0.0); - - $set = auth()->user()->accounts()->where('account_type_id', $accountType->id)->where('id', '!=', $ignore)->get(); + /** @var array $search */ + $search = Config::get('firefly.accountTypeByIdentifier.' . $type); + /** @var Collection $accountTypes */ + $accountTypes = AccountType::whereIn('type', $search)->get(); + $ignore = (int)($parameters[0] ?? 0.0); + $accountTypeIds = $accountTypes->pluck('id')->toArray(); + /** @var Collection $set */ + $set = auth()->user()->accounts()->whereIn('account_type_id', $accountTypeIds)->where('id', '!=', $ignore)->get(); /** @var Account $entry */ foreach ($set as $entry) { if ($entry->name === $value) { diff --git a/app/Validation/RecurrenceValidation.php b/app/Validation/RecurrenceValidation.php index 98ad0d0342..91e4cc47de 100644 --- a/app/Validation/RecurrenceValidation.php +++ b/app/Validation/RecurrenceValidation.php @@ -27,6 +27,7 @@ use Carbon\Carbon; use Exception; use Illuminate\Validation\Validator; use InvalidArgumentException; +use Log; /** * Trait RecurrenceValidation @@ -36,6 +37,39 @@ use InvalidArgumentException; */ trait RecurrenceValidation { + /** + * Adds an error to the validator when there are no repetitions in the array of data. + * + * @param Validator $validator + */ + public function validateOneRepetition(Validator $validator): void + { + $data = $validator->getData(); + $repetitions = $data['repetitions'] ?? []; + // need at least one transaction + if (0 === \count($repetitions)) { + $validator->errors()->add('description', (string)trans('validation.at_least_one_repetition')); + } + } + + /** + * Validates that the recurrence has valid repetition information. It either doesn't stop, + * or stops after X times or at X date. Not both of them., + * + * @param Validator $validator + */ + public function validateRecurrenceRepetition(Validator $validator): void + { + $data = $validator->getData(); + $repetitions = $data['nr_of_repetitions'] ?? null; + $repeatUntil = $data['repeat_until'] ?? null; + if (null !== $repetitions && null !== $repeatUntil) { + // expect a date OR count: + $validator->errors()->add('repeat_until', (string)trans('validation.require_repeat_until')); + $validator->errors()->add('nr_of_repetitions', (string)trans('validation.require_repeat_until')); + } + } + /** * @param Validator $validator */ @@ -72,39 +106,6 @@ trait RecurrenceValidation } } - /** - * Adds an error to the validator when there are no repetitions in the array of data. - * - * @param Validator $validator - */ - public function validateOneRepetition(Validator $validator): void - { - $data = $validator->getData(); - $repetitions = $data['repetitions'] ?? []; - // need at least one transaction - if (\count($repetitions) === 0) { - $validator->errors()->add('description', (string)trans('validation.at_least_one_repetition')); - } - } - - /** - * Validates that the recurrence has valid repetition information. It either doesn't stop, - * or stops after X times or at X date. Not both of them., - * - * @param Validator $validator - */ - public function validateRecurrenceRepetition(Validator $validator): void - { - $data = $validator->getData(); - $repetitions = $data['nr_of_repetitions'] ?? null; - $repeatUntil = $data['repeat_until'] ?? null; - if (null !== $repetitions && null !== $repeatUntil) { - // expect a date OR count: - $validator->errors()->add('repeat_until', (string)trans('validation.require_repeat_until')); - $validator->errors()->add('nr_of_repetitions', (string)trans('validation.require_repeat_until')); - } - } - /** * If the repetition type is daily, the moment should be empty. * @@ -144,7 +145,7 @@ trait RecurrenceValidation protected function validateNdom(Validator $validator, int $index, string $moment): void { $parameters = explode(',', $moment); - if (\count($parameters) !== 2) { + if (2 !== \count($parameters)) { $validator->errors()->add(sprintf('repetitions.%d.moment', $index), (string)trans('validation.valid_recurrence_rep_moment')); return; @@ -187,6 +188,7 @@ trait RecurrenceValidation try { Carbon::createFromFormat('Y-m-d', $moment); } catch (InvalidArgumentException|Exception $e) { + Log::debug(sprintf('Invalid argument for Carbon: %s', $e->getMessage())); $validator->errors()->add(sprintf('repetitions.%d.moment', $index), (string)trans('validation.valid_recurrence_rep_moment')); } } diff --git a/app/Validation/TransactionValidation.php b/app/Validation/TransactionValidation.php index 0c874afd03..cac645acdd 100644 --- a/app/Validation/TransactionValidation.php +++ b/app/Validation/TransactionValidation.php @@ -112,10 +112,10 @@ trait TransactionValidation { $data = $validator->getData(); $transactions = $data['transactions'] ?? []; - $journalDescription = (string)($data['description'] ?? ''); + $journalDescription = (string)($data['description'] ?? null); $validDescriptions = 0; foreach ($transactions as $index => $transaction) { - if (\strlen((string)($transaction['description'] ?? '')) > 0) { + if (\strlen((string)($transaction['description'] ?? null)) > 0) { $validDescriptions++; } } @@ -157,9 +157,9 @@ trait TransactionValidation { $data = $validator->getData(); $transactions = $data['transactions'] ?? []; - $journalDescription = (string)($data['description'] ?? ''); + $journalDescription = (string)($data['description'] ?? null); foreach ($transactions as $index => $transaction) { - $description = (string)($transaction['description'] ?? ''); + $description = (string)($transaction['description'] ?? null); // description cannot be equal to journal description. if ($description === $journalDescription) { $validator->errors()->add('transactions.' . $index . '.description', (string)trans('validation.equal_description')); @@ -249,7 +249,7 @@ trait TransactionValidation $data = $validator->getData(); $transactions = $data['transactions'] ?? []; foreach ($transactions as $index => $transaction) { - $description = (string)($transaction['description'] ?? ''); + $description = (string)($transaction['description'] ?? null); // filled description is mandatory for split transactions. if ('' === $description && \count($transactions) > 1) { $validator->errors()->add( @@ -338,13 +338,13 @@ trait TransactionValidation if ($accountId < 1 && '' === $accountName) { return null; } - if ($accountId !== 0) { + if (0 !== $accountId) { // ID belongs to user and is $type account: /** @var AccountRepositoryInterface $repository */ $repository = app(AccountRepositoryInterface::class); $repository->setUser($admin); $set = $repository->getAccountsById([$accountId]); - if ($set->count() === 1) { + if (1 === $set->count()) { /** @var Account $first */ $first = $set->first(); if ($first->accountType->type !== $type) { diff --git a/changelog.md b/changelog.md index 121443f5bf..2756b9a2af 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,56 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [4.7.6] - 2018-09-02 +### Added +- [Issue 145](https://github.com/firefly-iii/firefly-iii/issues/145) You can now download transactions from YNAB. +- [Issue 306](https://github.com/firefly-iii/firefly-iii/issues/306) You can now add liabilities to Firefly III. +- [Issue 740](https://github.com/firefly-iii/firefly-iii/issues/740) Various charts are now currency aware. +- [Issue 833](https://github.com/firefly-iii/firefly-iii/issues/833) Bills can use non-default currencies. +- [Issue 1578](https://github.com/firefly-iii/firefly-iii/issues/1578) Firefly III will notify you if the cron job hasn't fired. +- [Issue 1623](https://github.com/firefly-iii/firefly-iii/issues/1623) New transactions will link back from the success message. +- [Issue 1624](https://github.com/firefly-iii/firefly-iii/issues/1624) transactions will link to the object. +- You can call the cron job over the web now (see docs). +- You don't need to call the cron job every minute any more. +- Various charts are now red/green to signify income and expenses. +- Option to add or remove accounts from the net worth calculations. + +### Deprecated +- This will be the last release on PHP 7.1. Future versions will require PHP 7.2. + +### Fixed +- [Issue 1460](https://github.com/firefly-iii/firefly-iii/issues/1460) Downloading transactions from bunq should go more smoothly. +- [Issue 1464](https://github.com/firefly-iii/firefly-iii/issues/1464) Fixed the docker file to work on Raspberry Pi's. +- [Issue 1540](https://github.com/firefly-iii/firefly-iii/issues/1540) The Docker file now has a working cron job for recurring transactions. +- [Issue 1564](https://github.com/firefly-iii/firefly-iii/issues/1564) Fix double transfers when importing from bunq. +- [Issue 1575](https://github.com/firefly-iii/firefly-iii/issues/1575) Some views would give a XSRF token warning +- [Issue 1576](https://github.com/firefly-iii/firefly-iii/issues/1576) Fix assigning budgets +- [Issue 1580](https://github.com/firefly-iii/firefly-iii/issues/1580) Missing string for translation +- [Issue 1581](https://github.com/firefly-iii/firefly-iii/issues/1581) Expand help text +- [Issue 1584](https://github.com/firefly-iii/firefly-iii/issues/1584) Link to administration is back. +- [Issue 1586](https://github.com/firefly-iii/firefly-iii/issues/1586) Date fields in import were mislabeled. +- [Issue 1593](https://github.com/firefly-iii/firefly-iii/issues/1593) Link types are translatable. +- [Issue 1594](https://github.com/firefly-iii/firefly-iii/issues/1594) Very long breadcrumbs are weird. +- [Issue 1598](https://github.com/firefly-iii/firefly-iii/issues/1598) Fix budget calculations. +- [Issue 1597](https://github.com/firefly-iii/firefly-iii/issues/1597) Piggy banks are always inactive. +- [Issue 1605](https://github.com/firefly-iii/firefly-iii/issues/1605) System will ignore foreign currency setting if user doesn't indicate the amount. +- [Issue 1607](https://github.com/firefly-iii/firefly-iii/issues/1607) Firefly III trusts the Heroku load balancer, fixing deployment on Heroku. +- [Issue 1608](https://github.com/firefly-iii/firefly-iii/issues/1608) Spelling error in command line import. +- [Issue 1609](https://github.com/firefly-iii/firefly-iii/issues/1609) Link to budgets page was absolute. +- [Issue 1615](https://github.com/firefly-iii/firefly-iii/issues/1615) Fix currency bug in transactions. +- [Issue 1616](https://github.com/firefly-iii/firefly-iii/issues/1616) Fix null pointer exception in pie charts. +- [Issue 1617](https://github.com/firefly-iii/firefly-iii/issues/1617) Fix for complex tag names in URL's. +- [Issue 1620](https://github.com/firefly-iii/firefly-iii/issues/1620) Fixed index reference in API. +- [Issue 1642](https://github.com/firefly-iii/firefly-iii/issues/1642) Fix issue with split journals. +- [Issue 1643](https://github.com/firefly-iii/firefly-iii/issues/1643) Fix reconciliation issue. +- Users can no longer give expenses a budget. +- Fix bug in Spectre import. +- Heroku would not make you owner. + +### Security +- Add `.htaccess` files to all public directories. +- New secure headers will make Firefly III slightly more secure. + ## [4.7.5.3] - 2017-07-28 ### Added - Many updated French translations thanks to [@bubka](https://crowdin.com/profile/bubka). diff --git a/composer.lock b/composer.lock index 7ecd5c1198..416c0a5e1a 100644 --- a/composer.lock +++ b/composer.lock @@ -114,7 +114,7 @@ "payment", "sepa" ], - "time": "2018-07-24T18:37:12+00:00" + "time": "2018-09-01T12:54:04+00:00" }, { "name": "davejamesmiller/laravel-breadcrumbs", @@ -240,16 +240,16 @@ }, { "name": "doctrine/cache", - "version": "v1.7.1", + "version": "v1.8.0", "source": { "type": "git", "url": "https://github.com/doctrine/cache.git", - "reference": "b3217d58609e9c8e661cd41357a54d926c4a2a1a" + "reference": "d768d58baee9a4862ca783840eca1b9add7a7f57" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/b3217d58609e9c8e661cd41357a54d926c4a2a1a", - "reference": "b3217d58609e9c8e661cd41357a54d926c4a2a1a", + "url": "https://api.github.com/repos/doctrine/cache/zipball/d768d58baee9a4862ca783840eca1b9add7a7f57", + "reference": "d768d58baee9a4862ca783840eca1b9add7a7f57", "shasum": "" }, "require": { @@ -260,8 +260,9 @@ }, "require-dev": { "alcaeus/mongo-php-adapter": "^1.1", + "doctrine/coding-standard": "^4.0", "mongodb/mongodb": "^1.1", - "phpunit/phpunit": "^5.7", + "phpunit/phpunit": "^7.0", "predis/predis": "~1.0" }, "suggest": { @@ -270,7 +271,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.7.x-dev" + "dev-master": "1.8.x-dev" } }, "autoload": { @@ -305,12 +306,12 @@ } ], "description": "Caching library offering an object-oriented API for many cache backends", - "homepage": "http://www.doctrine-project.org", + "homepage": "https://www.doctrine-project.org", "keywords": [ "cache", "caching" ], - "time": "2017-08-25T07:02:50+00:00" + "time": "2018-08-21T18:01:43+00:00" }, { "name": "doctrine/dbal", @@ -637,16 +638,16 @@ }, { "name": "egulias/email-validator", - "version": "2.1.4", + "version": "2.1.5", "source": { "type": "git", "url": "https://github.com/egulias/EmailValidator.git", - "reference": "8790f594151ca6a2010c6218e09d96df67173ad3" + "reference": "54859fabea8b3beecbb1a282888d5c990036b9e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/8790f594151ca6a2010c6218e09d96df67173ad3", - "reference": "8790f594151ca6a2010c6218e09d96df67173ad3", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/54859fabea8b3beecbb1a282888d5c990036b9e3", + "reference": "54859fabea8b3beecbb1a282888d5c990036b9e3", "shasum": "" }, "require": { @@ -690,7 +691,7 @@ "validation", "validator" ], - "time": "2018-04-10T10:11:19+00:00" + "time": "2018-08-16T20:49:45+00:00" }, { "name": "erusev/parsedown", @@ -1021,16 +1022,16 @@ }, { "name": "laravel/framework", - "version": "v5.6.29", + "version": "v5.6.35", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "acc6b5c54ab196d3358f60acc5f55d9ebaaccc02" + "reference": "02abbe2091404ebe032a6d5a83075031b7072ebe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/acc6b5c54ab196d3358f60acc5f55d9ebaaccc02", - "reference": "acc6b5c54ab196d3358f60acc5f55d9ebaaccc02", + "url": "https://api.github.com/repos/laravel/framework/zipball/02abbe2091404ebe032a6d5a83075031b7072ebe", + "reference": "02abbe2091404ebe032a6d5a83075031b7072ebe", "shasum": "" }, "require": { @@ -1156,7 +1157,7 @@ "framework", "laravel" ], - "time": "2018-07-26T16:01:26+00:00" + "time": "2018-08-27T13:18:24+00:00" }, { "name": "laravel/passport", @@ -1297,16 +1298,16 @@ }, { "name": "lcobucci/jwt", - "version": "3.2.2", + "version": "3.2.4", "source": { "type": "git", "url": "https://github.com/lcobucci/jwt.git", - "reference": "0b5930be73582369e10c4d4bb7a12bac927a203c" + "reference": "c9704b751315d21735dc98d78d4f37bd73596da7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/jwt/zipball/0b5930be73582369e10c4d4bb7a12bac927a203c", - "reference": "0b5930be73582369e10c4d4bb7a12bac927a203c", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/c9704b751315d21735dc98d78d4f37bd73596da7", + "reference": "c9704b751315d21735dc98d78d4f37bd73596da7", "shasum": "" }, "require": { @@ -1351,7 +1352,7 @@ "JWS", "jwt" ], - "time": "2017-09-01T08:23:26+00:00" + "time": "2018-08-03T11:23:50+00:00" }, { "name": "league/commonmark", @@ -1541,16 +1542,16 @@ }, { "name": "league/flysystem", - "version": "1.0.45", + "version": "1.0.46", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "a99f94e63b512d75f851b181afcdf0ee9ebef7e6" + "reference": "f3e0d925c18b92cf3ce84ea5cc58d62a1762a2b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/a99f94e63b512d75f851b181afcdf0ee9ebef7e6", - "reference": "a99f94e63b512d75f851b181afcdf0ee9ebef7e6", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/f3e0d925c18b92cf3ce84ea5cc58d62a1762a2b2", + "reference": "f3e0d925c18b92cf3ce84ea5cc58d62a1762a2b2", "shasum": "" }, "require": { @@ -1562,7 +1563,7 @@ "require-dev": { "ext-fileinfo": "*", "phpspec/phpspec": "^3.4", - "phpunit/phpunit": "^5.7" + "phpunit/phpunit": "^5.7.10" }, "suggest": { "ext-fileinfo": "Required for MimeType", @@ -1621,7 +1622,7 @@ "sftp", "storage" ], - "time": "2018-05-07T08:44:23+00:00" + "time": "2018-08-22T07:45:22+00:00" }, { "name": "league/fractal", @@ -2091,21 +2092,21 @@ }, { "name": "pragmarx/google2fa", - "version": "v3.0.1", + "version": "v3.0.3", "source": { "type": "git", "url": "https://github.com/antonioribeiro/google2fa.git", - "reference": "40b3ce025bed0f9cd0c1c8ab7fc8265344c73de0" + "reference": "6949226739e4424f40031e6f1c96b1fd64047335" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/40b3ce025bed0f9cd0c1c8ab7fc8265344c73de0", - "reference": "40b3ce025bed0f9cd0c1c8ab7fc8265344c73de0", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/6949226739e4424f40031e6f1c96b1fd64047335", + "reference": "6949226739e4424f40031e6f1c96b1fd64047335", "shasum": "" }, "require": { "paragonie/constant_time_encoding": "~1.0|~2.0", - "paragonie/random_compat": "~1.4|~2.0", + "paragonie/random_compat": ">=1", "php": ">=5.4", "symfony/polyfill-php56": "~1.2" }, @@ -2148,7 +2149,7 @@ "google2fa", "laravel" ], - "time": "2018-03-15T23:14:19+00:00" + "time": "2018-08-29T13:28:06+00:00" }, { "name": "pragmarx/google2fa-laravel", @@ -2630,16 +2631,16 @@ }, { "name": "symfony/console", - "version": "v4.1.2", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "5c31f6a97c1c240707f6d786e7e59bfacdbc0219" + "reference": "ca80b8ced97cf07390078b29773dc384c39eee1f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/5c31f6a97c1c240707f6d786e7e59bfacdbc0219", - "reference": "5c31f6a97c1c240707f6d786e7e59bfacdbc0219", + "url": "https://api.github.com/repos/symfony/console/zipball/ca80b8ced97cf07390078b29773dc384c39eee1f", + "reference": "ca80b8ced97cf07390078b29773dc384c39eee1f", "shasum": "" }, "require": { @@ -2694,20 +2695,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2018-07-16T14:05:40+00:00" + "time": "2018-07-26T11:24:31+00:00" }, { "name": "symfony/css-selector", - "version": "v4.1.2", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "03ac71606ecb0b0ce792faa17d74cc32c2949ef4" + "reference": "2a4df7618f869b456f9096781e78c57b509d76c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/03ac71606ecb0b0ce792faa17d74cc32c2949ef4", - "reference": "03ac71606ecb0b0ce792faa17d74cc32c2949ef4", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/2a4df7618f869b456f9096781e78c57b509d76c7", + "reference": "2a4df7618f869b456f9096781e78c57b509d76c7", "shasum": "" }, "require": { @@ -2747,20 +2748,20 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", - "time": "2018-05-30T07:26:09+00:00" + "time": "2018-07-26T09:10:45+00:00" }, { "name": "symfony/debug", - "version": "v4.1.2", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "a1f2118cedb8731c45e945cdd2b808ca82abc4b5" + "reference": "47ead688f1f2877f3f14219670f52e4722ee7052" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/a1f2118cedb8731c45e945cdd2b808ca82abc4b5", - "reference": "a1f2118cedb8731c45e945cdd2b808ca82abc4b5", + "url": "https://api.github.com/repos/symfony/debug/zipball/47ead688f1f2877f3f14219670f52e4722ee7052", + "reference": "47ead688f1f2877f3f14219670f52e4722ee7052", "shasum": "" }, "require": { @@ -2803,20 +2804,20 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2018-07-06T14:52:28+00:00" + "time": "2018-08-03T11:13:38+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v4.1.2", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "00d64638e4f0703a00ab7fc2c8ae5f75f3b4020f" + "reference": "bfb30c2ad377615a463ebbc875eba64a99f6aa3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/00d64638e4f0703a00ab7fc2c8ae5f75f3b4020f", - "reference": "00d64638e4f0703a00ab7fc2c8ae5f75f3b4020f", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/bfb30c2ad377615a463ebbc875eba64a99f6aa3e", + "reference": "bfb30c2ad377615a463ebbc875eba64a99f6aa3e", "shasum": "" }, "require": { @@ -2866,20 +2867,20 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2018-07-10T11:02:47+00:00" + "time": "2018-07-26T09:10:45+00:00" }, { "name": "symfony/finder", - "version": "v4.1.2", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "84714b8417d19e4ba02ea78a41a975b3efaafddb" + "reference": "e162f1df3102d0b7472805a5a9d5db9fcf0a8068" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/84714b8417d19e4ba02ea78a41a975b3efaafddb", - "reference": "84714b8417d19e4ba02ea78a41a975b3efaafddb", + "url": "https://api.github.com/repos/symfony/finder/zipball/e162f1df3102d0b7472805a5a9d5db9fcf0a8068", + "reference": "e162f1df3102d0b7472805a5a9d5db9fcf0a8068", "shasum": "" }, "require": { @@ -2915,20 +2916,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2018-06-19T21:38:16+00:00" + "time": "2018-07-26T11:24:31+00:00" }, { "name": "symfony/http-foundation", - "version": "v4.1.2", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "8da9ea68ab2d80dfabd41e0d14b9606bb47a10c0" + "reference": "3a5c91e133b220bb882b3cd773ba91bf39989345" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/8da9ea68ab2d80dfabd41e0d14b9606bb47a10c0", - "reference": "8da9ea68ab2d80dfabd41e0d14b9606bb47a10c0", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/3a5c91e133b220bb882b3cd773ba91bf39989345", + "reference": "3a5c91e133b220bb882b3cd773ba91bf39989345", "shasum": "" }, "require": { @@ -2969,20 +2970,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2018-07-16T14:05:40+00:00" + "time": "2018-08-27T17:47:02+00:00" }, { "name": "symfony/http-kernel", - "version": "v4.1.2", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "ebd28f4f88a2ca0a0488882ad73c4004f3afdbe3" + "reference": "33de0a1ff2e1720096189e3ced682d7a4e8f5e35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/ebd28f4f88a2ca0a0488882ad73c4004f3afdbe3", - "reference": "ebd28f4f88a2ca0a0488882ad73c4004f3afdbe3", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/33de0a1ff2e1720096189e3ced682d7a4e8f5e35", + "reference": "33de0a1ff2e1720096189e3ced682d7a4e8f5e35", "shasum": "" }, "require": { @@ -3056,29 +3057,32 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2018-07-23T17:16:22+00:00" + "time": "2018-08-28T06:17:42+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.8.0", + "version": "v1.9.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae" + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/7cc359f1b7b80fc25ed7796be7d96adc9b354bae", - "reference": "7cc359f1b7b80fc25ed7796be7d96adc9b354bae", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19", + "reference": "e3d826245268269cd66f8326bd8bc066687b4a19", "shasum": "" }, "require": { "php": ">=5.3.3" }, + "suggest": { + "ext-ctype": "For best performance" + }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -3111,20 +3115,20 @@ "polyfill", "portable" ], - "time": "2018-04-30T19:57:29+00:00" + "time": "2018-08-06T14:22:27+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.8.0", + "version": "v1.9.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "3296adf6a6454a050679cde90f95350ad604b171" + "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/3296adf6a6454a050679cde90f95350ad604b171", - "reference": "3296adf6a6454a050679cde90f95350ad604b171", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d0cd638f4634c16d8df4508e847f14e9e43168b8", + "reference": "d0cd638f4634c16d8df4508e847f14e9e43168b8", "shasum": "" }, "require": { @@ -3136,7 +3140,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -3170,20 +3174,20 @@ "portable", "shim" ], - "time": "2018-04-26T10:06:28+00:00" + "time": "2018-08-06T14:22:27+00:00" }, { "name": "symfony/polyfill-php56", - "version": "v1.8.0", + "version": "v1.9.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php56.git", - "reference": "af98553c84912459db3f636329567809d639a8f6" + "reference": "7b4fc009172cc0196535b0328bd1226284a28000" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/af98553c84912459db3f636329567809d639a8f6", - "reference": "af98553c84912459db3f636329567809d639a8f6", + "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/7b4fc009172cc0196535b0328bd1226284a28000", + "reference": "7b4fc009172cc0196535b0328bd1226284a28000", "shasum": "" }, "require": { @@ -3193,7 +3197,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -3226,20 +3230,20 @@ "portable", "shim" ], - "time": "2018-04-26T10:06:28+00:00" + "time": "2018-08-06T14:22:27+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.8.0", + "version": "v1.9.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "a4576e282d782ad82397f3e4ec1df8e0f0cafb46" + "reference": "95c50420b0baed23852452a7f0c7b527303ed5ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/a4576e282d782ad82397f3e4ec1df8e0f0cafb46", - "reference": "a4576e282d782ad82397f3e4ec1df8e0f0cafb46", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/95c50420b0baed23852452a7f0c7b527303ed5ae", + "reference": "95c50420b0baed23852452a7f0c7b527303ed5ae", "shasum": "" }, "require": { @@ -3248,7 +3252,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -3281,20 +3285,20 @@ "portable", "shim" ], - "time": "2018-04-26T10:06:28+00:00" + "time": "2018-08-06T14:22:27+00:00" }, { "name": "symfony/polyfill-util", - "version": "v1.8.0", + "version": "v1.9.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-util.git", - "reference": "1a5ad95d9436cbff3296034fe9f8d586dce3fb3a" + "reference": "8e15d04ba3440984d23e7964b2ee1d25c8de1581" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/1a5ad95d9436cbff3296034fe9f8d586dce3fb3a", - "reference": "1a5ad95d9436cbff3296034fe9f8d586dce3fb3a", + "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/8e15d04ba3440984d23e7964b2ee1d25c8de1581", + "reference": "8e15d04ba3440984d23e7964b2ee1d25c8de1581", "shasum": "" }, "require": { @@ -3303,7 +3307,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.8-dev" + "dev-master": "1.9-dev" } }, "autoload": { @@ -3333,20 +3337,20 @@ "polyfill", "shim" ], - "time": "2018-04-26T10:06:28+00:00" + "time": "2018-08-06T14:22:27+00:00" }, { "name": "symfony/process", - "version": "v4.1.2", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "1d1677391ecf00d1c5b9482d6050c0c27aa3ac3a" + "reference": "86cdb930a6a855b0ab35fb60c1504cb36184f843" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/1d1677391ecf00d1c5b9482d6050c0c27aa3ac3a", - "reference": "1d1677391ecf00d1c5b9482d6050c0c27aa3ac3a", + "url": "https://api.github.com/repos/symfony/process/zipball/86cdb930a6a855b0ab35fb60c1504cb36184f843", + "reference": "86cdb930a6a855b0ab35fb60c1504cb36184f843", "shasum": "" }, "require": { @@ -3382,38 +3386,39 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2018-05-31T10:17:53+00:00" + "time": "2018-08-03T11:13:38+00:00" }, { "name": "symfony/psr-http-message-bridge", - "version": "v1.0.2", + "version": "v1.1.0", "source": { "type": "git", "url": "https://github.com/symfony/psr-http-message-bridge.git", - "reference": "c2b757934f2d9681a287e662efbc27c41fe8ef86" + "reference": "53c15a6a7918e6c2ab16ae370ea607fb40cab196" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/c2b757934f2d9681a287e662efbc27c41fe8ef86", - "reference": "c2b757934f2d9681a287e662efbc27c41fe8ef86", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/53c15a6a7918e6c2ab16ae370ea607fb40cab196", + "reference": "53c15a6a7918e6c2ab16ae370ea607fb40cab196", "shasum": "" }, "require": { - "php": ">=5.3.3", - "psr/http-message": "~1.0", - "symfony/http-foundation": "~2.3|~3.0|~4.0" + "php": "^5.3.3 || ^7.0", + "psr/http-message": "^1.0", + "symfony/http-foundation": "^2.3.42 || ^3.4 || ^4.0" }, "require-dev": { - "symfony/phpunit-bridge": "~3.2|4.0" + "symfony/phpunit-bridge": "^3.4 || 4.0" }, "suggest": { + "psr/http-factory-implementation": "To use the PSR-17 factory", "psr/http-message-implementation": "To use the HttpFoundation factory", "zendframework/zend-diactoros": "To use the Zend Diactoros factory" }, "type": "symfony-bridge", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "1.1-dev" } }, "autoload": { @@ -3442,20 +3447,20 @@ "http-message", "psr-7" ], - "time": "2017-12-19T00:31:44+00:00" + "time": "2018-08-30T16:28:28+00:00" }, { "name": "symfony/routing", - "version": "v4.1.2", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "73770bf3682b4407b017c2bdcb2b11cdcbce5322" + "reference": "a5784c2ec4168018c87b38f0e4f39d2278499f51" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/73770bf3682b4407b017c2bdcb2b11cdcbce5322", - "reference": "73770bf3682b4407b017c2bdcb2b11cdcbce5322", + "url": "https://api.github.com/repos/symfony/routing/zipball/a5784c2ec4168018c87b38f0e4f39d2278499f51", + "reference": "a5784c2ec4168018c87b38f0e4f39d2278499f51", "shasum": "" }, "require": { @@ -3519,20 +3524,20 @@ "uri", "url" ], - "time": "2018-06-28T06:30:33+00:00" + "time": "2018-08-03T07:58:40+00:00" }, { "name": "symfony/translation", - "version": "v4.1.2", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "2dd74d6b2dcbd46a93971e6ce7d245cf3123e957" + "reference": "fa2182669f7983b7aa5f1a770d053f79f0ef144f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/2dd74d6b2dcbd46a93971e6ce7d245cf3123e957", - "reference": "2dd74d6b2dcbd46a93971e6ce7d245cf3123e957", + "url": "https://api.github.com/repos/symfony/translation/zipball/fa2182669f7983b7aa5f1a770d053f79f0ef144f", + "reference": "fa2182669f7983b7aa5f1a770d053f79f0ef144f", "shasum": "" }, "require": { @@ -3588,20 +3593,20 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2018-07-23T08:20:20+00:00" + "time": "2018-08-07T12:45:11+00:00" }, { "name": "symfony/var-dumper", - "version": "v4.1.2", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "9f882aed43f364de1d43038e8fb39703c577afc1" + "reference": "a05426e27294bba7b0226ffc17dd01a3c6ef9777" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9f882aed43f364de1d43038e8fb39703c577afc1", - "reference": "9f882aed43f364de1d43038e8fb39703c577afc1", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/a05426e27294bba7b0226ffc17dd01a3c6ef9777", + "reference": "a05426e27294bba7b0226ffc17dd01a3c6ef9777", "shasum": "" }, "require": { @@ -3663,7 +3668,7 @@ "debug", "dump" ], - "time": "2018-07-05T11:54:23+00:00" + "time": "2018-08-02T09:24:26+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -3780,16 +3785,16 @@ }, { "name": "vlucas/phpdotenv", - "version": "v2.5.0", + "version": "v2.5.1", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "6ae3e2e6494bb5e58c2decadafc3de7f1453f70a" + "reference": "8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/6ae3e2e6494bb5e58c2decadafc3de7f1453f70a", - "reference": "6ae3e2e6494bb5e58c2decadafc3de7f1453f70a", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e", + "reference": "8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e", "shasum": "" }, "require": { @@ -3826,20 +3831,20 @@ "env", "environment" ], - "time": "2018-07-01T10:25:50+00:00" + "time": "2018-07-29T20:33:41+00:00" }, { "name": "zendframework/zend-diactoros", - "version": "1.8.3", + "version": "1.8.5", "source": { "type": "git", "url": "https://github.com/zendframework/zend-diactoros.git", - "reference": "72c13834fb3db2a962e913758b384ff2e6425d6e" + "reference": "3e4edb822c942f37ade0d09579cfbab11e2fee87" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/72c13834fb3db2a962e913758b384ff2e6425d6e", - "reference": "72c13834fb3db2a962e913758b384ff2e6425d6e", + "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/3e4edb822c942f37ade0d09579cfbab11e2fee87", + "reference": "3e4edb822c942f37ade0d09579cfbab11e2fee87", "shasum": "" }, "require": { @@ -3889,7 +3894,7 @@ "psr", "psr-7" ], - "time": "2018-07-24T21:54:38+00:00" + "time": "2018-08-10T14:16:32+00:00" } ], "packages-dev": [ @@ -4729,16 +4734,16 @@ }, { "name": "phpspec/prophecy", - "version": "1.7.6", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712" + "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/33a7e3c4fda54e912ff6338c48823bd5c0f0b712", - "reference": "33a7e3c4fda54e912ff6338c48823bd5c0f0b712", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06", + "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06", "shasum": "" }, "require": { @@ -4750,12 +4755,12 @@ }, "require-dev": { "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.7.x-dev" + "dev-master": "1.8.x-dev" } }, "autoload": { @@ -4788,7 +4793,7 @@ "spy", "stub" ], - "time": "2018-04-18T13:57:24+00:00" + "time": "2018-08-05T17:53:17+00:00" }, { "name": "phpunit/php-code-coverage", @@ -5041,16 +5046,16 @@ }, { "name": "phpunit/phpunit", - "version": "7.2.7", + "version": "7.3.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "8e878aff7917ef66e702e03d1359b16eee254e2c" + "reference": "1bd5629cccfb2c0a9ef5474b4ff772349e1ec898" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8e878aff7917ef66e702e03d1359b16eee254e2c", - "reference": "8e878aff7917ef66e702e03d1359b16eee254e2c", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1bd5629cccfb2c0a9ef5474b4ff772349e1ec898", + "reference": "1bd5629cccfb2c0a9ef5474b4ff772349e1ec898", "shasum": "" }, "require": { @@ -5095,7 +5100,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "7.2-dev" + "dev-master": "7.3-dev" } }, "autoload": { @@ -5121,22 +5126,11 @@ "testing", "xunit" ], - "time": "2018-07-15T05:20:50+00:00" + "time": "2018-09-01T15:49:55+00:00" }, { "name": "roave/security-advisories", "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "053766d789f6393e5bc0896635d35abf8d2d362e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/053766d789f6393e5bc0896635d35abf8d2d362e", - "reference": "053766d789f6393e5bc0896635d35abf8d2d362e", - "shasum": "" - }, "conflict": { "3f/pygmentize": "<1.2", "adodb/adodb-php": "<5.20.12", @@ -5177,11 +5171,12 @@ "gregwar/rst": "<1.0.3", "guzzlehttp/guzzle": ">=6,<6.2.1|>=4.0.0-rc2,<4.2.4|>=5,<5.3.1", "illuminate/auth": ">=4,<4.0.99|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.10", + "illuminate/cookie": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.42|>=5.6,<5.6.30", "illuminate/database": ">=4,<4.0.99|>=4.1,<4.1.29", "illuminate/encryption": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.40|>=5.6,<5.6.15", "joomla/session": "<1.3.1", "kreait/firebase-php": ">=3.2,<3.8.1", - "laravel/framework": ">=4,<4.0.99|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.40|>=5.6,<5.6.15", + "laravel/framework": ">=4,<4.0.99|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.42|>=5.6,<5.6.30", "laravel/socialite": ">=1,<1.0.99|>=2,<2.0.10", "magento/magento1ce": "<1.9.3.9", "magento/magento1ee": ">=1.9,<1.14.3.2", @@ -5221,7 +5216,7 @@ "symfony/dependency-injection": ">=2,<2.0.17", "symfony/form": ">=2.3,<2.3.35|>=2.4,<2.6.12|>=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13", "symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2", - "symfony/http-foundation": ">=2,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", + "symfony/http-foundation": ">=2,<2.7.49|>=2.8,<2.8.44|>=3,<3.3.18|>=3.4,<3.4.14|>=4,<4.0.14|>=4.1,<4.1.3", "symfony/http-kernel": ">=2,<2.3.29|>=2.4,<2.5.12|>=2.6,<2.6.8", "symfony/intl": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13", "symfony/routing": ">=2,<2.0.19", @@ -5232,7 +5227,7 @@ "symfony/security-guard": ">=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", "symfony/serializer": ">=2,<2.0.11", - "symfony/symfony": ">=2,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", + "symfony/symfony": ">=2,<2.7.49|>=2.8,<2.8.44|>=3,<3.3.18|>=3.4,<3.4.14|>=4,<4.0.14|>=4.1,<4.1.3", "symfony/translation": ">=2,<2.0.17", "symfony/validator": ">=2,<2.0.24|>=2.1,<2.1.12|>=2.2,<2.2.5|>=2.3,<2.3.3", "symfony/web-profiler-bundle": ">=2,<2.3.19|>=2.4,<2.4.9|>=2.5,<2.5.4", @@ -5258,9 +5253,10 @@ "zendframework/zend-captcha": ">=2,<2.4.9|>=2.5,<2.5.2", "zendframework/zend-crypt": ">=2,<2.4.9|>=2.5,<2.5.2", "zendframework/zend-db": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.10|>=2.3,<2.3.5", - "zendframework/zend-diactoros": ">=1,<1.0.4", + "zendframework/zend-diactoros": ">=1,<1.8.4", + "zendframework/zend-feed": ">=1,<2.10.3", "zendframework/zend-form": ">=2,<2.2.7|>=2.3,<2.3.1", - "zendframework/zend-http": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.3,<2.3.8|>=2.4,<2.4.1", + "zendframework/zend-http": ">=1,<2.8.1", "zendframework/zend-json": ">=2.1,<2.1.6|>=2.2,<2.2.6", "zendframework/zend-ldap": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.8|>=2.3,<2.3.3", "zendframework/zend-mail": ">=2,<2.4.11|>=2.5,<2.7.2", @@ -5269,7 +5265,7 @@ "zendframework/zend-validator": ">=2.3,<2.3.6", "zendframework/zend-view": ">=2,<2.2.7|>=2.3,<2.3.1", "zendframework/zend-xmlrpc": ">=2.1,<2.1.6|>=2.2,<2.2.6", - "zendframework/zendframework": ">=2,<2.4.11|>=2.5,<2.5.1", + "zendframework/zendframework": "<2.5.1", "zendframework/zendframework1": "<1.12.20", "zendframework/zendopenid": ">=2,<2.0.2", "zendframework/zendxml": ">=1,<1.0.1", @@ -5291,7 +5287,7 @@ } ], "description": "Prevents installation of composer packages with known security vulnerabilities: no API, simply require it", - "time": "2018-07-18T13:51:34+00:00" + "time": "2018-08-14T15:39:17+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -5858,16 +5854,16 @@ }, { "name": "symfony/class-loader", - "version": "v3.4.13", + "version": "v3.4.15", "source": { "type": "git", "url": "https://github.com/symfony/class-loader.git", - "reference": "e63c12699822bb3b667e7216ba07fbcc3a3e203e" + "reference": "31db283fc86d3143e7ff87e922177b457d909c30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/class-loader/zipball/e63c12699822bb3b667e7216ba07fbcc3a3e203e", - "reference": "e63c12699822bb3b667e7216ba07fbcc3a3e203e", + "url": "https://api.github.com/repos/symfony/class-loader/zipball/31db283fc86d3143e7ff87e922177b457d909c30", + "reference": "31db283fc86d3143e7ff87e922177b457d909c30", "shasum": "" }, "require": { @@ -5910,20 +5906,20 @@ ], "description": "Symfony ClassLoader Component", "homepage": "https://symfony.com", - "time": "2018-01-03T07:37:34+00:00" + "time": "2018-07-26T11:19:56+00:00" }, { "name": "symfony/config", - "version": "v4.1.2", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "e57e7b573df9d0eaa8c0152768c708ee7ea2b8e5" + "reference": "76015a3cc372b14d00040ff58e18e29f69eba717" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/e57e7b573df9d0eaa8c0152768c708ee7ea2b8e5", - "reference": "e57e7b573df9d0eaa8c0152768c708ee7ea2b8e5", + "url": "https://api.github.com/repos/symfony/config/zipball/76015a3cc372b14d00040ff58e18e29f69eba717", + "reference": "76015a3cc372b14d00040ff58e18e29f69eba717", "shasum": "" }, "require": { @@ -5973,20 +5969,20 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2018-06-20T11:15:17+00:00" + "time": "2018-08-08T06:37:38+00:00" }, { "name": "symfony/filesystem", - "version": "v4.1.2", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "562bf7005b55fd80d26b582d28e3e10f2dd5ae9c" + "reference": "c0f5f62db218fa72195b8b8700e4b9b9cf52eb5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/562bf7005b55fd80d26b582d28e3e10f2dd5ae9c", - "reference": "562bf7005b55fd80d26b582d28e3e10f2dd5ae9c", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/c0f5f62db218fa72195b8b8700e4b9b9cf52eb5e", + "reference": "c0f5f62db218fa72195b8b8700e4b9b9cf52eb5e", "shasum": "" }, "require": { @@ -6023,20 +6019,20 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2018-05-30T07:26:09+00:00" + "time": "2018-08-18T16:52:46+00:00" }, { "name": "symfony/stopwatch", - "version": "v4.1.2", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "07463bbbbbfe119045a24c4a516f92ebd2752784" + "reference": "966c982df3cca41324253dc0c7ffe76b6076b705" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/07463bbbbbfe119045a24c4a516f92ebd2752784", - "reference": "07463bbbbbfe119045a24c4a516f92ebd2752784", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/966c982df3cca41324253dc0c7ffe76b6076b705", + "reference": "966c982df3cca41324253dc0c7ffe76b6076b705", "shasum": "" }, "require": { @@ -6072,20 +6068,20 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "time": "2018-02-19T16:51:42+00:00" + "time": "2018-07-26T11:00:49+00:00" }, { "name": "symfony/yaml", - "version": "v4.1.2", + "version": "v4.1.4", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "80e4bfa9685fc4a09acc4a857ec16974a9cd944e" + "reference": "b832cc289608b6d305f62149df91529a2ab3c314" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/80e4bfa9685fc4a09acc4a857ec16974a9cd944e", - "reference": "80e4bfa9685fc4a09acc4a857ec16974a9cd944e", + "url": "https://api.github.com/repos/symfony/yaml/zipball/b832cc289608b6d305f62149df91529a2ab3c314", + "reference": "b832cc289608b6d305f62149df91529a2ab3c314", "shasum": "" }, "require": { @@ -6131,7 +6127,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2018-05-30T07:26:09+00:00" + "time": "2018-08-18T16:52:46+00:00" }, { "name": "theseer/tokenizer", diff --git a/config/app.php b/config/app.php index 5127530c6d..47d80fc8ba 100644 --- a/config/app.php +++ b/config/app.php @@ -32,8 +32,6 @@ return [ 'fallback_locale' => 'en_US', 'key' => env('APP_KEY'), 'cipher' => 'AES-256-CBC', - 'log' => envNonEmpty('APP_LOG', 'errorlog'), - 'log_level' => envNonEmpty('APP_LOG_LEVEL', 'info'), 'providers' => [ /* diff --git a/config/firefly.php b/config/firefly.php index 1af5d5b4a7..c182081c1e 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -88,8 +88,8 @@ return [ 'is_demo_site' => false, ], 'encryption' => null === env('USE_ENCRYPTION') || env('USE_ENCRYPTION') === true, - 'version' => '4.7.5.3', - 'api_version' => '0.6', + 'version' => '4.7.6', + 'api_version' => '0.7', 'db_version' => 4, 'maxUploadSize' => 15242880, 'allowedMimes' => [ @@ -176,10 +176,12 @@ return [ ], 'subTitlesByIdentifier' => [ - 'asset' => 'Asset accounts', - 'expense' => 'Expense accounts', - 'revenue' => 'Revenue accounts', - 'cash' => 'Cash accounts', + 'asset' => 'Asset accounts', + 'expense' => 'Expense accounts', + 'revenue' => 'Revenue accounts', + 'cash' => 'Cash accounts', + 'liabilities' => 'Liabilities', + 'liability' => 'Liabilities', ], 'subIconsByIdentifier' => [ @@ -194,23 +196,27 @@ return [ 'Revenue account' => 'fa-download', 'import' => 'fa-download', 'Import account' => 'fa-download', + 'liabilities' => 'fa-ticket', ], 'accountTypesByIdentifier' => [ - 'asset' => ['Default account', 'Asset account'], - 'expense' => ['Expense account', 'Beneficiary account'], - 'revenue' => ['Revenue account'], - 'import' => ['Import account'], + 'asset' => ['Default account', 'Asset account'], + 'expense' => ['Expense account', 'Beneficiary account'], + 'revenue' => ['Revenue account'], + 'import' => ['Import account'], + 'liabilities' => ['Loan', 'Debt', 'Credit card', 'Mortgage'], ], 'accountTypeByIdentifier' => [ - 'asset' => 'Asset account', - 'expense' => 'Expense account', - 'revenue' => 'Revenue account', - 'opening' => 'Initial balance account', - 'initial' => 'Initial balance account', - 'import' => 'Import account', - 'reconcile' => 'Reconciliation account', + 'asset' => ['Asset account'], + 'expense' => ['Expense account'], + 'revenue' => ['Revenue account'], + 'opening' => ['Initial balance account'], + 'initial' => ['Initial balance account'], + 'import' => ['Import account'], + 'reconcile' => ['Reconciliation account'], + 'liabilities' => ['Loan', 'Debt', 'Mortgage', 'Credit card'], + 'liability' => ['Loan', 'Debt', 'Mortgage', 'Credit card'], ], 'shortNamesByFullName' => [ @@ -221,6 +227,10 @@ return [ 'Beneficiary account' => 'expense', 'Revenue account' => 'revenue', 'Cash account' => 'cash', + 'Credit card' => 'liabilities', + 'Loan' => 'liabilities', + 'Debt' => 'liabilities', + 'Mortgage' => 'liabilities', ], 'languages' => [ // completed languages @@ -267,7 +277,7 @@ return [ 'journalLink' => \FireflyIII\Models\TransactionJournalLink::class, 'currency' => \FireflyIII\Models\TransactionCurrency::class, 'piggyBank' => \FireflyIII\Models\PiggyBank::class, - 'preference' => \FireflyIII\Models\Preference::class, + 'preference' => \FireflyIII\Models\Preference::class, 'tj' => \FireflyIII\Models\TransactionJournal::class, 'tag' => \FireflyIII\Models\Tag::class, 'recurrence' => \FireflyIII\Models\Recurrence::class, @@ -299,6 +309,7 @@ return [ 'fromCurrencyCode' => \FireflyIII\Support\Binder\CurrencyCode::class, 'toCurrencyCode' => \FireflyIII\Support\Binder\CurrencyCode::class, 'unfinishedJournal' => \FireflyIII\Support\Binder\UnfinishedJournal::class, + 'cliToken' => \FireflyIII\Support\Binder\CLIToken::class, ], diff --git a/config/import.php b/config/import.php index ed49fa8417..5f6a724700 100644 --- a/config/import.php +++ b/config/import.php @@ -26,13 +26,16 @@ use FireflyIII\Import\JobConfiguration\BunqJobConfiguration; use FireflyIII\Import\JobConfiguration\FakeJobConfiguration; use FireflyIII\Import\JobConfiguration\FileJobConfiguration; use FireflyIII\Import\JobConfiguration\SpectreJobConfiguration; +use FireflyIII\Import\JobConfiguration\YnabJobConfiguration; use FireflyIII\Import\Prerequisites\BunqPrerequisites; use FireflyIII\Import\Prerequisites\FakePrerequisites; use FireflyIII\Import\Prerequisites\SpectrePrerequisites; +use FireflyIII\Import\Prerequisites\YnabPrerequisites; use FireflyIII\Import\Routine\BunqRoutine; use FireflyIII\Import\Routine\FakeRoutine; use FireflyIII\Import\Routine\FileRoutine; use FireflyIII\Import\Routine\SpectreRoutine; +use FireflyIII\Import\Routine\YnabRoutine; use FireflyIII\Support\Import\Routine\File\CSVProcessor; return [ @@ -42,6 +45,7 @@ return [ 'file' => true, 'bunq' => true, 'spectre' => true, + 'ynab' => true, 'plaid' => false, 'quovo' => false, 'yodlee' => false, @@ -53,6 +57,7 @@ return [ 'file' => false, 'bunq' => false, 'spectre' => false, + 'ynab' => false, 'plaid' => false, 'quovo' => false, 'yodlee' => false, @@ -63,6 +68,7 @@ return [ 'file' => true, 'bunq' => true, 'spectre' => true, + 'ynab' => true, 'plaid' => true, 'quovo' => true, 'yodlee' => true, @@ -73,6 +79,7 @@ return [ 'file' => false, 'bunq' => true, 'spectre' => true, + 'ynab' => true, 'plaid' => true, 'quovo' => true, 'yodlee' => true, @@ -83,6 +90,7 @@ return [ 'file' => false, 'bunq' => BunqPrerequisites::class, 'spectre' => SpectrePrerequisites::class, + 'ynab' => YnabPrerequisites::class, 'plaid' => false, 'quovo' => false, 'yodlee' => false, @@ -93,6 +101,7 @@ return [ 'file' => true, 'bunq' => true, 'spectre' => true, + 'ynab' => true, 'plaid' => false, 'quovo' => false, 'yodlee' => false, @@ -103,6 +112,7 @@ return [ 'file' => FileJobConfiguration::class, 'bunq' => BunqJobConfiguration::class, 'spectre' => SpectreJobConfiguration::class, + 'ynab' => YnabJobConfiguration::class, 'plaid' => false, 'quovo' => false, 'yodlee' => false, @@ -113,6 +123,7 @@ return [ 'file' => FileRoutine::class, 'bunq' => BunqRoutine::class, 'spectre' => SpectreRoutine::class, + 'ynab' => YnabRoutine::class, 'plaid' => false, 'quovo' => false, 'yodlee' => false, @@ -140,6 +151,10 @@ return [ 'spectre' => [ 'server' => 'www.saltedge.com', ], + 'ynab' => [ + 'live' => 'api.youneedabudget.com', + 'version' => 'v1', + ], 'plaid' => [], 'quovo' => [], 'yodlee' => [], diff --git a/config/logging.php b/config/logging.php index 3e1dcfd1f3..1ac3fb0284 100644 --- a/config/logging.php +++ b/config/logging.php @@ -59,7 +59,12 @@ return [ 'single' => [ 'driver' => 'single', 'path' => storage_path('logs/laravel.log'), - 'level' => 'debug', + 'level' => envNonEmpty('APP_LOG_LEVEL', 'info'), + ], + 'stdout' => [ + 'driver' => 'single', + 'path' => 'php://stdout', + 'level' => envNonEmpty('APP_LOG_LEVEL', 'info'), ], 'daily' => [ diff --git a/config/twigbridge.php b/config/twigbridge.php index fa0557b7c9..d1e807a6be 100644 --- a/config/twigbridge.php +++ b/config/twigbridge.php @@ -185,17 +185,11 @@ return [ 'Config', 'Request', 'ExpandedForm' => [ - 'is_safe' => [ - 'date', 'text', 'select', 'balance', 'optionsList', 'checkbox', 'amount', 'tags', 'integer', 'textarea', 'location', - 'file', 'staticText', 'password', 'nonSelectableAmount', - 'number', 'assetAccountList','amountNoCurrency','currencyList','ruleGroupList','assetAccountCheckList','ruleGroupListWithEmpty', - 'piggyBankList','currencyListEmpty','activeAssetAccountList' - ], - ], - 'Form' => [ - 'is_safe' => [ - 'input', 'select', 'checkbox', 'model', 'open', 'radio', 'textarea', 'file', - ], + 'is_safe' => ['date', 'text', 'select', 'balance', 'optionsList', 'checkbox', 'amount', 'tags', 'integer', 'textarea', 'location', 'file', + 'staticText', 'password', 'nonSelectableAmount', 'number', 'assetAccountList', 'amountNoCurrency', 'currencyList', + 'ruleGroupList', 'assetAccountCheckList', 'ruleGroupListWithEmpty', 'piggyBankList', 'currencyListEmpty', + 'activeAssetAccountList', 'percentage', 'activeLongAccountList', 'longAccountList',],], + 'Form' => ['is_safe' => ['input', 'select', 'checkbox', 'model', 'open', 'radio', 'textarea', 'file',], ], ], diff --git a/config/upgrade.php b/config/upgrade.php index 58def7e7f9..d24e17c798 100644 --- a/config/upgrade.php +++ b/config/upgrade.php @@ -30,6 +30,7 @@ return [ '4.6.4' => 'This version of Firefly III requires PHP7.1.', '4.7.3' => 'This version of Firefly III handles bills differently. See http://bit.ly/FF3-new-bills for more information.', '4.7.4' => 'This version of Firefly III has a new import routine. See http://bit.ly/FF3-new-import for more information.', + '4.7.6' => 'This will be the last version to require PHP7.1. Future versions will require PHP7.2 minimum.', ], 'install' => [ @@ -38,6 +39,7 @@ return [ '4.6.4' => 'This version of Firefly III requires PHP7.1.', '4.7.3' => 'This version of Firefly III handles bills differently. See http://bit.ly/FF3-new-bills for more information.', '4.7.4' => 'This version of Firefly III has a new import routine. See http://bit.ly/FF3-new-import for more information.', + '4.7.6' => 'This will be the last version to require PHP7.1. Future versions will require PHP7.2 minimum.', ], ], ]; diff --git a/database/seeds/AccountTypeSeeder.php b/database/seeds/AccountTypeSeeder.php index 0ebde97fb3..f695c310d8 100644 --- a/database/seeds/AccountTypeSeeder.php +++ b/database/seeds/AccountTypeSeeder.php @@ -41,12 +41,15 @@ class AccountTypeSeeder extends Seeder AccountType::IMPORT, AccountType::LOAN, AccountType::RECONCILIATION, + AccountType::DEBT, + AccountType::MORTGAGE, + AccountType::CREDITCARD, ]; foreach ($types as $type) { try { AccountType::create(['type' => $type]); } catch (PDOException $e) { - Log::warning(sprintf('Could not create account type "%s". It might exist already.', $type)); + Log::warning(sprintf('Could not create account type "%s". It might exist already: %s', $type , $e->getMessage())); } } } diff --git a/docker-compose.yml b/docker-compose.yml index 28210fcf1b..8175a631d0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,13 +6,15 @@ services: firefly_iii_app: environment: - FF_DB_HOST=firefly_iii_db - - FF_DB_NAME=firefly_db - - FF_DB_USER=firefly_db - - FF_DB_PASSWORD=firefly_db_secret + - FF_DB_NAME=firefly + - FF_DB_USER=firefly + - FF_DB_PASSWORD=firefly - FF_APP_KEY=S0m3R@nd0mStr1ngOf32Ch@rsEx@ctly - FF_APP_ENV=local + - FF_DB_CONNECTION=pgsql - TZ=Europe/Amsterdam - image: jc5x/firefly-iii + - APP_LOG_LEVEL=debug + image: jc5x/firefly-iii:develop links: - firefly_iii_db networks: @@ -30,15 +32,13 @@ services: type: volume firefly_iii_db: environment: - - MYSQL_DATABASE=firefly_db - - MYSQL_USER=firefly_db - - MYSQL_PASSWORD=firefly_db_secret - - MYSQL_RANDOM_ROOT_PASSWORD=yes - image: "mariadb:latest" + - POSTGRES_PASSWORD=firefly + - POSTGRES_USER=firefly + image: "postgres:latest" networks: - firefly_iii_net volumes: - - "firefly_iii_db:/var/lib/mysql" + - "firefly_iii_db:/var/lib/postgresql/data" version: "3.2" volumes: firefly_iii_db: ~ diff --git a/nginx_app.conf b/nginx_app.conf index 97bdb62378..39dedf0155 100644 --- a/nginx_app.conf +++ b/nginx_app.conf @@ -2,6 +2,9 @@ fastcgi_param HTTP_PROXY ""; location / { + if ($http_x_forwarded_proto != "https") { + return 301 https://$host$request_uri; + } # try to serve file directly, fallback to rewrite try_files $uri @rewriteapp; } diff --git a/public/css/.htaccess b/public/css/.htaccess new file mode 100644 index 0000000000..45552cb63e --- /dev/null +++ b/public/css/.htaccess @@ -0,0 +1 @@ +Options -Indexes \ No newline at end of file diff --git a/public/css/app.css b/public/css/app.css deleted file mode 100644 index 99c3d14a5b..0000000000 --- a/public/css/app.css +++ /dev/null @@ -1,14 +0,0 @@ -/*! - * Bootstrap v3.3.7 (http://getbootstrap.com) - * Copyright 2011-2016 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */ - -/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0} - -/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */@media print{*,:after,:before{background:transparent!important;color:#000!important;-webkit-box-shadow:none!important;box-shadow:none!important;text-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}@font-face{font-family:Glyphicons Halflings;src:url(../fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.eot?f4769f9bdb7466be65088239c12046d1);src:url(../fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.eot?f4769f9bdb7466be65088239c12046d1?#iefix) format("embedded-opentype"),url(../fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.woff2?448c34a56d699c29117adc64c43affeb) format("woff2"),url(../fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.woff?fa2772327f55d8198301fdb8bcfc8158) format("woff"),url(../fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.ttf?e18bbf611f2a2e43afc071aa2f4e1512) format("truetype"),url(../fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.svg?89889688147bd7575d6327160d64e760#glyphicons_halflingsregular) format("svg")}.glyphicon{position:relative;top:1px;display:inline-block;font-family:Glyphicons Halflings;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"*"}.glyphicon-plus:before{content:"+"}.glyphicon-eur:before,.glyphicon-euro:before{content:"\20AC"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270F"}.glyphicon-glass:before{content:"\E001"}.glyphicon-music:before{content:"\E002"}.glyphicon-search:before{content:"\E003"}.glyphicon-heart:before{content:"\E005"}.glyphicon-star:before{content:"\E006"}.glyphicon-star-empty:before{content:"\E007"}.glyphicon-user:before{content:"\E008"}.glyphicon-film:before{content:"\E009"}.glyphicon-th-large:before{content:"\E010"}.glyphicon-th:before{content:"\E011"}.glyphicon-th-list:before{content:"\E012"}.glyphicon-ok:before{content:"\E013"}.glyphicon-remove:before{content:"\E014"}.glyphicon-zoom-in:before{content:"\E015"}.glyphicon-zoom-out:before{content:"\E016"}.glyphicon-off:before{content:"\E017"}.glyphicon-signal:before{content:"\E018"}.glyphicon-cog:before{content:"\E019"}.glyphicon-trash:before{content:"\E020"}.glyphicon-home:before{content:"\E021"}.glyphicon-file:before{content:"\E022"}.glyphicon-time:before{content:"\E023"}.glyphicon-road:before{content:"\E024"}.glyphicon-download-alt:before{content:"\E025"}.glyphicon-download:before{content:"\E026"}.glyphicon-upload:before{content:"\E027"}.glyphicon-inbox:before{content:"\E028"}.glyphicon-play-circle:before{content:"\E029"}.glyphicon-repeat:before{content:"\E030"}.glyphicon-refresh:before{content:"\E031"}.glyphicon-list-alt:before{content:"\E032"}.glyphicon-lock:before{content:"\E033"}.glyphicon-flag:before{content:"\E034"}.glyphicon-headphones:before{content:"\E035"}.glyphicon-volume-off:before{content:"\E036"}.glyphicon-volume-down:before{content:"\E037"}.glyphicon-volume-up:before{content:"\E038"}.glyphicon-qrcode:before{content:"\E039"}.glyphicon-barcode:before{content:"\E040"}.glyphicon-tag:before{content:"\E041"}.glyphicon-tags:before{content:"\E042"}.glyphicon-book:before{content:"\E043"}.glyphicon-bookmark:before{content:"\E044"}.glyphicon-print:before{content:"\E045"}.glyphicon-camera:before{content:"\E046"}.glyphicon-font:before{content:"\E047"}.glyphicon-bold:before{content:"\E048"}.glyphicon-italic:before{content:"\E049"}.glyphicon-text-height:before{content:"\E050"}.glyphicon-text-width:before{content:"\E051"}.glyphicon-align-left:before{content:"\E052"}.glyphicon-align-center:before{content:"\E053"}.glyphicon-align-right:before{content:"\E054"}.glyphicon-align-justify:before{content:"\E055"}.glyphicon-list:before{content:"\E056"}.glyphicon-indent-left:before{content:"\E057"}.glyphicon-indent-right:before{content:"\E058"}.glyphicon-facetime-video:before{content:"\E059"}.glyphicon-picture:before{content:"\E060"}.glyphicon-map-marker:before{content:"\E062"}.glyphicon-adjust:before{content:"\E063"}.glyphicon-tint:before{content:"\E064"}.glyphicon-edit:before{content:"\E065"}.glyphicon-share:before{content:"\E066"}.glyphicon-check:before{content:"\E067"}.glyphicon-move:before{content:"\E068"}.glyphicon-step-backward:before{content:"\E069"}.glyphicon-fast-backward:before{content:"\E070"}.glyphicon-backward:before{content:"\E071"}.glyphicon-play:before{content:"\E072"}.glyphicon-pause:before{content:"\E073"}.glyphicon-stop:before{content:"\E074"}.glyphicon-forward:before{content:"\E075"}.glyphicon-fast-forward:before{content:"\E076"}.glyphicon-step-forward:before{content:"\E077"}.glyphicon-eject:before{content:"\E078"}.glyphicon-chevron-left:before{content:"\E079"}.glyphicon-chevron-right:before{content:"\E080"}.glyphicon-plus-sign:before{content:"\E081"}.glyphicon-minus-sign:before{content:"\E082"}.glyphicon-remove-sign:before{content:"\E083"}.glyphicon-ok-sign:before{content:"\E084"}.glyphicon-question-sign:before{content:"\E085"}.glyphicon-info-sign:before{content:"\E086"}.glyphicon-screenshot:before{content:"\E087"}.glyphicon-remove-circle:before{content:"\E088"}.glyphicon-ok-circle:before{content:"\E089"}.glyphicon-ban-circle:before{content:"\E090"}.glyphicon-arrow-left:before{content:"\E091"}.glyphicon-arrow-right:before{content:"\E092"}.glyphicon-arrow-up:before{content:"\E093"}.glyphicon-arrow-down:before{content:"\E094"}.glyphicon-share-alt:before{content:"\E095"}.glyphicon-resize-full:before{content:"\E096"}.glyphicon-resize-small:before{content:"\E097"}.glyphicon-exclamation-sign:before{content:"\E101"}.glyphicon-gift:before{content:"\E102"}.glyphicon-leaf:before{content:"\E103"}.glyphicon-fire:before{content:"\E104"}.glyphicon-eye-open:before{content:"\E105"}.glyphicon-eye-close:before{content:"\E106"}.glyphicon-warning-sign:before{content:"\E107"}.glyphicon-plane:before{content:"\E108"}.glyphicon-calendar:before{content:"\E109"}.glyphicon-random:before{content:"\E110"}.glyphicon-comment:before{content:"\E111"}.glyphicon-magnet:before{content:"\E112"}.glyphicon-chevron-up:before{content:"\E113"}.glyphicon-chevron-down:before{content:"\E114"}.glyphicon-retweet:before{content:"\E115"}.glyphicon-shopping-cart:before{content:"\E116"}.glyphicon-folder-close:before{content:"\E117"}.glyphicon-folder-open:before{content:"\E118"}.glyphicon-resize-vertical:before{content:"\E119"}.glyphicon-resize-horizontal:before{content:"\E120"}.glyphicon-hdd:before{content:"\E121"}.glyphicon-bullhorn:before{content:"\E122"}.glyphicon-bell:before{content:"\E123"}.glyphicon-certificate:before{content:"\E124"}.glyphicon-thumbs-up:before{content:"\E125"}.glyphicon-thumbs-down:before{content:"\E126"}.glyphicon-hand-right:before{content:"\E127"}.glyphicon-hand-left:before{content:"\E128"}.glyphicon-hand-up:before{content:"\E129"}.glyphicon-hand-down:before{content:"\E130"}.glyphicon-circle-arrow-right:before{content:"\E131"}.glyphicon-circle-arrow-left:before{content:"\E132"}.glyphicon-circle-arrow-up:before{content:"\E133"}.glyphicon-circle-arrow-down:before{content:"\E134"}.glyphicon-globe:before{content:"\E135"}.glyphicon-wrench:before{content:"\E136"}.glyphicon-tasks:before{content:"\E137"}.glyphicon-filter:before{content:"\E138"}.glyphicon-briefcase:before{content:"\E139"}.glyphicon-fullscreen:before{content:"\E140"}.glyphicon-dashboard:before{content:"\E141"}.glyphicon-paperclip:before{content:"\E142"}.glyphicon-heart-empty:before{content:"\E143"}.glyphicon-link:before{content:"\E144"}.glyphicon-phone:before{content:"\E145"}.glyphicon-pushpin:before{content:"\E146"}.glyphicon-usd:before{content:"\E148"}.glyphicon-gbp:before{content:"\E149"}.glyphicon-sort:before{content:"\E150"}.glyphicon-sort-by-alphabet:before{content:"\E151"}.glyphicon-sort-by-alphabet-alt:before{content:"\E152"}.glyphicon-sort-by-order:before{content:"\E153"}.glyphicon-sort-by-order-alt:before{content:"\E154"}.glyphicon-sort-by-attributes:before{content:"\E155"}.glyphicon-sort-by-attributes-alt:before{content:"\E156"}.glyphicon-unchecked:before{content:"\E157"}.glyphicon-expand:before{content:"\E158"}.glyphicon-collapse-down:before{content:"\E159"}.glyphicon-collapse-up:before{content:"\E160"}.glyphicon-log-in:before{content:"\E161"}.glyphicon-flash:before{content:"\E162"}.glyphicon-log-out:before{content:"\E163"}.glyphicon-new-window:before{content:"\E164"}.glyphicon-record:before{content:"\E165"}.glyphicon-save:before{content:"\E166"}.glyphicon-open:before{content:"\E167"}.glyphicon-saved:before{content:"\E168"}.glyphicon-import:before{content:"\E169"}.glyphicon-export:before{content:"\E170"}.glyphicon-send:before{content:"\E171"}.glyphicon-floppy-disk:before{content:"\E172"}.glyphicon-floppy-saved:before{content:"\E173"}.glyphicon-floppy-remove:before{content:"\E174"}.glyphicon-floppy-save:before{content:"\E175"}.glyphicon-floppy-open:before{content:"\E176"}.glyphicon-credit-card:before{content:"\E177"}.glyphicon-transfer:before{content:"\E178"}.glyphicon-cutlery:before{content:"\E179"}.glyphicon-header:before{content:"\E180"}.glyphicon-compressed:before{content:"\E181"}.glyphicon-earphone:before{content:"\E182"}.glyphicon-phone-alt:before{content:"\E183"}.glyphicon-tower:before{content:"\E184"}.glyphicon-stats:before{content:"\E185"}.glyphicon-sd-video:before{content:"\E186"}.glyphicon-hd-video:before{content:"\E187"}.glyphicon-subtitles:before{content:"\E188"}.glyphicon-sound-stereo:before{content:"\E189"}.glyphicon-sound-dolby:before{content:"\E190"}.glyphicon-sound-5-1:before{content:"\E191"}.glyphicon-sound-6-1:before{content:"\E192"}.glyphicon-sound-7-1:before{content:"\E193"}.glyphicon-copyright-mark:before{content:"\E194"}.glyphicon-registration-mark:before{content:"\E195"}.glyphicon-cloud-download:before{content:"\E197"}.glyphicon-cloud-upload:before{content:"\E198"}.glyphicon-tree-conifer:before{content:"\E199"}.glyphicon-tree-deciduous:before{content:"\E200"}.glyphicon-cd:before{content:"\E201"}.glyphicon-save-file:before{content:"\E202"}.glyphicon-open-file:before{content:"\E203"}.glyphicon-level-up:before{content:"\E204"}.glyphicon-copy:before{content:"\E205"}.glyphicon-paste:before{content:"\E206"}.glyphicon-alert:before{content:"\E209"}.glyphicon-equalizer:before{content:"\E210"}.glyphicon-king:before{content:"\E211"}.glyphicon-queen:before{content:"\E212"}.glyphicon-pawn:before{content:"\E213"}.glyphicon-bishop:before{content:"\E214"}.glyphicon-knight:before{content:"\E215"}.glyphicon-baby-formula:before{content:"\E216"}.glyphicon-tent:before{content:"\26FA"}.glyphicon-blackboard:before{content:"\E218"}.glyphicon-bed:before{content:"\E219"}.glyphicon-apple:before{content:"\F8FF"}.glyphicon-erase:before{content:"\E221"}.glyphicon-hourglass:before{content:"\231B"}.glyphicon-lamp:before{content:"\E223"}.glyphicon-duplicate:before{content:"\E224"}.glyphicon-piggy-bank:before{content:"\E225"}.glyphicon-scissors:before{content:"\E226"}.glyphicon-bitcoin:before,.glyphicon-btc:before,.glyphicon-xbt:before{content:"\E227"}.glyphicon-jpy:before,.glyphicon-yen:before{content:"\A5"}.glyphicon-rub:before,.glyphicon-ruble:before{content:"\20BD"}.glyphicon-scale:before{content:"\E230"}.glyphicon-ice-lolly:before{content:"\E231"}.glyphicon-ice-lolly-tasted:before{content:"\E232"}.glyphicon-education:before{content:"\E233"}.glyphicon-option-horizontal:before{content:"\E234"}.glyphicon-option-vertical:before{content:"\E235"}.glyphicon-menu-hamburger:before{content:"\E236"}.glyphicon-modal-window:before{content:"\E237"}.glyphicon-oil:before{content:"\E238"}.glyphicon-grain:before{content:"\E239"}.glyphicon-sunglasses:before{content:"\E240"}.glyphicon-text-size:before{content:"\E241"}.glyphicon-text-color:before{content:"\E242"}.glyphicon-text-background:before{content:"\E243"}.glyphicon-object-align-top:before{content:"\E244"}.glyphicon-object-align-bottom:before{content:"\E245"}.glyphicon-object-align-horizontal:before{content:"\E246"}.glyphicon-object-align-left:before{content:"\E247"}.glyphicon-object-align-vertical:before{content:"\E248"}.glyphicon-object-align-right:before{content:"\E249"}.glyphicon-triangle-right:before{content:"\E250"}.glyphicon-triangle-left:before{content:"\E251"}.glyphicon-triangle-bottom:before{content:"\E252"}.glyphicon-triangle-top:before{content:"\E253"}.glyphicon-console:before{content:"\E254"}.glyphicon-superscript:before{content:"\E255"}.glyphicon-subscript:before{content:"\E256"}.glyphicon-menu-left:before{content:"\E257"}.glyphicon-menu-right:before{content:"\E258"}.glyphicon-menu-down:before{content:"\E259"}.glyphicon-menu-up:before{content:"\E260"}*,:after,:before{-webkit-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:transparent}body{font-family:Raleway,sans-serif;font-size:14px;line-height:1.6;color:#636b6f;background-color:#f5f8fa}button,input,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#3097d1;text-decoration:none}a:focus,a:hover{color:#216a94;text-decoration:underline}a:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive{display:block;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{padding:4px;line-height:1.6;background-color:#f5f8fa;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:22px;margin-bottom:22px;border:0;border-top:1px solid #eee}[role=button]{cursor:pointer}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-weight:400;line-height:1;color:#777}.h1,.h2,.h3,h1,h2,h3{margin-top:22px;margin-bottom:11px}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small{font-size:65%}.h4,.h5,.h6,h4,h5,h6{margin-top:11px;margin-bottom:11px}.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{font-size:75%}.h1,h1{font-size:36px}.h2,h2{font-size:30px}.h3,h3{font-size:24px}.h4,h4{font-size:18px}.h5,h5{font-size:14px}.h6,h6{font-size:12px}p{margin:0 0 11px}.lead{margin-bottom:22px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}.small,small{font-size:85%}.mark,mark{background-color:#fcf8e3;padding:.2em}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.initialism,.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#3097d1}a.text-primary:focus,a.text-primary:hover{color:#2579a9}.text-success{color:#3c763d}a.text-success:focus,a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:focus,a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:focus,a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:focus,a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#3097d1}a.bg-primary:focus,a.bg-primary:hover{background-color:#2579a9}.bg-success{background-color:#dff0d8}a.bg-success:focus,a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:focus,a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:focus,a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:focus,a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:10px;margin:44px 0 22px;border-bottom:1px solid #eee}ol,ul{margin-top:0;margin-bottom:11px}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}.list-inline,.list-unstyled{padding-left:0;list-style:none}.list-inline{margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:22px}dd,dt{line-height:1.6}dt{font-weight:700}dd{margin-left:0}.dl-horizontal dd:after,.dl-horizontal dd:before{content:" ";display:table}.dl-horizontal dd:after{clear:both}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[data-original-title],abbr[title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%}blockquote{padding:11px 22px;margin:0 0 22px;font-size:17.5px;border-left:5px solid #eee}blockquote ol:last-child,blockquote p:last-child,blockquote ul:last-child{margin-bottom:0}blockquote .small,blockquote footer,blockquote small{display:block;font-size:80%;line-height:1.6;color:#777}blockquote .small:before,blockquote footer:before,blockquote small:before{content:"\2014 \A0"}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}.blockquote-reverse .small:before,.blockquote-reverse footer:before,.blockquote-reverse small:before,blockquote.pull-right .small:before,blockquote.pull-right footer:before,blockquote.pull-right small:before{content:""}.blockquote-reverse .small:after,.blockquote-reverse footer:after,.blockquote-reverse small:after,blockquote.pull-right .small:after,blockquote.pull-right footer:after,blockquote.pull-right small:after{content:"\A0 \2014"}address{margin-bottom:22px;font-style:normal;line-height:1.6}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,Courier New,monospace}code{color:#c7254e;background-color:#f9f2f4;border-radius:4px}code,kbd{padding:2px 4px;font-size:90%}kbd{color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;font-weight:700;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:10.5px;margin:0 0 11px;font-size:13px;line-height:1.6;word-break:break-all;word-wrap:break-word;color:#333;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.container:after,.container:before{content:" ";display:table}.container:after{clear:both}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.container-fluid:after,.container-fluid:before{content:" ";display:table}.container-fluid:after{clear:both}.row{margin-left:-15px;margin-right:-15px}.row:after,.row:before{content:" ";display:table}.row:after{clear:both}.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12,.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12,.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12,.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-1{width:8.33333333%}.col-xs-2{width:16.66666667%}.col-xs-3{width:25%}.col-xs-4{width:33.33333333%}.col-xs-5{width:41.66666667%}.col-xs-6{width:50%}.col-xs-7{width:58.33333333%}.col-xs-8{width:66.66666667%}.col-xs-9{width:75%}.col-xs-10{width:83.33333333%}.col-xs-11{width:91.66666667%}.col-xs-12{width:100%}.col-xs-pull-0{right:auto}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-3{right:25%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-6{right:50%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-9{right:75%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-12{right:100%}.col-xs-push-0{left:auto}.col-xs-push-1{left:8.33333333%}.col-xs-push-2{left:16.66666667%}.col-xs-push-3{left:25%}.col-xs-push-4{left:33.33333333%}.col-xs-push-5{left:41.66666667%}.col-xs-push-6{left:50%}.col-xs-push-7{left:58.33333333%}.col-xs-push-8{left:66.66666667%}.col-xs-push-9{left:75%}.col-xs-push-10{left:83.33333333%}.col-xs-push-11{left:91.66666667%}.col-xs-push-12{left:100%}.col-xs-offset-0{margin-left:0}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-12{margin-left:100%}@media (min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-1{width:8.33333333%}.col-sm-2{width:16.66666667%}.col-sm-3{width:25%}.col-sm-4{width:33.33333333%}.col-sm-5{width:41.66666667%}.col-sm-6{width:50%}.col-sm-7{width:58.33333333%}.col-sm-8{width:66.66666667%}.col-sm-9{width:75%}.col-sm-10{width:83.33333333%}.col-sm-11{width:91.66666667%}.col-sm-12{width:100%}.col-sm-pull-0{right:auto}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-3{right:25%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-6{right:50%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-9{right:75%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-12{right:100%}.col-sm-push-0{left:auto}.col-sm-push-1{left:8.33333333%}.col-sm-push-2{left:16.66666667%}.col-sm-push-3{left:25%}.col-sm-push-4{left:33.33333333%}.col-sm-push-5{left:41.66666667%}.col-sm-push-6{left:50%}.col-sm-push-7{left:58.33333333%}.col-sm-push-8{left:66.66666667%}.col-sm-push-9{left:75%}.col-sm-push-10{left:83.33333333%}.col-sm-push-11{left:91.66666667%}.col-sm-push-12{left:100%}.col-sm-offset-0{margin-left:0}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-12{margin-left:100%}}@media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-1{width:8.33333333%}.col-md-2{width:16.66666667%}.col-md-3{width:25%}.col-md-4{width:33.33333333%}.col-md-5{width:41.66666667%}.col-md-6{width:50%}.col-md-7{width:58.33333333%}.col-md-8{width:66.66666667%}.col-md-9{width:75%}.col-md-10{width:83.33333333%}.col-md-11{width:91.66666667%}.col-md-12{width:100%}.col-md-pull-0{right:auto}.col-md-pull-1{right:8.33333333%}.col-md-pull-2{right:16.66666667%}.col-md-pull-3{right:25%}.col-md-pull-4{right:33.33333333%}.col-md-pull-5{right:41.66666667%}.col-md-pull-6{right:50%}.col-md-pull-7{right:58.33333333%}.col-md-pull-8{right:66.66666667%}.col-md-pull-9{right:75%}.col-md-pull-10{right:83.33333333%}.col-md-pull-11{right:91.66666667%}.col-md-pull-12{right:100%}.col-md-push-0{left:auto}.col-md-push-1{left:8.33333333%}.col-md-push-2{left:16.66666667%}.col-md-push-3{left:25%}.col-md-push-4{left:33.33333333%}.col-md-push-5{left:41.66666667%}.col-md-push-6{left:50%}.col-md-push-7{left:58.33333333%}.col-md-push-8{left:66.66666667%}.col-md-push-9{left:75%}.col-md-push-10{left:83.33333333%}.col-md-push-11{left:91.66666667%}.col-md-push-12{left:100%}.col-md-offset-0{margin-left:0}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-3{margin-left:25%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-6{margin-left:50%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-9{margin-left:75%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-12{margin-left:100%}}@media (min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-1{width:8.33333333%}.col-lg-2{width:16.66666667%}.col-lg-3{width:25%}.col-lg-4{width:33.33333333%}.col-lg-5{width:41.66666667%}.col-lg-6{width:50%}.col-lg-7{width:58.33333333%}.col-lg-8{width:66.66666667%}.col-lg-9{width:75%}.col-lg-10{width:83.33333333%}.col-lg-11{width:91.66666667%}.col-lg-12{width:100%}.col-lg-pull-0{right:auto}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-3{right:25%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-6{right:50%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-9{right:75%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-12{right:100%}.col-lg-push-0{left:auto}.col-lg-push-1{left:8.33333333%}.col-lg-push-2{left:16.66666667%}.col-lg-push-3{left:25%}.col-lg-push-4{left:33.33333333%}.col-lg-push-5{left:41.66666667%}.col-lg-push-6{left:50%}.col-lg-push-7{left:58.33333333%}.col-lg-push-8{left:66.66666667%}.col-lg-push-9{left:75%}.col-lg-push-10{left:83.33333333%}.col-lg-push-11{left:91.66666667%}.col-lg-push-12{left:100%}.col-lg-offset-0{margin-left:0}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-12{margin-left:100%}}table{background-color:transparent}caption{padding-top:8px;padding-bottom:8px;color:#777}caption,th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:22px}.table>tbody>tr>td,.table>tbody>tr>th,.table>tfoot>tr>td,.table>tfoot>tr>th,.table>thead>tr>td,.table>thead>tr>th{padding:8px;line-height:1.6;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>td,.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>td,.table>thead:first-child>tr:first-child>th{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#f5f8fa}.table-condensed>tbody>tr>td,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>td,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>thead>tr>th{padding:5px}.table-bordered,.table-bordered>tbody>tr>td,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>td,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border:1px solid #ddd}.table-bordered>thead>tr>td,.table-bordered>thead>tr>th{border-bottom-width:2px}.table-striped>tbody>tr:nth-of-type(odd){background-color:#f9f9f9}.table-hover>tbody>tr:hover{background-color:#f5f5f5}table col[class*=col-]{position:static;float:none;display:table-column}table td[class*=col-],table th[class*=col-]{position:static;float:none;display:table-cell}.table>tbody>tr.active>td,.table>tbody>tr.active>th,.table>tbody>tr>td.active,.table>tbody>tr>th.active,.table>tfoot>tr.active>td,.table>tfoot>tr.active>th,.table>tfoot>tr>td.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>thead>tr.active>th,.table>thead>tr>td.active,.table>thead>tr>th.active{background-color:#f5f5f5}.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover{background-color:#e8e8e8}.table>tbody>tr.success>td,.table>tbody>tr.success>th,.table>tbody>tr>td.success,.table>tbody>tr>th.success,.table>tfoot>tr.success>td,.table>tfoot>tr.success>th,.table>tfoot>tr>td.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>thead>tr.success>th,.table>thead>tr>td.success,.table>thead>tr>th.success{background-color:#dff0d8}.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover{background-color:#d0e9c6}.table>tbody>tr.info>td,.table>tbody>tr.info>th,.table>tbody>tr>td.info,.table>tbody>tr>th.info,.table>tfoot>tr.info>td,.table>tfoot>tr.info>th,.table>tfoot>tr>td.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>thead>tr.info>th,.table>thead>tr>td.info,.table>thead>tr>th.info{background-color:#d9edf7}.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover{background-color:#c4e3f3}.table>tbody>tr.warning>td,.table>tbody>tr.warning>th,.table>tbody>tr>td.warning,.table>tbody>tr>th.warning,.table>tfoot>tr.warning>td,.table>tfoot>tr.warning>th,.table>tfoot>tr>td.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>thead>tr.warning>th,.table>thead>tr>td.warning,.table>thead>tr>th.warning{background-color:#fcf8e3}.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover{background-color:#faf2cc}.table>tbody>tr.danger>td,.table>tbody>tr.danger>th,.table>tbody>tr>td.danger,.table>tbody>tr>th.danger,.table>tfoot>tr.danger>td,.table>tfoot>tr.danger>th,.table>tfoot>tr>td.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>thead>tr.danger>th,.table>thead>tr>td.danger,.table>thead>tr>th.danger{background-color:#f2dede}.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover{background-color:#ebcccc}.table-responsive{overflow-x:auto;min-height:.01%}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:16.5px;overflow-y:hidden;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>td,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>thead>tr>th{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}}fieldset{margin:0;min-width:0}fieldset,legend{padding:0;border:0}legend{display:block;width:100%;margin-bottom:22px;font-size:21px;line-height:inherit;color:#333;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;box-sizing:border-box}input[type=checkbox],input[type=radio]{margin:4px 0 0;margin-top:1px\9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{padding-top:7px}.form-control,output{display:block;font-size:14px;line-height:1.6;color:#555}.form-control{width:100%;height:36px;padding:6px 12px;background-color:#fff;background-image:none;border:1px solid #ccd0d2;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-transition:border-color .15s ease-in-out,-webkit-box-shadow .15s ease-in-out;transition:border-color .15s ease-in-out,-webkit-box-shadow .15s ease-in-out;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out,-webkit-box-shadow .15s ease-in-out}.form-control:focus{border-color:#98cbe8;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(152,203,232,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(152,203,232,.6)}.form-control::-moz-placeholder{color:#b1b7ba;opacity:1}.form-control:-ms-input-placeholder{color:#b1b7ba}.form-control::-webkit-input-placeholder{color:#b1b7ba}.form-control::-ms-expand{border:0;background-color:transparent}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{background-color:#eee;opacity:1}.form-control[disabled],fieldset[disabled] .form-control{cursor:not-allowed}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}@media screen and (-webkit-min-device-pixel-ratio:0){input[type=date].form-control,input[type=datetime-local].form-control,input[type=month].form-control,input[type=time].form-control{line-height:36px}.input-group-sm>.input-group-btn>input[type=date].btn,.input-group-sm>.input-group-btn>input[type=datetime-local].btn,.input-group-sm>.input-group-btn>input[type=month].btn,.input-group-sm>.input-group-btn>input[type=time].btn,.input-group-sm>input[type=date].form-control,.input-group-sm>input[type=date].input-group-addon,.input-group-sm>input[type=datetime-local].form-control,.input-group-sm>input[type=datetime-local].input-group-addon,.input-group-sm>input[type=month].form-control,.input-group-sm>input[type=month].input-group-addon,.input-group-sm>input[type=time].form-control,.input-group-sm>input[type=time].input-group-addon,.input-group-sm input[type=date],.input-group-sm input[type=datetime-local],.input-group-sm input[type=month],.input-group-sm input[type=time],input[type=date].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm,input[type=time].input-sm{line-height:30px}.input-group-lg>.input-group-btn>input[type=date].btn,.input-group-lg>.input-group-btn>input[type=datetime-local].btn,.input-group-lg>.input-group-btn>input[type=month].btn,.input-group-lg>.input-group-btn>input[type=time].btn,.input-group-lg>input[type=date].form-control,.input-group-lg>input[type=date].input-group-addon,.input-group-lg>input[type=datetime-local].form-control,.input-group-lg>input[type=datetime-local].input-group-addon,.input-group-lg>input[type=month].form-control,.input-group-lg>input[type=month].input-group-addon,.input-group-lg>input[type=time].form-control,.input-group-lg>input[type=time].input-group-addon,.input-group-lg input[type=date],.input-group-lg input[type=datetime-local],.input-group-lg input[type=month],.input-group-lg input[type=time],input[type=date].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg,input[type=time].input-lg{line-height:46px}}.form-group{margin-bottom:15px}.checkbox,.radio{position:relative;display:block;margin-top:10px;margin-bottom:10px}.checkbox label,.radio label{min-height:22px;padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.checkbox-inline input[type=checkbox],.checkbox input[type=checkbox],.radio-inline input[type=radio],.radio input[type=radio]{position:absolute;margin-left:-20px;margin-top:4px\9}.checkbox+.checkbox,.radio+.radio{margin-top:-5px}.checkbox-inline,.radio-inline{position:relative;display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:400;cursor:pointer}.checkbox-inline+.checkbox-inline,.radio-inline+.radio-inline{margin-top:0;margin-left:10px}.checkbox-inline.disabled,.checkbox.disabled label,.radio-inline.disabled,.radio.disabled label,fieldset[disabled] .checkbox-inline,fieldset[disabled] .checkbox label,fieldset[disabled] .radio-inline,fieldset[disabled] .radio label,fieldset[disabled] input[type=checkbox],fieldset[disabled] input[type=radio],input[type=checkbox].disabled,input[type=checkbox][disabled],input[type=radio].disabled,input[type=radio][disabled]{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0;min-height:36px}.form-control-static.input-lg,.form-control-static.input-sm,.input-group-lg>.form-control-static.form-control,.input-group-lg>.form-control-static.input-group-addon,.input-group-lg>.input-group-btn>.form-control-static.btn,.input-group-sm>.form-control-static.form-control,.input-group-sm>.form-control-static.input-group-addon,.input-group-sm>.input-group-btn>.form-control-static.btn{padding-left:0;padding-right:0}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn,.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.input-group-sm>.input-group-btn>select.btn,.input-group-sm>select.form-control,.input-group-sm>select.input-group-addon,select.input-sm{height:30px;line-height:30px}.input-group-sm>.input-group-btn>select[multiple].btn,.input-group-sm>.input-group-btn>textarea.btn,.input-group-sm>select[multiple].form-control,.input-group-sm>select[multiple].input-group-addon,.input-group-sm>textarea.form-control,.input-group-sm>textarea.input-group-addon,select[multiple].input-sm,textarea.input-sm{height:auto}.form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.form-group-sm select.form-control{height:30px;line-height:30px}.form-group-sm select[multiple].form-control,.form-group-sm textarea.form-control{height:auto}.form-group-sm .form-control-static{height:30px;min-height:34px;padding:6px 10px;font-size:12px;line-height:1.5}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn,.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.input-group-lg>.input-group-btn>select.btn,.input-group-lg>select.form-control,.input-group-lg>select.input-group-addon,select.input-lg{height:46px;line-height:46px}.input-group-lg>.input-group-btn>select[multiple].btn,.input-group-lg>.input-group-btn>textarea.btn,.input-group-lg>select[multiple].form-control,.input-group-lg>select[multiple].input-group-addon,.input-group-lg>textarea.form-control,.input-group-lg>textarea.input-group-addon,select[multiple].input-lg,textarea.input-lg{height:auto}.form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.form-group-lg select.form-control{height:46px;line-height:46px}.form-group-lg select[multiple].form-control,.form-group-lg textarea.form-control{height:auto}.form-group-lg .form-control-static{height:46px;min-height:40px;padding:11px 16px;font-size:18px;line-height:1.3333333}.has-feedback{position:relative}.has-feedback .form-control{padding-right:45px}.form-control-feedback{position:absolute;top:0;right:0;z-index:2;display:block;width:36px;height:36px;line-height:36px;text-align:center;pointer-events:none}.form-group-lg .form-control+.form-control-feedback,.input-group-lg+.form-control-feedback,.input-group-lg>.form-control+.form-control-feedback,.input-group-lg>.input-group-addon+.form-control-feedback,.input-group-lg>.input-group-btn>.btn+.form-control-feedback,.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.form-group-sm .form-control+.form-control-feedback,.input-group-sm+.form-control-feedback,.input-group-sm>.form-control+.form-control-feedback,.input-group-sm>.input-group-addon+.form-control-feedback,.input-group-sm>.input-group-btn>.btn+.form-control-feedback,.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .checkbox,.has-success .checkbox-inline,.has-success.checkbox-inline label,.has-success.checkbox label,.has-success .control-label,.has-success .help-block,.has-success .radio,.has-success .radio-inline,.has-success.radio-inline label,.has-success.radio label{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;border-color:#3c763d;background-color:#dff0d8}.has-success .form-control-feedback{color:#3c763d}.has-warning .checkbox,.has-warning .checkbox-inline,.has-warning.checkbox-inline label,.has-warning.checkbox label,.has-warning .control-label,.has-warning .help-block,.has-warning .radio,.has-warning .radio-inline,.has-warning.radio-inline label,.has-warning.radio label{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;border-color:#8a6d3b;background-color:#fcf8e3}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .checkbox,.has-error .checkbox-inline,.has-error.checkbox-inline label,.has-error.checkbox label,.has-error .control-label,.has-error .help-block,.has-error .radio,.has-error .radio-inline,.has-error.radio-inline label,.has-error.radio label{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;border-color:#a94442;background-color:#f2dede}.has-error .form-control-feedback{color:#a94442}.has-feedback label~.form-control-feedback{top:27px}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#a4aaae}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .form-control,.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .checkbox,.form-inline .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .checkbox label,.form-inline .radio label{padding-left:0}.form-inline .checkbox input[type=checkbox],.form-inline .radio input[type=radio]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .checkbox,.form-horizontal .checkbox-inline,.form-horizontal .radio,.form-horizontal .radio-inline{margin-top:0;margin-bottom:0;padding-top:7px}.form-horizontal .checkbox,.form-horizontal .radio{min-height:29px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}.form-horizontal .form-group:after,.form-horizontal .form-group:before{content:" ";display:table}.form-horizontal .form-group:after{clear:both}@media (min-width:768px){.form-horizontal .control-label{text-align:right;margin-bottom:0;padding-top:7px}}.form-horizontal .has-feedback .form-control-feedback{right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:11px;font-size:18px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px;font-size:12px}}.btn{display:inline-block;margin-bottom:0;font-weight:400;text-align:center;vertical-align:middle;-ms-touch-action:manipulation;touch-action:manipulation;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:6px 12px;font-size:14px;line-height:1.6;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn.active.focus,.btn.active:focus,.btn.focus,.btn:active.focus,.btn:active:focus,.btn:focus{outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn.focus,.btn:focus,.btn:hover{color:#636b6f;text-decoration:none}.btn.active,.btn:active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-default{color:#636b6f;background-color:#fff;border-color:#ccc}.btn-default.focus,.btn-default:focus{color:#636b6f;background-color:#e6e5e5;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.btn-default:hover,.open>.btn-default.dropdown-toggle{color:#636b6f;background-color:#e6e5e5;border-color:#adadad}.btn-default.active.focus,.btn-default.active:focus,.btn-default.active:hover,.btn-default:active.focus,.btn-default:active:focus,.btn-default:active:hover,.open>.btn-default.dropdown-toggle.focus,.open>.btn-default.dropdown-toggle:focus,.open>.btn-default.dropdown-toggle:hover{color:#636b6f;background-color:#d4d4d4;border-color:#8c8c8c}.btn-default.active,.btn-default:active,.open>.btn-default.dropdown-toggle{background-image:none}.btn-default.disabled.focus,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled].focus,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#636b6f}.btn-primary{color:#fff;background-color:#3097d1;border-color:#2a88bd}.btn-primary.focus,.btn-primary:focus{color:#fff;background-color:#2579a9;border-color:#133d55}.btn-primary.active,.btn-primary:active,.btn-primary:hover,.open>.btn-primary.dropdown-toggle{color:#fff;background-color:#2579a9;border-color:#1f648b}.btn-primary.active.focus,.btn-primary.active:focus,.btn-primary.active:hover,.btn-primary:active.focus,.btn-primary:active:focus,.btn-primary:active:hover,.open>.btn-primary.dropdown-toggle.focus,.open>.btn-primary.dropdown-toggle:focus,.open>.btn-primary.dropdown-toggle:hover{color:#fff;background-color:#1f648b;border-color:#133d55}.btn-primary.active,.btn-primary:active,.open>.btn-primary.dropdown-toggle{background-image:none}.btn-primary.disabled.focus,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled].focus,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#3097d1;border-color:#2a88bd}.btn-primary .badge{color:#3097d1;background-color:#fff}.btn-success{color:#fff;background-color:#2ab27b;border-color:#259d6d}.btn-success.focus,.btn-success:focus{color:#fff;background-color:#20895e;border-color:#0d3625}.btn-success.active,.btn-success:active,.btn-success:hover,.open>.btn-success.dropdown-toggle{color:#fff;background-color:#20895e;border-color:#196c4b}.btn-success.active.focus,.btn-success.active:focus,.btn-success.active:hover,.btn-success:active.focus,.btn-success:active:focus,.btn-success:active:hover,.open>.btn-success.dropdown-toggle.focus,.open>.btn-success.dropdown-toggle:focus,.open>.btn-success.dropdown-toggle:hover{color:#fff;background-color:#196c4b;border-color:#0d3625}.btn-success.active,.btn-success:active,.open>.btn-success.dropdown-toggle{background-image:none}.btn-success.disabled.focus,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled].focus,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#2ab27b;border-color:#259d6d}.btn-success .badge{color:#2ab27b;background-color:#fff}.btn-info{color:#fff;background-color:#8eb4cb;border-color:#7da8c3}.btn-info.focus,.btn-info:focus{color:#fff;background-color:#6b9dbb;border-color:#3d6983}.btn-info.active,.btn-info:active,.btn-info:hover,.open>.btn-info.dropdown-toggle{color:#fff;background-color:#6b9dbb;border-color:#538db0}.btn-info.active.focus,.btn-info.active:focus,.btn-info.active:hover,.btn-info:active.focus,.btn-info:active:focus,.btn-info:active:hover,.open>.btn-info.dropdown-toggle.focus,.open>.btn-info.dropdown-toggle:focus,.open>.btn-info.dropdown-toggle:hover{color:#fff;background-color:#538db0;border-color:#3d6983}.btn-info.active,.btn-info:active,.open>.btn-info.dropdown-toggle{background-image:none}.btn-info.disabled.focus,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled].focus,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#8eb4cb;border-color:#7da8c3}.btn-info .badge{color:#8eb4cb;background-color:#fff}.btn-warning{color:#fff;background-color:#cbb956;border-color:#c5b143}.btn-warning.focus,.btn-warning:focus{color:#fff;background-color:#b6a338;border-color:#685d20}.btn-warning.active,.btn-warning:active,.btn-warning:hover,.open>.btn-warning.dropdown-toggle{color:#fff;background-color:#b6a338;border-color:#9b8a30}.btn-warning.active.focus,.btn-warning.active:focus,.btn-warning.active:hover,.btn-warning:active.focus,.btn-warning:active:focus,.btn-warning:active:hover,.open>.btn-warning.dropdown-toggle.focus,.open>.btn-warning.dropdown-toggle:focus,.open>.btn-warning.dropdown-toggle:hover{color:#fff;background-color:#9b8a30;border-color:#685d20}.btn-warning.active,.btn-warning:active,.open>.btn-warning.dropdown-toggle{background-image:none}.btn-warning.disabled.focus,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled].focus,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#cbb956;border-color:#c5b143}.btn-warning .badge{color:#cbb956;background-color:#fff}.btn-danger{color:#fff;background-color:#bf5329;border-color:#aa4a24}.btn-danger.focus,.btn-danger:focus{color:#fff;background-color:#954120;border-color:#411c0e}.btn-danger.active,.btn-danger:active,.btn-danger:hover,.open>.btn-danger.dropdown-toggle{color:#fff;background-color:#954120;border-color:#78341a}.btn-danger.active.focus,.btn-danger.active:focus,.btn-danger.active:hover,.btn-danger:active.focus,.btn-danger:active:focus,.btn-danger:active:hover,.open>.btn-danger.dropdown-toggle.focus,.open>.btn-danger.dropdown-toggle:focus,.open>.btn-danger.dropdown-toggle:hover{color:#fff;background-color:#78341a;border-color:#411c0e}.btn-danger.active,.btn-danger:active,.open>.btn-danger.dropdown-toggle{background-image:none}.btn-danger.disabled.focus,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled].focus,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#bf5329;border-color:#aa4a24}.btn-danger .badge{color:#bf5329;background-color:#fff}.btn-link{color:#3097d1;font-weight:400;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:active,.btn-link:focus,.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#216a94;text-decoration:underline;background-color:transparent}.btn-link[disabled]:focus,.btn-link[disabled]:hover,fieldset[disabled] .btn-link:focus,fieldset[disabled] .btn-link:hover{color:#777;text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:10px 16px;font-size:18px;line-height:1.3333333;border-radius:6px}.btn-group-sm>.btn,.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-xs>.btn,.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition-property:height,visibility;transition-property:height,visibility;-webkit-transition-duration:.35s;transition-duration:.35s;-webkit-transition-timing-function:ease;transition-timing-function:ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px dashed;border-top:4px solid\9;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown,.dropup{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:14px;text-align:left;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:10px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.6;color:#333;white-space:nowrap}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{text-decoration:none;color:#262626;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{color:#fff;text-decoration:none;outline:0;background-color:#3097d1}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{color:#777}.dropdown-menu>.disabled>a:focus,.dropdown-menu>.disabled>a:hover{text-decoration:none;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.6;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px dashed;border-bottom:4px solid\9;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:2px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;float:left}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar:after,.btn-toolbar:before{content:" ";display:table}.btn-toolbar:after{clear:both}.btn-toolbar .btn,.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group-lg.btn-group>.btn+.dropdown-toggle,.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-group-lg>.btn .caret,.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-group-lg>.btn .caret,.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group:after,.btn-group-vertical>.btn-group:before{content:" ";display:table}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-top-left-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-top-left-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio],[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group .form-control:focus{z-index:3}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccd0d2;border-radius:4px}.input-group-addon.input-sm,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.input-group-addon.btn{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.input-group-addon.btn{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn-group:not(:last-child)>.btn,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group .form-control:first-child{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group-addon:last-child,.input-group-btn:first-child>.btn-group:not(:first-child)>.btn,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group .form-control:last-child{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{font-size:0;white-space:nowrap}.input-group-btn,.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{z-index:2;margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav:after,.nav:before{content:" ";display:table}.nav:after{clear:both}.nav>li,.nav>li>a{position:relative;display:block}.nav>li>a{padding:10px 15px}.nav>li>a:focus,.nav>li>a:hover{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:focus,.nav>li.disabled>a:hover{color:#777;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:focus,.nav .open>a:hover{background-color:#eee;border-color:#3097d1}.nav .nav-divider{height:1px;margin:10px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.6;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:focus,.nav-tabs>li.active>a:hover{color:#555;background-color:#f5f8fa;border:1px solid #ddd;border-bottom-color:transparent;cursor:default}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:focus,.nav-pills>li.active>a:hover{color:#fff;background-color:#3097d1}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified,.nav-tabs.nav-justified{width:100%}.nav-justified>li,.nav-tabs.nav-justified>li{float:none}.nav-justified>li>a,.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li,.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a,.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified,.nav-tabs.nav-justified{border-bottom:0}.nav-tabs-justified>li>a,.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a,.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:focus,.nav-tabs-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:focus,.nav-tabs.nav-justified>.active>a:hover{border-bottom-color:#f5f8fa}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:22px;border:1px solid transparent}.navbar:after,.navbar:before{content:" ";display:table}.navbar:after{clear:both}@media (min-width:768px){.navbar{border-radius:4px}}.navbar-header:after,.navbar-header:before{content:" ";display:table}.navbar-header:after{clear:both}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 hsla(0,0%,100%,.1);box-shadow:inset 0 1px 0 hsla(0,0%,100%,.1);-webkit-overflow-scrolling:touch}.navbar-collapse:after,.navbar-collapse:before{content:" ";display:table}.navbar-collapse:after{clear:both}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse{padding-left:0;padding-right:0}}.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:340px}@media (max-device-width:480px) and (orientation:landscape){.navbar-fixed-bottom .navbar-collapse,.navbar-fixed-top .navbar-collapse{max-height:200px}}.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container-fluid>.navbar-collapse,.container-fluid>.navbar-header,.container>.navbar-collapse,.container>.navbar-header{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-bottom,.navbar-fixed-top{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-bottom,.navbar-fixed-top{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:14px 15px;font-size:18px;line-height:22px;height:50px}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-brand>img{display:block}@media (min-width:768px){.navbar>.container-fluid .navbar-brand,.navbar>.container .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:8px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:22px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu .dropdown-header,.navbar-nav .open .dropdown-menu>li>a{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:22px}.navbar-nav .open .dropdown-menu>li>a:focus,.navbar-nav .open .dropdown-menu>li>a:hover{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:14px;padding-bottom:14px}}.navbar-form{margin:7px -15px;padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 hsla(0,0%,100%,.1),0 1px 0 hsla(0,0%,100%,.1);box-shadow:inset 0 1px 0 hsla(0,0%,100%,.1),0 1px 0 hsla(0,0%,100%,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .form-control-static{display:inline-block}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .form-control,.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .checkbox,.navbar-form .radio{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .checkbox label,.navbar-form .radio label{padding-left:0}.navbar-form .checkbox input[type=checkbox],.navbar-form .radio input[type=radio]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}.navbar-form .form-group:last-child{margin-bottom:0}}@media (min-width:768px){.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{margin-bottom:0;border-top-right-radius:4px;border-top-left-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:7px;margin-bottom:7px}.btn-group-sm>.navbar-btn.btn,.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.btn-group-xs>.navbar-btn.btn,.navbar-btn.btn-xs,.navbar-text{margin-top:14px;margin-bottom:14px}@media (min-width:768px){.navbar-text{float:left;margin-left:15px;margin-right:15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important;margin-right:-15px}.navbar-right~.navbar-right{margin-right:0}}.navbar-default{background-color:#fff;border-color:#d3e0e9}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:focus,.navbar-default .navbar-brand:hover{color:#5e5d5d;background-color:transparent}.navbar-default .navbar-nav>li>a,.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a:focus,.navbar-default .navbar-nav>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:focus,.navbar-default .navbar-nav>.active>a:hover{color:#555;background-color:#eee}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:focus,.navbar-default .navbar-nav>.disabled>a:hover{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:focus,.navbar-default .navbar-toggle:hover{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#d3e0e9}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:focus,.navbar-default .navbar-nav>.open>a:hover{background-color:#eee;color:#555}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover{color:#555;background-color:#eee}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:focus,.navbar-default .btn-link:hover{color:#333}.navbar-default .btn-link[disabled]:focus,.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:focus,fieldset[disabled] .navbar-default .btn-link:hover{color:#ccc}.navbar-inverse{background-color:#222;border-color:#090909}.navbar-inverse .navbar-brand{color:#9d9d9d}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>li>a,.navbar-inverse .navbar-text{color:#9d9d9d}.navbar-inverse .navbar-nav>li>a:focus,.navbar-inverse .navbar-nav>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:focus,.navbar-inverse .navbar-nav>.active>a:hover{color:#fff;background-color:#090909}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:focus,.navbar-inverse .navbar-nav>.disabled>a:hover{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:focus,.navbar-inverse .navbar-toggle:hover{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:focus,.navbar-inverse .navbar-nav>.open>a:hover{background-color:#090909;color:#fff}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#090909}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#090909}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#9d9d9d}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-color:#090909}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#9d9d9d}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#9d9d9d}.navbar-inverse .btn-link:focus,.navbar-inverse .btn-link:hover{color:#fff}.navbar-inverse .btn-link[disabled]:focus,.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:focus,fieldset[disabled] .navbar-inverse .btn-link:hover{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:22px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{content:"/\A0";padding:0 5px;color:#ccc}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:22px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;line-height:1.6;text-decoration:none;color:#3097d1;background-color:#fff;border:1px solid #ddd;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:4px;border-top-right-radius:4px}.pagination>li>a:focus,.pagination>li>a:hover,.pagination>li>span:focus,.pagination>li>span:hover{z-index:2;color:#216a94;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>a:focus,.pagination>.active>a:hover,.pagination>.active>span,.pagination>.active>span:focus,.pagination>.active>span:hover{z-index:3;color:#fff;background-color:#3097d1;border-color:#3097d1;cursor:default}.pagination>.disabled>a,.pagination>.disabled>a:focus,.pagination>.disabled>a:hover,.pagination>.disabled>span,.pagination>.disabled>span:focus,.pagination>.disabled>span:hover{color:#777;background-color:#fff;border-color:#ddd;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px;line-height:1.3333333}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:6px;border-top-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px;line-height:1.5}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:3px;border-top-right-radius:3px}.pager{padding-left:0;margin:22px 0;list-style:none;text-align:center}.pager:after,.pager:before{content:" ";display:table}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:focus,.pager li>a:hover{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:focus,.pager .disabled>a:hover,.pager .disabled>span{color:#777;background-color:#fff;cursor:not-allowed}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label:empty{display:none}.btn .label{position:relative;top:-1px}a.label:focus,a.label:hover{color:#fff;text-decoration:none;cursor:pointer}.label-default{background-color:#777}.label-default[href]:focus,.label-default[href]:hover{background-color:#5e5e5e}.label-primary{background-color:#3097d1}.label-primary[href]:focus,.label-primary[href]:hover{background-color:#2579a9}.label-success{background-color:#2ab27b}.label-success[href]:focus,.label-success[href]:hover{background-color:#20895e}.label-info{background-color:#8eb4cb}.label-info[href]:focus,.label-info[href]:hover{background-color:#6b9dbb}.label-warning{background-color:#cbb956}.label-warning[href]:focus,.label-warning[href]:hover{background-color:#b6a338}.label-danger{background-color:#bf5329}.label-danger[href]:focus,.label-danger[href]:hover{background-color:#954120}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;color:#fff;line-height:1;vertical-align:middle;white-space:nowrap;text-align:center;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-group-xs>.btn .badge,.btn-xs .badge{top:0;padding:1px 5px}.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#3097d1;background-color:#fff}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}.nav-pills>li>a>.badge{margin-left:3px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.jumbotron{padding-top:30px;padding-bottom:30px;margin-bottom:30px;background-color:#eee}.jumbotron,.jumbotron .h1,.jumbotron h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container-fluid .jumbotron,.container .jumbotron{border-radius:6px;padding-left:15px;padding-right:15px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container-fluid .jumbotron,.container .jumbotron{padding-left:60px;padding-right:60px}.jumbotron .h1,.jumbotron h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:22px;line-height:1.6;background-color:#f5f8fa;border:1px solid #ddd;border-radius:4px;-webkit-transition:border .2s ease-in-out;transition:border .2s ease-in-out}.thumbnail>img,.thumbnail a>img{display:block;max-width:100%;height:auto;margin-left:auto;margin-right:auto}.thumbnail .caption{padding:9px;color:#636b6f}a.thumbnail.active,a.thumbnail:focus,a.thumbnail:hover{border-color:#3097d1}.alert{padding:15px;margin-bottom:22px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#3c763d}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#31708f}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faebcc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebccd1;color:#a94442}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{0%{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{0%{background-position:40px 0}to{background-position:0 0}}.progress{overflow:hidden;height:22px;margin-bottom:22px;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:22px;color:#fff;text-align:center;background-color:#3097d1;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;transition:width .6s ease}.progress-bar-striped,.progress-striped .progress-bar{background-image:linear-gradient(45deg,hsla(0,0%,100%,.15) 25%,transparent 0,transparent 50%,hsla(0,0%,100%,.15) 0,hsla(0,0%,100%,.15) 75%,transparent 0,transparent);background-size:40px 40px}.progress-bar.active,.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#2ab27b}.progress-striped .progress-bar-success{background-image:linear-gradient(45deg,hsla(0,0%,100%,.15) 25%,transparent 0,transparent 50%,hsla(0,0%,100%,.15) 0,hsla(0,0%,100%,.15) 75%,transparent 0,transparent)}.progress-bar-info{background-color:#8eb4cb}.progress-striped .progress-bar-info{background-image:linear-gradient(45deg,hsla(0,0%,100%,.15) 25%,transparent 0,transparent 50%,hsla(0,0%,100%,.15) 0,hsla(0,0%,100%,.15) 75%,transparent 0,transparent)}.progress-bar-warning{background-color:#cbb956}.progress-striped .progress-bar-warning{background-image:linear-gradient(45deg,hsla(0,0%,100%,.15) 25%,transparent 0,transparent 50%,hsla(0,0%,100%,.15) 0,hsla(0,0%,100%,.15) 75%,transparent 0,transparent)}.progress-bar-danger{background-color:#bf5329}.progress-striped .progress-bar-danger{background-image:linear-gradient(45deg,hsla(0,0%,100%,.15) 25%,transparent 0,transparent 50%,hsla(0,0%,100%,.15) 0,hsla(0,0%,100%,.15) 75%,transparent 0,transparent)}.media{margin-top:15px}.media:first-child{margin-top:0}.media,.media-body{zoom:1;overflow:hidden}.media-body{width:10000px}.media-object{display:block}.media-object.img-thumbnail{max-width:none}.media-right,.media>.pull-right{padding-left:10px}.media-left,.media>.pull-left{padding-right:10px}.media-body,.media-left,.media-right{display:table-cell;vertical-align:top}.media-middle{vertical-align:middle}.media-bottom{vertical-align:bottom}.media-heading{margin-top:0;margin-bottom:5px}.media-list{padding-left:0;list-style:none}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #d3e0e9}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}a.list-group-item,button.list-group-item{color:#555}a.list-group-item .list-group-item-heading,button.list-group-item .list-group-item-heading{color:#333}a.list-group-item:focus,a.list-group-item:hover,button.list-group-item:focus,button.list-group-item:hover{text-decoration:none;color:#555;background-color:#f5f5f5}button.list-group-item{width:100%;text-align:left}.list-group-item.disabled,.list-group-item.disabled:focus,.list-group-item.disabled:hover{background-color:#eee;color:#777;cursor:not-allowed}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{z-index:2;color:#fff;background-color:#3097d1;border-color:#3097d1}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:focus .list-group-item-text,.list-group-item.active:hover .list-group-item-text{color:#d7ebf6}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:focus,a.list-group-item-success.active:hover,button.list-group-item-success.active,button.list-group-item-success.active:focus,button.list-group-item-success.active:hover{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:focus,a.list-group-item-info.active:hover,button.list-group-item-info.active,button.list-group-item-info.active:focus,button.list-group-item-info.active:hover{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:focus,a.list-group-item-warning.active:hover,button.list-group-item-warning.active,button.list-group-item-warning.active:focus,button.list-group-item-warning.active:hover{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:focus,a.list-group-item-danger.active:hover,button.list-group-item-danger.active,button.list-group-item-danger.active:focus,button.list-group-item-danger.active:hover{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:22px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-body:after,.panel-body:before{content:" ";display:table}.panel-body:after{clear:both}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-heading>.dropdown .dropdown-toggle,.panel-title{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px}.panel-title>.small,.panel-title>.small>a,.panel-title>a,.panel-title>small,.panel-title>small>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #d3e0e9;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group,.panel>.panel-collapse>.list-group{margin-bottom:0}.panel>.list-group .list-group-item,.panel>.panel-collapse>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child,.panel>.panel-collapse>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:3px;border-top-left-radius:3px}.panel>.list-group:last-child .list-group-item:last-child,.panel>.panel-collapse>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.panel-heading+.panel-collapse>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.list-group+.panel-footer,.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.panel-collapse>.table,.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-collapse>.table caption,.panel>.table-responsive>.table caption,.panel>.table caption{padding-left:15px;padding-right:15px}.panel>.table-responsive:first-child>.table:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child,.panel>.table:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child,.panel>.table:first-child>thead:first-child>tr:first-child{border-top-right-radius:3px;border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table-responsive:last-child>.table:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child,.panel>.table:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive,.panel>.table+.panel-body,.panel>.table-responsive+.panel-body{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child td,.panel>.table>tbody:first-child>tr:first-child th{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child{border-left:0}.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child{border-right:0}.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th{border-bottom:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:22px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.list-group,.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #d3e0e9}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #d3e0e9}.panel-default{border-color:#d3e0e9}.panel-default>.panel-heading{color:#333;background-color:#fff;border-color:#d3e0e9}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d3e0e9}.panel-default>.panel-heading .badge{color:#fff;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d3e0e9}.panel-primary{border-color:#3097d1}.panel-primary>.panel-heading{color:#fff;background-color:#3097d1;border-color:#3097d1}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#3097d1}.panel-primary>.panel-heading .badge{color:#3097d1;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#3097d1}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;left:0;bottom:0;height:100%;width:100%;border:0}.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.modal,.modal-open{overflow:hidden}.modal{display:none;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translateY(-25%);transform:translateY(-25%);-webkit-transition:-webkit-transform .3s ease-out;transition:-webkit-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0);transform:translate(0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5);background-clip:padding-box;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #e5e5e5}.modal-header:after,.modal-header:before{content:" ";display:table}.modal-header:after{clear:both}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.6}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer:after,.modal-footer:before{content:" ";display:table}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:Raleway,sans-serif;font-style:normal;font-weight:400;letter-spacing:normal;line-break:auto;line-height:1.6;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;word-wrap:normal;font-size:12px;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{right:5px}.tooltip.top-left .tooltip-arrow,.tooltip.top-right .tooltip-arrow{bottom:0;margin-bottom:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{left:5px}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;right:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;left:5px;margin-top:-5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;font-family:Raleway,sans-serif;font-style:normal;font-weight:400;letter-spacing:normal;line-break:auto;line-height:1.6;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;word-wrap:normal;font-size:14px;background-color:#fff;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{margin:0;padding:8px 14px;font-size:14px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{border-width:10px;content:""}.popover.top>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:#999;border-top-color:rgba(0,0,0,.25);bottom:-11px}.popover.top>.arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#fff}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:#999;border-right-color:rgba(0,0,0,.25)}.popover.right>.arrow:after{content:" ";left:1px;bottom:-10px;border-left-width:0;border-right-color:#fff}.popover.bottom>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25);top:-11px}.popover.bottom>.arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{content:" ";right:1px;border-right-width:0;border-left-color:#fff;bottom:-10px}.carousel,.carousel-inner{position:relative}.carousel-inner{overflow:hidden;width:100%}.carousel-inner>.item{display:none;position:relative;-webkit-transition:left .6s ease-in-out;transition:left .6s ease-in-out}.carousel-inner>.item>a>img,.carousel-inner>.item>img{display:block;max-width:100%;height:auto;line-height:1}@media (-webkit-transform-3d),(transform-3d){.carousel-inner>.item{-webkit-transition:-webkit-transform .6s ease-in-out;transition:-webkit-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}.carousel-inner>.item.active.right,.carousel-inner>.item.next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);left:0}.carousel-inner>.item.active.left,.carousel-inner>.item.prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);left:0}.carousel-inner>.item.active,.carousel-inner>.item.next.left,.carousel-inner>.item.prev.right{-webkit-transform:translateZ(0);transform:translateZ(0);left:0}}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;left:0;bottom:0;width:15%;opacity:.5;filter:alpha(opacity=50);font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);background-color:transparent}.carousel-control.left{background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(90deg,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001));background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="#80000000",endColorstr="#00000000",GradientType=1)}.carousel-control.right{left:auto;right:0;background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(90deg,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5));background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr="#00000000",endColorstr="#80000000",GradientType=1)}.carousel-control:focus,.carousel-control:hover{outline:0;color:#fff;text-decoration:none;opacity:.9;filter:alpha(opacity=90)}.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{position:absolute;top:50%;margin-top:-10px;z-index:5;display:inline-block}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{left:50%;margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{right:50%;margin-right:-10px}.carousel-control .icon-next,.carousel-control .icon-prev{width:20px;height:20px;line-height:1;font-family:serif}.carousel-control .icon-prev:before{content:"\2039"}.carousel-control .icon-next:before{content:"\203A"}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;margin-left:-30%;padding-left:0;list-style:none;text-align:center}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;border:1px solid #fff;border-radius:10px;cursor:pointer;background-color:#000\9;background-color:transparent}.carousel-indicators .active{margin:0;width:12px;height:12px;background-color:#fff}.carousel-caption{position:absolute;left:15%;right:15%;bottom:20px;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next,.carousel-control .icon-prev{width:30px;height:30px;margin-top:-10px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-10px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-10px}.carousel-caption{left:20%;right:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:after,.clearfix:before{content:" ";display:table}.clearfix:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-lg,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block,.visible-md,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-sm,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-xs,.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table!important}tr.visible-xs{display:table-row!important}td.visible-xs,th.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table!important}tr.visible-sm{display:table-row!important}td.visible-sm,th.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table!important}tr.visible-md{display:table-row!important}td.visible-md,th.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table!important}tr.visible-lg{display:table-row!important}td.visible-lg,th.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table!important}tr.visible-print{display:table-row!important}td.visible-print,th.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} - -/*! - * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:FontAwesome;src:url(../fonts/vendor/font-awesome/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(../fonts/vendor/font-awesome/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713) format("embedded-opentype"),url(../fonts/vendor/font-awesome/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(../fonts/vendor/font-awesome/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(../fonts/vendor/font-awesome/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(../fonts/vendor/font-awesome/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde) format("svg");font-weight:400;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\F000"}.fa-music:before{content:"\F001"}.fa-search:before{content:"\F002"}.fa-envelope-o:before{content:"\F003"}.fa-heart:before{content:"\F004"}.fa-star:before{content:"\F005"}.fa-star-o:before{content:"\F006"}.fa-user:before{content:"\F007"}.fa-film:before{content:"\F008"}.fa-th-large:before{content:"\F009"}.fa-th:before{content:"\F00A"}.fa-th-list:before{content:"\F00B"}.fa-check:before{content:"\F00C"}.fa-close:before,.fa-remove:before,.fa-times:before{content:"\F00D"}.fa-search-plus:before{content:"\F00E"}.fa-search-minus:before{content:"\F010"}.fa-power-off:before{content:"\F011"}.fa-signal:before{content:"\F012"}.fa-cog:before,.fa-gear:before{content:"\F013"}.fa-trash-o:before{content:"\F014"}.fa-home:before{content:"\F015"}.fa-file-o:before{content:"\F016"}.fa-clock-o:before{content:"\F017"}.fa-road:before{content:"\F018"}.fa-download:before{content:"\F019"}.fa-arrow-circle-o-down:before{content:"\F01A"}.fa-arrow-circle-o-up:before{content:"\F01B"}.fa-inbox:before{content:"\F01C"}.fa-play-circle-o:before{content:"\F01D"}.fa-repeat:before,.fa-rotate-right:before{content:"\F01E"}.fa-refresh:before{content:"\F021"}.fa-list-alt:before{content:"\F022"}.fa-lock:before{content:"\F023"}.fa-flag:before{content:"\F024"}.fa-headphones:before{content:"\F025"}.fa-volume-off:before{content:"\F026"}.fa-volume-down:before{content:"\F027"}.fa-volume-up:before{content:"\F028"}.fa-qrcode:before{content:"\F029"}.fa-barcode:before{content:"\F02A"}.fa-tag:before{content:"\F02B"}.fa-tags:before{content:"\F02C"}.fa-book:before{content:"\F02D"}.fa-bookmark:before{content:"\F02E"}.fa-print:before{content:"\F02F"}.fa-camera:before{content:"\F030"}.fa-font:before{content:"\F031"}.fa-bold:before{content:"\F032"}.fa-italic:before{content:"\F033"}.fa-text-height:before{content:"\F034"}.fa-text-width:before{content:"\F035"}.fa-align-left:before{content:"\F036"}.fa-align-center:before{content:"\F037"}.fa-align-right:before{content:"\F038"}.fa-align-justify:before{content:"\F039"}.fa-list:before{content:"\F03A"}.fa-dedent:before,.fa-outdent:before{content:"\F03B"}.fa-indent:before{content:"\F03C"}.fa-video-camera:before{content:"\F03D"}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:"\F03E"}.fa-pencil:before{content:"\F040"}.fa-map-marker:before{content:"\F041"}.fa-adjust:before{content:"\F042"}.fa-tint:before{content:"\F043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\F044"}.fa-share-square-o:before{content:"\F045"}.fa-check-square-o:before{content:"\F046"}.fa-arrows:before{content:"\F047"}.fa-step-backward:before{content:"\F048"}.fa-fast-backward:before{content:"\F049"}.fa-backward:before{content:"\F04A"}.fa-play:before{content:"\F04B"}.fa-pause:before{content:"\F04C"}.fa-stop:before{content:"\F04D"}.fa-forward:before{content:"\F04E"}.fa-fast-forward:before{content:"\F050"}.fa-step-forward:before{content:"\F051"}.fa-eject:before{content:"\F052"}.fa-chevron-left:before{content:"\F053"}.fa-chevron-right:before{content:"\F054"}.fa-plus-circle:before{content:"\F055"}.fa-minus-circle:before{content:"\F056"}.fa-times-circle:before{content:"\F057"}.fa-check-circle:before{content:"\F058"}.fa-question-circle:before{content:"\F059"}.fa-info-circle:before{content:"\F05A"}.fa-crosshairs:before{content:"\F05B"}.fa-times-circle-o:before{content:"\F05C"}.fa-check-circle-o:before{content:"\F05D"}.fa-ban:before{content:"\F05E"}.fa-arrow-left:before{content:"\F060"}.fa-arrow-right:before{content:"\F061"}.fa-arrow-up:before{content:"\F062"}.fa-arrow-down:before{content:"\F063"}.fa-mail-forward:before,.fa-share:before{content:"\F064"}.fa-expand:before{content:"\F065"}.fa-compress:before{content:"\F066"}.fa-plus:before{content:"\F067"}.fa-minus:before{content:"\F068"}.fa-asterisk:before{content:"\F069"}.fa-exclamation-circle:before{content:"\F06A"}.fa-gift:before{content:"\F06B"}.fa-leaf:before{content:"\F06C"}.fa-fire:before{content:"\F06D"}.fa-eye:before{content:"\F06E"}.fa-eye-slash:before{content:"\F070"}.fa-exclamation-triangle:before,.fa-warning:before{content:"\F071"}.fa-plane:before{content:"\F072"}.fa-calendar:before{content:"\F073"}.fa-random:before{content:"\F074"}.fa-comment:before{content:"\F075"}.fa-magnet:before{content:"\F076"}.fa-chevron-up:before{content:"\F077"}.fa-chevron-down:before{content:"\F078"}.fa-retweet:before{content:"\F079"}.fa-shopping-cart:before{content:"\F07A"}.fa-folder:before{content:"\F07B"}.fa-folder-open:before{content:"\F07C"}.fa-arrows-v:before{content:"\F07D"}.fa-arrows-h:before{content:"\F07E"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\F080"}.fa-twitter-square:before{content:"\F081"}.fa-facebook-square:before{content:"\F082"}.fa-camera-retro:before{content:"\F083"}.fa-key:before{content:"\F084"}.fa-cogs:before,.fa-gears:before{content:"\F085"}.fa-comments:before{content:"\F086"}.fa-thumbs-o-up:before{content:"\F087"}.fa-thumbs-o-down:before{content:"\F088"}.fa-star-half:before{content:"\F089"}.fa-heart-o:before{content:"\F08A"}.fa-sign-out:before{content:"\F08B"}.fa-linkedin-square:before{content:"\F08C"}.fa-thumb-tack:before{content:"\F08D"}.fa-external-link:before{content:"\F08E"}.fa-sign-in:before{content:"\F090"}.fa-trophy:before{content:"\F091"}.fa-github-square:before{content:"\F092"}.fa-upload:before{content:"\F093"}.fa-lemon-o:before{content:"\F094"}.fa-phone:before{content:"\F095"}.fa-square-o:before{content:"\F096"}.fa-bookmark-o:before{content:"\F097"}.fa-phone-square:before{content:"\F098"}.fa-twitter:before{content:"\F099"}.fa-facebook-f:before,.fa-facebook:before{content:"\F09A"}.fa-github:before{content:"\F09B"}.fa-unlock:before{content:"\F09C"}.fa-credit-card:before{content:"\F09D"}.fa-feed:before,.fa-rss:before{content:"\F09E"}.fa-hdd-o:before{content:"\F0A0"}.fa-bullhorn:before{content:"\F0A1"}.fa-bell:before{content:"\F0F3"}.fa-certificate:before{content:"\F0A3"}.fa-hand-o-right:before{content:"\F0A4"}.fa-hand-o-left:before{content:"\F0A5"}.fa-hand-o-up:before{content:"\F0A6"}.fa-hand-o-down:before{content:"\F0A7"}.fa-arrow-circle-left:before{content:"\F0A8"}.fa-arrow-circle-right:before{content:"\F0A9"}.fa-arrow-circle-up:before{content:"\F0AA"}.fa-arrow-circle-down:before{content:"\F0AB"}.fa-globe:before{content:"\F0AC"}.fa-wrench:before{content:"\F0AD"}.fa-tasks:before{content:"\F0AE"}.fa-filter:before{content:"\F0B0"}.fa-briefcase:before{content:"\F0B1"}.fa-arrows-alt:before{content:"\F0B2"}.fa-group:before,.fa-users:before{content:"\F0C0"}.fa-chain:before,.fa-link:before{content:"\F0C1"}.fa-cloud:before{content:"\F0C2"}.fa-flask:before{content:"\F0C3"}.fa-cut:before,.fa-scissors:before{content:"\F0C4"}.fa-copy:before,.fa-files-o:before{content:"\F0C5"}.fa-paperclip:before{content:"\F0C6"}.fa-floppy-o:before,.fa-save:before{content:"\F0C7"}.fa-square:before{content:"\F0C8"}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:"\F0C9"}.fa-list-ul:before{content:"\F0CA"}.fa-list-ol:before{content:"\F0CB"}.fa-strikethrough:before{content:"\F0CC"}.fa-underline:before{content:"\F0CD"}.fa-table:before{content:"\F0CE"}.fa-magic:before{content:"\F0D0"}.fa-truck:before{content:"\F0D1"}.fa-pinterest:before{content:"\F0D2"}.fa-pinterest-square:before{content:"\F0D3"}.fa-google-plus-square:before{content:"\F0D4"}.fa-google-plus:before{content:"\F0D5"}.fa-money:before{content:"\F0D6"}.fa-caret-down:before{content:"\F0D7"}.fa-caret-up:before{content:"\F0D8"}.fa-caret-left:before{content:"\F0D9"}.fa-caret-right:before{content:"\F0DA"}.fa-columns:before{content:"\F0DB"}.fa-sort:before,.fa-unsorted:before{content:"\F0DC"}.fa-sort-desc:before,.fa-sort-down:before{content:"\F0DD"}.fa-sort-asc:before,.fa-sort-up:before{content:"\F0DE"}.fa-envelope:before{content:"\F0E0"}.fa-linkedin:before{content:"\F0E1"}.fa-rotate-left:before,.fa-undo:before{content:"\F0E2"}.fa-gavel:before,.fa-legal:before{content:"\F0E3"}.fa-dashboard:before,.fa-tachometer:before{content:"\F0E4"}.fa-comment-o:before{content:"\F0E5"}.fa-comments-o:before{content:"\F0E6"}.fa-bolt:before,.fa-flash:before{content:"\F0E7"}.fa-sitemap:before{content:"\F0E8"}.fa-umbrella:before{content:"\F0E9"}.fa-clipboard:before,.fa-paste:before{content:"\F0EA"}.fa-lightbulb-o:before{content:"\F0EB"}.fa-exchange:before{content:"\F0EC"}.fa-cloud-download:before{content:"\F0ED"}.fa-cloud-upload:before{content:"\F0EE"}.fa-user-md:before{content:"\F0F0"}.fa-stethoscope:before{content:"\F0F1"}.fa-suitcase:before{content:"\F0F2"}.fa-bell-o:before{content:"\F0A2"}.fa-coffee:before{content:"\F0F4"}.fa-cutlery:before{content:"\F0F5"}.fa-file-text-o:before{content:"\F0F6"}.fa-building-o:before{content:"\F0F7"}.fa-hospital-o:before{content:"\F0F8"}.fa-ambulance:before{content:"\F0F9"}.fa-medkit:before{content:"\F0FA"}.fa-fighter-jet:before{content:"\F0FB"}.fa-beer:before{content:"\F0FC"}.fa-h-square:before{content:"\F0FD"}.fa-plus-square:before{content:"\F0FE"}.fa-angle-double-left:before{content:"\F100"}.fa-angle-double-right:before{content:"\F101"}.fa-angle-double-up:before{content:"\F102"}.fa-angle-double-down:before{content:"\F103"}.fa-angle-left:before{content:"\F104"}.fa-angle-right:before{content:"\F105"}.fa-angle-up:before{content:"\F106"}.fa-angle-down:before{content:"\F107"}.fa-desktop:before{content:"\F108"}.fa-laptop:before{content:"\F109"}.fa-tablet:before{content:"\F10A"}.fa-mobile-phone:before,.fa-mobile:before{content:"\F10B"}.fa-circle-o:before{content:"\F10C"}.fa-quote-left:before{content:"\F10D"}.fa-quote-right:before{content:"\F10E"}.fa-spinner:before{content:"\F110"}.fa-circle:before{content:"\F111"}.fa-mail-reply:before,.fa-reply:before{content:"\F112"}.fa-github-alt:before{content:"\F113"}.fa-folder-o:before{content:"\F114"}.fa-folder-open-o:before{content:"\F115"}.fa-smile-o:before{content:"\F118"}.fa-frown-o:before{content:"\F119"}.fa-meh-o:before{content:"\F11A"}.fa-gamepad:before{content:"\F11B"}.fa-keyboard-o:before{content:"\F11C"}.fa-flag-o:before{content:"\F11D"}.fa-flag-checkered:before{content:"\F11E"}.fa-terminal:before{content:"\F120"}.fa-code:before{content:"\F121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\F122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\F123"}.fa-location-arrow:before{content:"\F124"}.fa-crop:before{content:"\F125"}.fa-code-fork:before{content:"\F126"}.fa-chain-broken:before,.fa-unlink:before{content:"\F127"}.fa-question:before{content:"\F128"}.fa-info:before{content:"\F129"}.fa-exclamation:before{content:"\F12A"}.fa-superscript:before{content:"\F12B"}.fa-subscript:before{content:"\F12C"}.fa-eraser:before{content:"\F12D"}.fa-puzzle-piece:before{content:"\F12E"}.fa-microphone:before{content:"\F130"}.fa-microphone-slash:before{content:"\F131"}.fa-shield:before{content:"\F132"}.fa-calendar-o:before{content:"\F133"}.fa-fire-extinguisher:before{content:"\F134"}.fa-rocket:before{content:"\F135"}.fa-maxcdn:before{content:"\F136"}.fa-chevron-circle-left:before{content:"\F137"}.fa-chevron-circle-right:before{content:"\F138"}.fa-chevron-circle-up:before{content:"\F139"}.fa-chevron-circle-down:before{content:"\F13A"}.fa-html5:before{content:"\F13B"}.fa-css3:before{content:"\F13C"}.fa-anchor:before{content:"\F13D"}.fa-unlock-alt:before{content:"\F13E"}.fa-bullseye:before{content:"\F140"}.fa-ellipsis-h:before{content:"\F141"}.fa-ellipsis-v:before{content:"\F142"}.fa-rss-square:before{content:"\F143"}.fa-play-circle:before{content:"\F144"}.fa-ticket:before{content:"\F145"}.fa-minus-square:before{content:"\F146"}.fa-minus-square-o:before{content:"\F147"}.fa-level-up:before{content:"\F148"}.fa-level-down:before{content:"\F149"}.fa-check-square:before{content:"\F14A"}.fa-pencil-square:before{content:"\F14B"}.fa-external-link-square:before{content:"\F14C"}.fa-share-square:before{content:"\F14D"}.fa-compass:before{content:"\F14E"}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:"\F150"}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:"\F151"}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:"\F152"}.fa-eur:before,.fa-euro:before{content:"\F153"}.fa-gbp:before{content:"\F154"}.fa-dollar:before,.fa-usd:before{content:"\F155"}.fa-inr:before,.fa-rupee:before{content:"\F156"}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:"\F157"}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:"\F158"}.fa-krw:before,.fa-won:before{content:"\F159"}.fa-bitcoin:before,.fa-btc:before{content:"\F15A"}.fa-file:before{content:"\F15B"}.fa-file-text:before{content:"\F15C"}.fa-sort-alpha-asc:before{content:"\F15D"}.fa-sort-alpha-desc:before{content:"\F15E"}.fa-sort-amount-asc:before{content:"\F160"}.fa-sort-amount-desc:before{content:"\F161"}.fa-sort-numeric-asc:before{content:"\F162"}.fa-sort-numeric-desc:before{content:"\F163"}.fa-thumbs-up:before{content:"\F164"}.fa-thumbs-down:before{content:"\F165"}.fa-youtube-square:before{content:"\F166"}.fa-youtube:before{content:"\F167"}.fa-xing:before{content:"\F168"}.fa-xing-square:before{content:"\F169"}.fa-youtube-play:before{content:"\F16A"}.fa-dropbox:before{content:"\F16B"}.fa-stack-overflow:before{content:"\F16C"}.fa-instagram:before{content:"\F16D"}.fa-flickr:before{content:"\F16E"}.fa-adn:before{content:"\F170"}.fa-bitbucket:before{content:"\F171"}.fa-bitbucket-square:before{content:"\F172"}.fa-tumblr:before{content:"\F173"}.fa-tumblr-square:before{content:"\F174"}.fa-long-arrow-down:before{content:"\F175"}.fa-long-arrow-up:before{content:"\F176"}.fa-long-arrow-left:before{content:"\F177"}.fa-long-arrow-right:before{content:"\F178"}.fa-apple:before{content:"\F179"}.fa-windows:before{content:"\F17A"}.fa-android:before{content:"\F17B"}.fa-linux:before{content:"\F17C"}.fa-dribbble:before{content:"\F17D"}.fa-skype:before{content:"\F17E"}.fa-foursquare:before{content:"\F180"}.fa-trello:before{content:"\F181"}.fa-female:before{content:"\F182"}.fa-male:before{content:"\F183"}.fa-gittip:before,.fa-gratipay:before{content:"\F184"}.fa-sun-o:before{content:"\F185"}.fa-moon-o:before{content:"\F186"}.fa-archive:before{content:"\F187"}.fa-bug:before{content:"\F188"}.fa-vk:before{content:"\F189"}.fa-weibo:before{content:"\F18A"}.fa-renren:before{content:"\F18B"}.fa-pagelines:before{content:"\F18C"}.fa-stack-exchange:before{content:"\F18D"}.fa-arrow-circle-o-right:before{content:"\F18E"}.fa-arrow-circle-o-left:before{content:"\F190"}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:"\F191"}.fa-dot-circle-o:before{content:"\F192"}.fa-wheelchair:before{content:"\F193"}.fa-vimeo-square:before{content:"\F194"}.fa-try:before,.fa-turkish-lira:before{content:"\F195"}.fa-plus-square-o:before{content:"\F196"}.fa-space-shuttle:before{content:"\F197"}.fa-slack:before{content:"\F198"}.fa-envelope-square:before{content:"\F199"}.fa-wordpress:before{content:"\F19A"}.fa-openid:before{content:"\F19B"}.fa-bank:before,.fa-institution:before,.fa-university:before{content:"\F19C"}.fa-graduation-cap:before,.fa-mortar-board:before{content:"\F19D"}.fa-yahoo:before{content:"\F19E"}.fa-google:before{content:"\F1A0"}.fa-reddit:before{content:"\F1A1"}.fa-reddit-square:before{content:"\F1A2"}.fa-stumbleupon-circle:before{content:"\F1A3"}.fa-stumbleupon:before{content:"\F1A4"}.fa-delicious:before{content:"\F1A5"}.fa-digg:before{content:"\F1A6"}.fa-pied-piper-pp:before{content:"\F1A7"}.fa-pied-piper-alt:before{content:"\F1A8"}.fa-drupal:before{content:"\F1A9"}.fa-joomla:before{content:"\F1AA"}.fa-language:before{content:"\F1AB"}.fa-fax:before{content:"\F1AC"}.fa-building:before{content:"\F1AD"}.fa-child:before{content:"\F1AE"}.fa-paw:before{content:"\F1B0"}.fa-spoon:before{content:"\F1B1"}.fa-cube:before{content:"\F1B2"}.fa-cubes:before{content:"\F1B3"}.fa-behance:before{content:"\F1B4"}.fa-behance-square:before{content:"\F1B5"}.fa-steam:before{content:"\F1B6"}.fa-steam-square:before{content:"\F1B7"}.fa-recycle:before{content:"\F1B8"}.fa-automobile:before,.fa-car:before{content:"\F1B9"}.fa-cab:before,.fa-taxi:before{content:"\F1BA"}.fa-tree:before{content:"\F1BB"}.fa-spotify:before{content:"\F1BC"}.fa-deviantart:before{content:"\F1BD"}.fa-soundcloud:before{content:"\F1BE"}.fa-database:before{content:"\F1C0"}.fa-file-pdf-o:before{content:"\F1C1"}.fa-file-word-o:before{content:"\F1C2"}.fa-file-excel-o:before{content:"\F1C3"}.fa-file-powerpoint-o:before{content:"\F1C4"}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:"\F1C5"}.fa-file-archive-o:before,.fa-file-zip-o:before{content:"\F1C6"}.fa-file-audio-o:before,.fa-file-sound-o:before{content:"\F1C7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\F1C8"}.fa-file-code-o:before{content:"\F1C9"}.fa-vine:before{content:"\F1CA"}.fa-codepen:before{content:"\F1CB"}.fa-jsfiddle:before{content:"\F1CC"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:"\F1CD"}.fa-circle-o-notch:before{content:"\F1CE"}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:"\F1D0"}.fa-empire:before,.fa-ge:before{content:"\F1D1"}.fa-git-square:before{content:"\F1D2"}.fa-git:before{content:"\F1D3"}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:"\F1D4"}.fa-tencent-weibo:before{content:"\F1D5"}.fa-qq:before{content:"\F1D6"}.fa-wechat:before,.fa-weixin:before{content:"\F1D7"}.fa-paper-plane:before,.fa-send:before{content:"\F1D8"}.fa-paper-plane-o:before,.fa-send-o:before{content:"\F1D9"}.fa-history:before{content:"\F1DA"}.fa-circle-thin:before{content:"\F1DB"}.fa-header:before{content:"\F1DC"}.fa-paragraph:before{content:"\F1DD"}.fa-sliders:before{content:"\F1DE"}.fa-share-alt:before{content:"\F1E0"}.fa-share-alt-square:before{content:"\F1E1"}.fa-bomb:before{content:"\F1E2"}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:"\F1E3"}.fa-tty:before{content:"\F1E4"}.fa-binoculars:before{content:"\F1E5"}.fa-plug:before{content:"\F1E6"}.fa-slideshare:before{content:"\F1E7"}.fa-twitch:before{content:"\F1E8"}.fa-yelp:before{content:"\F1E9"}.fa-newspaper-o:before{content:"\F1EA"}.fa-wifi:before{content:"\F1EB"}.fa-calculator:before{content:"\F1EC"}.fa-paypal:before{content:"\F1ED"}.fa-google-wallet:before{content:"\F1EE"}.fa-cc-visa:before{content:"\F1F0"}.fa-cc-mastercard:before{content:"\F1F1"}.fa-cc-discover:before{content:"\F1F2"}.fa-cc-amex:before{content:"\F1F3"}.fa-cc-paypal:before{content:"\F1F4"}.fa-cc-stripe:before{content:"\F1F5"}.fa-bell-slash:before{content:"\F1F6"}.fa-bell-slash-o:before{content:"\F1F7"}.fa-trash:before{content:"\F1F8"}.fa-copyright:before{content:"\F1F9"}.fa-at:before{content:"\F1FA"}.fa-eyedropper:before{content:"\F1FB"}.fa-paint-brush:before{content:"\F1FC"}.fa-birthday-cake:before{content:"\F1FD"}.fa-area-chart:before{content:"\F1FE"}.fa-pie-chart:before{content:"\F200"}.fa-line-chart:before{content:"\F201"}.fa-lastfm:before{content:"\F202"}.fa-lastfm-square:before{content:"\F203"}.fa-toggle-off:before{content:"\F204"}.fa-toggle-on:before{content:"\F205"}.fa-bicycle:before{content:"\F206"}.fa-bus:before{content:"\F207"}.fa-ioxhost:before{content:"\F208"}.fa-angellist:before{content:"\F209"}.fa-cc:before{content:"\F20A"}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:"\F20B"}.fa-meanpath:before{content:"\F20C"}.fa-buysellads:before{content:"\F20D"}.fa-connectdevelop:before{content:"\F20E"}.fa-dashcube:before{content:"\F210"}.fa-forumbee:before{content:"\F211"}.fa-leanpub:before{content:"\F212"}.fa-sellsy:before{content:"\F213"}.fa-shirtsinbulk:before{content:"\F214"}.fa-simplybuilt:before{content:"\F215"}.fa-skyatlas:before{content:"\F216"}.fa-cart-plus:before{content:"\F217"}.fa-cart-arrow-down:before{content:"\F218"}.fa-diamond:before{content:"\F219"}.fa-ship:before{content:"\F21A"}.fa-user-secret:before{content:"\F21B"}.fa-motorcycle:before{content:"\F21C"}.fa-street-view:before{content:"\F21D"}.fa-heartbeat:before{content:"\F21E"}.fa-venus:before{content:"\F221"}.fa-mars:before{content:"\F222"}.fa-mercury:before{content:"\F223"}.fa-intersex:before,.fa-transgender:before{content:"\F224"}.fa-transgender-alt:before{content:"\F225"}.fa-venus-double:before{content:"\F226"}.fa-mars-double:before{content:"\F227"}.fa-venus-mars:before{content:"\F228"}.fa-mars-stroke:before{content:"\F229"}.fa-mars-stroke-v:before{content:"\F22A"}.fa-mars-stroke-h:before{content:"\F22B"}.fa-neuter:before{content:"\F22C"}.fa-genderless:before{content:"\F22D"}.fa-facebook-official:before{content:"\F230"}.fa-pinterest-p:before{content:"\F231"}.fa-whatsapp:before{content:"\F232"}.fa-server:before{content:"\F233"}.fa-user-plus:before{content:"\F234"}.fa-user-times:before{content:"\F235"}.fa-bed:before,.fa-hotel:before{content:"\F236"}.fa-viacoin:before{content:"\F237"}.fa-train:before{content:"\F238"}.fa-subway:before{content:"\F239"}.fa-medium:before{content:"\F23A"}.fa-y-combinator:before,.fa-yc:before{content:"\F23B"}.fa-optin-monster:before{content:"\F23C"}.fa-opencart:before{content:"\F23D"}.fa-expeditedssl:before{content:"\F23E"}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:"\F240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\F241"}.fa-battery-2:before,.fa-battery-half:before{content:"\F242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\F243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\F244"}.fa-mouse-pointer:before{content:"\F245"}.fa-i-cursor:before{content:"\F246"}.fa-object-group:before{content:"\F247"}.fa-object-ungroup:before{content:"\F248"}.fa-sticky-note:before{content:"\F249"}.fa-sticky-note-o:before{content:"\F24A"}.fa-cc-jcb:before{content:"\F24B"}.fa-cc-diners-club:before{content:"\F24C"}.fa-clone:before{content:"\F24D"}.fa-balance-scale:before{content:"\F24E"}.fa-hourglass-o:before{content:"\F250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\F251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\F252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\F253"}.fa-hourglass:before{content:"\F254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\F255"}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:"\F256"}.fa-hand-scissors-o:before{content:"\F257"}.fa-hand-lizard-o:before{content:"\F258"}.fa-hand-spock-o:before{content:"\F259"}.fa-hand-pointer-o:before{content:"\F25A"}.fa-hand-peace-o:before{content:"\F25B"}.fa-trademark:before{content:"\F25C"}.fa-registered:before{content:"\F25D"}.fa-creative-commons:before{content:"\F25E"}.fa-gg:before{content:"\F260"}.fa-gg-circle:before{content:"\F261"}.fa-tripadvisor:before{content:"\F262"}.fa-odnoklassniki:before{content:"\F263"}.fa-odnoklassniki-square:before{content:"\F264"}.fa-get-pocket:before{content:"\F265"}.fa-wikipedia-w:before{content:"\F266"}.fa-safari:before{content:"\F267"}.fa-chrome:before{content:"\F268"}.fa-firefox:before{content:"\F269"}.fa-opera:before{content:"\F26A"}.fa-internet-explorer:before{content:"\F26B"}.fa-television:before,.fa-tv:before{content:"\F26C"}.fa-contao:before{content:"\F26D"}.fa-500px:before{content:"\F26E"}.fa-amazon:before{content:"\F270"}.fa-calendar-plus-o:before{content:"\F271"}.fa-calendar-minus-o:before{content:"\F272"}.fa-calendar-times-o:before{content:"\F273"}.fa-calendar-check-o:before{content:"\F274"}.fa-industry:before{content:"\F275"}.fa-map-pin:before{content:"\F276"}.fa-map-signs:before{content:"\F277"}.fa-map-o:before{content:"\F278"}.fa-map:before{content:"\F279"}.fa-commenting:before{content:"\F27A"}.fa-commenting-o:before{content:"\F27B"}.fa-houzz:before{content:"\F27C"}.fa-vimeo:before{content:"\F27D"}.fa-black-tie:before{content:"\F27E"}.fa-fonticons:before{content:"\F280"}.fa-reddit-alien:before{content:"\F281"}.fa-edge:before{content:"\F282"}.fa-credit-card-alt:before{content:"\F283"}.fa-codiepie:before{content:"\F284"}.fa-modx:before{content:"\F285"}.fa-fort-awesome:before{content:"\F286"}.fa-usb:before{content:"\F287"}.fa-product-hunt:before{content:"\F288"}.fa-mixcloud:before{content:"\F289"}.fa-scribd:before{content:"\F28A"}.fa-pause-circle:before{content:"\F28B"}.fa-pause-circle-o:before{content:"\F28C"}.fa-stop-circle:before{content:"\F28D"}.fa-stop-circle-o:before{content:"\F28E"}.fa-shopping-bag:before{content:"\F290"}.fa-shopping-basket:before{content:"\F291"}.fa-hashtag:before{content:"\F292"}.fa-bluetooth:before{content:"\F293"}.fa-bluetooth-b:before{content:"\F294"}.fa-percent:before{content:"\F295"}.fa-gitlab:before{content:"\F296"}.fa-wpbeginner:before{content:"\F297"}.fa-wpforms:before{content:"\F298"}.fa-envira:before{content:"\F299"}.fa-universal-access:before{content:"\F29A"}.fa-wheelchair-alt:before{content:"\F29B"}.fa-question-circle-o:before{content:"\F29C"}.fa-blind:before{content:"\F29D"}.fa-audio-description:before{content:"\F29E"}.fa-volume-control-phone:before{content:"\F2A0"}.fa-braille:before{content:"\F2A1"}.fa-assistive-listening-systems:before{content:"\F2A2"}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:"\F2A3"}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:"\F2A4"}.fa-glide:before{content:"\F2A5"}.fa-glide-g:before{content:"\F2A6"}.fa-sign-language:before,.fa-signing:before{content:"\F2A7"}.fa-low-vision:before{content:"\F2A8"}.fa-viadeo:before{content:"\F2A9"}.fa-viadeo-square:before{content:"\F2AA"}.fa-snapchat:before{content:"\F2AB"}.fa-snapchat-ghost:before{content:"\F2AC"}.fa-snapchat-square:before{content:"\F2AD"}.fa-pied-piper:before{content:"\F2AE"}.fa-first-order:before{content:"\F2B0"}.fa-yoast:before{content:"\F2B1"}.fa-themeisle:before{content:"\F2B2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\F2B3"}.fa-fa:before,.fa-font-awesome:before{content:"\F2B4"}.fa-handshake-o:before{content:"\F2B5"}.fa-envelope-open:before{content:"\F2B6"}.fa-envelope-open-o:before{content:"\F2B7"}.fa-linode:before{content:"\F2B8"}.fa-address-book:before{content:"\F2B9"}.fa-address-book-o:before{content:"\F2BA"}.fa-address-card:before,.fa-vcard:before{content:"\F2BB"}.fa-address-card-o:before,.fa-vcard-o:before{content:"\F2BC"}.fa-user-circle:before{content:"\F2BD"}.fa-user-circle-o:before{content:"\F2BE"}.fa-user-o:before{content:"\F2C0"}.fa-id-badge:before{content:"\F2C1"}.fa-drivers-license:before,.fa-id-card:before{content:"\F2C2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\F2C3"}.fa-quora:before{content:"\F2C4"}.fa-free-code-camp:before{content:"\F2C5"}.fa-telegram:before{content:"\F2C6"}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:"\F2C7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\F2C8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\F2C9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\F2CA"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\F2CB"}.fa-shower:before{content:"\F2CC"}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:"\F2CD"}.fa-podcast:before{content:"\F2CE"}.fa-window-maximize:before{content:"\F2D0"}.fa-window-minimize:before{content:"\F2D1"}.fa-window-restore:before{content:"\F2D2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\F2D3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\F2D4"}.fa-bandcamp:before{content:"\F2D5"}.fa-grav:before{content:"\F2D6"}.fa-etsy:before{content:"\F2D7"}.fa-imdb:before{content:"\F2D8"}.fa-ravelry:before{content:"\F2D9"}.fa-eercast:before{content:"\F2DA"}.fa-microchip:before{content:"\F2DB"}.fa-snowflake-o:before{content:"\F2DC"}.fa-superpowers:before{content:"\F2DD"}.fa-wpexplorer:before{content:"\F2DE"}.fa-meetup:before{content:"\F2E0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} \ No newline at end of file diff --git a/public/css/jquery-ui/.htaccess b/public/css/jquery-ui/.htaccess new file mode 100644 index 0000000000..45552cb63e --- /dev/null +++ b/public/css/jquery-ui/.htaccess @@ -0,0 +1 @@ +Options -Indexes \ No newline at end of file diff --git a/public/css/jquery-ui/images/.htaccess b/public/css/jquery-ui/images/.htaccess new file mode 100644 index 0000000000..45552cb63e --- /dev/null +++ b/public/css/jquery-ui/images/.htaccess @@ -0,0 +1 @@ +Options -Indexes \ No newline at end of file diff --git a/public/fonts/.htaccess b/public/fonts/.htaccess new file mode 100644 index 0000000000..45552cb63e --- /dev/null +++ b/public/fonts/.htaccess @@ -0,0 +1 @@ +Options -Indexes \ No newline at end of file diff --git a/public/fonts/vendor/.htaccess b/public/fonts/vendor/.htaccess new file mode 100644 index 0000000000..45552cb63e --- /dev/null +++ b/public/fonts/vendor/.htaccess @@ -0,0 +1 @@ +Options -Indexes \ No newline at end of file diff --git a/public/fonts/vendor/bootstrap-sass/.htaccess b/public/fonts/vendor/bootstrap-sass/.htaccess new file mode 100644 index 0000000000..45552cb63e --- /dev/null +++ b/public/fonts/vendor/bootstrap-sass/.htaccess @@ -0,0 +1 @@ +Options -Indexes \ No newline at end of file diff --git a/public/fonts/vendor/bootstrap-sass/bootstrap/.htaccess b/public/fonts/vendor/bootstrap-sass/bootstrap/.htaccess new file mode 100644 index 0000000000..45552cb63e --- /dev/null +++ b/public/fonts/vendor/bootstrap-sass/bootstrap/.htaccess @@ -0,0 +1 @@ +Options -Indexes \ No newline at end of file diff --git a/public/fonts/vendor/font-awesome/.htaccess b/public/fonts/vendor/font-awesome/.htaccess new file mode 100644 index 0000000000..45552cb63e --- /dev/null +++ b/public/fonts/vendor/font-awesome/.htaccess @@ -0,0 +1 @@ +Options -Indexes \ No newline at end of file diff --git a/public/images/.htaccess b/public/images/.htaccess new file mode 100644 index 0000000000..45552cb63e --- /dev/null +++ b/public/images/.htaccess @@ -0,0 +1 @@ +Options -Indexes \ No newline at end of file diff --git a/public/images/flags/.htaccess b/public/images/flags/.htaccess new file mode 100644 index 0000000000..45552cb63e --- /dev/null +++ b/public/images/flags/.htaccess @@ -0,0 +1 @@ +Options -Indexes \ No newline at end of file diff --git a/public/images/flags/de_DE.png b/public/images/flags/de_DE.png new file mode 100755 index 0000000000..ac4a977362 Binary files /dev/null and b/public/images/flags/de_DE.png differ diff --git a/public/images/flags/es_ES.png b/public/images/flags/es_ES.png new file mode 100755 index 0000000000..c2de2d7111 Binary files /dev/null and b/public/images/flags/es_ES.png differ diff --git a/public/images/flags/fr_FR.png b/public/images/flags/fr_FR.png new file mode 100755 index 0000000000..8332c4ec23 Binary files /dev/null and b/public/images/flags/fr_FR.png differ diff --git a/public/images/flags/id_ID.png b/public/images/flags/id_ID.png new file mode 100755 index 0000000000..c6bc0fafac Binary files /dev/null and b/public/images/flags/id_ID.png differ diff --git a/public/images/flags/it_IT.png b/public/images/flags/it_IT.png new file mode 100755 index 0000000000..89692f74f0 Binary files /dev/null and b/public/images/flags/it_IT.png differ diff --git a/public/images/flags/nl_NL.png b/public/images/flags/nl_NL.png new file mode 100755 index 0000000000..fe44791e32 Binary files /dev/null and b/public/images/flags/nl_NL.png differ diff --git a/public/images/flags/pl_PL.png b/public/images/flags/pl_PL.png new file mode 100755 index 0000000000..d413d010b5 Binary files /dev/null and b/public/images/flags/pl_PL.png differ diff --git a/public/images/flags/pt_BR.png b/public/images/flags/pt_BR.png new file mode 100755 index 0000000000..9b1a5538b2 Binary files /dev/null and b/public/images/flags/pt_BR.png differ diff --git a/public/images/flags/ru_RU.png b/public/images/flags/ru_RU.png new file mode 100755 index 0000000000..47da4214fd Binary files /dev/null and b/public/images/flags/ru_RU.png differ diff --git a/public/images/flags/tr_TR.png b/public/images/flags/tr_TR.png new file mode 100755 index 0000000000..be32f77e99 Binary files /dev/null and b/public/images/flags/tr_TR.png differ diff --git a/public/images/logos/.htaccess b/public/images/logos/.htaccess new file mode 100644 index 0000000000..45552cb63e --- /dev/null +++ b/public/images/logos/.htaccess @@ -0,0 +1 @@ +Options -Indexes \ No newline at end of file diff --git a/public/images/logos/ynab.png b/public/images/logos/ynab.png new file mode 100644 index 0000000000..289ea469f5 Binary files /dev/null and b/public/images/logos/ynab.png differ diff --git a/public/js/.htaccess b/public/js/.htaccess new file mode 100644 index 0000000000..45552cb63e --- /dev/null +++ b/public/js/.htaccess @@ -0,0 +1 @@ +Options -Indexes \ No newline at end of file diff --git a/public/js/app.js b/public/js/app.js index 3b1cd0f4ee..16deb1ca6c 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -1 +1 @@ -!function(t){var e={};function n(r){if(e[r])return e[r].exports;var i=e[r]={i:r,l:!1,exports:{}};return t[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{configurable:!1,enumerable:!0,get:r})},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=12)}([function(t,e,n){"use strict";var r=n(6),i=n(21),o=Object.prototype.toString;function a(t){return"[object Array]"===o.call(t)}function s(t){return null!==t&&"object"==typeof t}function u(t){return"[object Function]"===o.call(t)}function c(t,e){if(null!==t&&void 0!==t)if("object"!=typeof t&&(t=[t]),a(t))for(var n=0,r=t.length;n{{ 'show_the_current_period_and_overview'|_ }} diff --git a/resources/views/budgets/show.twig b/resources/views/budgets/show.twig index 8fdc7a414e..e40e0ed0cc 100644 --- a/resources/views/budgets/show.twig +++ b/resources/views/budgets/show.twig @@ -89,7 +89,11 @@
diff --git a/resources/views/categories/no-category.twig b/resources/views/categories/no-category.twig index da8077c8b7..ca2dc081a8 100644 --- a/resources/views/categories/no-category.twig +++ b/resources/views/categories/no-category.twig @@ -1,7 +1,7 @@ {% extends "./layout/default" %} {% block breadcrumbs %} - {{ Breadcrumbs.render(Route.getCurrentRoute.getName, moment, start, end) }} + {{ Breadcrumbs.render(Route.getCurrentRoute.getName, start, end) }} {% endblock %} {% block content %} @@ -10,7 +10,7 @@ {% if periods.count > 0 %}
{% endif %} @@ -22,13 +22,15 @@- {{ 'show_all_no_filter'|_ }} + {{ 'show_all_no_filter'|_ }}
{% else %} + {% include 'list.transactions' with {showBudgets:true, showBill:true} %}{{ 'show_the_current_period_and_overview'|_ }} diff --git a/resources/views/categories/show.twig b/resources/views/categories/show.twig index d3ccd0f5e6..09d6992601 100644 --- a/resources/views/categories/show.twig +++ b/resources/views/categories/show.twig @@ -1,12 +1,12 @@ {% extends "./layout/default" %} {% block breadcrumbs %} - {{ Breadcrumbs.render(Route.getCurrentRoute.getName, category, '', start, end) }} + {{ Breadcrumbs.render(Route.getCurrentRoute.getName, category, start, end) }} {% endblock %} {% block content %}

