Compare commits

...

No commits in common. "lastFOSS" and "v5-develop" have entirely different histories.

5303 changed files with 3170074 additions and 501789 deletions

3
.babelrc Normal file
View file

@ -0,0 +1,3 @@
{
"plugins": ["@babel/plugin-proposal-class-properties"]
}

View file

@ -1,3 +0,0 @@
{
"directory": "./resources/assets/bower"
}

5
.codacy.yml Normal file
View file

@ -0,0 +1,5 @@
# Codacy configuration file for Invoice Ninja
exclude_paths:
- 'public/css/*'
- 'public/js/*'

View file

@ -1,37 +0,0 @@
engines:
csslint:
enabled: true
duplication:
enabled: true
config:
languages:
- ruby
- javascript
- python
- php
eslint:
enabled: false
fixme:
enabled: true
phpmd:
enabled: true
ratings:
paths:
- "**.css"
- "**.inc"
- "**.js"
- "**.jsx"
- "**.module"
- "**.php"
- "**.py"
- "**.rb"
exclude_paths:
- "bootstrap/cache"
- "resources/"
- "storage/"
- "tests/"
- "public/"
- "**.md"
- "**.min.js"
- "**.min.php"
- "**.min.css"

15
.editorconfig Normal file
View file

@ -0,0 +1,15 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.yml]
indent_size = 2

23
.env.ci Normal file
View file

@ -0,0 +1,23 @@
APP_NAME="Invoice Ninja"
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://ninja.test
MULTI_DB_ENABLED=false
# database
DB_CONNECTION=mysql
DB_DATABASE1=ninja
DB_USERNAME1=root
DB_PASSWORD1=ninja
DB_HOST1=127.0.0.1
DB_PORT1=32768
DB_PORT=32768
DB_DATABASE=ninja
DB_USERNAME=root
DB_PASSWORD=ninja
DB_HOST=127.0.0.1
NINJA_ENVIRONMENT=hosted
COMPOSER_AUTH='{"github-oauth": {"github.com": "${{ secrets.GITHUB_TOKEN }}"}}'
TRAVIS=true
API_SECRET=superdoopersecrethere
PHANTOMJS_PDF_GENERATION=false

27
.env.dusk.example Normal file
View file

@ -0,0 +1,27 @@
APP_ENV=local
APP_DEBUG=true
APP_LOCALE=en
APP_URL=http://127.0.0.1:8000
APP_KEY=s7epnjtomsdond5zgfqgaqmwhhcjct02
REQUIRE_HTTPS=false
NINJA_ENVIRONMENT=development
DB_TYPE=mysql
DB_STRICT=false
DB_HOST=localhost
DB_USERNAME=ninja
DB_PASSWORD=ninja
DB_CONNECTION=db-ninja-01
DB_DATABASE1=db-ninja-01
DB_DATABASE2=db-ninja-02
MAIL_MAILER=log
MAIL_PORT=587
MAIL_ENCRYPTION=tls
MAIL_HOST=
MAIL_USERNAME=
MAIL_FROM_NAME=
MAIL_FROM_ADDRESS=
MAIL_PASSWORD=
MAILGUN_DOMAIN=
MAILGUN_SECRET=

View file

@ -1,107 +1,70 @@
APP_NAME="Invoice Ninja"
APP_ENV=production APP_ENV=production
APP_KEY=base64:RR++yx2rJ9kdxbdh3+AmbHLDQu+Q76i++co9Y8ybbno=
APP_DEBUG=false APP_DEBUG=false
APP_URL=http://www.ninja.test
APP_KEY=SomeRandomStringSomeRandomString
APP_CIPHER=AES-256-CBC
APP_LOCALE=en
DB_TYPE=mysql APP_URL=http://localhost
DB_STRICT=false
DB_CONNECTION=mysql
MULTI_DB_ENABLED=false
DB_HOST=localhost DB_HOST=localhost
DB_DATABASE=ninja DB_DATABASE=ninja
DB_USERNAME=ninja DB_USERNAME=ninja
DB_PASSWORD=ninja DB_PASSWORD=ninja
DB_PORT=3306
MAIL_DRIVER=smtp DEMO_MODE=false
MAIL_PORT=587
MAIL_ENCRYPTION=tls
MAIL_HOST
MAIL_USERNAME
MAIL_FROM_ADDRESS
MAIL_FROM_NAME
MAIL_PASSWORD
MAILGUN_DOMAIN= BROADCAST_DRIVER=log
MAILGUN_SECRET= LOG_CHANNEL=stack
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
#POSTMARK_API_TOKEN= REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
PHANTOMJS_CLOUD_KEY='a-demo-key-with-low-quota-per-ip-address' MAIL_MAILER=smtp
#PHANTOMJS_BIN_PATH=/usr/local/bin/phantomjs MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS='user@example.com'
MAIL_FROM_NAME='Self Hosted User'
LOG=single POSTMARK_API_TOKEN=
REQUIRE_HTTPS=false REQUIRE_HTTPS=false
API_SECRET=password
#TRUSTED_PROXIES= GOOGLE_MAPS_API_KEY=
ERROR_EMAIL=
TRUSTED_PROXIES=
#SESSION_DRIVER= NINJA_ENVIRONMENT=selfhost
#SESSION_DOMAIN=
#SESSION_ENCRYPT=
#SESSION_SECURE=
#CACHE_DRIVER= #options - snappdf / phantom / hosted_ninja
#CACHE_HOST= PDF_GENERATOR=hosted_ninja
#REDIS_HOST=
#CACHE_PORT1=
#CACHE_PORT2=
#GOOGLE_CLIENT_ID= PHANTOMJS_KEY='a-demo-key-with-low-quota-per-ip-address'
#GOOGLE_CLIENT_SECRET= PHANTOMJS_SECRET=secret
#GOOGLE_OAUTH_REDIRECT=http://ninja.test/auth/google
GOOGLE_MAPS_ENABLED=true UPDATE_SECRET=secret
#GOOGLE_MAPS_API_KEY=
# Create a cookie to stay logged in DELETE_PDF_DAYS=60
#REMEMBER_ME_ENABLED=true DELETE_BACKUP_DAYS=60
# Immediately expire cookie on the browser closing COMPOSER_AUTH='{"github-oauth": {"github.com": "${{ secrets.GITHUB_TOKEN }}"}}'
#SESSION_EXPIRE_ON_CLOSE=false
# The app automatically logs the user out after this number of seconds GOOGLE_PLAY_PACKAGE_NAME=
#AUTO_LOGOUT_SECONDS=28800 APPSTORE_PASSWORD=
#S3_KEY= MICROSOFT_CLIENT_ID=
#S3_SECRET= MICROSOFT_CLIENT_SECRET=
#S3_REGION= MICROSOFT_REDIRECT_URI=
#S3_BUCKET=
#RACKSPACE_USERNAME= APPLE_CLIENT_ID=
#RACKSPACE_KEY= APPLE_CLIENT_SECRET=
#RACKSPACE_CONTAINER= APPLE_REDIRECT_URI=
#RACKSPACE_REGION=
#RACKSPACE_TEMP_URL_SECRET=
# If this is set to anything, the URL secret will be set the next
# time a file is downloaded through the client portal.
# Only set this temporarily, as it slows things down.
#RACKSPACE_TEMP_URL_SECRET_SET=
#DOCUMENT_FILESYSTEM=
#MAX_DOCUMENT_SIZE # KB
#MAX_EMAIL_DOCUMENTS_SIZE # Total KB
#MAX_ZIP_DOCUMENTS_SIZE # Total KB (uncompressed)
#DOCUMENT_PREVIEW_SIZE # Pixels
WEPAY_CLIENT_ID=
WEPAY_CLIENT_SECRET=
WEPAY_ENVIRONMENT=production # production or stage
WEPAY_AUTO_UPDATE=true # Requires permission from WePay
WEPAY_FEE_PAYER=payee
WEPAY_APP_FEE_CC_MULTIPLIER=0
WEPAY_APP_FEE_ACH_MULTIPLIER=0
WEPAY_APP_FEE_FIXED=0
WEPAY_THEME='{"name":"Invoice Ninja","primary_color":"0b4d78","secondary_color":"0b4d78","background_color":"f8f8f8","button_color":"33b753"}' # See https://www.wepay.com/developer/reference/structures#theme
BLUEVINE_PARTNER_UNIQUE_ID=
BLUEVINE_PARTNER_TOKEN=
CLOUDFLARE_DNS_ENABLED=false
CLOUDFLARE_API_KEY=
CLOUDFLARE_EMAIL=
CLOUDFLARE_TARGET_IP_ADDRESS=
CLOUDFLARE_ZONE_IDS={}

View file

@ -1,25 +1,24 @@
APP_ENV=development APP_ENV=local
APP_DEBUG=false APP_DEBUG=false
APP_URL=http://ninja.test APP_URL=http://127.0.0.1:8000
APP_KEY=SomeRandomStringSomeRandomString APP_KEY=SomeRandomStringSomeRandomString
APP_CIPHER=AES-256-CBC APP_CIPHER=AES-256-CBC
APP_LOCALE=en APP_LOCALE=en
MULTI_DB_ENABLED=true
MULTI_DB_CACHE_ENABLED=true
DB_TYPE=db-ninja-1
DB_STRICT=false DB_STRICT=false
DB_HOST=localhost DB_HOST=localhost
DB_USERNAME=ninja DB_USERNAME=ninja
DB_PASSWORD=ninja DB_PASSWORD=ninja
DB_DATABASE0=ninja0 DB_CONNECTION=db-ninja-01
DB_DATABASE1=ninja DB_DATABASE1=ninja01
DB_DATABASE2=ninja2 DB_DATABASE2=ninja02
MAIL_DRIVER=log MAIL_MAILER=log
TRAVIS=true TRAVIS=true
API_SECRET=password API_SECRET=password
TEST_USERNAME=user@example.com TEST_USERNAME=user@example.com
TEST_PERMISSIONS_USERNAME=permissions@example.com TEST_PERMISSIONS_USERNAME=permissions@example.com
MULTI_DB_ENABLED=true
NINJA_ENVIRONMENT=development

9
.gitattributes vendored
View file

@ -1,8 +1,5 @@
* text=auto * text=auto
*.css linguist-vendored *.css linguist-vendored
*.less linguist-vendored *.scss linguist-vendored
.gitattributes export-ignore *.js linguist-vendored
.gitignore export-ignore CHANGELOG.md export-ignore
.codeclimate.yml export-ignore
.travis.yml export-ignore
.styleci.yml export-ignore

View file

@ -7,35 +7,36 @@ assignees: ''
--- ---
**What version of Invoice Ninja are you running? ie v4.5.25 / v5.0.30** <!-- Before posting please check our "Troubleshooting" category in the docs:
https://invoiceninja.github.io/docs/self-host-troubleshooting/ -->
**What environment are you running?** ## Setup
Docker - Version: <!-- i.e. v4.5.25 / v5.0.30 -->
Shared Hosting - Environment: <!-- Docker/Shared Hosting/ZIP/Other -->
ZIP
Other
**Have you checked log files (storage/logs/) Please provide redacted output** ## Checklist
- Can you replicate the issue on our v5 demo site https://demo.invoiceninja.com or https://react.invoicing.co/demo?
- Have you searched existing issues?
- Have you reported this to Slack/forum before posting?
- Have you inspected the logs in storage/logs/laravel.log for any errors?
**Have you searched existing issues?** ## Describe the bug
<!-- A clear and concise description of the bug. -->
**Have you reported this to Slack/forum before posting?** ### Steps To Reproduce
<!-- Please list the steps to reproduce the issue. -->
**Describe the bug** ### Expected Behavior
A clear and concise description of what the bug is. <!-- A clear and concise description of what you expected to happen. -->
**Steps To Reproduce** ## Additional context
Please list the steps to reproduce the issue <!-- Add any other context about the problem here. -->
**Expected behavior** ### Screenshots
A clear and concise description of what you expected to happen. <!-- If applicable, add screenshots to help explain your problem. -->
**Screenshots** ### Logs
If applicable, add screenshots to help explain your problem. <!-- Please check the log files (storage/logs/) and provide redacted output -->
```
**Additional context** ```
Add any other context about the problem here.
Note: Before posting don't forget to check our "Troubleshooting" category in the [docs](https://invoiceninja.github.io/docs/self-host-troubleshooting/) (https://invoiceninja.github.io/docs/self-host-troubleshooting/).
**(v5) Can you replicate the issue on our demo site? https://demo.invoiceninja.com**

133
.github/workflows/phpunit.yml vendored Normal file
View file

@ -0,0 +1,133 @@
on:
push:
branches:
- v5-develop
- v5-stable
pull_request:
branches:
- v5-develop
name: phpunit
jobs:
run:
runs-on: ${{ matrix.operating-system }}
strategy:
matrix:
operating-system: ['ubuntu-20.04', 'ubuntu-22.04']
php-versions: ['8.1','8.2']
phpunit-versions: ['latest']
ci_node_total: [ 8 ]
ci_node_index: [ 0, 1, 2, 3, 4, 5, 6, 7]
laravel: [9.*]
dependency-version: [prefer-stable]
env:
DB_DATABASE1: ninja
DB_USERNAME1: root
DB_PASSWORD1: ninja
DB_HOST1: '127.0.0.1'
DB_DATABASE: ninja
DB_USERNAME: root
DB_PASSWORD: ninja
DB_HOST: '127.0.0.1'
REDIS_PORT: 6379
BROADCAST_DRIVER: log
CACHE_DRIVER: redis
QUEUE_CONNECTION: redis
SESSION_DRIVER: redis
NINJA_ENVIRONMENT: hosted
MULTI_DB_ENABLED: false
NINJA_LICENSE: ${{ secrets.ninja_license }}
TRAVIS: true
MAIL_MAILER: log
services:
mariadb:
image: mariadb:latest
ports:
- 32768:3306
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_USER: ninja
MYSQL_PASSWORD: ninja
MYSQL_DATABASE: ninja
MYSQL_ROOT_PASSWORD: ninja
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
redis:
image: redis
ports:
- 6379/tcp
options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- name: Add hosts to /etc/hosts
run: |
sudo echo "127.0.0.1 ninja.test" | sudo tee -a /etc/hosts
- name: Start MariaDB service
run: |
sudo systemctl start mysql.service
- name: Verify MariaDB connection
env:
DB_PORT: ${{ job.services.mariadb.ports[3306] }}
DB_PORT1: ${{ job.services.mariadb.ports[3306] }}
run: |
while ! mysqladmin ping -h"127.0.0.1" -P"$DB_PORT" --silent; do
sleep 1
done
- name: Setup PHP shivammathur/setup-php@v2
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
extensions: mysql, mysqlnd, sqlite3, bcmath, gmp, gd, curl, zip, openssl, mbstring, xml, redis
- uses: actions/checkout@v1
with:
ref: v5-develop
fetch-depth: 1
- name: Copy .env
run: |
cp .env.ci .env
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
- uses: actions/cache@v2
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-${{ matrix.php }}-composer-
- name: Install composer dependencies
run: |
composer config -g github-oauth.github.com ${{ secrets.GITHUB_TOKEN }}
composer install
- name: Prepare Laravel Application
env:
REDIS_PORT: ${{ job.services.redis.ports['6379'] }}
run: |
php artisan key:generate
php artisan optimize
php artisan cache:clear
php artisan config:cache
php artisan ninja:post-update
- name: Migrate Database
run: |
php artisan migrate:fresh --seed --force && php artisan db:seed --force
- name: Run Testsuite
run: |
cat .env
vendor/bin/snappdf download
tests/ci
env:
DB_PORT: ${{ job.services.mysql.ports[3306] }}
PHP_CS_FIXER_IGNORE_ENV: true
CI_NODE_TOTAL: ${{ matrix.ci_node_total }}
CI_NODE_INDEX: ${{ matrix.ci_node_index }}

77
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,77 @@
on:
release:
types: [released]
name: Upload Release Asset
jobs:
build:
name: Upload Release Asset
runs-on: ubuntu-latest
steps:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.1
extensions: mysql, mysqlnd, sqlite3, bcmath, gd, curl, zip, openssl, mbstring, xml
- name: Checkout code
uses: actions/checkout@v1
with:
ref: v5-develop
- name: Copy .env file
run: |
cp .env.example .env
- name: Install composer dependencies
run: |
composer config -g github-oauth.github.com ${{ secrets.GITHUB_TOKEN }}
composer install --no-dev
- name: Prepare Laravel Application
run: |
cp .env.example .env
php artisan key:generate
php artisan optimize
php artisan storage:link
sudo php artisan cache:clear
sudo find ./vendor/bin/ -type f -exec chmod +x {} \;
sudo find ./ -type d -exec chmod 755 {} \;
- name: Prepare React FrontEnd
run: |
git clone https://${{secrets.commit_secret}}@github.com/invoiceninja/ui.git
cd ui
git checkout main
npm i
npm run build
cp -r dist/react/* ../public/react
cd ..
rm -rf ui
php artisan ninja:react
- name: Prepare JS/CSS assets
run: |
npm i
npm run production
- name: Cleanup Builds
run: |
sudo rm -rf bootstrap/cache/*
sudo rm -rf node_modules
sudo rm -rf .git
sudo rm .env
- name: Build project
run: |
zip -r ./invoiceninja.zip .* -x "../*"
- name: Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: |
invoiceninja.zip

80
.gitignore vendored
View file

@ -1,46 +1,40 @@
/config/staging
/config/development
/config/production
/config/fortrabbit
/config/ubuntu
/config/packages/anahkiasen/rocketeer/
/public/logo
/public/build
/public/packages
/public/vendor
/resources/assets/bower
/storage/*.key
/storage/documents
/storage/import
/storage/migrations
/bootstrap/compiled.php
/bootstrap/environment.php
/vendor
/node_modules /node_modules
/.DS_Store /public/hot
Thumbs.db /public/storage
/.env /public/react
/.env.development.php /storage/*.key
/.env.php /storage/debugbar
/docs/_build /storage/*
/error_log /tests/bootstrap/
/auth.json /vendor
/public/error_log /app/Console/Commands/vendor/
/Modules
/ninja.sublime-project
/ninja.sublime-workspace
/.phpstorm.meta.php
/_ide_helper.php
/.idea /.idea
/.project /.vscode
tests/_output/ /.vagrant
tests/_bootstrap.php /tests/_output
tests/_support/_generated/ Homestead.json
Homestead.yaml
# composer stuff npm-debug.log
/c3.php yarn-error.log
local_version.txt
.env
.phpunit.result.cache
_ide_helper.php _ide_helper.php
storage/version.txt
storage/framework/.DS_Store /resources/assets/bower
/public/logo
.env.dusk.local
.env.cypress
/public/vendors/*
*.log
# Ignore local migrations
storage/migrations
nbproject
.php_cs.cache
public/test.pdf
public/storage/test.pdf
/Modules
_ide_helper_models.php
_ide_helper.php

View file

@ -1,11 +1,10 @@
<IfModule mod_rewrite.c> <IfModule mod_rewrite.c>
RewriteEngine On RewriteEngine On
RewriteRule "^.env" - [F,L] RewriteRule "^.env" - [F,L]
RewriteRule "^storage" - [F,L] # RewriteRule "^storage" - [F,L]
RewriteRule ^(.well-known)($|/) - [L] RewriteRule ^(.well-known)($|/) - [L]
# https://coderwall.com/p/erbaig/laravel-s-htaccess-to-remove-public-from-url RewriteRule ^(.*)$ public/$1 [L]
# RewriteRule ^(.*)$ public/$1 [L]
</IfModule> </IfModule>
# https://github.com/h5bp/server-configs-apache/blob/master/dist/.htaccess # https://github.com/h5bp/server-configs-apache/blob/master/dist/.htaccess

View file

@ -1,98 +0,0 @@
<?php
$finder = PhpCsFixer\Finder::create()
->notPath('bootstrap/cache')
->notPath('storage')
->notPath('vendor')
->in(__DIR__)
->name('*.php')
->notName('*.blade.php')
->ignoreDotFiles(true)
->ignoreVCS(true);
return PhpCsFixer\Config::create()
->setRules([
'@PSR2' => true,
'binary_operator_spaces' => true,
'blank_line_after_namespace' => true,
'blank_line_after_opening_tag' => true,
'blank_line_before_return' => true,
'braces' => true,
'cast_spaces' => true,
'class_definition' => true,
'elseif' => true,
'encoding' => true,
'full_opening_tag' => true,
'function_declaration' => true,
'function_typehint_space' => true,
'hash_to_slash_comment' => true,
'heredoc_to_nowdoc' => true,
'include' => true,
'linebreak_after_opening_tag' => true,
'lowercase_cast' => true,
'lowercase_constants' => true,
'lowercase_keywords' => true,
'method_argument_space' => true,
'method_separation' => true,
'native_function_casing' => true,
'new_with_braces' => true,
'no_alias_functions' => true,
'no_blank_lines_after_class_opening' => true,
'no_blank_lines_after_phpdoc' => true,
'no_closing_tag' => true,
'no_empty_phpdoc' => true,
'no_extra_consecutive_blank_lines' => true,
'no_leading_import_slash' => true,
'no_leading_namespace_whitespace' => true,
'no_multiline_whitespace_around_double_arrow' => true,
'no_multiline_whitespace_before_semicolons' => true,
'no_short_bool_cast' => true,
'no_singleline_whitespace_before_semicolons' => true,
'no_spaces_after_function_name' => true,
'no_spaces_inside_parenthesis' => true,
'no_trailing_comma_in_list_call' => true,
'no_trailing_comma_in_singleline_array' => true,
'no_trailing_whitespace' => true,
'no_trailing_whitespace_in_comment' => true,
'no_unneeded_control_parentheses' => true,
'no_unreachable_default_argument_value' => true,
'no_unused_imports' => true,
'no_useless_return' => true,
'no_whitespace_before_comma_in_array' => true,
'not_operator_with_successor_space' => true,
'object_operator_without_whitespace' => true,
'ordered_imports' => true,
'phpdoc_add_missing_param_annotation' => true,
'phpdoc_align' => true,
'phpdoc_indent' => true,
'phpdoc_inline_tag' => true,
'phpdoc_no_access' => true,
'phpdoc_no_package' => true,
'phpdoc_order' => true,
'phpdoc_scalar' => true,
'phpdoc_separation' => true,
'phpdoc_summary' => true,
'phpdoc_to_comment' => true,
'phpdoc_trim' => true,
'phpdoc_types' => true,
'phpdoc_var_without_name' => true,
'self_accessor' => true,
'short_scalar_cast' => true,
'single_blank_line_at_eof' => true,
'single_blank_line_before_namespace' => true,
'single_import_per_statement' => true,
'single_line_after_imports' => true,
'single_quote' => true,
'space_after_semicolon' => true,
'standardize_not_equals' => true,
'switch_case_semicolon_to_colon' => true,
'switch_case_space' => true,
'ternary_operator_spaces' => true,
'trailing_comma_in_multiline_array' => true,
'trim_array_spaces' => true,
'unary_operator_spaces' => true,
'visibility_required' => true,
'whitespace_after_comma_in_array' => true,
'array_syntax' => array('syntax' => 'short'),
])
->setFinder($finder);

6
.prettierrc Normal file
View file

@ -0,0 +1,6 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"arrowParens": "always"
}

View file

@ -1,18 +0,0 @@
preset: laravel
risky: false
enabled:
- no_useless_else
- phpdoc_align
- phpdoc_no_empty_return
- phpdoc_order
- phpdoc_separation
finder:
exclude:
- "resources"
- "storage"
- "tests"
not-path:
- "bootstrap/cache"

View file

@ -1,138 +0,0 @@
language: php
services:
- mysql
sudo: true
# Prevent tests from taking more than 50 minutes
group: deprecated-2017Q4
php:
- 7.2
- 7.3
addons:
hosts:
- www.ninja.test
cache:
directories:
- vendor
- $HOME/.composer/cache
env:
global:
- COMPOSER_DISCARD_CHANGES=true
- COMPOSER_NO_INTERACTION=1
- COMPOSER_DISABLE_XDEBUG_WARN=1
before_install:
# set GitHub token and update composer
- if [ -n "$GH_TOKEN" ]; then composer config github-oauth.github.com ${GH_TOKEN}; fi;
- composer self-update 1.10.19 && composer -V
# - export USE_ZEND_ALLOC=0
- rvm use 1.9.3 --install --fuzzy
install:
# install Composer dependencies
# - rm composer.lock
# these providers require referencing git commit's which cause Travis to fail
# - sed -i '/mollie/d' composer.json
# - sed -i '/2checkout/d' composer.json
- travis_retry composer install --prefer-dist;
before_script:
# prevent MySQL went away error
- mysql -u root -e 'SET @@GLOBAL.wait_timeout=28800;'
# copy configuration files
- cp .env.travis .env
- cp tests/_bootstrap.php.default tests/_bootstrap.php
- php artisan key:generate --no-interaction
- sed -i '$a NINJA_DEV=true' .env
# create the database and user
- mysql -u root -e "create database IF NOT EXISTS ninja0;"
- mysql -u root -e "create database IF NOT EXISTS ninja;"
- mysql -u root -e "create database IF NOT EXISTS ninja2;"
- mysql -u root -e "GRANT ALL PRIVILEGES ON ninja0.* To 'ninja'@'localhost' IDENTIFIED BY 'ninja'; FLUSH PRIVILEGES;"
- mysql -u root -e "GRANT ALL PRIVILEGES ON ninja.* To 'ninja'@'localhost' IDENTIFIED BY 'ninja'; FLUSH PRIVILEGES;"
- mysql -u root -e "GRANT ALL PRIVILEGES ON ninja2.* To 'ninja'@'localhost' IDENTIFIED BY 'ninja'; FLUSH PRIVILEGES;"
# migrate and seed the database
- php artisan migrate --database=db-ninja-0 --seed --no-interaction
- php artisan migrate --database=db-ninja-1 --seed --no-interaction
- php artisan migrate --database=db-ninja-2 --seed --no-interaction
# Start webserver on ninja.test:8000
- php artisan serve --host=www.ninja.test --port=8000 & # '&' allows to run in background
# Start PhantomJS
- phantomjs --webdriver=4444 & # '&' allows to run in background
# Give it some time to start
- sleep 5
# Make sure the app is up-to-date
- curl -L http://www.ninja.test:8000/update
- php artisan ninja:create-test-data 4 true
- php artisan db:seed --no-interaction --class=UserTableSeeder # development seed
- sed -i 's/DB_TYPE=db-ninja-1/DB_TYPE=db-ninja-2/g' .env
- sed -i 's/user@example.com/user2@example.com/g' .env
- sed -i 's/permissions@example.com/permissions2@example.com/g' .env
- php artisan db:seed --no-interaction --class=UserTableSeeder # development seed
script:
- php ./vendor/codeception/codeception/codecept run --debug acceptance APICest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance TaxRatesCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance CheckBalanceCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance ClientCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance ExpenseCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance CreditCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance InvoiceCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance QuoteCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance InvoiceDesignCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance OnlinePaymentCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance PaymentCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance TaskCest.php
- php ./vendor/codeception/codeception/codecept run --debug acceptance AllPagesCept.php
- php ./vendor/codeception/codeception/codecept run --debug functional PermissionsCest.php
#- sed -i 's/NINJA_DEV=true/NINJA_PROD=true/g' .env
#- php ./vendor/codeception/codeception/codecept run --debug run acceptance GoProCest.php
#- php ./vendor/codeception/codeception/codecept run --debug acceptance GatewayFeesCest.php
#- php ./vendor/codeception/codeception/codecept run --debug acceptance DiscountCest.php
after_script:
- php artisan ninja:check-data --no-interaction --database='db-ninja-1'
- php artisan ninja:check-data --no-interaction --database='db-ninja-2'
- php artisan ninja:init-lookup --validate=true --database='db-ninja-1'
- php artisan ninja:init-lookup --validate=true --database='db-ninja-2'
- cat storage/logs/laravel-error.log
- cat storage/logs/laravel-info.log
- cat .env
- mysql -u root -e 'select * from lookup_companies;' ninja0
- mysql -u root -e 'select * from lookup_accounts;' ninja0
- mysql -u root -e 'select * from lookup_contacts;' ninja0
- mysql -u root -e 'select * from lookup_invitations;' ninja0
- mysql -u root -e 'select * from accounts;' ninja
- mysql -u root -e 'select * from users;' ninja
- mysql -u root -e 'select * from lookup_users;' ninja
- mysql -u root -e 'select * from account_gateways;' ninja
- mysql -u root -e 'select * from clients;' ninja
- mysql -u root -e 'select * from contacts;' ninja
- mysql -u root -e 'select id, public_id, account_id, invoice_number, amount, balance from invoices;' ninja
- mysql -u root -e 'select * from invoice_items;' ninja
- mysql -u root -e 'select * from invitations;' ninja
- mysql -u root -e 'select id, public_id, account_id, invoice_id, amount, transaction_reference from payments;' ninja
- mysql -u root -e 'select * from credits;' ninja
- mysql -u root -e 'select * from expenses;' ninja
- mysql -u root -e 'select * from accounts;' ninja
- mysql -u root -e 'select * from fonts;' ninja
- mysql -u root -e 'select * from banks;' ninja
- mysql -u root -e 'select * from account_gateway_tokens;' ninja
- mysql -u root -e 'select * from payment_methods;' ninja
- FILES=$(find tests/_output -type f -name '*.png' | sort -nr)
- for i in $FILES; do echo $i; base64 "$i"; break; done
notifications:
email:
on_success: never
on_failure: change
slack:
invoiceninja: SLraaKBDvjeRuRtY9o3Yvp1b

View file

@ -1,47 +1,119 @@
# Contributing to Invoice Ninja # Contributing to CONTRIBUTING.md
Thanks for your contributions! First off, thanks for taking the time to contribute!
## Submit bug reports or feature requests All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions.
Please discuss the changes with us ahead of time to ensure they will be merged. > And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about:
> - Star the project
### Submit pull requests > - Tweet about it
* [Fork](https://github.com/invoiceninja/invoiceninja#fork-destination-box) the [Invoice Ninja repository](https://github.com/invoiceninja/invoiceninja) > - Refer this project in your project's readme
* Create a new branch with the name `#issue_number-Short-description` > - Mention the project at local meetups and tell your friends/colleagues
* _Example:_ `#100-Add-GoogleAnalytics`
* Make your changes and commit
* Check if your branch is still in sync with the repositorys **`develop`** branch
* _Read:_ [Syncing a fork](https://help.github.com/articles/syncing-a-fork/)
* _Also read:_ [How to rebase a pull request](https://github.com/edx/edx-platform/wiki/How-to-Rebase-a-Pull-Request)
* Push your branch and create a PR against the Invoice Ninja **`develop`** branch
* Update the [Changelog](CHANGELOG.md)
### Some rules
To make the contribution process nice and easy for anyone, please follow some rules:
* Each contribution(bug or feature) should have an [issue on Github](https://github.com/invoiceninja/invoiceninja/issues)
to give a more detailed explanation.
* Only one feature/bugfix per issue. If you want to submit more, create multiple issues.
* Only one feature/bugfix per PR(pull request). Split more changes into multiple PRs.
#### Coding Style
Try to follow the [PSR-2 guidlines](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)
_Example styling:_
```php
/**
* Gets a preview of the email
*
* @param TemplateService $templateService
*
* @return \Illuminate\Http\Response
*/
public function previewEmail(TemplateService $templateService)
{
//
}
```
## Translations ## Table of Contents
For helping us with translating Invoice Ninja, please use [Transifex](https://www.transifex.com/invoice-ninja/invoice-ninja/).
- [Code of Conduct](#code-of-conduct)
- [I Have a Question](#i-have-a-question)
- [I Want To Contribute](#i-want-to-contribute)
- [Reporting Bugs](#reporting-bugs)
- [Suggesting Enhancements](#suggesting-enhancements)
- [Your First Code Contribution](#your-first-code-contribution)
- [Improving The Documentation](#improving-the-documentation)
- [Styleguides](#styleguides)
- [Commit Messages](#commit-messages)
- [Join The Project Team](#join-the-project-team)
## Code of Conduct
This project and everyone participating in it is governed by the
[CONTRIBUTING.md Code of Conduct](blob/v5-stable/CODE_OF_CONDUCT.md).
By participating, you are expected to uphold this code. Please report unacceptable behavior
to <>.
## I Have a Question
> If you want to ask a question, we assume that you have read the available [Documentation](https://invoiceninja.github.io), Searched the [Forum](https://forum.invoiceninja.com), or tried chatting with us on [Slack](https://invoiceninja.slack.com) [generate a slack invite here](http://slack.invoiceninja.com)
Please reserve issues for bugs, general questions posted in the issues section will be closed and redirected to other support venues.
## I Want To Contribute
> ### Legal Notice
> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license. Please note that you'll need to sign the CLA for any PRs to be accepted
### Reporting Bugs
#### Before Submitting a Bug Report
A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible.
- Make sure that you are using the latest version.
- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](https://invoiceninja.github.io). If you are looking for support, you might want to check [this section](#i-have-a-question)).
- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](issues?q=label%3Abug).
- Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue.
- Collect information about the bug:
- Stack trace (Traceback)
- OS, Platform and Version (Windows, Linux, macOS, x86, ARM)
- Version of the interpreter, compiler, SDK, runtime environment, package manager, depending on what seems relevant.
- Possibly your input and the output
- Can you reliably reproduce the issue? And can you also reproduce it with older versions?
#### How Do I Submit a Good Bug Report?
> You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to <>.
We use GitHub issues to track bugs and errors. If you run into an issue with the project:
- Open an [Issue](/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.)
- Explain the behavior you would expect and the actual behavior.
- Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case.
- Provide the information you collected in the previous section.
Once it's filed:
- The project team will label the issue accordingly.
- A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced.
- If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be [implemented by someone](#your-first-code-contribution).
### Suggesting Enhancements
This section guides you through submitting an enhancement suggestion for CONTRIBUTING.md, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions.
#### Before Submitting an Enhancement
- Make sure that you are using the latest version.
- Read the [documentation]() carefully and find out if the functionality is already covered, maybe by an individual configuration.
- Perform a [search](/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library.
#### How Do I Submit a Good Enhancement Suggestion?
Enhancement suggestions are tracked as [GitHub issues](/issues).
- Use a **clear and descriptive title** for the issue to identify the suggestion.
- Provide a **step-by-step description of the suggested enhancement** in as many details as possible.
- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you.
- You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux.
- **Explain why this enhancement would be useful** to most CONTRIBUTING.md users. You may also want to point out the other projects that solved it better and which could serve as inspiration.
### Your First Code Contribution
All PRs should be created against the v5-develop branch.
## Attribution
This guide is based on the **contributing.md**. [Make your own](https://contributing.md/)!

43
Gruntfile.js vendored
View file

@ -1,43 +0,0 @@
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
dump_dir: (function() {
var out = {};
grunt.file.expand({ filter: 'isDirectory'}, 'public/fonts/invoice-fonts/*').forEach(function(path) {
var fontName = /[^/]*$/.exec(path)[0],
files = {},
license='';
// Add license text
grunt.file.expand({ filter: 'isFile'}, path+'/*.txt').forEach(function(path) {
var licenseText = grunt.file.read(path);
// Fix anything that could escape from the comment
licenseText = licenseText.replace(/\*\//g,'*\\/');
license += "/*\n"+licenseText+"\n*/";
});
// Create files list
files['public/js/vfs_fonts/'+fontName+'.js'] = [path+'/*.ttf'];
out[fontName] = {
options: {
pre: license+'window.ninjaFontVfs=window.ninjaFontVfs||{};window.ninjaFontVfs.'+fontName+'=',
rootPath: path+'/'
},
files: files
};
});
// Return the computed object
return out;
}())
});
grunt.loadNpmTasks('grunt-dump-dir');
grunt.registerTask('default', ['dump_dir']);
};

88
LICENSE
View file

@ -1,47 +1,47 @@
Copyright (c) 2018 by Hillel Coren Elastic License 2.0 (ELv2)
Invoice Ninja * https://www.invoiceninja.com Elastic License
"CREATE. SEND. GET PAID"
All Rights Reserved Acceptance
ATTRIBUTION ASSURANCE LICENSE (adapted from the original BSD license) By using the software, you agree to all of the terms and conditions below.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the conditions below are met.
These conditions require a modest attribution to InvoiceNinja.com (the
"Author"), who hopes that its promotional value may help justify the
thousands of dollars in otherwise billable time invested in writing
this and other freely available, open-source software.
1. Redistributions of source code, in whole or part and with or without Copyright License
modification (the "Code"), must prominently display this GPG-signed The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject to the limitations and conditions below
text in verifiable form.
2. Redistributions of the Code in binary form must be accompanied by
this GPG-signed text in any documentation and, each time the resulting
executable program or a program dependent thereon is launched, a
prominent display (e.g., splash screen or banner text) of the Author's
attribution information, which includes:
(a) Name ("Hillel Coren"),
(b) Professional identification ("Invoice Ninja"), and
(c) URL ("https://www.invoiceninja.com").
3. Neither the name nor any trademark of the Author may be used to
endorse or promote products derived from this software without specific
prior written permission.
4. Users are entirely responsible, to the exclusion of the Author and
any other persons, for compliance with (1) regulations set by owners or
administrators of employed equipment, (2) licensing terms of any other
software, and (3) local regulations regarding use, including those
regarding import, export, and use of encryption software.
THIS FREE SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND Limitations
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT You may not provide the software to third parties as a hosted or managed service, where the service provides users with access to any substantial set of the features or functionality of the software.
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO You may not move, change, disable, or circumvent the license key functionality in the software, and you may not remove or obscure any functionality in the software that is protected by the license key.
EVENT SHALL THE AUTHOR OR ANY CONTRIBUTOR BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR You may not alter, remove, or obscure any licensing, copyright, or other notices of the licensor in the software. Any use of the licensors trademarks is subject to applicable law.
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
EFFECTS OF UNAUTHORIZED OR MALICIOUS NETWORK ACCESS; Patents
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, The licensor grants you a license, under any patent claims the licensor can license, or becomes able to license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case subject to the limitations and conditions in this license. This license does not cover any patent claims that you cause to be infringed by modifications or additions to the software. If you or your company make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT Notices
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms.
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. If you modify the software, you must include in any modified copies of the software prominent notices stating that you have modified the software.
No Other Rights
These terms do not imply any licenses other than those expressly granted in these terms.
Termination
If you use the software in violation of these terms, such use is not licensed, and your licenses will automatically terminate. If the licensor provides you with a notice of your violation, and you cease all violation of this license no later than 30 days after you receive that notice, your licenses will be reinstated retroactively. However, if you violate these terms after such reinstatement, any additional violation of these terms will cause your licenses to terminate automatically and permanently.
No Liability
As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.
Definitions
The licensor is the entity offering these terms, and the software is the software the licensor makes available under these terms, including any portion of it.
you refers to the individual or entity agreeing to these terms.
your company is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. control means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
your licenses are all the licenses granted to you for the software under these terms.
use means anything you do with the software requiring one of your licenses.
trademark means trademarks, service marks, and similar rights.
For more information regarding the interpretation of this license please see here: https://invoiceninja.github.io/docs/legal/license/

127
README.md
View file

@ -2,95 +2,100 @@
<img src="https://raw.githubusercontent.com/hillelcoren/invoice-ninja/master/public/images/round_logo.png" alt="Sublime's custom image"/> <img src="https://raw.githubusercontent.com/hillelcoren/invoice-ninja/master/public/images/round_logo.png" alt="Sublime's custom image"/>
</p> </p>
# Invoice Ninja ![v5-develop phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-develop)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/d16c78aad8574466bf83232b513ef4fb)](https://www.codacy.com/gh/turbo124/invoiceninja/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=turbo124/invoiceninja&amp;utm_campaign=Badge_Grade)
<a href="https://cla-assistant.io/invoiceninja/invoiceninja"><img src="https://cla-assistant.io/readme/badge/invoiceninja/invoiceninja" alt="CLA assistant" /></a>
[![Build Status](https://travis-ci.org/invoiceninja/invoiceninja.svg?branch=master)](https://travis-ci.org/invoiceninja/invoiceninja) # Invoice Ninja 5
[![Docs](https://readthedocs.org/projects/invoice-ninja/badge/?version=latest)](https://invoice-ninja.readthedocs.io/en/latest/?badge=latest)
## [Hosted](https://www.invoiceninja.com) | [Self-Hosted](https://www.invoiceninja.org) ## [Hosted](https://www.invoiceninja.com) | [Self-Hosted](https://www.invoiceninja.org)
### We're on Slack, join us at [slack.invoiceninja.com](http://slack.invoiceninja.com) or if you like [StackOverflow](https://stackoverflow.com/tags/invoice-ninja/) Join us on [Slack](http://slack.invoiceninja.com), [Discord](https://discord.gg/ZwEdtfCwXA), [Support Forum](https://forum.invoiceninja.com)
Just make sure to add the `invoice-ninja` tag to your question. ## Introduction
#### Note: v5 is now tagged Stable! To upgrade from v4 you need to [install v5](https://invoiceninja.github.io/docs/self-host-installation/) as a separate app and then use the migration tool in the latest version of v4 on Settings > Account Management. Version 5 of Invoice Ninja is here!
We took the best parts of version 4 and add the most requested features
to produce a invoicing application like no other.
All Pro and Enterprise features from the hosted app are included in the open-source code. We offer a $30 per year white-label license to remove the Invoice Ninja branding from client facing parts of the app. All Pro and Enterprise features from the hosted app are included in the open code.
We offer a $30 per year white-label license to remove the Invoice Ninja branding from client facing parts of the app.
The self-host zip includes all third party libraries whereas downloading the code from GitHub requires using Composer to install the dependencies. * [Videos](https://www.youtube.com/@appinvoiceninja)
* [API Documentation](https://api-docs.invoicing.co/)
* [APP Documentation](https://invoiceninja.github.io/)
* [Support Forum](https://forum.invoiceninja.com)
* [Features](https://www.invoiceninja.com/invoicing-features/) ## Setup
* [Videos](https://www.youtube.com/channel/UCXAHcBvhW05PDtWYIq7WDFA/videos)
* [User Guide](https://docs.invoiceninja.com/)
* [Support Forum](https://www.invoiceninja.com/forums/forum/support/)
* [StackOverflow](https://stackoverflow.com/tags/invoice-ninja/)
## Referral Program ### Mobile Apps
* Earn 50% of Pro & Enterprise Plans up to 4 years - [Learn more](https://www.invoiceninja.com/referral-program/) * [iPhone](https://apps.apple.com/app/id1503970375?platform=iphone)
* [Android](https://play.google.com/store/apps/details?id=com.invoiceninja.app)
* [F-Droid](https://f-droid.org/en/packages/com.invoiceninja.app)
## Mobile App ### Desktop Apps
* [iPhone](https://itunes.apple.com/us/app/invoice-ninja/id1435514417?ls=1&mt=8) * [macOS](https://apps.apple.com/app/id1503970375?platform=mac)
* [Android](https://play.google.com/store/apps/details?id=com.invoiceninja.flutter) * [Windows](https://microsoft.com/en-us/p/invoice-ninja/9n3f2bbcfdr6)
* [Source Code](https://github.com/invoiceninja/flutter-mobile) * [Linux](https://snapcraft.io/invoiceninja)
## Installation Options ### Installation Options
* [Ansible](https://github.com/invoiceninja/ansible-installer)
* [Self-Host Zip](https://docs.invoiceninja.com/install.html)
* [Docker File](https://hub.docker.com/r/invoiceninja/invoiceninja/) * [Docker File](https://hub.docker.com/r/invoiceninja/invoiceninja/)
* [Cloudron](https://cloudron.io/store/com.invoiceninja.cloudronapp.html) * [Cloudron](https://cloudron.io/store/com.invoiceninja.cloudronapp.html)
* [Softaculous](https://www.softaculous.com/apps/ecommerce/Invoice_Ninja) * [Softaculous](https://www.softaculous.com/apps/ecommerce/Invoice_Ninja)
* [Lando](https://github.com/invoiceninja/invoiceninja/issues/2880)
* [Yunohost](https://github.com/YunoHost-Apps/invoiceninja_ynh) ### Recommended Providers
## Recommended Providers
* [Stripe](https://stripe.com/) * [Stripe](https://stripe.com/)
* [Postmark](https://postmarkapp.com/) * [Postmark](https://postmarkapp.com/)
## Development ## Quick Hosting Setup
* [API Documentation](https://invoice-ninja.readthedocs.io/en/latest/api.html)
* [PHP SDK](https://github.com/invoiceninja/sdk-php)
* [Zend Framework](https://github.com/alexz707/InvoiceNinjaModule)
* [Custom Module](https://invoice-ninja.readthedocs.io/en/latest/custom_modules.html) | [Watch Video](https://www.youtube.com/watch?v=8jJ-PYuq85k)
## Third Party Modules ```sh
* [Event Scheduler](https://github.com/cytech/Scheduler-InvoiceNinja) git clone https://github.com/invoiceninja/invoiceninja.git
* [Manufacturer Module](https://github.com/dicarlosystems/manufacturer-invoiceninja) git checkout v5-stable
* [Point of Sale](https://github.com/dicarlosystems/pointofsale-invoiceninja) cp .env.example .env
* [Invoice Design Import/Export](https://github.com/feyst/invoicedesignexport) composer i -o --no-dev
php artisan key:generate
```
> Feel free to email us for help if you're working on a module, we're happy to provide developer support. Please Note:
Your APP_KEY in the .env file is used to encrypt data, if you lose this you will not be able to run the application.
## Third Party Tools Run if you want to load sample data, remember to configure .env
* [InvoicePlane Import](https://github.com/turbo124/Plane2Ninja) ```sh
* [Toggl Sync](https://github.com/Matth--/toggl-invoiceninja-sync) php artisan migrate:fresh --seed && php artisan db:seed && php artisan ninja:create-test-data
* [Shopping Cart](https://github.com/Scifabric/invoiceninjashoppingcart) ```
## Third Party Developers To run the web server
* [Bold Compass](https://boldcompass.com/customize-invoice-ninja/) ```sh
php artisan serve
```
## Contributing Navigate to (replace localhost with the appropriate domain)
All contributors are welcome! ```
For information on how contribute to Invoice Ninja, please see our [contributing guide](CONTRIBUTING.md). http://localhost:8000/setup - To setup your configuration if you did not load sample data.
http://localhost:8000/ - For Administrator Logon
user: small@example.com
pass: password
http://localhost:8000/client/login - For Client Portal
user: user@example.com
pass: password
```
## Credits ## Credits
* [Hillel Coren](https://hillelcoren.com/) * [Hillel Coren](https://hillelcoren.com/)
* [David Bomba](https://github.com/turbo124) * [David Bomba](https://github.com/turbo124)
* [All contributors](https://github.com/invoiceninja/invoiceninja/graphs/contributors) * [Benjamin Beganović](https://github.com/beganovich)
* [All Contributors](https://github.com/invoiceninja/invoiceninja/graphs/contributors)
**Special thanks to:** ## Security
* [Troels Liebe Bentsen](https://github.com/tlbdk)
* [Jeramy Simpson](https://github.com/JeramyMywork) - [MyWork](https://www.mywork.com.au) If you find a security issue with this application, please send an email to contact@invoiceninja.com.
* [Sigitas Limontas](https://lt.linkedin.com/in/sigitaslimontas) Please follow responsible disclosure procedures if you detect an issue.
* [Joshua Dwire](https://github.com/joshuadwire) - [Bold Compass](https://boldcompass.com/) For further information on responsible disclosure please read [here](https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html).
* [Holger Lösken](https://github.com/codedge) - [codedge](http://codedge.de)
* [Samuel Laulhau](https://github.com/lalop) - [Lalop](http://lalop.co/)
* [Alexander Zamponi](https://github.com/alexz707)
* [Matthieu Calie](https://github.com/Matth--)
* [Christopher Di Carlo](https://github.com/dicarlosystems) - [Di Carlo Systems Inc.](https://www.dicarlosystems.ca)
* [Kristian Feldsam](https://github.com/feldsam) - [FeldHost™](https://www.feldhost.net)
* [Suhas Sunil Gaikwad](https://github.com/Suhas-Gaikwad)
* [Mike Skaggs](https://github.com/titan-fail)
## License ## License
Invoice Ninja is released under the Attribution Assurance License. Invoice Ninja is released under the Elastic License.
See [LICENSE](LICENSE) for details. See [LICENSE](LICENSE) for details.

1
VERSION.txt Normal file
View file

@ -0,0 +1 @@
5.5.88

17
_ide_helper_custom.php Normal file
View file

@ -0,0 +1,17 @@
<?php
namespace Illuminate\Contracts\Mail
{
class Mailer
{
public function postmark_config(string $key)
{
return true;
}
public function mailgun_config(string $key)
{
return true;
}
}
}

View file

@ -1,7 +0,0 @@
<?php
namespace App\Commands;
abstract class Command
{
}

View file

@ -0,0 +1,204 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Console\Commands;
use App\Libraries\MultiDB;
use App\Models\Backup;
use App\Models\Client;
use App\Models\Company;
use App\Models\Document;
use App\Models\GroupSetting;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class BackupUpdate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:backup-files {--disk=}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Shift files between object storage locations';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
//always return state to first DB
$current_db = config('database.default');
if (! config('ninja.db.multi_db_enabled')) {
$this->handleOnDb();
} else {
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$this->handleOnDb();
}
MultiDB::setDB($current_db);
}
}
private function handleOnDb()
{
set_time_limit(0);
//logos
Company::cursor()
->each(function ($company) {
$company_logo_path = $company->settings->company_logo;
if ($company_logo_path == 'https://invoicing.co/images/new_logo.png' || $company_logo_path == '') {
return;
}
$logo = @file_get_contents($company_logo_path);
$extension = @pathinfo($company->settings->company_logo, PATHINFO_EXTENSION);
if ($logo && $extension) {
$path = "{$company->company_key}/{$company->company_key}.{$extension}";
Storage::disk($this->option('disk'))->put($path, $logo);
$url = Storage::disk($this->option('disk'))->url($path);
nlog("Company - Moving {$company_logo_path} logo to {$this->option('disk')} final URL = {$url}}");
$settings = $company->settings;
$settings->company_logo = $url;
$company->settings = $settings;
;
$company->save();
}
});
Client::withTrashed()
->whereNotNull('settings->company_logo')
->cursor()
->each(function ($client) {
$company_logo_path = $client->settings->company_logo;
$logo = @file_get_contents($company_logo_path);
$extension = @pathinfo($company_logo_path, PATHINFO_EXTENSION);
if ($logo && $extension) {
$path = "{$client->company->company_key}/{$client->client_hash}.{$extension}";
Storage::disk($this->option('disk'))->put($path, $logo);
$url = Storage::disk($this->option('disk'))->url($path);
nlog("Client - Moving {$company_logo_path} logo to {$this->option('disk')} final URL = {$url}}");
$settings = $client->settings;
$settings->company_logo = $url;
$client->settings = $settings;
;
$client->saveQuietly();
}
});
GroupSetting::withTrashed()
->whereNotNull('settings->company_logo')
->orWhere('settings->company_logo', '!=', '')
->cursor()
->each(function ($group) {
$company_logo_path = $group->settings->company_logo;
if (!$company_logo_path) {
return;
}
$logo = @file_get_contents($company_logo_path);
$extension = @pathinfo($company_logo_path, PATHINFO_EXTENSION);
if ($logo && $extension) {
$path = "{$group->company->company_key}/{$group->hashed_id}.{$extension}";
Storage::disk($this->option('disk'))->put($path, $logo);
$url = Storage::disk($this->option('disk'))->url($path);
nlog("Group - Moving {$company_logo_path} logo to {$this->option('disk')} final URL = {$url}}");
$settings = $group->settings;
$settings->company_logo = $url;
$group->settings = $settings;
;
$group->saveQuietly();
}
});
//documents
Document::cursor()
->each(function (Document $document) {
$doc_bin = false;
try {
$doc_bin = $document->getFile();
} catch(\Exception $e) {
nlog($e->getMessage());
}
if ($doc_bin) {
Storage::disk($this->option('disk'))->put($document->url, $doc_bin);
$document->disk = $this->option('disk');
$document->saveQuietly();
nlog("Documents - Moving {$document->url} to {$this->option('disk')}");
}
});
//backups
Backup::whereNotNull('filename')
->where('filename', '!=', '')
->cursor()
->each(function ($backup) {
$backup_bin = Storage::disk('s3')->get($backup->filename);
if ($backup_bin) {
Storage::disk($this->option('disk'))->put($backup->filename, $backup_bin);
nlog("Backups - Moving {$backup->filename} to {$this->option('disk')}");
}
});
}
}

View file

@ -1,126 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\DbServer;
use App\Models\User;
use App\Models\Company;
use App\Libraries\CurlUtils;
class CalculatePayouts extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:calculate-payouts {--type=} {--url=} {--password=}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Calculate payouts';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$type = strtolower($this->option('type'));
switch ($type) {
case 'referral':
$this->referralPayouts();
break;
case 'reseller':
$this->resellerPayouts();
break;
}
}
private function referralPayouts()
{
$servers = DbServer::orderBy('id')->get(['name']);
$userMap = [];
foreach ($servers as $server) {
config(['database.default' => $server->name]);
$users = User::where('referral_code', '!=', '')
->get(['email', 'referral_code']);
foreach ($users as $user) {
$userMap[$user->referral_code] = $user->email;
}
}
foreach ($servers as $server) {
config(['database.default' => $server->name]);
$companies = Company::where('referral_code', '!=', '')
->with('payment.client.payments')
->whereNotNull('payment_id')
->get();
$this->info('User,Client,Date,Amount,Reference');
foreach ($companies as $company) {
if (!isset($userMap[$company->referral_code])) {
continue;
}
$user = $userMap[$company->referral_code];
$payment = $company->payment;
if ($payment) {
$client = $payment->client;
foreach ($client->payments as $payment) {
$amount = $payment->getCompletedAmount();
$this->info('"' . $user . '",' .
'"' . $client->getDisplayName() . '",' .
$payment->payment_date . ',' .
$amount . ',' .
$payment->transaction_reference
);
}
}
}
}
}
private function resellerPayouts()
{
$response = CurlUtils::post($this->option('url') . '/reseller_stats', [
'password' => $this->option('password')
]);
$this->info('Response:');
$this->info($response);
}
protected function getOptions()
{
return [
['type', null, InputOption::VALUE_OPTIONAL, 'Type', null],
['url', null, InputOption::VALUE_OPTIONAL, 'Url', null],
['password', null, InputOption::VALUE_OPTIONAL, 'Password', null],
];
}
}

View file

@ -1,132 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Account;
use App\Models\Invoice;
use App\Ninja\Mailers\ContactMailer as Mailer;
use App\Ninja\Repositories\AccountRepository;
use App\Services\PaymentService;
use Illuminate\Console\Command;
use Carbon;
use Symfony\Component\Console\Input\InputOption;
/**
* Class ChargeRenewalInvoices.
*/
class ChargeRenewalInvoices extends Command
{
/**
* @var string
*/
protected $name = 'ninja:charge-renewals';
/**
* @var string
*/
protected $description = 'Charge renewal invoices';
/**
* @var Mailer
*/
protected $mailer;
/**
* @var AccountRepository
*/
protected $accountRepo;
/**
* @var PaymentService
*/
protected $paymentService;
/**
* ChargeRenewalInvoices constructor.
*
* @param Mailer $mailer
* @param AccountRepository $repo
* @param PaymentService $paymentService
*/
public function __construct(Mailer $mailer, AccountRepository $repo, PaymentService $paymentService)
{
parent::__construct();
$this->mailer = $mailer;
$this->accountRepo = $repo;
$this->paymentService = $paymentService;
}
public function handle()
{
$this->info(date('r').' ChargeRenewalInvoices...');
if ($database = $this->option('database')) {
config(['database.default' => $database]);
}
$ninjaAccount = $this->accountRepo->getNinjaAccount();
$invoices = Invoice::whereAccountId($ninjaAccount->id)
->whereDueDate(date('Y-m-d'))
->where('balance', '>', 0)
->with('client')
->orderBy('id')
->get();
$this->info($invoices->count() . ' invoices found');
foreach ($invoices as $invoice) {
// check if account has switched to free since the invoice was created
$account = Account::find($invoice->client->public_id);
if (! $account) {
continue;
}
$company = $account->company;
if (! $company->plan || $company->plan == PLAN_FREE) {
continue;
}
if (Carbon::parse($company->plan_expires)->isFuture()) {
$this->info('Skipping invoice ' . $invoice->invoice_number . ' [plan not expired]');
continue;
}
$this->info("Charging invoice {$invoice->invoice_number}");
if (! $this->paymentService->autoBillInvoice($invoice)) {
$this->info('Failed to auto-bill, emailing invoice');
$this->mailer->sendInvoice($invoice);
}
}
$this->info('Done');
if ($errorEmail = env('ERROR_EMAIL')) {
\Mail::raw('EOM', function ($message) use ($errorEmail) {
$message->to($errorEmail)
->from(CONTACT_EMAIL)
->subject('ChargeRenewalInvoices: Finished successfully');
});
}
}
/**
* @return array
*/
protected function getArguments()
{
return [];
}
/**
* @return array
*/
protected function getOptions()
{
return [
['database', null, InputOption::VALUE_OPTIONAL, 'Database', null],
];
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,143 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Console\Commands;
use App\Models\Account;
use App\Models\Activity;
use App\Models\Backup;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\ClientGatewayToken;
use App\Models\Company;
use App\Models\CompanyGateway;
use App\Models\CompanyLedger;
use App\Models\CompanyToken;
use App\Models\CompanyUser;
use App\Models\Credit;
use App\Models\CreditInvitation;
use App\Models\Design;
use App\Models\Document;
use App\Models\Expense;
use App\Models\ExpenseCategory;
use App\Models\Gateway;
use App\Models\GroupSetting;
use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Models\Payment;
use App\Models\Paymentable;
use App\Models\PaymentHash;
use App\Models\Product;
use App\Models\Project;
use App\Models\Quote;
use App\Models\QuoteInvitation;
use App\Models\RecurringInvoice;
use App\Models\RecurringInvoiceInvitation;
use App\Models\Subscription;
use App\Models\SystemLog;
use App\Models\Task;
use App\Models\TaskStatus;
use App\Models\TaxRate;
use App\Models\User;
use App\Models\Vendor;
use App\Models\VendorContact;
use App\Models\Webhook;
use Illuminate\Console\Command;
/**
* Class CheckDb.
*/
class CheckDb extends Command
{
protected $signature = 'ninja:check-db';
protected $description = 'Check MultiDB';
protected $log = '';
private $entities = [
Account::class,
Activity::class,
Backup::class,
Client::class,
ClientContact::class,
ClientGatewayToken::class,
Company::class,
CompanyGateway::class,
CompanyLedger::class,
CompanyToken::class,
CompanyUser::class,
Credit::class,
CreditInvitation::class,
Design::class,
Document::class,
Expense::class,
ExpenseCategory::class,
Gateway::class,
GroupSetting::class,
Invoice::class,
InvoiceInvitation::class,
Payment::class,
Paymentable::class,
PaymentHash::class,
Product::class,
Project::class,
Quote::class,
QuoteInvitation::class,
RecurringInvoice::class,
RecurringInvoiceInvitation::class,
Subscription::class,
SystemLog::class,
Task::class,
TaskStatus::class,
TaxRate::class,
User::class,
Vendor::class,
VendorContact::class,
WebHook::class,
];
public function handle()
{
$this->LogMessage('Checking - V5_DB1');
foreach ($this->entities as $entity) {
$count_db_1 = $entity::on('db-ninja-01')->count();
$count_db_2 = $entity::on('db-ninja-02a')->count();
$diff = $count_db_1 - $count_db_2;
if ($diff != 0) {
$this->logMessage("{$entity} DB1: {$count_db_1} - DB2: {$count_db_2} - diff = {$diff}");
}
}
$this->LogMessage('Checking - V5_DB2');
foreach ($this->entities as $entity) {
$count_db_1 = $entity::on('db-ninja-02')->count();
$count_db_2 = $entity::on('db-ninja-01a')->count();
$diff = $count_db_1 - $count_db_2;
if ($diff != 0) {
$this->logMessage("{$entity} DB1: {$count_db_1} - DB2: {$count_db_2} - diff = {$diff}");
}
}
}
private function logMessage($str)
{
$str = date('Y-m-d h:i:s').' '.$str;
$this->info($str);
$this->log .= $str."\n";
}
}

View file

@ -0,0 +1,145 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Console\Commands;
use App\DataMapper\ClientRegistrationFields;
use App\DataMapper\CompanySettings;
use App\Jobs\Company\CreateCompanyPaymentTerms;
use App\Jobs\Company\CreateCompanyTaskStatuses;
use App\Jobs\Util\VersionCheck;
use App\Models\Account;
use App\Models\Company;
use App\Models\CompanyToken;
use App\Models\User;
use App\Repositories\InvoiceRepository;
use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\MakesHash;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class CreateAccount extends Command
{
use MakesHash, GeneratesCounter;
/**
* @var string
*/
protected $description = 'Create Single Account';
/**
* @var string
*/
protected $signature = 'ninja:create-account {--email=} {--password=}';
/**
* Create a new command instance.
*
* @param InvoiceRepository $invoice_repo
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->info(date('r').' Create Single Account...');
$this->warmCache();
$this->createAccount();
}
private function createAccount()
{
$account = Account::factory()->create();
$company = Company::factory()->create([
'account_id' => $account->id,
'portal_domain' => config('ninja.app_url'),
'portal_mode' => 'domain',
]);
$company->client_registration_fields = ClientRegistrationFields::generate();
$company->save();
$account->default_company_id = $company->id;
$account->save();
$email = $this->option('email') ?? 'admin@example.com';
$password = $this->option('password') ?? 'changeme!';
$user = User::factory()->create([
'account_id' => $account->id,
'email' => $email,
'password' => Hash::make($password),
'confirmation_code' => $this->createDbHash(config('database.default')),
'email_verified_at' => now(),
'first_name' => 'New',
'last_name' => 'User',
'phone' => '',
]);
$company_token = new CompanyToken;
$company_token->user_id = $user->id;
$company_token->company_id = $company->id;
$company_token->account_id = $account->id;
$company_token->name = 'User Token';
$company_token->token = Str::random(64);
$company_token->is_system = true;
$company_token->save();
$user->companies()->attach($company->id, [
'account_id' => $account->id,
'is_owner' => 1,
'is_admin' => 1,
'is_locked' => 0,
'notifications' => CompanySettings::notificationDefaults(),
'settings' => null,
]);
(new CreateCompanyPaymentTerms($company, $user))->handle();
(new CreateCompanyTaskStatuses($company, $user))->handle();
(new VersionCheck())->handle();
$this->warmCache();
}
private function warmCache()
{
/* Warm up the cache !*/
$cached_tables = config('ninja.cached_tables');
foreach ($cached_tables as $name => $class) {
if ($name == 'payment_terms') {
$orderBy = 'num_days';
} elseif ($name == 'fonts') {
$orderBy = 'sort_order';
} elseif (in_array($name, ['currencies', 'industries', 'languages', 'countries', 'banks'])) {
$orderBy = 'name';
} else {
$orderBy = 'id';
}
$tableData = $class::orderBy($orderBy)->get();
if ($tableData->count()) {
Cache::forever($name, $tableData);
}
}
}
}

View file

@ -1,223 +0,0 @@
<?php
namespace App\Console\Commands;
use Utils;
use stdClass;
use App\Models\Account;
use Faker\Factory;
use Illuminate\Console\Command;
/**
* Class CreateLuisData.
*/
class CreateLuisData extends Command
{
/**
* @var string
*/
protected $description = 'Create LUIS Data';
/**
* @var string
*/
protected $signature = 'ninja:create-luis-data {faker_field=name}';
/**
* CreateLuisData constructor.
*
*/
public function __construct()
{
parent::__construct();
$this->faker = Factory::create();
}
/**
* @return bool
*/
public function handle()
{
$this->fakerField = $this->argument('faker_field');
$intents = [];
$entityTypes = [
ENTITY_INVOICE,
ENTITY_QUOTE,
ENTITY_CLIENT,
ENTITY_CREDIT,
ENTITY_EXPENSE,
ENTITY_PAYMENT,
ENTITY_PRODUCT,
ENTITY_RECURRING_INVOICE,
ENTITY_TASK,
ENTITY_VENDOR,
];
foreach ($entityTypes as $entityType) {
$intents = array_merge($intents, $this->createIntents($entityType));
}
$intents = array_merge($intents, $this->getNavigateToIntents($entityType));
$this->info(json_encode($intents));
}
private function createIntents($entityType)
{
$intents = [];
$intents = array_merge($intents, $this->getCreateEntityIntents($entityType));
$intents = array_merge($intents, $this->getFindEntityIntents($entityType));
$intents = array_merge($intents, $this->getListEntityIntents($entityType));
return $intents;
}
private function getCreateEntityIntents($entityType)
{
$intents = [];
$phrases = [
"create new {$entityType}",
"new {$entityType}",
"make a {$entityType}",
];
foreach ($phrases as $phrase) {
$intents[] = $this->createIntent('CreateEntity', $phrase, [
$entityType => 'EntityType',
]);
if ($entityType != ENTITY_CLIENT) {
$client = $this->faker->{$this->fakerField};
$phrase .= " for {$client}";
$intents[] = $this->createIntent('CreateEntity', $phrase, [
$entityType => 'EntityType',
$client => 'Name',
]);
}
}
return $intents;
}
private function getFindEntityIntents($entityType)
{
$intents = [];
if (in_array($entityType, [ENTITY_CLIENT, ENTITY_INVOICE, ENTITY_QUOTE])) {
$name = $entityType === ENTITY_CLIENT ? $this->faker->{$this->fakerField} : $this->faker->randomNumber(4);
$intents[] = $this->createIntent('FindEntity', "find {$entityType} {$name}", [
$entityType => 'EntityType',
$name => 'Name',
]);
if ($entityType === ENTITY_CLIENT) {
$name = $this->faker->{$this->fakerField};
$intents[] = $this->createIntent('FindEntity', "find {$name}", [
$name => 'Name',
]);
}
}
return $intents;
}
private function getListEntityIntents($entityType)
{
$intents = [];
$entityTypePlural = Utils::pluralizeEntityType($entityType);
$intents[] = $this->createIntent('ListEntity', "show me {$entityTypePlural}", [
$entityTypePlural => 'EntityType',
]);
$intents[] = $this->createIntent('ListEntity', "list {$entityTypePlural}", [
$entityTypePlural => 'EntityType',
]);
$intents[] = $this->createIntent('ListEntity', "show me active {$entityTypePlural}", [
$entityTypePlural => 'EntityType',
'active' => 'Filter',
]);
$intents[] = $this->createIntent('ListEntity', "list archived and deleted {$entityTypePlural}", [
$entityTypePlural => 'EntityType',
'archived' => 'Filter',
'deleted' => 'Filter',
]);
if ($entityType != ENTITY_CLIENT) {
$client = $this->faker->{$this->fakerField};
$intents[] = $this->createIntent('ListEntity', "list {$entityTypePlural} for {$client}", [
$entityTypePlural => 'EntityType',
$client => 'Name',
]);
$intents[] = $this->createIntent('ListEntity', "show me {$client}'s {$entityTypePlural}", [
$entityTypePlural => 'EntityType',
$client . '\'s' => 'Name',
]);
$intents[] = $this->createIntent('ListEntity', "show me {$client}'s active {$entityTypePlural}", [
$entityTypePlural => 'EntityType',
$client . '\'s' => 'Name',
'active' => 'Filter',
]);
}
return $intents;
}
private function getNavigateToIntents($entityType)
{
$intents = [];
$locations = array_merge(Account::$basicSettings, Account::$advancedSettings);
foreach ($locations as $location) {
$location = str_replace('_', ' ', $location);
$intents[] = $this->createIntent('NavigateTo', "go to {$location}", [
$location => 'Location',
]);
$intents[] = $this->createIntent('NavigateTo', "show me {$location}", [
$location => 'Location',
]);
}
return $intents;
}
private function createIntent($name, $text, $entities)
{
$intent = new stdClass();
$intent->intent = $name;
$intent->text = $text;
$intent->entities = [];
foreach ($entities as $value => $entity) {
$startPos = strpos($text, (string)$value);
if (! $startPos) {
dd("Failed to find {$value} in {$text}");
}
$entityClass = new stdClass();
$entityClass->entity = $entity;
$entityClass->startPos = $startPos;
$entityClass->endPos = $entityClass->startPos + strlen($value) - 1;
$intent->entities[] = $entityClass;
}
return $intent;
}
/**
* @return array
*/
protected function getArguments()
{
return [];
}
/**
* @return array
*/
protected function getOptions()
{
return [];
}
}

View file

@ -0,0 +1,907 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Console\Commands;
use App\DataMapper\ClientRegistrationFields;
use App\DataMapper\CompanySettings;
use App\DataMapper\FeesAndLimits;
use App\Events\Invoice\InvoiceWasCreated;
use App\Events\RecurringInvoice\RecurringInvoiceWasCreated;
use App\Factory\GroupSettingFactory;
use App\Factory\InvoiceFactory;
use App\Factory\InvoiceItemFactory;
use App\Factory\RecurringInvoiceFactory;
use App\Factory\SubscriptionFactory;
use App\Helpers\Invoice\InvoiceSum;
use App\Jobs\Company\CreateCompanyTaskStatuses;
use App\Libraries\MultiDB;
use App\Models\Account;
use App\Models\BankIntegration;
use App\Models\BankTransaction;
use App\Models\BankTransactionRule;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Company;
use App\Models\CompanyGateway;
use App\Models\CompanyToken;
use App\Models\Country;
use App\Models\Credit;
use App\Models\Expense;
use App\Models\Product;
use App\Models\Project;
use App\Models\Quote;
use App\Models\RecurringInvoice;
use App\Models\Task;
use App\Models\TaxRate;
use App\Models\User;
use App\Models\Vendor;
use App\Models\VendorContact;
use App\Repositories\InvoiceRepository;
use App\Utils\Ninja;
use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\MakesHash;
use Carbon\Carbon;
use Faker\Factory;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Schema;
use stdClass;
class CreateSingleAccount extends Command
{
use MakesHash, GeneratesCounter;
protected $description = 'Create Single Sample Account';
protected $signature = 'ninja:create-single-account {gateway=all} {--database=db-ninja-01}';
protected $invoice_repo;
protected $count;
protected $gateway;
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if (Ninja::isHosted() || config('ninja.is_docker') || !$this->confirm('Are you sure you want to inject dummy data?')) {
return;
}
$this->invoice_repo = new InvoiceRepository();
MultiDB::setDb($this->option('database'));
$this->info(date('r').' Create Single Sample Account...');
$this->count = 5;
$this->gateway = $this->argument('gateway');
$this->info('Warming up cache');
$this->warmCache();
$this->createSmallAccount();
}
private function createSmallAccount()
{
$this->info('Creating Small Account and Company');
if ($user = User::where('email', 'small@example.com')->first()) {
$user->account->delete();
}
$account = Account::factory()->create();
$company = Company::factory()->create([
'account_id' => $account->id,
'slack_webhook_url' => config('ninja.notification.slack'),
'default_password_timeout' => 30*60000,
'portal_mode' => 'domain',
'portal_domain' => 'http://ninja.test:8000',
'track_inventory' => true
]);
$faker = \Faker\Factory::create();
$settings = $company->settings;
$settings->invoice_terms = 'Default company invoice terms';
$settings->quote_terms = 'Default company quote terms';
$settings->invoice_footer = 'Default invoice footer';
$settings->company_logo = 'https://pdf.invoicing.co/favicon-v2.png';
$settings->name = $faker->name();
$settings->email = $faker->safeEmail();
$settings->phone = $faker->phoneNumber();
$settings->website = $faker->url();
$settings->address1 = $faker->streetName();
$settings->address2 = $faker->streetAddress();
$settings->city = $faker->city();
$settings->state = $faker->state();
$settings->postal_code = $faker->postcode();
$settings->country_id = '840';
$settings->vat_number = 'vat number';
$settings->id_number = 'id number';
$settings->use_credits_payment = 'always';
$settings->timezone_id = '1';
$settings->entity_send_time = 0;
$settings->name = $faker->name();
$company->settings = $settings;
$company->client_registration_fields = ClientRegistrationFields::generate();
$company->save();
$account->default_company_id = $company->id;
$account->save();
$user = User::whereEmail('small@example.com')->first();
if (! $user) {
$user = User::factory()->create([
'account_id' => $account->id,
'email' => 'small@example.com',
'confirmation_code' => $this->createDbHash(config('database.default')),
]);
}
$company_token = new CompanyToken;
$company_token->user_id = $user->id;
$company_token->company_id = $company->id;
$company_token->account_id = $account->id;
$company_token->name = 'test token';
$company_token->token = 'company-token-test';
$company_token->is_system = true;
$company_token->save();
$user->companies()->attach($company->id, [
'account_id' => $account->id,
'is_owner' => 1,
'is_admin' => 1,
'is_locked' => 0,
'notifications' => CompanySettings::notificationDefaults(),
'settings' => null,
]);
Product::factory()->count(1)->create([
'user_id' => $user->id,
'company_id' => $company->id,
]);
TaxRate::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'name' => 'GST',
'rate' => 10
]);
TaxRate::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'name' => 'VAT',
'rate' => 17.5
]);
TaxRate::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'name' => 'CA Sales Tax',
'rate' => 5
]);
$bi = BankIntegration::factory()->create([
'account_id' => $account->id,
'company_id' => $company->id,
'user_id' => $user->id,
]);
BankTransaction::factory()->count(50)->create([
'bank_integration_id' => $bi->id,
'user_id' => $user->id,
'company_id' => $company->id,
]);
$btr = BankTransactionRule::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'applies_to' => (bool)rand(0, 1) ? 'CREDIT' : 'DEBIT',
]);
$client = Client::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'name' => 'cypress'
]);
ClientContact::factory()->create([
'user_id' => $user->id,
'client_id' => $client->id,
'company_id' => $company->id,
'is_primary' => 1,
'email' => 'cypress@example.com',
'password' => Hash::make('password'),
]);
$this->info('Creating '.$this->count.' clients');
for ($x = 0; $x < $this->count; $x++) {
$z = $x + 1;
$this->info('Creating client # '.$z);
$this->createClient($company, $user);
}
(new CreateCompanyTaskStatuses($company, $user))->handle();
for ($x = 0; $x < $this->count; $x++) {
$client = $company->clients->random();
$this->info('creating invoice for client #'.$client->id);
$this->createInvoice($client);
$this->info('creating invoice for client #'.$client->id);
$this->createInvoice($client);
$this->info('creating invoice for client #'.$client->id);
$this->createInvoice($client);
$this->info('creating invoice for client #'.$client->id);
$this->createInvoice($client);
$this->info('creating invoice for client #'.$client->id);
$this->createInvoice($client);
$client = $company->clients->random();
// $this->info('creating credit for client #'.$client->id);
// $this->createCredit($client); /** Prevents Stripe from running payments. */
$client = $company->clients->random();
$this->info('creating quote for client #'.$client->id);
$this->createQuote($client);
$client = $company->clients->random();
$this->info('creating expense for client #'.$client->id);
$this->createExpense($client);
$client = $company->clients->random();
$this->info('creating vendor for client #'.$client->id);
$this->createVendor($client);
$client = $company->clients->random();
$this->info('creating task for client #' . $client->id);
$this->createTask($client);
$client = $company->clients->random();
$this->info('creating project for client #' . $client->id);
$this->createProject($client);
$this->info('creating credit for client #' . $client->id);
$this->createCredit($client);
$this->info('creating recurring invoice for client # ' . $client->id);
$this->createRecurringInvoice($client);
}
$this->createGateways($company, $user);
$this->createSubsData($company, $user);
}
private function createSubsData($company, $user)
{
$gs = GroupSettingFactory::create($company->id, $user->id);
$gs->name = "plans";
$gs->save();
$p1 = Product::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'product_key' => 'pro_plan',
'notes' => 'The Pro Plan',
'cost' => 10,
'price' => 10,
'quantity' => 1,
]);
$p2 = Product::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'product_key' => 'enterprise_plan',
'notes' => 'The Enterprise Plan',
'cost' => 14,
'price' => 14,
'quantity' => 1,
]);
$p3 = Product::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'product_key' => 'free_plan',
'notes' => 'The Free Plan',
'cost' => 0,
'price' => 0,
'quantity' => 1,
]);
$webhook_config = [
'post_purchase_url' => 'http://ninja.test:8000/api/admin/plan',
'post_purchase_rest_method' => 'post',
'post_purchase_headers' => [config('ninja.ninja_hosted_header') => config('ninja.ninja_hosted_secret')],
];
$sub = SubscriptionFactory::create($company->id, $user->id);
$sub->name = "Pro Plan";
$sub->group_id = $gs->id;
$sub->recurring_product_ids = "{$p1->hashed_id}";
$sub->webhook_configuration = $webhook_config;
$sub->allow_plan_changes = true;
$sub->frequency_id = RecurringInvoice::FREQUENCY_MONTHLY;
$sub->save();
$sub = SubscriptionFactory::create($company->id, $user->id);
$sub->name = "Enterprise Plan";
$sub->group_id = $gs->id;
$sub->recurring_product_ids = "{$p2->hashed_id}";
$sub->webhook_configuration = $webhook_config;
$sub->allow_plan_changes = true;
$sub->frequency_id = RecurringInvoice::FREQUENCY_MONTHLY;
$sub->save();
$sub = SubscriptionFactory::create($company->id, $user->id);
$sub->name = "Free Plan";
$sub->group_id = $gs->id;
$sub->recurring_product_ids = "{$p3->hashed_id}";
$sub->webhook_configuration = $webhook_config;
$sub->allow_plan_changes = true;
$sub->frequency_id = RecurringInvoice::FREQUENCY_MONTHLY;
$sub->save();
}
private function createClient($company, $user)
{
// dispatch(function () use ($company, $user) {
// });
$client = Client::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
]);
ClientContact::factory()->create([
'user_id' => $user->id,
'client_id' => $client->id,
'company_id' => $company->id,
'is_primary' => 1,
'email' => 'user@example.com',
]);
ClientContact::factory()->count(rand(1, 2))->create([
'user_id' => $user->id,
'client_id' => $client->id,
'company_id' => $company->id,
]);
$client->number = $this->getNextClientNumber($client);
$settings = $client->settings;
$settings->currency_id = "1";
// $settings->use_credits_payment = "always";
$client->settings = $settings;
$country = Country::all()->random();
$client->country_id = $country->id;
$client->save();
}
private function createExpense($client)
{
Expense::factory()->count(rand(1, 20))->create([
'user_id' => $client->user->id,
'client_id' => $client->id,
'company_id' => $client->company->id,
]);
}
private function createVendor($client)
{
$vendor = Vendor::factory()->create([
'user_id' => $client->user->id,
'company_id' => $client->company->id,
]);
VendorContact::factory()->create([
'user_id' => $client->user->id,
'vendor_id' => $vendor->id,
'company_id' => $client->company->id,
'is_primary' => 1,
]);
VendorContact::factory()->count(rand(1, 2))->create([
'user_id' => $client->user->id,
'vendor_id' => $vendor->id,
'company_id' => $client->company->id,
'is_primary' => 0,
]);
}
private function createTask($client)
{
$vendor = Task::factory()->create([
'user_id' => $client->user->id,
'company_id' => $client->company->id,
]);
}
private function createProject($client)
{
$vendor = Project::factory()->create([
'user_id' => $client->user->id,
'company_id' => $client->company->id,
'client_id' => $client->id,
]);
}
private function createInvoice($client)
{
$faker = Factory::create();
$invoice = InvoiceFactory::create($client->company->id, $client->user->id); //stub the company and user_id
$invoice->client_id = $client->id;
$dateable = Carbon::now()->subDays(rand(0, 90));
$invoice->date = $dateable;
$invoice->line_items = $this->buildLineItems(rand(1, 10));
$invoice->uses_inclusive_taxes = false;
if (rand(0, 1)) {
$invoice->tax_name1 = 'GST';
$invoice->tax_rate1 = 10.00;
}
if (rand(0, 1)) {
$invoice->tax_name2 = 'VAT';
$invoice->tax_rate2 = 17.50;
}
if (rand(0, 1)) {
$invoice->tax_name3 = 'CA Sales Tax';
$invoice->tax_rate3 = 5;
}
$invoice->custom_value1 = $faker->date;
$invoice->custom_value2 = rand(0, 1) ? 'yes' : 'no';
$invoice->save();
$invoice_calc = new InvoiceSum($invoice);
$invoice_calc->build();
$invoice = $invoice_calc->getInvoice();
if ($this->gateway === 'braintree') {
$invoice->amount = 100; // Braintree sandbox only allows payments under 2,000 to complete successfully.
}
$invoice->save();
$invoice->service()->createInvitations()->markSent();
$this->invoice_repo->markSent($invoice);
event(new InvoiceWasCreated($invoice, $invoice->company, Ninja::eventVars()));
}
private function createCredit($client)
{
$faker = Factory::create();
$credit = Credit::factory()->create(['user_id' => $client->user->id, 'company_id' => $client->company->id, 'client_id' => $client->id]);
$dateable = Carbon::now()->subDays(rand(0, 90));
$credit->date = $dateable;
$credit->line_items = $this->buildCreditItem();
$credit->uses_inclusive_taxes = false;
$credit->save();
$invoice_calc = new InvoiceSum($credit);
$invoice_calc->build();
$credit = $invoice_calc->getCredit();
$credit->save();
$credit->service()->markSent()->save();
$credit->service()->createInvitations();
}
private function createQuote($client)
{
$faker = Factory::create();
$quote = Quote::factory()->create(['user_id' => $client->user->id, 'company_id' => $client->company->id, 'client_id' => $client->id]);
$quote->date = $faker->date();
$quote->client_id = $client->id;
$quote->setRelation('client', $client);
$quote->line_items = $this->buildLineItems(rand(1, 10));
$quote->uses_inclusive_taxes = false;
if (rand(0, 1)) {
$quote->tax_name1 = 'GST';
$quote->tax_rate1 = 10.00;
}
if (rand(0, 1)) {
$quote->tax_name2 = 'VAT';
$quote->tax_rate2 = 17.50;
}
if (rand(0, 1)) {
$quote->tax_name3 = 'CA Sales Tax';
$quote->tax_rate3 = 5;
}
$quote->save();
$quote_calc = new InvoiceSum($quote);
$quote_calc->build();
$quote = $quote_calc->getQuote();
$quote->save();
$quote->service()->markSent()->save();
$quote->service()->createInvitations();
}
private function buildCreditItem()
{
$line_items = [];
$item = InvoiceItemFactory::create();
$item->quantity = 1;
$item->cost = 1000;
$product = Product::all()->random();
$item->cost = (float) $product->cost;
$item->product_key = $product->product_key;
$item->notes = $product->notes;
$item->custom_value1 = $product->custom_value1;
$item->custom_value2 = $product->custom_value2;
$item->custom_value3 = $product->custom_value3;
$item->custom_value4 = $product->custom_value4;
$line_items[] = $item;
return $line_items;
}
private function buildLineItems($count = 1)
{
$line_items = [];
for ($x = 0; $x < $count; $x++) {
$item = InvoiceItemFactory::create();
$item->quantity = 1;
//$item->cost = 10;
if (rand(0, 1)) {
$item->tax_name1 = 'GST';
$item->tax_rate1 = 10.00;
}
if (rand(0, 1)) {
$item->tax_name1 = 'VAT';
$item->tax_rate1 = 17.50;
}
if (rand(0, 1)) {
$item->tax_name1 = 'Sales Tax';
$item->tax_rate1 = 5;
}
$product = Product::all()->random();
$item->cost = (float) $product->cost;
$item->product_key = $product->product_key;
$item->notes = $product->notes;
$item->custom_value1 = $product->custom_value1;
$item->custom_value2 = $product->custom_value2;
$item->custom_value3 = $product->custom_value3;
$item->custom_value4 = $product->custom_value4;
$line_items[] = $item;
}
return $line_items;
}
private function warmCache()
{
/* Warm up the cache !*/
$cached_tables = config('ninja.cached_tables');
foreach ($cached_tables as $name => $class) {
// check that the table exists in case the migration is pending
if (! Schema::hasTable((new $class())->getTable())) {
continue;
}
if ($name == 'payment_terms') {
$orderBy = 'num_days';
} elseif ($name == 'fonts') {
$orderBy = 'sort_order';
} elseif (in_array($name, ['currencies', 'industries', 'languages', 'countries', 'banks'])) {
$orderBy = 'name';
} else {
$orderBy = 'id';
}
$tableData = $class::orderBy($orderBy)->get();
if ($tableData->count()) {
Cache::forever($name, $tableData);
}
}
}
private function createGateways($company, $user)
{
if (config('ninja.testvars.stripe') && ($this->gateway == 'all' || $this->gateway == 'stripe')) {
$cg = new CompanyGateway;
$cg->company_id = $company->id;
$cg->user_id = $user->id;
$cg->gateway_key = 'd14dd26a37cecc30fdd65700bfb55b23';
$cg->require_cvv = true;
$cg->require_billing_address = true;
$cg->require_shipping_address = true;
$cg->update_details = true;
$cg->config = encrypt(config('ninja.testvars.stripe'));
$cg->save();
$gateway_types = $cg->driver()->gatewayTypes();
$fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
}
if (config('ninja.testvars.paypal') && ($this->gateway == 'all' || $this->gateway == 'paypal')) {
$cg = new CompanyGateway;
$cg->company_id = $company->id;
$cg->user_id = $user->id;
$cg->gateway_key = '38f2c48af60c7dd69e04248cbb24c36e';
$cg->require_cvv = true;
$cg->require_billing_address = true;
$cg->require_shipping_address = true;
$cg->update_details = true;
$cg->config = encrypt(config('ninja.testvars.paypal'));
$cg->save();
$gateway_types = $cg->driver()->gatewayTypes();
$fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
}
if (config('ninja.testvars.checkout') && ($this->gateway == 'all' || $this->gateway == 'checkout')) {
$cg = new CompanyGateway;
$cg->company_id = $company->id;
$cg->user_id = $user->id;
$cg->gateway_key = '3758e7f7c6f4cecf0f4f348b9a00f456';
$cg->require_cvv = true;
$cg->require_billing_address = true;
$cg->require_shipping_address = true;
$cg->update_details = true;
$cg->config = encrypt(config('ninja.testvars.checkout'));
$cg->save();
$gateway_types = $cg->driver(new Client)->gatewayTypes();
$fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
}
if (config('ninja.testvars.authorize') && ($this->gateway == 'all' || $this->gateway == 'authorizenet')) {
$cg = new CompanyGateway;
$cg->company_id = $company->id;
$cg->user_id = $user->id;
$cg->gateway_key = '3b6621f970ab18887c4f6dca78d3f8bb';
$cg->require_cvv = true;
$cg->require_billing_address = true;
$cg->require_shipping_address = true;
$cg->update_details = true;
$cg->config = encrypt(config('ninja.testvars.authorize'));
$cg->save();
$gateway_types = $cg->driver(new Client)->gatewayTypes();
$fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
}
if (config('ninja.testvars.wepay') && ($this->gateway == 'all' || $this->gateway == 'wepay')) {
$cg = new CompanyGateway;
$cg->company_id = $company->id;
$cg->user_id = $user->id;
$cg->gateway_key = '8fdeed552015b3c7b44ed6c8ebd9e992';
$cg->require_cvv = true;
$cg->require_billing_address = true;
$cg->require_shipping_address = true;
$cg->update_details = true;
$cg->config = encrypt(config('ninja.testvars.wepay'));
$cg->save();
$gateway_types = $cg->driver()->gatewayTypes();
$fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
}
if (config('ninja.testvars.braintree') && ($this->gateway == 'all' || $this->gateway == 'braintree')) {
$cg = new CompanyGateway;
$cg->company_id = $company->id;
$cg->user_id = $user->id;
$cg->gateway_key = 'f7ec488676d310683fb51802d076d713';
$cg->require_cvv = true;
$cg->require_billing_address = true;
$cg->require_shipping_address = true;
$cg->update_details = true;
$cg->config = encrypt(config('ninja.testvars.braintree'));
$cg->save();
$gateway_types = $cg->driver()->gatewayTypes();
$fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
}
if (config('ninja.testvars.paytrace.decrypted') && ($this->gateway == 'all' || $this->gateway == 'paytrace')) {
$cg = new CompanyGateway;
$cg->company_id = $company->id;
$cg->user_id = $user->id;
$cg->gateway_key = 'bbd736b3254b0aabed6ad7fda1298c88';
$cg->require_cvv = true;
$cg->require_billing_address = true;
$cg->require_shipping_address = true;
$cg->update_details = true;
$cg->config = encrypt(config('ninja.testvars.paytrace.decrypted'));
$cg->save();
$gateway_types = $cg->driver()->gatewayTypes();
$fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
}
if (config('ninja.testvars.mollie') && ($this->gateway == 'all' || $this->gateway == 'mollie')) {
$cg = new CompanyGateway;
$cg->company_id = $company->id;
$cg->user_id = $user->id;
$cg->gateway_key = '1bd651fb213ca0c9d66ae3c336dc77e8';
$cg->require_cvv = true;
$cg->require_billing_address = true;
$cg->require_shipping_address = true;
$cg->update_details = true;
$cg->config = encrypt(config('ninja.testvars.mollie'));
$cg->save();
$gateway_types = $cg->driver()->gatewayTypes();
$fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
}
if (config('ninja.testvars.square') && ($this->gateway == 'all' || $this->gateway == 'square')) {
$cg = new CompanyGateway;
$cg->company_id = $company->id;
$cg->user_id = $user->id;
$cg->gateway_key = '65faab2ab6e3223dbe848b1686490baz';
$cg->require_cvv = true;
$cg->require_billing_address = true;
$cg->require_shipping_address = true;
$cg->update_details = true;
$cg->config = encrypt(config('ninja.testvars.square'));
$cg->save();
$gateway_types = $cg->driver()->gatewayTypes();
$fees_and_limits = new stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
}
}
private function createRecurringInvoice($client)
{
$faker = Factory::create();
$invoice = RecurringInvoiceFactory::create($client->company->id, $client->user->id); //stub the company and user_id
$invoice->client_id = $client->id;
$dateable = Carbon::now()->subDays(rand(0, 90));
$invoice->date = $dateable;
$invoice->line_items = $this->buildLineItems(rand(1, 10));
$invoice->uses_inclusive_taxes = false;
if (rand(0, 1)) {
$invoice->tax_name1 = 'GST';
$invoice->tax_rate1 = 10.00;
}
if (rand(0, 1)) {
$invoice->tax_name2 = 'VAT';
$invoice->tax_rate2 = 17.50;
}
if (rand(0, 1)) {
$invoice->tax_name3 = 'CA Sales Tax';
$invoice->tax_rate3 = 5;
}
$invoice->custom_value1 = $faker->date;
$invoice->custom_value2 = rand(0, 1) ? 'yes' : 'no';
$invoice->status_id = RecurringInvoice::STATUS_ACTIVE;
$invoice->save();
$invoice_calc = new InvoiceSum($invoice);
$invoice_calc->build();
$invoice = $invoice_calc->getInvoice();
$invoice->save();
event(new RecurringInvoiceWasCreated($invoice, $invoice->company, Ninja::eventVars()));
}
}

View file

@ -1,314 +1,700 @@
<?php <?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Ninja\Repositories\AccountRepository; use App\DataMapper\CompanySettings;
use App\Ninja\Repositories\ClientRepository; use App\Events\Invoice\InvoiceWasCreated;
use App\Ninja\Repositories\ExpenseRepository; use App\Factory\InvoiceFactory;
use App\Ninja\Repositories\InvoiceRepository; use App\Factory\InvoiceItemFactory;
use App\Ninja\Repositories\PaymentRepository; use App\Factory\QuoteFactory;
use App\Ninja\Repositories\VendorRepository; use App\Helpers\Invoice\InvoiceSum;
use App\Ninja\Repositories\TaskRepository; use App\Models\Account;
use App\Ninja\Repositories\ProjectRepository;
use App\Models\Client; use App\Models\Client;
use App\Models\TaxRate; use App\Models\ClientContact;
use App\Models\Company;
use App\Models\CompanyToken;
use App\Models\Country;
use App\Models\Credit;
use App\Models\Document;
use App\Models\Expense;
use App\Models\Product;
use App\Models\Project; use App\Models\Project;
use App\Models\ExpenseCategory; use App\Models\Quote;
use Auth; use App\Models\RecurringInvoice;
use App\Models\Task;
use App\Models\User;
use App\Models\Vendor;
use App\Models\VendorContact;
use App\Repositories\InvoiceRepository;
use App\Utils\Ninja;
use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\MakesHash;
use Carbon\Carbon;
use Faker\Factory; use Faker\Factory;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Utils; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
/**
* Class CreateTestData.
*/
class CreateTestData extends Command class CreateTestData extends Command
{ {
use MakesHash, GeneratesCounter;
/** /**
* @var string * @var string
*/ */
protected $description = 'Create Test Data'; protected $description = 'Create Test Data';
/** /**
* @var string * @var string
*/ */
protected $signature = 'ninja:create-test-data {count=1} {create_account=false} {--database}'; protected $signature = 'ninja:create-test-data {count=1}';
protected $invoice_repo;
/** /**
* @var * Execute the console command.
*/
protected $token;
/**
* CreateTestData constructor.
* *
* @param ClientRepository $clientRepo * @return mixed
* @param InvoiceRepository $invoiceRepo
* @param PaymentRepository $paymentRepo
* @param VendorRepository $vendorRepo
* @param ExpenseRepository $expenseRepo
* @param TaskRepository $taskRepo
* @param AccountRepository $accountRepo
*/
public function __construct(
ClientRepository $clientRepo,
InvoiceRepository $invoiceRepo,
PaymentRepository $paymentRepo,
VendorRepository $vendorRepo,
ExpenseRepository $expenseRepo,
TaskRepository $taskRepo,
ProjectRepository $projectRepo,
AccountRepository $accountRepo)
{
parent::__construct();
$this->faker = Factory::create();
$this->clientRepo = $clientRepo;
$this->invoiceRepo = $invoiceRepo;
$this->paymentRepo = $paymentRepo;
$this->vendorRepo = $vendorRepo;
$this->expenseRepo = $expenseRepo;
$this->taskRepo = $taskRepo;
$this->projectRepo = $projectRepo;
$this->accountRepo = $accountRepo;
}
/**
* @return bool
*/ */
public function handle() public function handle()
{ {
if (Utils::isNinjaProd()) { if (config('ninja.is_docker')) {
$this->info('Unable to run in production'); return;
return false;
} }
if (! $this->confirm('Are you sure you want to inject dummy data?')) {
return;
}
$this->invoice_repo = new InvoiceRepository();
$this->info(date('r').' Running CreateTestData...'); $this->info(date('r').' Running CreateTestData...');
$this->count = $this->argument('count'); $this->count = $this->argument('count');
if ($database = $this->option('database')) { $this->info('Warming up cache');
config(['database.default' => $database]);
}
if (filter_var($this->argument('create_account'), FILTER_VALIDATE_BOOLEAN)) { $this->warmCache();
$this->info('Creating new account...');
$account = $this->accountRepo->create(
$this->faker->firstName,
$this->faker->lastName,
$this->faker->safeEmail
);
Auth::login($account->users[0]);
} else {
$this->info('Using first account...');
Auth::loginUsingId(1);
}
$this->createClients(); $this->createSmallAccount();
$this->createVendors(); $this->createMediumAccount();
$this->createOtherObjects(); $this->createLargeAccount();
$this->info('Done');
} }
private function createClients() private function createSmallAccount()
{ {
for ($i = 0; $i < $this->count; $i++) { $this->info('Creating Small Account and Company');
$data = [
'name' => $this->faker->name,
'address1' => $this->faker->streetAddress,
'address2' => $this->faker->secondaryAddress,
'city' => $this->faker->city,
'state' => $this->faker->state,
'postal_code' => $this->faker->postcode,
'contacts' => [[
'first_name' => $this->faker->firstName,
'last_name' => $this->faker->lastName,
'email' => $this->faker->safeEmail,
'phone' => $this->faker->phoneNumber,
]],
];
$client = $this->clientRepo->save($data); $account = Account::factory()->create();
$this->info('Client: ' . $client->name); $company = Company::factory()->create([
'account_id' => $account->id,
'slack_webhook_url' => config('ninja.notification.slack'),
]);
$this->createInvoices($client); $account->default_company_id = $company->id;
$this->createInvoices($client, true); $account->save();
// $this->createTasks($client);
$user = User::whereEmail('small@example.com')->first();
if (! $user) {
$user = User::factory()->create([
'account_id' => $account->id,
'email' => 'small@example.com',
'confirmation_code' => $this->createDbHash(config('database.default')),
]);
}
$company_token = new CompanyToken;
$company_token->user_id = $user->id;
$company_token->company_id = $company->id;
$company_token->account_id = $account->id;
$company_token->name = 'test token';
$company_token->token = Str::random(64);
$company_token->is_system = true;
$company_token->save();
$user->companies()->attach($company->id, [
'account_id' => $account->id,
'is_owner' => 1,
'is_admin' => 1,
'is_locked' => 0,
'notifications' => CompanySettings::notificationDefaults(),
// 'permissions' => '',
'settings' => null,
]);
Product::factory()->count(50)->create([
'user_id' => $user->id,
'company_id' => $company->id,
]);
$this->info('Creating '.$this->count.' clients');
for ($x = 0; $x < $this->count; $x++) {
$z = $x + 1;
$this->info('Creating client # '.$z);
$this->createClient($company, $user);
}
for ($x = 0; $x < $this->count; $x++) {
$client = $company->clients->random();
$this->info('creating invoice for client #'.$client->id);
$this->createInvoice($client);
$client = $company->clients->random();
$this->info('creating credit for client #'.$client->id);
$this->createCredit($client);
$client = $company->clients->random();
$this->info('creating quote for client #'.$client->id);
$this->createQuote($client);
$client = $company->clients->random();
$this->info('creating expense for client #'.$client->id);
$this->createExpense($client);
$client = $company->clients->random();
$this->info('creating vendor for client #'.$client->id);
$this->createVendor($client);
$client = $company->clients->random();
$this->info('creating task for client #'.$client->id);
$this->createTask($client);
$client = $company->clients->random();
$this->info('creating project for client #'.$client->id);
$this->createProject($client);
} }
} }
/** private function createMediumAccount()
* @param $client
*/
private function createInvoices($client, $isQuote = false)
{ {
for ($i = 0; $i < $this->count; $i++) { $this->info('Creating Medium Account and Company');
$data = [
'is_public' => true,
'is_quote' => $isQuote,
'client_id' => $client->id,
'invoice_date_sql' => date_create()->modify(rand(-100, 100) . ' days')->format('Y-m-d'),
'due_date_sql' => date_create()->modify(rand(-100, 100) . ' days')->format('Y-m-d'),
'invoice_items' => [[
'product_key' => $this->faker->word,
'qty' => $this->faker->randomDigit + 1,
'cost' => $this->faker->randomFloat(2, 1, 10),
'notes' => $this->faker->text($this->faker->numberBetween(50, 300)),
]],
];
$invoice = $this->invoiceRepo->save($data); $account = Account::factory()->create();
$this->info('Invoice: ' . $invoice->invoice_number); $company = Company::factory()->create([
'account_id' => $account->id,
'slack_webhook_url' => config('ninja.notification.slack'),
]);
if (! $isQuote) { $account->default_company_id = $company->id;
$this->createPayment($client, $invoice); $account->save();
$user = User::whereEmail('medium@example.com')->first();
if (! $user) {
$user = User::factory()->create([
'account_id' => $account->id,
'email' => 'medium@example.com',
'confirmation_code' => $this->createDbHash(config('database.default')),
]);
}
$company_token = new CompanyToken;
$company_token->user_id = $user->id;
$company_token->company_id = $company->id;
$company_token->account_id = $account->id;
$company_token->name = 'test token';
$company_token->token = Str::random(64);
$company_token->is_system = true;
$company_token->save();
$user->companies()->attach($company->id, [
'account_id' => $account->id,
'is_owner' => 1,
'is_admin' => 1,
'is_locked' => 0,
'notifications' => CompanySettings::notificationDefaults(),
// 'permissions' => '',
'settings' => null,
]);
Product::factory()->count(50)->create([
'user_id' => $user->id,
'company_id' => $company->id,
]);
$this->count = $this->count * 10;
$this->info('Creating '.$this->count.' clients');
for ($x = 0; $x < $this->count; $x++) {
$z = $x + 1;
$this->info('Creating client # '.$z);
$this->createClient($company, $user);
}
for ($x = 0; $x < $this->count * 100; $x++) {
$client = $company->clients->random();
$this->info('creating invoice for client #'.$client->id);
$this->createInvoice($client);
$client = $company->clients->random();
$this->info('creating credit for client #'.$client->id);
$this->createCredit($client);
$client = $company->clients->random();
$this->info('creating quote for client #'.$client->id);
$this->createQuote($client);
$client = $company->clients->random();
$this->info('creating expense for client #'.$client->id);
$this->createExpense($client);
$client = $company->clients->random();
$this->info('creating vendor for client #'.$client->id);
$this->createVendor($client);
$client = $company->clients->random();
$this->info('creating task for client #'.$client->id);
$this->createTask($client);
$client = $company->clients->random();
$this->info('creating project for client #'.$client->id);
$this->createProject($client);
}
}
private function createLargeAccount()
{
$this->info('Creating Large Account and Company');
$account = Account::factory()->create();
$company = Company::factory()->create([
'account_id' => $account->id,
'slack_webhook_url' => config('ninja.notification.slack'),
'is_large' => true,
]);
$account->default_company_id = $company->id;
$account->save();
$user = User::whereEmail('large@example.com')->first();
if (! $user) {
$user = User::factory()->create([
'account_id' => $account->id,
'email' => 'large@example.com',
'confirmation_code' => $this->createDbHash(config('database.default')),
]);
}
$company_token = new CompanyToken;
$company_token->user_id = $user->id;
$company_token->company_id = $company->id;
$company_token->account_id = $account->id;
$company_token->name = 'test token';
$company_token->token = Str::random(64);
$company_token->is_system = true;
$company_token->save();
$user->companies()->attach($company->id, [
'account_id' => $account->id,
'is_owner' => 1,
'is_admin' => 1,
'is_locked' => 0,
'notifications' => CompanySettings::notificationDefaults(),
// 'permissions' => '',
'settings' => null,
]);
Product::factory()->count(15000)->create([
'user_id' => $user->id,
'company_id' => $company->id,
]);
$this->count = $this->count * 10;
$this->info('Creating '.$this->count.' clients');
for ($x = 0; $x < $this->count * 100; $x++) {
$z = $x + 1;
$this->info('Creating client # '.$z);
$this->createClient($company, $user);
}
for ($x = 0; $x < $this->count; $x++) {
$client = $company->clients->random();
$this->info('creating invoice for client #'.$client->id);
$this->createInvoice($client);
$client = $company->clients->random();
$this->info('creating credit for client #'.$client->id);
$this->createCredit($client);
$client = $company->clients->random();
$this->info('creating quote for client #'.$client->id);
$this->createQuote($client);
$client = $company->clients->random();
$this->info('creating expense for client #'.$client->id);
$this->createExpense($client);
$client = $company->clients->random();
$this->info('creating vendor for client #'.$client->id);
$this->createVendor($client);
$client = $company->clients->random();
$this->info('creating task for client #'.$client->id);
$this->createTask($client);
$client = $company->clients->random();
$this->info('creating project for client #'.$client->id);
$this->createProject($client);
}
}
private function createClient($company, $user)
{
// dispatch(function () use ($company, $user) {
// });
$client = Client::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
]);
Document::factory()->count(50)->create([
'user_id' => $user->id,
'company_id' => $company->id,
'documentable_type' => Client::class,
'documentable_id' => $client->id,
]);
ClientContact::factory()->create([
'user_id' => $user->id,
'client_id' => $client->id,
'company_id' => $company->id,
'is_primary' => 1,
]);
ClientContact::factory()->count(rand(1, 5))->create([
'user_id' => $user->id,
'client_id' => $client->id,
'company_id' => $company->id,
]);
$client->number = $this->getNextClientNumber($client);
$settings = $client->settings;
$settings->currency_id = (string) rand(1, 79);
$client->settings = $settings;
$country = Country::all()->random();
$client->country_id = $country->id;
$client->save();
}
private function createExpense($client)
{
Expense::factory()->count(rand(1, 5))->create([
'user_id' => $client->user->id,
'client_id' => $client->id,
'company_id' => $client->company->id,
]);
}
private function createVendor($client)
{
$vendor = Vendor::factory()->create([
'user_id' => $client->user->id,
'company_id' => $client->company->id,
]);
Document::factory()->count(1)->create([
'user_id' => $client->user->id,
'company_id' => $client->company_id,
'documentable_type' => Vendor::class,
'documentable_id' => $vendor->id,
]);
VendorContact::factory()->create([
'user_id' => $client->user->id,
'vendor_id' => $vendor->id,
'company_id' => $client->company->id,
'is_primary' => 1,
]);
VendorContact::factory()->count(rand(1, 5))->create([
'user_id' => $client->user->id,
'vendor_id' => $vendor->id,
'company_id' => $client->company->id,
'is_primary' => 0,
]);
}
private function createTask($client)
{
$vendor = Task::factory()->create([
'user_id' => $client->user->id,
'company_id' => $client->company->id,
]);
Document::factory()->count(5)->create([
'user_id' => $client->user->id,
'company_id' => $client->company_id,
'documentable_type' => Task::class,
'documentable_id' => $vendor->id,
]);
}
private function createProject($client)
{
$vendor = Project::factory()->create([
'user_id' => $client->user->id,
'company_id' => $client->company->id,
]);
Document::factory()->count(5)->create([
'user_id' => $client->user->id,
'company_id' => $client->company_id,
'documentable_type' => Project::class,
'documentable_id' => $vendor->id,
]);
}
private function createInvoice($client)
{
$faker = Factory::create();
$invoice = InvoiceFactory::create($client->company->id, $client->user->id); //stub the company and user_id
$invoice->client_id = $client->id;
// $invoice->date = $faker->date();
$dateable = Carbon::now()->subDays(rand(0, 90));
$invoice->date = $dateable;
$invoice->line_items = $this->buildLineItems(rand(1, 10));
$invoice->uses_inclusive_taxes = false;
if (rand(0, 1)) {
$invoice->tax_name1 = 'GST';
$invoice->tax_rate1 = 10.00;
}
if (rand(0, 1)) {
$invoice->tax_name2 = 'VAT';
$invoice->tax_rate2 = 17.50;
}
if (rand(0, 1)) {
$invoice->tax_name3 = 'CA Sales Tax';
$invoice->tax_rate3 = 5;
}
$invoice->custom_value1 = $faker->date();
$invoice->custom_value2 = rand(0, 1) ? 'yes' : 'no';
$invoice->save();
$invoice_calc = new InvoiceSum($invoice);
$invoice_calc->build();
$invoice = $invoice_calc->getInvoice();
$invoice->save();
$invoice->service()->createInvitations()->markSent();
if (rand(0, 1)) {
$this->invoice_repo->markSent($invoice);
}
if (rand(0, 1)) {
$invoice = $invoice->service()->markPaid()->save();
}
Document::factory()->count(5)->create([
'user_id' => $invoice->user->id,
'company_id' => $invoice->company_id,
'documentable_type' => Invoice::class,
'documentable_id' => $invoice->id,
]);
RecurringInvoice::factory()->create(['user_id' => $invoice->user->id, 'company_id' => $invoice->company->id, 'client_id' => $invoice->client_id]);
event(new InvoiceWasCreated($invoice, $invoice->company, Ninja::eventVars()));
}
private function createCredit($client)
{
$faker = Factory::create();
$credit = Credit::factory()->create(['user_id' => $client->user->id, 'company_id' => $client->company->id, 'client_id' => $client->id]);
$dateable = Carbon::now()->subDays(rand(0, 90));
$credit->date = $dateable;
$credit->line_items = $this->buildLineItems(rand(1, 10));
$credit->uses_inclusive_taxes = false;
if (rand(0, 1)) {
$credit->tax_name1 = 'GST';
$credit->tax_rate1 = 10.00;
}
if (rand(0, 1)) {
$credit->tax_name2 = 'VAT';
$credit->tax_rate2 = 17.50;
}
if (rand(0, 1)) {
$credit->tax_name3 = 'CA Sales Tax';
$credit->tax_rate3 = 5;
}
$credit->save();
$invoice_calc = new InvoiceSum($credit);
$invoice_calc->build();
$credit = $invoice_calc->getCredit();
$credit->save();
$credit->service()->markSent()->save();
$credit->service()->createInvitations();
}
private function createQuote($client)
{
$faker = Factory::create();
//$quote = QuoteFactory::create($client->company->id, $client->user->id);//stub the company and user_id
$quote = Quote::factory()->create(['user_id' => $client->user->id, 'company_id' => $client->company->id, 'client_id' => $client->id]);
$quote->date = $faker->date();
$quote->client_id = $client->id;
$quote->setRelation('client', $client);
$quote->line_items = $this->buildLineItems(rand(1, 10));
$quote->uses_inclusive_taxes = false;
if (rand(0, 1)) {
$quote->tax_name1 = 'GST';
$quote->tax_rate1 = 10.00;
}
if (rand(0, 1)) {
$quote->tax_name2 = 'VAT';
$quote->tax_rate2 = 17.50;
}
if (rand(0, 1)) {
$quote->tax_name3 = 'CA Sales Tax';
$quote->tax_rate3 = 5;
}
$quote->save();
$quote_calc = new InvoiceSum($quote);
$quote_calc->build();
$quote = $quote_calc->getQuote();
$quote->save();
$quote->service()->markSent()->save();
$quote->service()->createInvitations();
}
private function buildLineItems($count = 1)
{
$line_items = [];
for ($x = 0; $x < $count; $x++) {
$item = InvoiceItemFactory::create();
$item->quantity = 1;
//$item->cost = 10;
if (rand(0, 1)) {
$item->tax_name1 = 'GST';
$item->tax_rate1 = 10.00;
}
if (rand(0, 1)) {
$item->tax_name1 = 'VAT';
$item->tax_rate1 = 17.50;
}
if (rand(0, 1)) {
$item->tax_name1 = 'Sales Tax';
$item->tax_rate1 = 5;
}
$product = Product::all()->random();
$item->cost = (float) $product->cost;
$item->product_key = $product->product_key;
$item->notes = $product->notes;
$item->custom_value1 = $product->custom_value1;
$item->custom_value2 = $product->custom_value2;
$item->custom_value3 = $product->custom_value3;
$item->custom_value4 = $product->custom_value4;
$line_items[] = $item;
}
return $line_items;
}
private function warmCache()
{
/* Warm up the cache !*/
$cached_tables = config('ninja.cached_tables');
foreach ($cached_tables as $name => $class) {
if (! Cache::has($name)) {
// check that the table exists in case the migration is pending
if (! Schema::hasTable((new $class())->getTable())) {
continue;
}
if ($name == 'payment_terms') {
$orderBy = 'num_days';
} elseif ($name == 'fonts') {
$orderBy = 'sort_order';
} elseif (in_array($name, ['currencies', 'industries', 'languages', 'countries', 'banks'])) {
$orderBy = 'name';
} else {
$orderBy = 'id';
}
$tableData = $class::orderBy($orderBy)->get();
if ($tableData->count()) {
Cache::forever($name, $tableData);
}
} }
} }
} }
/**
* @param $client
* @param $invoice
*/
private function createPayment($client, $invoice)
{
$data = [
'invoice_id' => $invoice->id,
'client_id' => $client->id,
'amount' => $this->faker->randomFloat(2, 0, $invoice->amount),
'payment_date_sql' => date_create()->modify(rand(-100, 100) . ' days')->format('Y-m-d'),
];
$payment = $this->paymentRepo->save($data);
$this->info('Payment: ' . $payment->amount);
}
private function createTasks($client)
{
$data = [
'client_id' => $client->id,
'name' => $this->faker->sentence(3),
];
$project = $this->projectRepo->save($data);
for ($i = 0; $i < $this->count; $i++) {
$startTime = date_create()->modify(rand(-100, 100) . ' days')->format('U');
$endTime = $startTime + (60 * 60 * 2);
$timeLog = "[[{$startTime},{$endTime}]]";
$data = [
'client_id' => $client->id,
'project_id' => $project->id,
'description' => $this->faker->text($this->faker->numberBetween(50, 300)),
'time_log' => $timeLog,
];
$this->taskRepo->save(false, $data);
}
}
private function createVendors()
{
for ($i = 0; $i < $this->count; $i++) {
$data = [
'name' => $this->faker->name,
'address1' => $this->faker->streetAddress,
'address2' => $this->faker->secondaryAddress,
'city' => $this->faker->city,
'state' => $this->faker->state,
'postal_code' => $this->faker->postcode,
'vendor_contacts' => [[
'first_name' => $this->faker->firstName,
'last_name' => $this->faker->lastName,
'email' => $this->faker->safeEmail,
'phone' => $this->faker->phoneNumber,
]],
];
$vendor = $this->vendorRepo->save($data);
$this->info('Vendor: ' . $vendor->name);
$this->createExpense($vendor);
}
}
/**
* @param $vendor
*/
private function createExpense($vendor)
{
for ($i = 0; $i < $this->count; $i++) {
$data = [
'vendor_id' => $vendor->id,
'amount' => $this->faker->randomFloat(2, 1, 10),
'expense_date' => date_create()->modify(rand(-100, 100) . ' days')->format('Y-m-d'),
'public_notes' => '',
];
$expense = $this->expenseRepo->save($data);
$this->info('Expense: ' . $expense->amount);
}
}
private function createOtherObjects()
{
$this->createTaxRate('Tax 1', 10, 1);
$this->createTaxRate('Tax 2', 20, 2);
$this->createCategory('Category 1', 1);
$this->createCategory('Category 1', 2);
$this->createProject('Project 1', 1);
$this->createProject('Project 2', 2);
}
private function createTaxRate($name, $rate, $publicId)
{
$taxRate = new TaxRate();
$taxRate->name = $name;
$taxRate->rate = $rate;
$taxRate->account_id = 1;
$taxRate->user_id = 1;
$taxRate->public_id = $publicId;
$taxRate->save();
}
private function createCategory($name, $publicId)
{
$category = new ExpenseCategory();
$category->name = $name;
$category->account_id = 1;
$category->user_id = 1;
$category->public_id = $publicId;
$category->save();
}
private function createProject($name, $publicId)
{
$project = new Project();
$project->name = $name;
$project->account_id = 1;
$project->client_id = 1;
$project->user_id = 1;
$project->public_id = $publicId;
$project->save();
}
/**
* @return array
*/
protected function getArguments()
{
return [];
}
/**
* @return array
*/
protected function getOptions()
{
return [];
}
} }

View file

@ -0,0 +1,648 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Console\Commands;
use App\DataMapper\CompanySettings;
use App\Events\Invoice\InvoiceWasCreated;
use App\Factory\InvoiceFactory;
use App\Factory\InvoiceItemFactory;
use App\Factory\RecurringInvoiceFactory;
use App\Helpers\Invoice\InvoiceSum;
use App\Jobs\Company\CreateCompanyPaymentTerms;
use App\Jobs\Company\CreateCompanyTaskStatuses;
use App\Jobs\Ninja\CompanySizeCheck;
use App\Jobs\Util\VersionCheck;
use App\Models\Account;
use App\Models\BankIntegration;
use App\Models\BankTransaction;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Company;
use App\Models\CompanyToken;
use App\Models\Credit;
use App\Models\Expense;
use App\Models\Product;
use App\Models\Project;
use App\Models\Quote;
use App\Models\RecurringInvoice;
use App\Models\Task;
use App\Models\TaskStatus;
use App\Models\User;
use App\Models\Vendor;
use App\Models\VendorContact;
use App\Repositories\InvoiceRepository;
use App\Utils\Ninja;
use App\Utils\Traits\AppSetup;
use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\MakesHash;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
class DemoMode extends Command
{
use MakesHash, GeneratesCounter, AppSetup;
protected $signature = 'ninja:demo-mode';
protected $description = 'Setup demo mode';
protected $invoice_repo;
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
set_time_limit(0);
if (config('ninja.is_docker')) {
return;
}
$this->invoice_repo = new InvoiceRepository();
$cached_tables = config('ninja.cached_tables');
$this->info('Migrating');
Artisan::call('migrate:fresh --force');
$this->info('Seeding');
Artisan::call('db:seed --force');
$this->buildCache(true);
$this->info('Seeding Random Data');
$this->createSmallAccount();
(new VersionCheck())->handle();
(new CompanySizeCheck())->handle();
}
private function createSmallAccount()
{
$faker = \Faker\Factory::create();
$this->count = 25;
$this->info('Creating Small Account and Company');
$account = Account::factory()->create();
$company = Company::factory()->create([
'account_id' => $account->id,
'slack_webhook_url' => config('ninja.notification.slack'),
'enabled_modules' => 32767,
'company_key' => 'KEY',
'enable_shop_api' => true,
'markdown_email_enabled' => true,
'markdown_enabled' => false,
]);
$settings = $company->settings;
$settings->name = $faker->company();
$settings->address1 = $faker->buildingNumber();
$settings->address2 = $faker->streetAddress();
$settings->city = $faker->city();
$settings->state = $faker->state();
$settings->postal_code = $faker->postcode();
$settings->website = $faker->url();
$settings->vat_number = (string) $faker->numberBetween(123456789, 987654321);
$settings->phone = (string) $faker->phoneNumber();
$company->settings = $settings;
$company->save();
$account->default_company_id = $company->id;
$account->save();
$user = User::whereEmail('small@example.com')->first();
if (! $user) {
$user = User::factory()->create([
'account_id' => $account->id,
'email' => 'small@example.com',
'confirmation_code' => $this->createDbHash(config('database.default')),
'email_verified_at' => now(),
]);
}
(new CreateCompanyPaymentTerms($company, $user))->handle();
(new CreateCompanyTaskStatuses($company, $user))->handle();
$company_token = new CompanyToken;
$company_token->user_id = $user->id;
$company_token->company_id = $company->id;
$company_token->account_id = $account->id;
$company_token->name = 'test token';
$company_token->token = Str::random(64);
$company_token->is_system = true;
$company_token->save();
$user->companies()->attach($company->id, [
'account_id' => $account->id,
'is_owner' => 1,
'is_admin' => 1,
'is_locked' => 0,
'notifications' => CompanySettings::notificationDefaults(),
// 'permissions' => '',
'settings' => null,
]);
$u2 = User::where('email', 'demo@invoiceninja.com')->first();
if (! $u2) {
$u2 = User::factory()->create([
'email' => 'demo@invoiceninja.com',
'password' => Hash::make('Password0'),
'account_id' => $account->id,
'confirmation_code' => $this->createDbHash(config('database.default')),
'email_verified_at' => now(),
]);
$company_token = new CompanyToken;
$company_token->user_id = $u2->id;
$company_token->company_id = $company->id;
$company_token->account_id = $account->id;
$company_token->name = 'test token';
$company_token->token = 'TOKEN';
$company_token->save();
$u2->companies()->attach($company->id, [
'account_id' => $account->id,
'is_owner' => 1,
'is_admin' => 1,
'is_locked' => 0,
'notifications' => CompanySettings::notificationDefaults(),
'permissions' => '',
'settings' => null,
]);
}
Product::factory()->count(50)->create([
'user_id' => $user->id,
'company_id' => $company->id,
]);
$bi = BankIntegration::factory()->create([
'account_id' => $account->id,
'company_id' => $company->id,
'user_id' => $user->id,
]);
BankTransaction::factory()->count(50)->create([
'bank_integration_id' => $bi->id,
'user_id' => $user->id,
'company_id' => $company->id,
]);
$this->info('Creating '.$this->count.' clients');
for ($x = 0; $x < $this->count; $x++) {
$z = $x + 1;
$this->info('Creating client # '.$z);
$this->createClient($company, $user, $u2->id);
}
for ($x = 0; $x < $this->count; $x++) {
$client = $company->clients->random();
$this->info('creating entities for client #'.$client->id);
$this->createInvoice($client, $u2->id);
$this->createRecurringInvoice($client, $u2->id);
$client = $company->clients->random();
$this->createCredit($client, $u2->id);
$client = $company->clients->random();
$this->createQuote($client, $u2->id);
$client = $company->clients->random();
$this->createExpense($client);
$client = $company->clients->random();
$this->createVendor($client, $u2->id);
$client = $company->clients->random();
$this->createTask($client, $u2->id);
$client = $company->clients->random();
$this->createProject($client, $u2->id);
}
}
private function createClient($company, $user, $assigned_user_id = null)
{
// dispatch(function () use ($company, $user) {
// });
$client = Client::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
]);
ClientContact::factory()->create([
'user_id' => $user->id,
'client_id' => $client->id,
'company_id' => $company->id,
'is_primary' => 1,
]);
ClientContact::factory()->count(rand(1, 5))->create([
'user_id' => $user->id,
'client_id' => $client->id,
'company_id' => $company->id,
]);
$client->number = $this->getNextClientNumber($client);
$settings = $client->settings;
$settings->currency_id = (string) rand(1, 3);
$client->settings = $settings;
if (rand(0, 1)) {
$client->assigned_user_id = $assigned_user_id;
}
$client->country_id = array_rand([36, 392, 840, 124, 276, 826]);
$client->save();
}
private function createExpense($client)
{
Expense::factory()->count(rand(1, 5))->create([
'user_id' => $client->user_id,
'client_id' => $client->id,
'company_id' => $client->company_id,
]);
Expense::all()->each(function ($expense) {
$expense->number = $this->getNextExpenseNumber($expense);
$expense->save();
});
}
private function createVendor($client, $assigned_user_id = null)
{
$vendor = Vendor::factory()->create([
'user_id' => $client->user_id,
'company_id' => $client->company_id,
]);
VendorContact::factory()->create([
'user_id' => $client->user->id,
'vendor_id' => $vendor->id,
'company_id' => $client->company_id,
'is_primary' => 1,
]);
VendorContact::factory()->count(rand(1, 5))->create([
'user_id' => $client->user->id,
'vendor_id' => $vendor->id,
'company_id' => $client->company_id,
'is_primary' => 0,
]);
$vendor->number = $this->getNextVendorNumber($vendor);
$vendor->save();
}
private function createTask($client, $assigned_user_id = null)
{
$task = Task::factory()->create([
'user_id' => $client->user->id,
'company_id' => $client->company_id,
'client_id' => $client->id,
]);
$task->status_id = TaskStatus::all()->random()->id;
$task->number = $this->getNextTaskNumber($task);
$task->save();
}
private function createProject($client, $assigned_user_id = null)
{
$project = Project::factory()->create([
'user_id' => $client->user->id,
'company_id' => $client->company_id,
'client_id' => $client->id,
]);
$project->number = $this->getNextProjectNumber($project);
$project->save();
}
private function createInvoice($client, $assigned_user_id = null)
{
$faker = \Faker\Factory::create();
$invoice = InvoiceFactory::create($client->company->id, $client->user->id); //stub the company and user_id
$invoice->client_id = $client->id;
if ((bool) rand(0, 1)) {
$dateable = Carbon::now()->subDays(rand(0, 90));
} else {
$dateable = Carbon::now()->addDays(rand(0, 90));
}
$invoice->date = $dateable;
$invoice->line_items = $this->buildLineItems(rand(1, 10));
$invoice->uses_inclusive_taxes = false;
// if (rand(0, 1)) {
// $invoice->tax_name1 = 'GST';
// $invoice->tax_rate1 = 10.00;
// }
// if (rand(0, 1)) {
// $invoice->tax_name2 = 'VAT';
// $invoice->tax_rate2 = 17.50;
// }
// if (rand(0, 1)) {
// $invoice->tax_name3 = 'CA Sales Tax';
// $invoice->tax_rate3 = 5;
// }
// $invoice->custom_value1 = $faker->date();
// $invoice->custom_value2 = rand(0, 1) ? 'yes' : 'no';
$invoice->save();
$invoice_calc = new InvoiceSum($invoice);
$invoice_calc->build();
$invoice = $invoice_calc->getInvoice();
if (rand(0, 1)) {
$invoice->assigned_user_id = $assigned_user_id;
}
$invoice->save();
$invoice->service()->createInvitations()->markSent();
$this->invoice_repo->markSent($invoice);
if ((bool) rand(0, 2)) {
$invoice = $invoice->service()->markPaid()->save();
$invoice->payments->each(function ($payment) {
$payment->date = now()->addDays(rand(-30, 30));
$payment->save();
});
}
event(new InvoiceWasCreated($invoice, $invoice->company, Ninja::eventVars()));
}
private function createRecurringInvoice($client, $assigned_user_id = null)
{
$faker = \Faker\Factory::create();
$invoice = RecurringInvoiceFactory::create($client->company->id, $client->user->id); //stub the company and user_id
$invoice->client_id = $client->id;
$invoice->frequency_id = RecurringInvoice::FREQUENCY_MONTHLY;
$invoice->last_sent_date = now()->subMonth();
$invoice->next_send_date = now()->addMonthNoOverflow();
if ((bool) rand(0, 1)) {
$dateable = Carbon::now()->subDays(rand(0, 90));
} else {
$dateable = Carbon::now()->addDays(rand(0, 90));
}
$invoice->date = $dateable;
$invoice->line_items = $this->buildLineItems(rand(1, 10));
$invoice->uses_inclusive_taxes = false;
// if (rand(0, 1)) {
// $invoice->tax_name1 = 'GST';
// $invoice->tax_rate1 = 10.00;
// }
// if (rand(0, 1)) {
// $invoice->tax_name2 = 'VAT';
// $invoice->tax_rate2 = 17.50;
// }
// if (rand(0, 1)) {
// $invoice->tax_name3 = 'CA Sales Tax';
// $invoice->tax_rate3 = 5;
// }
// $invoice->custom_value1 = $faker->date();
// $invoice->custom_value2 = rand(0, 1) ? 'yes' : 'no';
$invoice->save();
$invoice_calc = new InvoiceSum($invoice);
$invoice_calc->build();
$invoice = $invoice_calc->getInvoice();
if (rand(0, 1)) {
$invoice->assigned_user_id = $assigned_user_id;
}
$invoice->number = $this->getNextRecurringInvoiceNumber($client, $invoice);
$invoice->save();
}
private function createCredit($client, $assigned_user_id = null)
{
$faker = \Faker\Factory::create();
$credit = Credit::factory()->create(['user_id' => $client->user->id, 'company_id' => $client->company->id, 'client_id' => $client->id]);
if ((bool) rand(0, 1)) {
$dateable = Carbon::now()->subDays(rand(0, 90));
} else {
$dateable = Carbon::now()->addDays(rand(0, 90));
}
$credit->date = $dateable;
$credit->line_items = $this->buildLineItems(rand(1, 10));
$credit->uses_inclusive_taxes = false;
// if (rand(0, 1)) {
// $credit->tax_name1 = 'GST';
// $credit->tax_rate1 = 10.00;
// }
// if (rand(0, 1)) {
// $credit->tax_name2 = 'VAT';
// $credit->tax_rate2 = 17.50;
// }
// if (rand(0, 1)) {
// $credit->tax_name3 = 'CA Sales Tax';
// $credit->tax_rate3 = 5;
// }
$credit->save();
$invoice_calc = new InvoiceSum($credit);
$invoice_calc->build();
$credit = $invoice_calc->getCredit();
if (rand(0, 1)) {
$credit->assigned_user_id = $assigned_user_id;
}
$credit->save();
$credit->service()->markSent()->save();
$credit->service()->createInvitations();
}
private function createQuote($client, $assigned_user_id = null)
{
$faker = \Faker\Factory::create();
$quote = Quote::factory()->create(['user_id' => $client->user->id, 'company_id' => $client->company_id, 'client_id' => $client->id]);
if ((bool) rand(0, 1)) {
$dateable = Carbon::now()->subDays(rand(1, 30));
$dateable_due = $dateable->addDays(rand(1, 30));
} else {
$dateable = Carbon::now()->addDays(rand(1, 30));
$dateable_due = $dateable->addDays(rand(-10, 30));
}
$quote->date = $dateable;
$quote->due_date = $dateable_due;
$quote->client_id = $client->id;
$quote->setRelation('client', $client);
$quote->line_items = $this->buildLineItems(rand(1, 10));
$quote->uses_inclusive_taxes = false;
// if (rand(0, 1)) {
// $quote->tax_name1 = 'GST';
// $quote->tax_rate1 = 10.00;
// }
// if (rand(0, 1)) {
// $quote->tax_name2 = 'VAT';
// $quote->tax_rate2 = 17.50;
// }
// if (rand(0, 1)) {
// $quote->tax_name3 = 'CA Sales Tax';
// $quote->tax_rate3 = 5;
// }
$quote->save();
$quote_calc = new InvoiceSum($quote);
$quote_calc->build();
$quote = $quote_calc->getQuote();
if (rand(0, 1)) {
$quote->assigned_user_id = $assigned_user_id;
}
$quote->save();
$quote->service()->markSent()->save();
$quote->service()->createInvitations();
}
private function buildLineItems($count = 1)
{
$line_items = [];
for ($x = 0; $x < $count; $x++) {
$item = InvoiceItemFactory::create();
$item->quantity = 1;
//$item->cost = 10;
// if (rand(0, 1)) {
// $item->tax_name1 = 'GST';
// $item->tax_rate1 = 10.00;
// }
// if (rand(0, 1)) {
// $item->tax_name1 = 'VAT';
// $item->tax_rate1 = 17.50;
// }
// if (rand(0, 1)) {
// $item->tax_name1 = 'Sales Tax';
// $item->tax_rate1 = 5;
// }
$product = Product::all()->random();
$item->cost = (float) $product->cost;
$item->product_key = $product->product_key;
$item->notes = $product->notes;
$item->custom_value1 = $product->custom_value1;
$item->custom_value2 = $product->custom_value2;
$item->custom_value3 = $product->custom_value3;
$item->custom_value4 = $product->custom_value4;
$line_items[] = $item;
}
return $line_items;
}
private function warmCache()
{
/* Warm up the cache !*/
$cached_tables = config('ninja.cached_tables');
foreach ($cached_tables as $name => $class) {
if (! Cache::has($name)) {
// check that the table exists in case the migration is pending
if (! Schema::hasTable((new $class())->getTable())) {
continue;
}
if ($name == 'payment_terms') {
$orderBy = 'num_days';
} elseif ($name == 'fonts') {
$orderBy = 'sort_order';
} elseif (in_array($name, ['currencies', 'industries', 'languages', 'countries', 'banks'])) {
$orderBy = 'name';
} else {
$orderBy = 'id';
}
$tableData = $class::orderBy($orderBy)->get();
if ($tableData->count()) {
Cache::forever($name, $tableData);
}
}
}
}
}

View file

@ -0,0 +1,88 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Console\Commands;
use App\Libraries\MultiDB;
use App\Models\Design;
use Illuminate\Console\Command;
use stdClass;
class DesignUpdate extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:design-update';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update the system designs when changes are made.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
//always return state to first DB
$current_db = config('database.default');
if (! config('ninja.db.multi_db_enabled')) {
$this->handleOnDb();
} else {
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$this->handleOnDb($db);
}
MultiDB::setDB($current_db);
}
}
private function handleOnDb()
{
foreach (Design::whereIsCustom(false)->get() as $design) {
$invoice_design = new \App\Services\PdfMaker\Design(strtolower($design->name));
$invoice_design->document();
$design_object = new stdClass;
$design_object->includes = $invoice_design->getSectionHTML('style');
$design_object->header = $invoice_design->getSectionHTML('header');
$design_object->body = $invoice_design->getSectionHTML('body');
$design_object->product = '';
$design_object->task = '';
$design_object->footer = $invoice_design->getSectionHTML('footer');
$design->design = $design_object;
$design->save();
}
}
}

View file

@ -1,115 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Libraries\Utils;
use App\Models\User;
use App\Traits\GenerateMigrationResources;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Auth;
class ExportMigrations extends Command
{
use GenerateMigrationResources;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'migrations:export {--user=} {--random=}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Export account migrations to folder.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->info('Note: Migrations will be stored inside of (storage/migrations) folder.');
if($this->option('user')) {
$record = User::findOrFail($this->option('user'));
return $this->export($record);
}
if($this->option('random')){
User::all()->random(200)->each(function ($user){
$this->export($user);
});
return;
}
$users = User::all();
foreach($users as $user) {
Auth::login($user);
$this->export($user);
}
}
private function export($user)
{
$this->account = $user->account;
$date = date('Y-m-d');
$accountKey = $this->account->account_key;
$output = fopen('php://output', 'w') or Utils::fatalError();
$fileName = "{$accountKey}-{$date}-invoiceninja";
$data['data'] = [
'account' => $this->getAccount(),
'company' => $this->getCompany(),
'users' => $this->getUsers(),
'tax_rates' => $this->getTaxRates(),
'payment_terms' => $this->getPaymentTerms(),
'clients' => $this->getClients(),
'company_gateways' => $this->getCompanyGateways(),
'client_gateway_tokens' => $this->getClientGatewayTokens(),
'vendors' => $this->getVendors(),
'projects' => $this->getProjects(),
'products' => $this->getProducts(),
'credits' => $this->getCreditsNotes(),
'invoices' => $this->getInvoices(),
'recurring_invoices' => $this->getRecurringInvoices(),
'quotes' => $this->getQuotes(),
'payments' => array_merge($this->getPayments(), $this->getCredits()),
'documents' => $this->getDocuments(),
'expense_categories' => $this->getExpenseCategories(),
'task_statuses' => $this->getTaskStatuses(),
'expenses' => $this->getExpenses(),
'tasks' => $this->getTasks(),
'documents' => $this->getDocuments(),
];
$file = storage_path("migrations/{$fileName}.zip");
$zip = new \ZipArchive();
$zip->open($file, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
$zip->addFromString('migration.json', json_encode($data, JSON_PRETTY_PRINT));
$zip->close();
$this->info('User with id #' . $user->id . ' exported.');
}
}

View file

@ -0,0 +1,117 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Console\Commands;
use App\Exceptions\MigrationValidatorFailed;
use App\Exceptions\NonExistingMigrationFile;
use App\Exceptions\ProcessingMigrationArchiveFailed;
use App\Exceptions\ResourceDependencyMissing;
use App\Exceptions\ResourceNotAvailableForMigration;
use App\Jobs\Util\Import;
use App\Libraries\MultiDB;
use App\Mail\MigrationFailed;
use App\Models\User;
use App\Utils\Traits\AppSetup;
use App\Utils\Traits\MakesHash;
use DirectoryIterator;
use Illuminate\Console\Command;
use ZipArchive;
class HostedMigrations extends Command
{
use MakesHash;
use AppSetup;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:import {--email=}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Import a v4 migration file';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->buildCache();
if (! MultiDB::userFindAndSetDb($this->option('email'))) {
$this->info('Could not find a user with that email address');
return;
}
$user = User::where('email', $this->option('email'))->first();
if (! $user) {
$this->info('There was a problem getting the user, did you set the right DB?');
return;
}
$path = public_path('storage/migrations/import');
$directory = new DirectoryIterator($path);
foreach ($directory as $file) {
if ($file->getExtension() === 'zip') {
$company = $user->companies()->first();
$this->info('Started processing: '.$file->getBasename().' at '.now());
$zip = new ZipArchive();
$archive = $zip->open($file->getRealPath());
try {
if (! $archive) {
throw new ProcessingMigrationArchiveFailed('Processing migration archive failed. Migration file is possibly corrupted.');
}
$filename = pathinfo($file->getRealPath(), PATHINFO_FILENAME);
$zip->extractTo(public_path("storage/migrations/{$filename}"));
$zip->close();
$import_file = public_path("storage/migrations/$filename/migration.json");
Import::dispatch($import_file, $user->companies()->first(), $user);
} catch (NonExistingMigrationFile | ProcessingMigrationArchiveFailed | ResourceNotAvailableForMigration | MigrationValidatorFailed | ResourceDependencyMissing $e) {
\Mail::to($this->user)->send(new MigrationFailed($e, $e->getMessage()));
if (app()->environment() !== 'production') {
info($e->getMessage());
}
}
}
}
}
}

View file

@ -0,0 +1,63 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Console\Commands;
use App\Models\Company;
use App\Utils\Ninja;
use Illuminate\Console\Command;
class HostedUsers extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:sync-users';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Syncs Invoice Ninja Users';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
Company::on('db-ninja-01')->each(function ($company) {
if (Ninja::isHosted()) {
(new \Modules\Admin\Jobs\Account\NinjaUser([], $company))->handle();
}
});
Company::on('db-ninja-02')->each(function ($company) {
if (Ninja::isHosted()) {
(new \Modules\Admin\Jobs\Account\NinjaUser([], $company))->handle();
}
});
}
}

View file

@ -0,0 +1,173 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Console\Commands;
use App\DataMapper\CompanySettings;
use App\Exceptions\MigrationValidatorFailed;
use App\Exceptions\NonExistingMigrationFile;
use App\Exceptions\ProcessingMigrationArchiveFailed;
use App\Exceptions\ResourceDependencyMissing;
use App\Exceptions\ResourceNotAvailableForMigration;
use App\Jobs\Util\Import;
use App\Jobs\Util\StartMigration;
use App\Mail\MigrationFailed;
use App\Models\Account;
use App\Models\Company;
use App\Models\CompanyToken;
use App\Models\User;
use App\Utils\Traits\AppSetup;
use App\Utils\Traits\MakesHash;
use DirectoryIterator;
use Faker\Factory;
use Faker\Generator;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
use ZipArchive;
class ImportMigrations extends Command
{
use MakesHash;
use AppSetup;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:old-import {--path=}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Massively import the migrations.';
/**
* @var Generator
*/
private $faker;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
$this->faker = Factory::create();
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->buildCache();
$path = $this->option('path') ?? public_path('storage/migrations/import');
$directory = new DirectoryIterator($path);
foreach ($directory as $file) {
if ($file->getExtension() === 'zip') {
$user = $this->getUser();
$company = $this->getUser()->companies()->first();
$this->info('Started processing: '.$file->getBasename().' at '.now());
$zip = new ZipArchive();
$archive = $zip->open($file->getRealPath());
try {
if (! $archive) {
throw new ProcessingMigrationArchiveFailed('Processing migration archive failed. Migration file is possibly corrupted.');
}
$filename = pathinfo($file->getRealPath(), PATHINFO_FILENAME);
$zip->extractTo(public_path("storage/migrations/{$filename}"));
$zip->close();
$import_file = public_path("storage/migrations/$filename/migration.json");
Import::dispatch($import_file, $this->getUser()->companies()->first(), $this->getUser());
// StartMigration::dispatch($file->getRealPath(), $this->getUser(), $this->getUser()->companies()->first());
} catch (NonExistingMigrationFile | ProcessingMigrationArchiveFailed | ResourceNotAvailableForMigration | MigrationValidatorFailed | ResourceDependencyMissing $e) {
\Mail::to($this->user)->send(new MigrationFailed($e, $e->getMessage()));
if (app()->environment() !== 'production') {
info($e->getMessage());
}
}
}
}
}
public function getUser(): User
{
$account = $this->getAccount();
$company = $this->getCompany($account);
$user = User::factory()->create([
'account_id' => $account->id,
'email' => Str::random(10).'@example.com',
'confirmation_code' => $this->createDbHash($company->db),
]);
CompanyToken::unguard();
$company_token = CompanyToken::create([
'user_id' => $user->id,
'company_id' => $company->id,
'account_id' => $account->id,
'name' => 'First token',
'token' => Str::random(64),
]);
$user->companies()->attach($company->id, [
'account_id' => $account->id,
'is_owner' => 1,
'is_admin' => 1,
'is_locked' => 0,
'notifications' => CompanySettings::notificationDefaults(),
'permissions' => '',
'settings' => null,
]);
return $user;
}
public function getAccount(): Account
{
return Account::factory()->create();
}
public function getCompany(Account $account): Company
{
$company = Company::factory()->create([
'account_id' => $account->id,
'is_disabled' => true,
]);
if (! $account->default_company_id) {
$account->default_company_id = $company->id;
$account->save();
}
return $company;
}
}

View file

@ -1,380 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use DB;
use Mail;
use Exception;
use App\Models\DbServer;
use App\Models\LookupCompany;
use App\Models\LookupAccount;
use App\Models\LookupUser;
use App\Models\LookupContact;
use App\Models\LookupAccountToken;
use App\Models\LookupInvitation;
class InitLookup extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:init-lookup {--truncate=} {--subdomain} {--validate=} {--update=} {--company_id=} {--page_size=100} {--database=db-ninja-1}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Initialize lookup tables';
protected $log = '';
protected $isValid = true;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->logMessage('Running InitLookup...');
config(['database.default' => DB_NINJA_LOOKUP]);
$database = $this->option('database');
$dbServer = DbServer::whereName($database)->first();
if ($this->option('subdomain')) {
$this->logMessage('Updating subdomains...');
$this->popuplateSubdomains();
} else if ($this->option('truncate')) {
$this->logMessage('Truncating data...');
$this->truncateTables();
} else {
config(['database.default' => $this->option('database')]);
$count = DB::table('companies')
->where('id', '>=', $this->option('company_id') ?: 1)
->count();
for ($i=0; $i<$count; $i += (int) $this->option('page_size')) {
$this->initCompanies($dbServer->id, $i);
}
}
$this->logMessage('Results: ' . ($this->isValid ? RESULT_SUCCESS : RESULT_FAILURE));
if ($this->option('validate')) {
if ($errorEmail = env('ERROR_EMAIL')) {
Mail::raw($this->log, function ($message) use ($errorEmail, $database) {
$message->to($errorEmail)
->from(CONTACT_EMAIL)
->subject("Check-Lookups [{$database}]: " . strtoupper($this->isValid ? RESULT_SUCCESS : RESULT_FAILURE));
});
} elseif (! $this->isValid) {
throw new Exception('Check lookups failed!!');
}
}
}
private function popuplateSubdomains()
{
$data = [];
config(['database.default' => $this->option('database')]);
$accounts = DB::table('accounts')
->orderBy('id')
->where('subdomain', '!=', '')
->get(['account_key', 'subdomain']);
foreach ($accounts as $account) {
$data[$account->account_key] = $account->subdomain;
}
config(['database.default' => DB_NINJA_LOOKUP]);
$validate = $this->option('validate');
$update = $this->option('update');
foreach ($data as $accountKey => $subdomain) {
LookupAccount::whereAccountKey($accountKey)->update(['subdomain' => $subdomain]);
}
}
private function initCompanies($dbServerId, $offset = 0)
{
$data = [];
config(['database.default' => $this->option('database')]);
$companies = DB::table('companies')
->offset($offset)
->limit((int) $this->option('page_size'))
->orderBy('id')
->where('id', '>=', $this->option('company_id') ?: 1)
->get(['id']);
foreach ($companies as $company) {
$data[$company->id] = $this->parseCompany($company->id);
}
config(['database.default' => DB_NINJA_LOOKUP]);
$validate = $this->option('validate');
$update = $this->option('update');
foreach ($data as $companyId => $company) {
$lookupCompany = false;
if ($validate || $update) {
$lookupCompany = LookupCompany::whereDbServerId($dbServerId)->whereCompanyId($companyId)->first();
}
if ($validate && ! $lookupCompany) {
$this->logError("LookupCompany - dbServerId: {$dbServerId}, companyId: {$companyId} | Not found!");
continue;
}
if (! $lookupCompany) {
$lookupCompany = LookupCompany::create([
'db_server_id' => $dbServerId,
'company_id' => $companyId,
]);
}
foreach ($company as $accountKey => $account) {
$lookupAccount = false;
if ($validate || $update) {
$lookupAccount = LookupAccount::whereLookupCompanyId($lookupCompany->id)->whereAccountKey($accountKey)->first();
}
if ($validate && ! $lookupAccount) {
$this->logError("LookupAccount - lookupCompanyId: {$lookupCompany->id}, accountKey {$accountKey} | Not found!");
continue;
}
if (! $lookupAccount) {
$lookupAccount = LookupAccount::create([
'lookup_company_id' => $lookupCompany->id,
'account_key' => $accountKey
]);
}
foreach ($account['users'] as $user) {
$lookupUser = false;
if ($validate || $update) {
$lookupUser = LookupUser::whereLookupAccountId($lookupAccount->id)->whereUserId($user['user_id'])->first();
}
if ($validate) {
if (! $lookupUser) {
$this->logError("LookupUser - lookupAccountId: {$lookupAccount->id}, userId: {$user['user_id']} | Not found!");
continue;
} elseif ($user['email'] != $lookupUser->email || $user['oauth_user_key'] != $lookupUser->oauth_user_key || $user['referral_code'] != $lookupUser->referral_code) {
$this->logError("LookupUser - lookupAccountId: {$lookupAccount->id}, userId: {$user['user_id']} | Out of date!");
continue;
}
}
if ($update && $lookupUser) {
if ($user['email'] != $lookupUser->email || $user['oauth_user_key'] != $lookupUser->oauth_user_key || $user['referral_code'] != $lookupUser->referral_code) {
$lookupUser->email = $user['email'];
$lookupUser->oauth_user_key = $user['oauth_user_key'];
$lookupUser->referral_code = $user['referral_code'];
$lookupUser->save();
}
} elseif (! $lookupUser) {
LookupUser::create([
'lookup_account_id' => $lookupAccount->id,
'email' => $user['email'] ?: null,
'user_id' => $user['user_id'],
'oauth_user_key' => $user['oauth_user_key'],
'referral_code' => $user['referral_code'],
]);
}
}
foreach ($account['contacts'] as $contact) {
$lookupContact = false;
if ($validate || $update) {
$lookupContact = LookupContact::whereLookupAccountId($lookupAccount->id)->whereContactKey($contact['contact_key'])->first();
}
if ($validate && ! $lookupContact) {
$this->logError("LookupContact - lookupAccountId: {$lookupAccount->id}, contactKey: {$contact['contact_key']} | Not found!");
continue;
}
if (! $lookupContact) {
LookupContact::create([
'lookup_account_id' => $lookupAccount->id,
'contact_key' => $contact['contact_key'],
]);
}
}
foreach ($account['invitations'] as $invitation) {
$lookupInvitation = false;
if ($validate || $update) {
$lookupInvitation = LookupInvitation::whereLookupAccountId($lookupAccount->id)->whereInvitationKey($invitation['invitation_key'])->first();
}
if ($validate) {
if (! $lookupInvitation) {
$this->logError("LookupInvitation - lookupAccountId: {$lookupAccount->id}, invitationKey: {$invitation['invitation_key']} | Not found!");
continue;
} elseif ($invitation['message_id'] && $lookupInvitation->message_id != $invitation['message_id']) {
$this->logError("LookupInvitation - lookupAccountId: {$lookupAccount->id}, invitationKey: {$invitation['invitation_key']} | Not the same!");
continue;
}
}
if ($update && $lookupInvitation) {
if ($invitation['message_id'] && $lookupInvitation->message_id != $invitation['message_id']) {
$lookupInvitation->message_id = $invitation['message_id'];
$lookupInvitation->save();
}
} elseif (! $lookupInvitation) {
LookupInvitation::create([
'lookup_account_id' => $lookupAccount->id,
'invitation_key' => $invitation['invitation_key'],
'message_id' => $invitation['message_id'] ?: null,
]);
}
}
foreach ($account['tokens'] as $token) {
$lookupToken = false;
if ($validate || $update) {
$lookupToken = LookupAccountToken::whereLookupAccountId($lookupAccount->id)->whereToken($token['token'])->first();
}
if ($validate && ! $lookupToken) {
$this->logError("LookupAccountToken - lookupAccountId: {$lookupAccount->id}, token: {$token['token']} | Not found!");
continue;
}
if (! $lookupToken) {
LookupAccountToken::create([
'lookup_account_id' => $lookupAccount->id,
'token' => $token['token'],
]);
}
}
}
}
}
private function parseCompany($companyId)
{
$data = [];
config(['database.default' => $this->option('database')]);
$accounts = DB::table('accounts')->whereCompanyId($companyId)->orderBy('id')->get([
'id', 'account_key'
]);
foreach ($accounts as $account) {
$data[$account->account_key] = $this->parseAccount($account->id);
}
return $data;
}
private function parseAccount($accountId)
{
$data = [
'users' => [],
'contacts' => [],
'invitations' => [],
'tokens' => [],
];
$users = DB::table('users')->whereAccountId($accountId)->orderBy('id')->get([
'email',
'id',
'oauth_user_id',
'oauth_provider_id',
'referral_code',
]);
foreach ($users as $user) {
$data['users'][] = [
'email' => $user->email,
'user_id' => $user->id,
'oauth_user_key' => ($user->oauth_provider_id && $user->oauth_user_id) ? ($user->oauth_provider_id . '-' . $user->oauth_user_id) : null,
'referral_code' => $user->referral_code,
];
}
$contacts = DB::table('contacts')->whereAccountId($accountId)->orderBy('id')->get([
'contact_key'
]);
foreach ($contacts as $contact) {
$data['contacts'][] = [
'contact_key' => $contact->contact_key,
];
}
$invitations = DB::table('invitations')->whereAccountId($accountId)->orderBy('id')->get([
'invitation_key',
'message_id'
]);
foreach ($invitations as $invitation) {
$data['invitations'][] = [
'invitation_key' => $invitation->invitation_key,
'message_id' => $invitation->message_id,
];
}
$tokens = DB::table('account_tokens')->whereAccountId($accountId)->orderBy('id')->get([
'token'
]);
foreach ($tokens as $token) {
$data['tokens'][] = [
'token' => $token->token,
];
}
return $data;
}
private function logMessage($str)
{
$str = date('Y-m-d h:i:s') . ' ' . $str;
$this->info($str);
$this->log .= $str . "\n";
}
private function logError($str)
{
$this->isValid = false;
$this->logMessage($str);
}
private function truncateTables()
{
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
DB::statement('truncate lookup_companies');
DB::statement('truncate lookup_accounts');
DB::statement('truncate lookup_users');
DB::statement('truncate lookup_contacts');
DB::statement('truncate lookup_invitations');
DB::statement('truncate lookup_proposal_invitations');
DB::statement('truncate lookup_account_tokens');
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
}
protected function getOptions()
{
return [
['subdomain', null, InputOption::VALUE_OPTIONAL, 'Subdomain', null],
['truncate', null, InputOption::VALUE_OPTIONAL, 'Truncate', null],
['company_id', null, InputOption::VALUE_OPTIONAL, 'Company Id', null],
['page_size', null, InputOption::VALUE_OPTIONAL, 'Page Size', null],
['database', null, InputOption::VALUE_OPTIONAL, 'Database', null],
['validate', null, InputOption::VALUE_OPTIONAL, 'Validate', null],
];
}
}

View file

@ -1,173 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Nwidart\Modules\Commands\GeneratorCommand;
use Nwidart\Modules\Support\Stub;
use Nwidart\Modules\Traits\ModuleCommandTrait;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
class MakeClass extends GeneratorCommand
{
use ModuleCommandTrait;
protected $argumentName = 'name';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $name = 'ninja:make-class';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create class stub';
protected function getArguments()
{
return [
['name', InputArgument::REQUIRED, 'The name of the module.'],
['module', InputArgument::REQUIRED, 'The name of module will be used.'],
['class', InputArgument::REQUIRED, 'The name of the class.'],
['prefix', InputArgument::OPTIONAL, 'The prefix of the class.'],
];
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['fields', null, InputOption::VALUE_OPTIONAL, 'The model attributes.', null],
['filename', null, InputOption::VALUE_OPTIONAL, 'The class filename.', null],
];
}
public function getTemplateContents()
{
$module = $this->laravel['modules']->findOrFail($this->getModuleName());
$path = str_replace('/', '\\', config('modules.paths.generator.' . $this->argument('class')));
return (new Stub('/' . $this->argument('prefix') . $this->argument('class') . '.stub', [
'NAMESPACE' => $this->getClassNamespace($module) . '\\' . $path,
'LOWER_NAME' => $module->getLowerName(),
'CLASS' => $this->getClass(),
'STUDLY_NAME' => Str::studly($module->getLowerName()),
'DATATABLE_COLUMNS' => $this->getColumns(),
'FORM_FIELDS' => $this->getFormFields(),
'DATABASE_FIELDS' => $this->getDatabaseFields($module),
'TRANSFORMER_FIELDS' => $this->getTransformerFields($module),
]))->render();
}
public function getDestinationFilePath()
{
$path = $this->laravel['modules']->getModulePath($this->getModuleName());
$seederPath = $this->laravel['modules']->config('paths.generator.' . $this->argument('class'));
return $path . $seederPath . '/' . $this->getFileName() . '.php';
}
/**
* @return string
*/
protected function getFileName()
{
if ($this->option('filename')) {
return $this->option('filename');
}
return studly_case($this->argument('prefix')) . studly_case($this->argument('name')) . Str::studly($this->argument('class'));
}
protected function getColumns()
{
$fields = $this->option('fields');
$fields = explode(',', $fields);
$str = '';
foreach ($fields as $field) {
if (! $field) {
continue;
}
$field = explode(':', $field)[0];
$str .= '[
\''. $field . '\',
function ($model) {
return $model->' . $field . ';
}
],';
}
return $str;
}
protected function getFormFields()
{
$fields = $this->option('fields');
$fields = explode(',', $fields);
$str = '';
foreach ($fields as $field) {
if (! $field) {
continue;
}
$parts = explode(':', $field);
$field = $parts[0];
$type = $parts[1];
if ($type == 'text') {
$str .= "{!! Former::textarea('" . $field . "') !!}\n";
} else {
$str .= "{!! Former::text('" . $field . "') !!}\n";
}
}
return $str;
}
protected function getDatabaseFields($module)
{
$fields = $this->option('fields');
$fields = explode(',', $fields);
$str = '';
foreach ($fields as $field) {
if (! $field) {
continue;
}
$field = explode(':', $field)[0];
$str .= "'" . $module->getLowerName() . ".{$field}', ";
}
return $str;
}
protected function getTransformerFields($module)
{
$fields = $this->option('fields');
$fields = explode(',', $fields);
$str = '';
foreach ($fields as $field) {
if (! $field) {
continue;
}
$field = explode(':', $field)[0];
$str .= "'{$field}' => $" . $module->getLowerName() . "->$field,\n ";
}
return rtrim($str);
}
}

View file

@ -1,151 +0,0 @@
<?php
namespace App\Console\Commands;
use Artisan;
use Illuminate\Console\Command;
use Symfony\Component\Console\Helper\ProgressBar;
class MakeModule extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:make-module {name : Module name} {fields? : Model fields} {--migrate : Run module migrations} {--p|--plain : Generate only base module scaffold}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate Module CRUD';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$name = $this->argument('name');
$fields = $this->argument('fields');
$migrate = $this->option('migrate');
$plain = $this->option('plain');
$lower = strtolower($name);
// convert 'name:string,description:text' to 'name,description'
$fillable = explode(',', $fields);
$fillable = array_map(function ($item) {
return explode(':', $item)[0];
}, $fillable);
$fillable = implode(',', $fillable);
ProgressBar::setFormatDefinition('custom', '%current%/%max% %elapsed:6s% [%bar%] %percent:3s%% %message%');
$progressBar = $this->output->createProgressBar($plain ? 2 : ($migrate ? 15 : 14));
$progressBar->setFormat('custom');
$this->info("Creating module: {$name}...");
$progressBar->setMessage("Starting module creation...");
Artisan::call('module:make', ['name' => [$name]]);
$progressBar->advance();
if (! $plain) {
$progressBar->setMessage("Creating migrations...");
Artisan::call('module:make-migration', ['name' => "create_{$lower}_table", '--fields' => $fields, 'module' => $name]);
$progressBar->advance();
$progressBar->setMessage("Creating models...");
Artisan::call('module:make-model', ['model' => $name, 'module' => $name, '--fillable' => $fillable]);
$progressBar->advance();
$progressBar->setMessage("Creating views...");
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'views', '--fields' => $fields, '--filename' => 'edit.blade']);
$progressBar->advance();
$progressBar->setMessage("Creating datatables...");
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'datatable', '--fields' => $fields]);
$progressBar->advance();
$progressBar->setMessage("Creating repositories...");
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'repository', '--fields' => $fields]);
$progressBar->advance();
$progressBar->setMessage("Creating presenters...");
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'presenter']);
$progressBar->advance();
$progressBar->setMessage("Creating requests...");
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'request']);
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'request', 'prefix' => 'create']);
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'request', 'prefix' => 'update']);
$progressBar->advance();
$progressBar->setMessage("Creating api-controllers...");
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'api-controller']);
$progressBar->advance();
$progressBar->setMessage("Creating transformers...");
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'transformer', '--fields' => $fields]);
$progressBar->advance();
// if the migrate flag was specified, run the migrations
if ($migrate) {
$progressBar->setMessage("Running migrations...");
Artisan::call('module:migrate', ['module' => $name]);
$progressBar->advance();
}
}
$progressBar->setMessage("Creating policies...");
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'policy']);
$progressBar->advance();
$progressBar->setMessage("Creating auth-providers...");
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'auth-provider']);
$progressBar->advance();
$progressBar->setMessage("Creating translations...");
Artisan::call('ninja:make-class', ['name' => $name, 'module' => $name, 'class' => 'lang', '--filename' => 'texts']);
$progressBar->advance();
$progressBar->setMessage("Dumping module auto-load...");
Artisan::call('module:dump');
$progressBar->finish();
$progressBar->clear();
$this->info('Done');
if (!$migrate && !$plain) {
$this->info("==> Migrations were not run because the --migrate flag was not specified.");
$this->info("==> Use the following command to run the migrations:\nphp artisan module:migrate $name");
}
}
protected function getArguments()
{
return [
['name', InputArgument::REQUIRED, 'The name of the module.'],
['fields', InputArgument::OPTIONAL, 'The fields of the module.'],
];
}
protected function getOptions()
{
return [
['migrate', null, InputOption::VALUE_NONE, 'Run module migrations.', null],
['plain', 'p', InputOption::VALUE_NONE, 'Generate only base module scaffold.', null],
];
}
}

View file

@ -1,12 +1,18 @@
<?php <?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Utils\CurlUtils;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use App\Models\DbServer;
use App\Models\User;
use App\Models\Company;
use App\Libraries\CurlUtils;
class MobileLocalization extends Command class MobileLocalization extends Command
{ {
@ -24,7 +30,6 @@ class MobileLocalization extends Command
*/ */
protected $description = 'Generate mobile localization resources'; protected $description = 'Generate mobile localization resources';
/** /**
* Create a new command instance. * Create a new command instance.
* *
@ -84,9 +89,9 @@ class MobileLocalization extends Command
$text = $resources->$key; $text = $resources->$key;
} }
$text = str_replace(array('<b>', '</b>'), '', $text); $text = str_replace(['<b>', '</b>'], '', $text);
$text = str_replace(array('<i>', '</i>'), '', $text); $text = str_replace(['<i>', '</i>'], '', $text);
$text = str_replace(array('<strong>', '</strong>'), '', $text); $text = str_replace(['<strong>', '</strong>'], '', $text);
echo "'$key': '$text',\n"; echo "'$key': '$text',\n";
} }
@ -104,11 +109,12 @@ class MobileLocalization extends Command
$end = strpos($data, '},', $start); $end = strpos($data, '},', $start);
$data = substr($data, $start, $end - $start - 5); $data = substr($data, $start, $end - $start - 5);
$data = str_replace("\n", "", $data); $data = str_replace("\n", '', $data);
$data = str_replace("\"", "\'", $data); $data = str_replace("\'", "\#", $data);
$data = str_replace("'", "\"", $data); $data = str_replace("'", '"', $data);
$data = str_replace("\#", "'", $data);
return json_decode('{' . rtrim($data, ',') . '}'); return json_decode('{'.rtrim($data, ',').'}');
} }
protected function getOptions() protected function getOptions()
@ -117,5 +123,4 @@ class MobileLocalization extends Command
['type', null, InputOption::VALUE_OPTIONAL, 'Type', null], ['type', null, InputOption::VALUE_OPTIONAL, 'Type', null],
]; ];
} }
} }

View file

@ -0,0 +1,120 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Console\Commands;
use DirectoryIterator;
use Faker\Factory;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class OpenApiYaml extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:openapi';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Build OpenApi YAML';
private array $directories = [
'/components/schemas',
'/paths/'
];
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
$this->faker = Factory::create();
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$path = base_path('openapi');
$directory = new DirectoryIterator($path);
$this->info($directory);
foreach ($directory as $file) {
$this->info($file);
}
Storage::disk('base')->delete('/openapi/api-docs.yaml');
Storage::disk('base')->append('/openapi/api-docs.yaml', file_get_contents($path.'/info.yaml'));
Storage::disk('base')->append('/openapi/api-docs.yaml', file_get_contents($path.'/paths.yaml'));
//iterate paths
$directory = new DirectoryIterator($path . '/paths/');
foreach ($directory as $file) {
if ($file->isFile() && ! $file->isDot()) {
Storage::disk('base')->append('/openapi/api-docs.yaml', file_get_contents("{$path}/paths/{$file->getFilename()}"));
}
}
Storage::disk('base')->append('/openapi/api-docs.yaml', file_get_contents($path.'/components.yaml'));
Storage::disk('base')->append('/openapi/api-docs.yaml', file_get_contents($path.'/components/responses.yaml'));
$directory = new DirectoryIterator($path . '/components/responses/');
foreach ($directory as $file) {
if ($file->isFile() && ! $file->isDot()) {
Storage::disk('base')->append('/openapi/api-docs.yaml', file_get_contents("{$path}/components/responses/{$file->getFilename()}"));
}
}
Storage::disk('base')->append('/openapi/api-docs.yaml', file_get_contents($path.'/components/parameters.yaml'));
$directory = new DirectoryIterator($path . '/components/parameters/');
foreach ($directory as $file) {
if ($file->isFile() && ! $file->isDot()) {
Storage::disk('base')->append('/openapi/api-docs.yaml', file_get_contents("{$path}/components/parameters/{$file->getFilename()}"));
}
}
Storage::disk('base')->append('/openapi/api-docs.yaml', file_get_contents($path.'/components/schemas.yaml'));
//iterate schemas
$directory = new DirectoryIterator($path . '/components/schemas/');
foreach ($directory as $file) {
if ($file->isFile() && ! $file->isDot()) {
Storage::disk('base')->append('/openapi/api-docs.yaml', file_get_contents("{$path}/components/schemas/{$file->getFilename()}"));
}
}
// Storage::disk('base')->append('/openapi/api-docs.yaml', file_get_contents($path.'/components/schemas/account.yaml'));
Storage::disk('base')->append('/openapi/api-docs.yaml', file_get_contents($path.'/misc/misc.yaml'));
}
}

View file

@ -0,0 +1,94 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Console\Commands;
use App\Jobs\Util\VersionCheck;
use App\Utils\Traits\AppSetup;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
class PostUpdate extends Command
{
use AppSetup;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:post-update';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Run basic upgrade commands';
/**
* Execute the console command.
*
* @return mixed
* @throws \Exception
*/
public function handle()
{
set_time_limit(0);
info('running post update');
try {
Artisan::call('migrate', ['--force' => true]);
} catch (\Exception $e) {
info("I wasn't able to migrate the data.");
}
info('finished migrating');
$output = [];
// exec('vendor/bin/composer install --no-dev -o', $output);
info(print_r($output, 1));
info('finished running composer install ');
try {
Artisan::call('optimize');
} catch (\Exception $e) {
info("I wasn't able to optimize.");
}
info('optimized');
try {
Artisan::call('view:clear');
} catch (\Exception $e) {
info("I wasn't able to clear the views.");
}
info('view cleared');
try {
Artisan::call('queue:restart');
} catch (\Exception $e) {
info("I wasn't able to restart the queue.");
}
info('queue restarted');
$this->buildCache(true);
VersionCheck::dispatch();
info('Sent for version check');
}
}

View file

@ -1,84 +0,0 @@
<?php
namespace App\Console\Commands;
use DB;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
/**
* Class PruneData.
*/
class PruneData extends Command
{
/**
* @var string
*/
protected $name = 'ninja:prune-data';
/**
* @var string
*/
protected $description = 'Delete inactive accounts';
public function handle()
{
$this->info(date('r').' Running PruneData...');
if ($database = $this->option('database')) {
config(['database.default' => $database]);
}
// delete accounts who never registered, didn't create any invoices,
// hansn't logged in within the past 6 months and isn't linked to another account
$sql = 'select c.id
from companies c
left join accounts a on a.company_id = c.id
left join clients cl on cl.account_id = a.id
left join tasks t on t.account_id = a.id
left join expenses e on e.account_id = a.id
left join users u on u.account_id = a.id and u.registered = 1
where c.created_at < DATE_SUB(now(), INTERVAL 6 MONTH)
and c.trial_started is null
and c.plan is null
group by c.id
having count(cl.id) = 0
and count(t.id) = 0
and count(e.id) = 0
and count(u.id) = 0';
$results = DB::select($sql);
foreach ($results as $result) {
$this->info("Deleting company: {$result->id}");
try {
DB::table('companies')
->where('id', '=', $result->id)
->delete();
} catch (\Illuminate\Database\QueryException $e) {
// most likely because a user_account record exists which doesn't cascade delete
$this->info("Unable to delete companyId: {$result->id}");
}
}
$this->info('Done');
}
/**
* @return array
*/
protected function getArguments()
{
return [];
}
/**
* @return array
*/
protected function getOptions()
{
return [
['database', null, InputOption::VALUE_OPTIONAL, 'Database', null],
];
}
}

View file

@ -0,0 +1,69 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Console\Commands;
use Illuminate\Console\Command;
class ReactBuilder extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:react';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Builds blade component for react includes';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$includes = '';
$directoryIterator = new \RecursiveDirectoryIterator(public_path('react'), \RecursiveDirectoryIterator::SKIP_DOTS);
foreach (new \RecursiveIteratorIterator($directoryIterator) as $file) {
if (str_contains($file->getFileName(), '.js') && !strpos($file->getFileName(), '.json')) {
if (str_contains($file->getFileName(), 'index.')) {
$includes .= '<script type="module" crossorigin src="/react/'.$file->getFileName().'"></script>'."\n";
} else {
$includes .= '<link rel="modulepreload" href="/react/'.$file->getFileName().'">'."\n";
}
}
if (str_contains($file->getFileName(), '.css')) {
$includes .= '<link rel="stylesheet" href="/react/'.$file->getFileName().'">'."\n";
}
}
file_put_contents(resource_path('views/react/head.blade.php'), $includes);
}
}

View file

@ -0,0 +1,52 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Console\Commands;
use App\Jobs\Cron\RecurringInvoicesCron;
use Illuminate\Console\Command;
class RecurringCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:send-recurring';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sends the recurring invoices';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
(new RecurringInvoicesCron())->handle();
}
}

View file

@ -1,61 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Document;
use DateTime;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
/**
* Class RemoveOrphanedDocuments.
*/
class RemoveOrphanedDocuments extends Command
{
/**
* @var string
*/
protected $name = 'ninja:remove-orphaned-documents';
/**
* @var string
*/
protected $description = 'Removes old documents not associated with an expense or invoice';
public function handle()
{
$this->info(date('r').' Running RemoveOrphanedDocuments...');
if ($database = $this->option('database')) {
config(['database.default' => $database]);
}
$documents = Document::whereRaw('invoice_id IS NULL AND expense_id IS NULL AND updated_at <= ?', [new DateTime('-1 hour')])
->get();
$this->info($documents->count() . ' orphaned document(s) found');
foreach ($documents as $document) {
$document->delete();
}
$this->info('Done');
}
/**
* @return array
*/
protected function getArguments()
{
return [];
}
/**
* @return array
*/
protected function getOptions()
{
return [
['database', null, InputOption::VALUE_OPTIONAL, 'Database', null],
];
}
}

View file

@ -1,52 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Utils;
use Symfony\Component\Console\Input\InputOption;
/**
* Class ResetData.
*/
class ResetData extends Command
{
/**
* @var string
*/
protected $name = 'ninja:reset-data';
/**
* @var string
*/
protected $description = 'Reset data';
public function handle()
{
$this->info(date('r') . ' Running ResetData...');
if (! Utils::isNinjaDev()) {
return;
}
if ($database = $this->option('database')) {
config(['database.default' => $database]);
}
Artisan::call('migrate:reset');
Artisan::call('migrate');
Artisan::call('db:seed');
}
/**
* @return array
*/
protected function getOptions()
{
return [
['fix', null, InputOption::VALUE_OPTIONAL, 'Fix data', null],
['client_id', null, InputOption::VALUE_OPTIONAL, 'Client id', null],
['database', null, InputOption::VALUE_OPTIONAL, 'Database', null],
];
}
}

View file

@ -0,0 +1,87 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Console\Commands;
use App\Models\Company;
use App\Utils\Ninja;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class S3Cleanup extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:s3-cleanup';
protected $log = '';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Remove orphan folders/files';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
if (!Ninja::isHosted()) {
return;
}
$c1 = Company::on('db-ninja-01')->pluck('company_key');
$c2 = Company::on('db-ninja-02')->pluck('company_key');
$merged = $c1->merge($c2)->toArray();
$directories = Storage::disk(config('filesystems.default'))->directories();
$this->LogMessage('Disk Cleanup');
foreach ($directories as $dir) {
if (! in_array($dir, $merged)) {
$this->logMessage("Deleting $dir");
/* Ensure we are not deleting the root folder */
if (strlen($dir) > 1) {
Storage::disk(config('filesystems.default'))->deleteDirectory($dir);
}
}
}
$this->logMessage('exiting');
}
private function logMessage($str)
{
$str = date('Y-m-d h:i:s').' '.$str;
$this->info($str);
$this->log .= $str."\n";
}
}

View file

@ -1,156 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Account;
use App\Models\Invoice;
use App\Models\RecurringExpense;
use App\Ninja\Repositories\InvoiceRepository;
use App\Ninja\Repositories\RecurringExpenseRepository;
use App\Jobs\SendInvoiceEmail;
use DateTime;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
use Auth;
use Exception;
use Utils;
/**
* Class SendRecurringInvoices.
*/
class SendRecurringInvoices extends Command
{
/**
* @var string
*/
protected $name = 'ninja:send-invoices';
/**
* @var string
*/
protected $description = 'Send recurring invoices';
/**
* @var InvoiceRepository
*/
protected $invoiceRepo;
/**
* SendRecurringInvoices constructor.
*
* @param InvoiceRepository $invoiceRepo
*/
public function __construct(InvoiceRepository $invoiceRepo, RecurringExpenseRepository $recurringExpenseRepo)
{
parent::__construct();
$this->invoiceRepo = $invoiceRepo;
$this->recurringExpenseRepo = $recurringExpenseRepo;
}
public function handle()
{
$this->info(date('r') . ' Running SendRecurringInvoices...');
if ($database = $this->option('database')) {
config(['database.default' => $database]);
}
$this->resetCounters();
$this->createInvoices();
$this->createExpenses();
$this->info(date('r') . ' Done');
}
private function resetCounters()
{
$accounts = Account::where('reset_counter_frequency_id', '>', 0)
->orderBy('id', 'asc')
->get();
foreach ($accounts as $account) {
$account->checkCounterReset();
}
}
private function createInvoices()
{
$today = new DateTime();
$invoices = Invoice::with('account.timezone', 'invoice_items', 'client', 'user')
->whereRaw('is_deleted IS FALSE AND deleted_at IS NULL AND is_recurring IS TRUE AND is_public IS TRUE AND frequency_id > 0 AND start_date <= ? AND (end_date IS NULL OR end_date >= ?)', [$today, $today])
->orderBy('id', 'asc')
->cursor();
$this->info(date('r ') . ' Recurring invoice(s) found');
foreach ($invoices as $recurInvoice) {
$shouldSendToday = $recurInvoice->shouldSendToday();
if (! $shouldSendToday) {
continue;
}
$this->info(date('r') . ' Processing Invoice: '. $recurInvoice->id);
$account = $recurInvoice->account;
$account->loadLocalizationSettings($recurInvoice->client);
Auth::loginUsingId($recurInvoice->activeUser()->id);
try {
$invoice = $this->invoiceRepo->createRecurringInvoice($recurInvoice);
if ($invoice && ! $invoice->isPaid() && $account->auto_email_invoice) {
$this->info(date('r') . ' Not billed - Sending Invoice');
dispatch(new SendInvoiceEmail($invoice, $invoice->user_id));
} elseif ($invoice) {
$this->info(date('r') . ' Successfully billed invoice');
}
} catch (Exception $exception) {
$this->info(date('r') . ' Error: ' . $exception->getMessage());
Utils::logError($exception);
}
Auth::logout();
}
}
private function createExpenses()
{
$today = new DateTime();
$expenses = RecurringExpense::with('client')
->whereRaw('is_deleted IS FALSE AND deleted_at IS NULL AND start_date <= ? AND (end_date IS NULL OR end_date >= ?)', [$today, $today])
->orderBy('id', 'asc')
->get();
$this->info(date('r ') . $expenses->count() . ' recurring expenses(s) found');
foreach ($expenses as $expense) {
$shouldSendToday = $expense->shouldSendToday();
if (! $shouldSendToday) {
continue;
}
$this->info(date('r') . ' Processing Expense: '. $expense->id);
$this->recurringExpenseRepo->createRecurringExpense($expense);
}
}
/**
* @return array
*/
protected function getArguments()
{
return [];
}
/**
* @return array
*/
protected function getOptions()
{
return [
['database', null, InputOption::VALUE_OPTIONAL, 'Database', null],
];
}
}

View file

@ -1,280 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Libraries\CurlUtils;
use Carbon;
use Str;
use Cache;
use Utils;
use Exception;
use DateTime;
use Auth;
use App\Jobs\SendInvoiceEmail;
use App\Models\Invoice;
use App\Models\Currency;
use App\Ninja\Mailers\UserMailer;
use App\Ninja\Repositories\AccountRepository;
use App\Ninja\Repositories\InvoiceRepository;
use App\Models\ScheduledReport;
use App\Services\PaymentService;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
use App\Jobs\ExportReportResults;
use App\Jobs\RunReport;
/**
* Class SendReminders.
*/
class SendReminders extends Command
{
/**
* @var string
*/
protected $name = 'ninja:send-reminders';
/**
* @var string
*/
protected $description = 'Send reminder emails';
/**
* @var InvoiceRepository
*/
protected $invoiceRepo;
/**
* @var accountRepository
*/
protected $accountRepo;
/**
* @var PaymentService
*/
protected $paymentService;
/**
* SendReminders constructor.
*
* @param Mailer $mailer
* @param InvoiceRepository $invoiceRepo
* @param accountRepository $accountRepo
*/
public function __construct(InvoiceRepository $invoiceRepo, PaymentService $paymentService, AccountRepository $accountRepo, UserMailer $userMailer)
{
parent::__construct();
$this->paymentService = $paymentService;
$this->invoiceRepo = $invoiceRepo;
$this->accountRepo = $accountRepo;
$this->userMailer = $userMailer;
}
public function handle()
{
$this->info(date('r') . ' Running SendReminders...');
if ($database = $this->option('database')) {
config(['database.default' => $database]);
}
$this->billInvoices();
$this->chargeLateFees();
$this->sendReminderEmails();
$this->sendScheduledReports();
$this->loadExchangeRates();
$this->info(date('r') . ' Done');
if ($errorEmail = env('ERROR_EMAIL')) {
\Mail::raw('EOM', function ($message) use ($errorEmail, $database) {
$message->to($errorEmail)
->from(CONTACT_EMAIL)
->subject("SendReminders [{$database}]: Finished successfully");
});
}
}
private function billInvoices()
{
$today = new DateTime();
$delayedAutoBillInvoices = Invoice::with('account.timezone', 'recurring_invoice', 'invoice_items', 'client', 'user')
->whereRaw('is_deleted IS FALSE AND deleted_at IS NULL AND is_recurring IS FALSE AND is_public IS TRUE
AND balance > 0 AND due_date = ? AND recurring_invoice_id IS NOT NULL',
[$today->format('Y-m-d')])
->orderBy('invoices.id', 'asc')
->get();
$this->info(date('r ') . $delayedAutoBillInvoices->count() . ' due recurring invoice instance(s) found');
/** @var Invoice $invoice */
foreach ($delayedAutoBillInvoices as $invoice) {
if ($invoice->isPaid()) {
continue;
}
if ($invoice->getAutoBillEnabled() && $invoice->client->autoBillLater()) {
$this->info(date('r') . ' Processing Autobill-delayed Invoice: ' . $invoice->id);
Auth::loginUsingId($invoice->activeUser()->id);
$this->paymentService->autoBillInvoice($invoice);
Auth::logout();
}
}
}
private function chargeLateFees()
{
$accounts = $this->accountRepo->findWithFees();
$this->info(date('r ') . $accounts->count() . ' accounts found with fees enabled');
foreach ($accounts as $account) {
if (! $account->hasFeature(FEATURE_EMAIL_TEMPLATES_REMINDERS)) {
continue;
}
$invoices = $this->invoiceRepo->findNeedingReminding($account, false);
$this->info(date('r ') . $account->name . ': ' . $invoices->count() . ' invoices found');
foreach ($invoices as $invoice) {
if ($reminder = $account->getInvoiceReminder($invoice, false)) {
$this->info(date('r') . ' Charge fee: ' . $invoice->id);
$account->loadLocalizationSettings($invoice->client); // support trans to add fee line item
$number = preg_replace('/[^0-9]/', '', $reminder);
$amount = $account->account_email_settings->{"late_fee{$number}_amount"};
$percent = $account->account_email_settings->{"late_fee{$number}_percent"};
$this->invoiceRepo->setLateFee($invoice, $amount, $percent);
}
}
}
}
private function sendReminderEmails()
{
$accounts = $this->accountRepo->findWithReminders();
$this->info(date('r ') . count($accounts) . ' accounts found with reminders enabled');
foreach ($accounts as $account) {
if (! $account->hasFeature(FEATURE_EMAIL_TEMPLATES_REMINDERS)) {
continue;
}
// standard reminders
$invoices = $this->invoiceRepo->findNeedingReminding($account);
$this->info(date('r ') . $account->name . ': ' . $invoices->count() . ' invoices found');
foreach ($invoices as $invoice) {
if ($reminder = $account->getInvoiceReminder($invoice)) {
if ($invoice->last_sent_date == date('Y-m-d')) {
continue;
}
$this->info(date('r') . ' Send email: ' . $invoice->id);
dispatch(new SendInvoiceEmail($invoice, $invoice->user_id, $reminder));
}
}
// endless reminders
$invoices = $this->invoiceRepo->findNeedingEndlessReminding($account);
$this->info(date('r ') . $account->name . ': ' . $invoices->count() . ' endless invoices found');
foreach ($invoices as $invoice) {
if ($invoice->last_sent_date == date('Y-m-d')) {
continue;
}
$this->info(date('r') . ' Send email: ' . $invoice->id);
dispatch(new SendInvoiceEmail($invoice, $invoice->user_id, 'reminder4'));
}
}
}
private function sendScheduledReports()
{
$scheduledReports = ScheduledReport::where('send_date', '<=', date('Y-m-d'))
->with('user', 'account.company')
->get();
$this->info(date('r ') . $scheduledReports->count() . ' scheduled reports');
foreach ($scheduledReports as $scheduledReport) {
$this->info(date('r') . ' Processing report: ' . $scheduledReport->id);
$user = $scheduledReport->user;
$account = $scheduledReport->account;
$account->loadLocalizationSettings();
if (! $account->hasFeature(FEATURE_REPORTS)) {
continue;
}
$config = (array) json_decode($scheduledReport->config);
$reportType = $config['report_type'];
// send email as user
auth()->onceUsingId($user->id);
$report = dispatch_now(new RunReport($scheduledReport->user, $reportType, $config, true));
$file = dispatch_now(new ExportReportResults($scheduledReport->user, $config['export_format'], $reportType, $report->exportParams));
if ($file) {
try {
$this->userMailer->sendScheduledReport($scheduledReport, $file);
$this->info(date('r') . ' Sent report');
} catch (Exception $exception) {
$this->info(date('r') . ' ERROR: ' . $exception->getMessage());
}
} else {
$this->info(date('r') . ' ERROR: Failed to run report');
}
$scheduledReport->updateSendDate();
auth()->logout();
}
}
private function loadExchangeRates()
{
if (Utils::isNinjaDev()) {
return;
}
if (config('ninja.exchange_rates_enabled')) {
$this->info(date('r') . ' Loading latest exchange rates...');
$response = CurlUtils::get(config('ninja.exchange_rates_url'));
$data = json_decode($response);
if ($data && property_exists($data, 'rates')) {
Currency::whereCode(config('ninja.exchange_rates_base'))->update(['exchange_rate' => 1]);
foreach ($data->rates as $code => $rate) {
Currency::whereCode($code)->update(['exchange_rate' => $rate]);
}
} else {
$this->info(date('r') . ' Error: failed to load exchange rates - ' . $response);
\DB::table('currencies')->update(['exchange_rate' => 1]);
}
} else {
\DB::table('currencies')->update(['exchange_rate' => 1]);
}
CurlUtils::get(SITE_URL . '?clear_cache=true');
}
/**
* @return array
*/
protected function getArguments()
{
return [];
}
/**
* @return array
*/
protected function getOptions()
{
return [
['database', null, InputOption::VALUE_OPTIONAL, 'Database', null],
];
}
}

View file

@ -0,0 +1,229 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Console\Commands;
use App\DataMapper\InvoiceItem;
use App\Events\Invoice\InvoiceWasEmailed;
use App\Jobs\Entity\EmailEntity;
use App\Jobs\Util\WebhookHandler;
use App\Libraries\MultiDB;
use App\Models\Invoice;
use App\Models\Quote;
use App\Models\Webhook;
use App\Utils\Ninja;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesReminders;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\App;
//@deprecated 27-11-2022 - only ever should be used for testing
class SendRemindersCron extends Command
{
use MakesReminders, MakesDates;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:send-reminders';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Force send all reminders';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
Invoice::where('next_send_date', '<=', now()->toDateTimeString())
->whereNull('deleted_at')
->where('is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->where('balance', '>', 0)
->whereHas('client', function ($query) {
$query->where('is_deleted', 0)
->where('deleted_at', null);
})
->whereHas('company', function ($query) {
$query->where('is_disabled', 0);
})
->with('invitations')->cursor()->each(function ($invoice) {
if ($invoice->isPayable()) {
$reminder_template = $invoice->calculateTemplate('invoice');
$invoice->service()->touchReminder($reminder_template)->save();
$invoice = $this->calcLateFee($invoice, $reminder_template);
//check if this reminder needs to be emailed
if (in_array($reminder_template, ['reminder1', 'reminder2', 'reminder3']) && $invoice->client->getSetting('enable_'.$reminder_template)) {
$invoice->invitations->each(function ($invitation) use ($invoice, $reminder_template) {
EmailEntity::dispatch($invitation, $invitation->company, $reminder_template);
nlog("Firing reminder email for invoice {$invoice->number}");
});
if ($invoice->invitations->count() > 0) {
event(new InvoiceWasEmailed($invoice->invitations->first(), $invoice->company, Ninja::eventVars(), $reminder_template));
}
}
$invoice->service()->setReminder()->save();
} else {
$invoice->next_send_date = null;
$invoice->save();
}
});
}
private function calcLateFee($invoice, $template) :Invoice
{
$late_fee_amount = 0;
$late_fee_percent = 0;
switch ($template) {
case 'reminder1':
$late_fee_amount = $invoice->client->getSetting('late_fee_amount1');
$late_fee_percent = $invoice->client->getSetting('late_fee_percent1');
break;
case 'reminder2':
$late_fee_amount = $invoice->client->getSetting('late_fee_amount2');
$late_fee_percent = $invoice->client->getSetting('late_fee_percent2');
break;
case 'reminder3':
$late_fee_amount = $invoice->client->getSetting('late_fee_amount3');
$late_fee_percent = $invoice->client->getSetting('late_fee_percent3');
break;
case 'endless_reminder':
$late_fee_amount = $invoice->client->getSetting('late_fee_endless_amount');
$late_fee_percent = $invoice->client->getSetting('late_fee_endless_percent');
break;
default:
$late_fee_amount = 0;
$late_fee_percent = 0;
break;
}
return $this->setLateFee($invoice, $late_fee_amount, $late_fee_percent);
}
/**
* Applies the late fee to the invoice line items
*
* @param Invoice $invoice
* @param float $amount The fee amount
* @param float $percent The fee percentage amount
*
* @return Invoice
*/
private function setLateFee($invoice, $amount, $percent) :Invoice
{
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations($invoice->client->getMergedSettings()));
App::setLocale($invoice->client->locale());
$temp_invoice_balance = $invoice->balance;
if ($amount <= 0 && $percent <= 0) {
return $invoice;
}
$fee = $amount;
if ($invoice->partial > 0) {
$fee += round($invoice->partial * $percent / 100, 2);
} else {
$fee += round($invoice->balance * $percent / 100, 2);
}
$invoice_item = new InvoiceItem;
$invoice_item->type_id = '5';
$invoice_item->product_key = ctrans('texts.fee');
$invoice_item->notes = ctrans('texts.late_fee_added', ['date' => $this->translateDate(now()->startOfDay(), $invoice->client->date_format(), $invoice->client->locale())]);
$invoice_item->quantity = 1;
$invoice_item->cost = $fee;
$invoice_items = $invoice->line_items;
$invoice_items[] = $invoice_item;
$invoice->line_items = $invoice_items;
/**Refresh Invoice values*/
$invoice->calc()->getInvoice()->save();
$invoice->fresh();
$invoice->service()->deletePdf()->save();
/* Refresh the client here to ensure the balance is fresh */
$client = $invoice->client;
$client = $client->fresh();
nlog('adjusting client balance and invoice balance by '.($invoice->balance - $temp_invoice_balance));
$client->service()->updateBalance($invoice->balance - $temp_invoice_balance)->save();
$invoice->ledger()->updateInvoiceBalance($invoice->balance - $temp_invoice_balance, "Late Fee Adjustment for invoice {$invoice->number}");
return $invoice;
}
private function webHookOverdueInvoices()
{
if (! config('ninja.db.multi_db_enabled')) {
$this->executeWebhooks();
} else {
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$this->executeWebhooks();
}
}
}
private function webHookExpiredQuotes()
{
}
private function executeWebhooks()
{
$invoices = Invoice::where('is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->where('balance', '>', 0)
->whereDate('due_date', '<=', now()->subDays(1)->startOfDay())
->cursor();
$invoices->each(function ($invoice) {
WebhookHandler::dispatch(Webhook::EVENT_LATE_INVOICE, $invoice, $invoice->company);
});
$quotes = Quote::where('is_deleted', 0)
->where('status_id', Quote::STATUS_SENT)
->whereDate('due_date', '<=', now()->subDays(1)->startOfDay())
->cursor();
$quotes->each(function ($quote) {
WebhookHandler::dispatch(Webhook::EVENT_EXPIRED_QUOTE, $quote, $quote->company);
});
}
}

View file

@ -1,128 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Company;
use App\Ninja\Mailers\ContactMailer as Mailer;
use App\Ninja\Repositories\AccountRepository;
use Illuminate\Console\Command;
use Utils;
use Symfony\Component\Console\Input\InputOption;
/**
* Class SendRenewalInvoices.
*/
class SendRenewalInvoices extends Command
{
/**
* @var string
*/
protected $name = 'ninja:send-renewals';
/**
* @var string
*/
protected $description = 'Send renewal invoices';
/**
* @var Mailer
*/
protected $mailer;
/**
* @var AccountRepository
*/
protected $accountRepo;
/**
* SendRenewalInvoices constructor.
*
* @param Mailer $mailer
* @param AccountRepository $repo
*/
public function __construct(Mailer $mailer, AccountRepository $repo)
{
parent::__construct();
$this->mailer = $mailer;
$this->accountRepo = $repo;
}
public function handle()
{
$this->info(date('r').' Running SendRenewalInvoices...');
if ($database = $this->option('database')) {
config(['database.default' => $database]);
}
// get all accounts with plans expiring in 10 days
$companies = Company::whereRaw("datediff(plan_expires, curdate()) = 10 and (plan = 'pro' or plan = 'enterprise')")
->orderBy('id')
->get();
$this->info($companies->count() . ' companies found renewing in 10 days');
foreach ($companies as $company) {
if (! $company->accounts->count()) {
continue;
}
$account = $company->accounts->sortBy('id')->first();
$plan = [];
$plan['plan'] = $company->plan;
$plan['term'] = $company->plan_term;
$plan['num_users'] = $company->num_users;
$plan['price'] = min($company->plan_price, Utils::getPlanPrice($plan));
if ($plan['plan'] == PLAN_FREE || ! $plan['plan'] || ! $plan['term'] || ! $plan['price']) {
continue;
}
$client = $this->accountRepo->getNinjaClient($account);
$invitation = $this->accountRepo->createNinjaInvoice($client, $account, $plan, 0, false);
// set the due date to 10 days from now
$invoice = $invitation->invoice;
$invoice->due_date = date('Y-m-d', strtotime('+ 10 days'));
$invoice->save();
$term = $plan['term'];
$plan = $plan['plan'];
if ($term == PLAN_TERM_YEARLY) {
$this->mailer->sendInvoice($invoice);
$this->info("Sent {$term}ly {$plan} invoice to {$client->getDisplayName()}");
} else {
$this->info("Created {$term}ly {$plan} invoice for {$client->getDisplayName()}");
}
}
$this->info('Done');
if ($errorEmail = env('ERROR_EMAIL')) {
\Mail::raw('EOM', function ($message) use ($errorEmail, $database) {
$message->to($errorEmail)
->from(CONTACT_EMAIL)
->subject("SendRenewalInvoices [{$database}]: Finished successfully");
});
}
}
/**
* @return array
*/
protected function getArguments()
{
return [];
}
/**
* @return array
*/
protected function getOptions()
{
return [
['database', null, InputOption::VALUE_OPTIONAL, 'Database', null],
];
}
}

View file

@ -0,0 +1,93 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Console\Commands;
use App\DataMapper\CompanySettings;
use App\DataMapper\DefaultSettings;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Mail\Migration\MaxCompanies;
use App\Models\Account;
use App\Models\Company;
use App\Models\User;
use Faker\Factory;
use Illuminate\Console\Command;
class SendTestEmails extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:send-test-emails';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Sends Test Emails to check templates';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$faker = Factory::create();
$account = Account::factory()->create();
$user = User::factory()->create([
'account_id' => $account->id,
'confirmation_code' => '123',
'email' => $faker->safeEmail(),
'first_name' => 'John',
'last_name' => 'Doe',
]);
$company = Company::factory()->create([
'account_id' => $account->id,
]);
$user->companies()->attach($company->id, [
'account_id' => $account->id,
'is_owner' => 1,
'is_admin' => 1,
'is_locked' => 0,
'permissions' => '',
'notifications' => CompanySettings::notificationDefaults(),
//'settings' => DefaultSettings::userSettings(),
'settings' => null,
]);
$nmo = new NinjaMailerObject;
$nmo->mailable = new MaxCompanies($user->account->companies()->first());
$nmo->company = $user->account->companies()->first();
$nmo->settings = $user->account->companies()->first()->settings;
$nmo->to_user = $user;
(new NinjaMailerJob($nmo))->handle();
}
}

View file

@ -1,44 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Services\BankAccountService;
use Illuminate\Console\Command;
/**
* Class TestOFX.
*/
class TestOFX extends Command
{
/**
* @var string
*/
protected $name = 'ninja:test-ofx';
/**
* @var string
*/
protected $description = 'Test OFX';
/**
* @var BankAccountService
*/
protected $bankAccountService;
/**
* TestOFX constructor.
*
* @param BankAccountService $bankAccountService
*/
public function __construct(BankAccountService $bankAccountService)
{
parent::__construct();
$this->bankAccountService = $bankAccountService;
}
public function handle()
{
$this->info(date('r').' Running TestOFX...');
}
}

View file

@ -0,0 +1,148 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Storage;
class TranslationsExport extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:translations {--type=} {--path=}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Transform translations to json';
protected $log = '';
private array $langs = [
'ar',
'bg',
'ca',
'cs',
'da',
'de',
'el',
'en',
'en_GB',
'es',
'es_ES',
'et',
'fa',
'fi',
'fr',
'fr_CA',
'he',
'hr',
'it',
'ja',
'lt',
'lv_LV',
'mk_MK',
'nb_NO',
'nl',
'pl',
'pt_BR',
'pt_PT',
'ro',
'ru_RU',
'sl',
'sk',
'sq',
'sr',
'sv',
'th',
'tr_TR',
'zh_TW',
];
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$type =$this->option('type') ?? 'export';
if ($type == 'import') {
$this->import();
}
if ($type == 'export') {
$this->export();
}
}
private function import()
{
//loop and
foreach ($this->langs as $lang) {
$import_file = "textsphp_{$lang}.php";
$dir = $this->option('path') ?? storage_path('lang_import/');
$path = $dir.$import_file;
if (file_exists($path)) {
$this->logMessage($path);
$trans = file_get_contents($path);
file_put_contents(lang_path("/{$lang}/texts.php"), $trans);
} else {
$this->logMessage("Could not open file");
$this->logMessage($path);
}
}
}
private function export()
{
Storage::disk('local')->makeDirectory('lang');
foreach ($this->langs as $lang) {
Storage::disk('local')->makeDirectory("lang/{$lang}");
$translations = Lang::getLoader()->load($lang, 'texts');
Storage::disk('local')->put("lang/{$lang}/{$lang}.json", json_encode(Arr::dot($translations), JSON_UNESCAPED_UNICODE));
}
}
private function logMessage($str)
{
$str = date('Y-m-d h:i:s').' '.$str;
$this->info($str);
$this->log .= $str."\n";
}
}

View file

@ -0,0 +1,137 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Console\Commands;
use App\Libraries\MultiDB;
use App\Models\Client;
use App\Models\Company;
use App\Utils\Traits\ClientGroupSettingsSaver;
use Illuminate\Console\Command;
class TypeCheck extends Command
{
use ClientGroupSettingsSaver;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:type-check {--all=} {--client_id=} {--company_id=}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Check Settings Types';
protected $log = '';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
//always return state to first DB
$current_db = config('database.default');
if ($this->option('all')) {
if (! config('ninja.db.multi_db_enabled')) {
$this->checkAll();
} else {
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$this->checkAll();
}
MultiDB::setDB($current_db);
}
}
if ($this->option('client_id')) {
$client = MultiDB::findAndSetDbByClientId($this->option('client_id'));
if ($client) {
$this->checkClient($client);
} else {
$this->logMessage(date('Y-m-d h:i:s').' Could not find this client');
}
}
if ($this->option('company_id')) {
$company = MultiDB::findAndSetDbByCompanyId($this->option('company_id'));
if ($company) {
$this->checkCompany($company);
} else {
$this->logMessage(date('Y-m-d h:i:s').' Could not find this company');
}
}
}
private function checkClient($client)
{
$this->logMessage(date('Y-m-d h:i:s').' Checking Client => '.$client->present()->name().' '.$client->id);
$entity_settings = $this->checkSettingType($client->settings);
$entity_settings->md5 = md5(time());
$client->settings = $entity_settings;
$client->save();
}
private function checkCompany($company)
{
$this->logMessage(date('Y-m-d h:i:s').' Checking Company => '.$company->present()->name().' '.$company->id);
$company->saveSettings((array) $company->settings, $company);
}
private function checkAll()
{
$this->logMessage(date('Y-m-d h:i:s').' Checking all clients and companies.');
Client::withTrashed()->cursor()->each(function ($client) {
$this->logMessage("Checking client {$client->id}");
$entity_settings = $this->checkSettingType($client->settings);
$entity_settings->md5 = md5(time());
$client->settings = $entity_settings;
$client->save();
});
Company::cursor()->each(function ($company) {
$this->logMessage("Checking company {$company->id}");
$company->saveSettings($company->settings, $company);
});
}
private function logMessage($str)
{
$str = date('Y-m-d h:i:s').' '.$str;
$this->info($str);
$this->log .= $str."\n";
}
}

View file

@ -1,148 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
use App\Models\AccountGateway;
use App\Models\BankAccount;
use App\Models\User;
use Artisan;
use Crypt;
use Illuminate\Encryption\Encrypter;
use Laravel\LegacyEncrypter\McryptEncrypter;
/**
* Class UpdateKey
*/
class UpdateKey extends Command
{
/**
* @var string
*/
protected $name = 'ninja:update-key {--database=} {--key=} {--legacy=}';
/**
* @var string
*/
protected $description = 'Update application key';
public function handle()
{
$this->info(date('r') . ' Running UpdateKey...');
if ($database = $this->option('database')) {
config(['database.default' => $database]);
}
if (! env('APP_KEY') || ! env('APP_CIPHER')) {
$this->info(date('r') . ' Error: app key and cipher are not set');
exit;
}
$legacy = false;
if ($this->option('legacy') == 'true') {
$legacy = new McryptEncrypter(env('APP_KEY'), env('APP_CIPHER'));
}
// load the current values
$gatewayConfigs = [];
$bankUsernames = [];
$twoFactorSecrets = [];
foreach (AccountGateway::withTrashed()->get() as $gateway) {
if ($legacy) {
$gatewayConfigs[$gateway->id] = json_decode($legacy->decrypt($gateway->config));
} else {
$gatewayConfigs[$gateway->id] = $gateway->getConfig();
}
}
foreach (BankAccount::withTrashed()->get() as $bank) {
if ($legacy) {
$bankUsernames[$bank->id] = $legacy->decrypt($bank->username);
} else {
$bankUsernames[$bank->id] = $bank->getUsername();
}
}
foreach (User::withTrashed()->where('google_2fa_secret', '!=', '')->get() as $user) {
if ($legacy) {
$twoFactorSecrets[$user->id] = $legacy->decrypt($user->google_2fa_secret);
} else {
$twoFactorSecrets[$user->id] = Crypt::decrypt($user->google_2fa_secret);
}
}
// check if we can write to the .env file
$envPath = base_path() . '/.env';
$envWriteable = file_exists($envPath) && @fopen($envPath, 'a');
if ($key = $this->option('key')) {
$key = base64_decode(str_replace('base64:', '', $key));
} elseif ($envWriteable) {
Artisan::call('key:generate');
$key = base64_decode(str_replace('base64:', '', config('app.key')));
} else {
$key = str_random(32);
}
$cipher = $legacy ? 'AES-256-CBC' : config('app.cipher');
$crypt = new Encrypter($key, $cipher);
// update values using the new key/encrypter
foreach (AccountGateway::withTrashed()->get() as $gateway) {
$config = $gatewayConfigs[$gateway->id];
$gateway->config = $crypt->encrypt(json_encode($config));
$gateway->save();
}
foreach (BankAccount::withTrashed()->get() as $bank) {
$username = $bankUsernames[$bank->id];
$bank->username = $crypt->encrypt($username);
$bank->save();
}
foreach (User::withTrashed()->where('google_2fa_secret', '!=', '')->get() as $user) {
$secret = $twoFactorSecrets[$user->id];
$user->google_2fa_secret = $crypt->encrypt($secret);
$user->save();
}
$message = date('r') . ' Successfully updated ';
if ($envWriteable) {
if ($legacy) {
$message .= 'the key, set the cipher in the .env file to AES-256-CBC';
} else {
$message .= 'the key';
}
} else {
if ($legacy) {
$message .= 'the data, make sure to set the new cipher/key: AES-256-CBC/' . $key;
} else {
$message .= 'the data, make sure to set the new key: ' . $key;
}
}
$this->info($message);
}
/**
* @return array
*/
protected function getArguments()
{
return [];
}
/**
* @return array
*/
protected function getOptions()
{
return [
['legacy', null, InputOption::VALUE_OPTIONAL, 'Legacy', null],
['database', null, InputOption::VALUE_OPTIONAL, 'Database', null],
['key', null, InputOption::VALUE_OPTIONAL, 'Key', null],
];
}
}

View file

@ -1,179 +0,0 @@
<?php
namespace $NAMESPACE$;
use App\Http\Controllers\BaseAPIController;
use Modules\$STUDLY_NAME$\Repositories\$STUDLY_NAME$Repository;
use Modules\$STUDLY_NAME$\Http\Requests\$STUDLY_NAME$Request;
use Modules\$STUDLY_NAME$\Http\Requests\Create$STUDLY_NAME$Request;
use Modules\$STUDLY_NAME$\Http\Requests\Update$STUDLY_NAME$Request;
class $STUDLY_NAME$ApiController extends BaseAPIController
{
protected $$STUDLY_NAME$Repo;
protected $entityType = '$LOWER_NAME$';
public function __construct($STUDLY_NAME$Repository $$LOWER_NAME$Repo)
{
parent::__construct();
$this->$LOWER_NAME$Repo = $$LOWER_NAME$Repo;
}
/**
* @SWG\Get(
* path="/$LOWER_NAME$",
* summary="List $LOWER_NAME$",
* operationId="list$STUDLY_NAME$s",
* tags={"$LOWER_NAME$"},
* @SWG\Response(
* response=200,
* description="A list of $LOWER_NAME$",
* @SWG\Schema(type="array", @SWG\Items(ref="#/definitions/$STUDLY_NAME$"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function index()
{
$data = $this->$LOWER_NAME$Repo->all();
return $this->listResponse($data);
}
/**
* @SWG\Get(
* path="/$LOWER_NAME$/{$LOWER_NAME$_id}",
* summary="Individual $STUDLY_NAME$",
* operationId="get$STUDLY_NAME$",
* tags={"$LOWER_NAME$"},
* @SWG\Parameter(
* in="path",
* name="$LOWER_NAME$_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="A single $LOWER_NAME$",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/$STUDLY_NAME$"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function show($STUDLY_NAME$Request $request)
{
return $this->itemResponse($request->entity());
}
/**
* @SWG\Post(
* path="/$LOWER_NAME$",
* summary="Create a $LOWER_NAME$",
* operationId="create$STUDLY_NAME$",
* tags={"$LOWER_NAME$"},
* @SWG\Parameter(
* in="body",
* name="$LOWER_NAME$",
* @SWG\Schema(ref="#/definitions/$STUDLY_NAME$")
* ),
* @SWG\Response(
* response=200,
* description="New $LOWER_NAME$",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/$STUDLY_NAME$"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function store(Create$STUDLY_NAME$Request $request)
{
$$LOWER_NAME$ = $this->$LOWER_NAME$Repo->save($request->input());
return $this->itemResponse($$LOWER_NAME$);
}
/**
* @SWG\Put(
* path="/$LOWER_NAME$/{$LOWER_NAME$_id}",
* summary="Update a $LOWER_NAME$",
* operationId="update$STUDLY_NAME$",
* tags={"$LOWER_NAME$"},
* @SWG\Parameter(
* in="path",
* name="$LOWER_NAME$_id",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* in="body",
* name="$LOWER_NAME$",
* @SWG\Schema(ref="#/definitions/$STUDLY_NAME$")
* ),
* @SWG\Response(
* response=200,
* description="Updated $LOWER_NAME$",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/$STUDLY_NAME$"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function update(Update$STUDLY_NAME$Request $request, $publicId)
{
if ($request->action) {
return $this->handleAction($request);
}
$$LOWER_NAME$ = $this->$LOWER_NAME$Repo->save($request->input(), $request->entity());
return $this->itemResponse($$LOWER_NAME$);
}
/**
* @SWG\Delete(
* path="/$LOWER_NAME$/{$LOWER_NAME$_id}",
* summary="Delete a $LOWER_NAME$",
* operationId="delete$STUDLY_NAME$",
* tags={"$LOWER_NAME$"},
* @SWG\Parameter(
* in="path",
* name="$LOWER_NAME$_id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="Deleted $LOWER_NAME$",
* @SWG\Schema(type="object", @SWG\Items(ref="#/definitions/$STUDLY_NAME$"))
* ),
* @SWG\Response(
* response="default",
* description="an ""unexpected"" error"
* )
* )
*/
public function destroy(Update$STUDLY_NAME$Request $request)
{
$$LOWER_NAME$ = $request->entity();
$this->$LOWER_NAME$Repo->delete($$LOWER_NAME$);
return $this->itemResponse($$LOWER_NAME$);
}
}

View file

@ -1,17 +0,0 @@
<?php
namespace $NAMESPACE$;
use App\Providers\AuthServiceProvider;
class $STUDLY_NAME$AuthProvider extends AuthServiceProvider
{
/**
* The policy mappings for the application.
*
* @var array
*/
protected $policies = [
\Modules\$STUDLY_NAME$\Models\$STUDLY_NAME$::class => \Modules\$STUDLY_NAME$\Policies\$STUDLY_NAME$Policy::class,
];
}

View file

@ -1,68 +0,0 @@
<?php
namespace $NAMESPACE$;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
class $CLASS$ extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $name = '$COMMAND_NAME$';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
//
}
/**
* Get the console command arguments.
*
* @return array
*/
protected function getArguments()
{
return [
['example', InputArgument::REQUIRED, 'An example argument.'],
];
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['example', null, InputOption::VALUE_OPTIONAL, 'An example option.', null],
];
}
}

View file

@ -1,15 +0,0 @@
{
"name": "$VENDOR$/$LOWER_NAME$",
"description": "",
"authors": [
{
"name": "$AUTHOR_NAME$",
"email": "$AUTHOR_EMAIL$"
}
],
"autoload": {
"psr-4": {
"$MODULE_NAMESPACE$\\$STUDLY_NAME$\\": ""
}
}
}

View file

@ -1,9 +0,0 @@
<?php
namespace $CLASS_NAMESPACE$;
use Illuminate\Routing\Controller;
class $CLASS$ extends Controller
{
}

View file

@ -1,131 +0,0 @@
<?php
namespace $CLASS_NAMESPACE$;
use Auth;
use App\Http\Controllers\BaseController;
use App\Services\DatatableService;
use Modules\$STUDLY_NAME$\Datatables\$STUDLY_NAME$Datatable;
use Modules\$STUDLY_NAME$\Repositories\$STUDLY_NAME$Repository;
use Modules\$STUDLY_NAME$\Http\Requests\$STUDLY_NAME$Request;
use Modules\$STUDLY_NAME$\Http\Requests\Create$STUDLY_NAME$Request;
use Modules\$STUDLY_NAME$\Http\Requests\Update$STUDLY_NAME$Request;
class $CLASS$ extends BaseController
{
protected $$STUDLY_NAME$Repo;
//protected $entityType = '$LOWER_NAME$';
public function __construct($STUDLY_NAME$Repository $$LOWER_NAME$Repo)
{
//parent::__construct();
$this->$LOWER_NAME$Repo = $$LOWER_NAME$Repo;
}
/**
* Display a listing of the resource.
* @return Response
*/
public function index()
{
return view('list_wrapper', [
'entityType' => '$LOWER_NAME$',
'datatable' => new $STUDLY_NAME$Datatable(),
'title' => mtrans('$LOWER_NAME$', '$LOWER_NAME$_list'),
]);
}
public function datatable(DatatableService $datatableService)
{
$search = request()->input('sSearch');
$userId = Auth::user()->filterId();
$datatable = new $STUDLY_NAME$Datatable();
$query = $this->$LOWER_NAME$Repo->find($search, $userId);
return $datatableService->createDatatable($datatable, $query);
}
/**
* Show the form for creating a new resource.
* @return Response
*/
public function create($STUDLY_NAME$Request $request)
{
$data = [
'$LOWER_NAME$' => null,
'method' => 'POST',
'url' => '$LOWER_NAME$',
'title' => mtrans('$LOWER_NAME$', 'new_$LOWER_NAME$'),
];
return view('$LOWER_NAME$::edit', $data);
}
/**
* Store a newly created resource in storage.
* @param Request $request
* @return Response
*/
public function store(Create$STUDLY_NAME$Request $request)
{
$$LOWER_NAME$ = $this->$LOWER_NAME$Repo->save($request->input());
return redirect()->to($$LOWER_NAME$->present()->editUrl)
->with('message', mtrans('$LOWER_NAME$', 'created_$LOWER_NAME$'));
}
/**
* Show the form for editing the specified resource.
* @return Response
*/
public function edit($STUDLY_NAME$Request $request)
{
$$LOWER_NAME$ = $request->entity();
$data = [
'$LOWER_NAME$' => $$LOWER_NAME$,
'method' => 'PUT',
'url' => '$LOWER_NAME$/' . $$LOWER_NAME$->public_id,
'title' => mtrans('$LOWER_NAME$', 'edit_$LOWER_NAME$'),
];
return view('$LOWER_NAME$::edit', $data);
}
/**
* Show the form for editing a resource.
* @return Response
*/
public function show($STUDLY_NAME$Request $request)
{
return redirect()->to("$LOWER_NAME$/{$request->$LOWER_NAME$}/edit");
}
/**
* Update the specified resource in storage.
* @param Request $request
* @return Response
*/
public function update(Update$STUDLY_NAME$Request $request)
{
$$LOWER_NAME$ = $this->$LOWER_NAME$Repo->save($request->input(), $request->entity());
return redirect()->to($$LOWER_NAME$->present()->editUrl)
->with('message', mtrans('$LOWER_NAME$', 'updated_$LOWER_NAME$'));
}
/**
* Update multiple resources
*/
public function bulk()
{
$action = request()->input('action');
$ids = request()->input('public_id') ?: request()->input('ids');
$count = $this->$LOWER_NAME$Repo->bulk($ids, $action);
return redirect()->to('$LOWER_NAME$')
->with('message', mtrans('$LOWER_NAME$', $action . '_$LOWER_NAME$_complete'));
}
}

View file

@ -1,28 +0,0 @@
<?php
namespace $NAMESPACE$;
class Create$CLASS$Request extends $CLASS$Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return $this->user()->can('create', '$LOWER_NAME$');
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
];
}
}

View file

@ -1,43 +0,0 @@
<?php
namespace $NAMESPACE$;
use Utils;
use URL;
use Auth;
use App\Ninja\Datatables\EntityDatatable;
class $CLASS$Datatable extends EntityDatatable
{
public $entityType = '$LOWER_NAME$';
public $sortCol = 1;
public function columns()
{
return [
$DATATABLE_COLUMNS$
[
'created_at',
function ($model) {
return Utils::fromSqlDateTime($model->created_at);
}
],
];
}
public function actions()
{
return [
[
mtrans('$LOWER_NAME$', 'edit_$LOWER_NAME$'),
function ($model) {
return URL::to("$LOWER_NAME$/{$model->public_id}/edit");
},
function ($model) {
return Auth::user()->can('editByOwner', ['$LOWER_NAME$', $model->user_id]);
}
],
];
}
}

View file

@ -1,30 +0,0 @@
<?php
namespace $NAMESPACE$;
use Illuminate\Queue\SerializesModels;
class $CLASS$
{
use SerializesModels;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Get the channels the event should be broadcast on.
*
* @return array
*/
public function broadcastOn()
{
return [];
}
}

View file

@ -1,33 +0,0 @@
<?php
namespace $NAMESPACE$;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Bus\Queueable;
class $CLASS$ implements ShouldQueue
{
use InteractsWithQueue, SerializesModels, Queueable;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
//
}
}

View file

@ -1,17 +0,0 @@
{
"name": "$STUDLY_NAME$",
"alias": "$LOWER_NAME$",
"description": "",
"keywords": [],
"active": 1,
"order": 0,
"providers": [
"$MODULE_NAMESPACE$\\$STUDLY_NAME$\\Providers\\$STUDLY_NAME$ServiceProvider"
],
"aliases":{},
"files": [
"start.php"
],
"icon": "th-large",
"plural": "$LOWER_NAME$"
}

View file

@ -1,19 +0,0 @@
<?php
$LANG = array(
'$LOWER_NAME$' => '$STUDLY_NAME$',
'$LOWER_NAME$_list' => '$STUDLY_NAME$ List',
'archive_$LOWER_NAME$' => 'Archive $STUDLY_NAME$',
'delete_$LOWER_NAME$' => 'Delete $STUDLY_NAME$',
'edit_$LOWER_NAME$' => 'Edit $STUDLY_NAME$',
'restore_$LOWER_NAME$' => 'Restore $STUDLY_NAME$',
'new_$LOWER_NAME$' => 'New $STUDLY_NAME$',
'created_$LOWER_NAME$' => 'Successfully created $LOWER_NAME$',
'updated_$LOWER_NAME$' => 'Successfully updated $LOWER_NAME$',
);
return $LANG;
?>

View file

@ -1,31 +0,0 @@
<?php
namespace $NAMESPACE$;
use $EVENTNAME$;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
class $CLASS$
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param \$EVENTNAME$ $event
* @return void
*/
public function handle(\$EVENTNAME$ $event)
{
//
}
}

View file

@ -1,33 +0,0 @@
<?php
namespace $NAMESPACE$;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
class $CLASS$ extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->view('view.name');
}
}

View file

@ -1,21 +0,0 @@
<?php
namespace $NAMESPACE$;
use Closure;
use Illuminate\Http\Request;
class $CLASS$
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
return $next($request);
}
}

View file

@ -1,31 +0,0 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class $CLASS$ extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('$TABLE$', function (Blueprint $table) {
$FIELDS_UP$
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('$TABLE$', function (Blueprint $table) {
$FIELDS_DOWN$
});
}
}

View file

@ -1,44 +0,0 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class $CLASS$ extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create(strtolower('$TABLE$'), function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('user_id')->index();
$table->unsignedInteger('account_id')->index();
$table->unsignedInteger('client_id')->index()->nullable();
$FIELDS$
$table->timestamps();
$table->softDeletes();
$table->boolean('is_deleted')->default(false);
$table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('client_id')->references('id')->on('clients')->onDelete('cascade');
$table->unsignedInteger('public_id')->index();
$table->unique( ['account_id', 'public_id'] );
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists(strtolower('$TABLE$'));
}
}

View file

@ -1,31 +0,0 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class $CLASS$ extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('$TABLE$', function (Blueprint $table) {
$FIELDS_UP$
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('$TABLE$', function (Blueprint $table) {
$FIELDS_DOWN$
});
}
}

View file

@ -1,31 +0,0 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class $CLASS$ extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::dropIfExists('$TABLE$');
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::create('$TABLE$', function (Blueprint $table) {
$table->increments('id');
$FIELDS$
$table->timestamps();
});
}
}

View file

@ -1,27 +0,0 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class $CLASS$ extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
//
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

View file

@ -1,34 +0,0 @@
<?php
namespace $NAMESPACE$;
use App\Models\EntityModel;
use Laracasts\Presenter\PresentableTrait;
use Illuminate\Database\Eloquent\SoftDeletes;
class $CLASS$ extends EntityModel
{
use PresentableTrait;
use SoftDeletes;
/**
* @var string
*/
protected $presenter = 'Modules\$CLASS$\Presenters\$CLASS$Presenter';
/**
* @var string
*/
protected $fillable = $FILLABLE$;
/**
* @var string
*/
protected $table = '$LOWER_NAME$';
public function getEntityType()
{
return '$LOWER_NAME$';
}
}

View file

@ -1,61 +0,0 @@
<?php
namespace $NAMESPACE$;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
class $CLASS$ extends Notification
{
use Queueable;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
return (new MailMessage)
->line('The introduction to the notification.')
->action('Notification Action', 'https://laravel.com')
->line('Thank you for using our application!');
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}

View file

@ -1,10 +0,0 @@
<?php
namespace $NAMESPACE$;
use App\Policies\EntityPolicy;
class $STUDLY_NAME$Policy extends EntityPolicy
{
}

View file

@ -1,10 +0,0 @@
<?php
namespace $NAMESPACE$;
use App\Ninja\Presenters\EntityPresenter;
class $STUDLY_NAME$Presenter extends EntityPresenter
{
}

View file

@ -1,44 +0,0 @@
<?php
namespace $NAMESPACE$;
use App\Providers\AuthServiceProvider;
class $CLASS$ extends AuthServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = false;
/**
* The policy mappings for the application.
*
* @var array
*/
protected $policies = [
\Modules\$STUDLY_NAME$\Models\$STUDLY_NAME$::class => \Modules\$STUDLY_NAME$\Policies\$STUDLY_NAME$Policy::class,
];
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
//
}
/**
* Get the services provided by the provider.
*
* @return array
*/
public function provides()
{
return [];
}
}

View file

@ -1,71 +0,0 @@
<?php
namespace $NAMESPACE$;
use DB;
use Modules\$STUDLY_NAME$\Models\$STUDLY_NAME$;
use App\Ninja\Repositories\BaseRepository;
//use App\Events\$STUDLY_NAME$WasCreated;
//use App\Events\$STUDLY_NAME$WasUpdated;
class $STUDLY_NAME$Repository extends BaseRepository
{
public function getClassName()
{
return 'Modules\$STUDLY_NAME$\Models\$STUDLY_NAME$';
}
public function all()
{
return $STUDLY_NAME$::scope()
->orderBy('created_at', 'desc')
->withTrashed();
}
public function find($filter = null, $userId = false)
{
$query = DB::table('$LOWER_NAME$')
->where('$LOWER_NAME$.account_id', '=', \Auth::user()->account_id)
->select(
$DATABASE_FIELDS$
'$LOWER_NAME$.public_id',
'$LOWER_NAME$.deleted_at',
'$LOWER_NAME$.created_at',
'$LOWER_NAME$.is_deleted',
'$LOWER_NAME$.user_id'
);
$this->applyFilters($query, '$LOWER_NAME$');
if ($userId) {
$query->where('clients.user_id', '=', $userId);
}
/*
if ($filter) {
$query->where();
}
*/
return $query;
}
public function save($data, $$LOWER_NAME$ = null)
{
$entity = $$LOWER_NAME$ ?: $STUDLY_NAME$::createNew();
$entity->fill($data);
$entity->save();
/*
if (!$publicId || intval($publicId) < 0) {
event(new ClientWasCreated($client));
} else {
event(new ClientWasUpdated($client));
}
*/
return $entity;
}
}

View file

@ -1,10 +0,0 @@
<?php
namespace $NAMESPACE$;
use App\Http\Requests\EntityRequest;
class $CLASS$Request extends EntityRequest
{
protected $entityType = '$LOWER_NAME$';
}

View file

@ -1,39 +0,0 @@
<?php
namespace $MODULE_NAMESPACE$\$MODULE$\Providers;
use Illuminate\Routing\Router;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
class $NAME$ extends ServiceProvider
{
/**
* The root namespace to assume when generating URLs to actions.
*
* @var string
*/
protected $rootUrlNamespace = '$MODULE_NAMESPACE$\$MODULE$\Http\Controllers';
/**
* Called before routes are registered.
*
* Register any model bindings or pattern based filters.
*
* @param Router $router
* @return void
*/
public function before(Router $router)
{
//
}
/**
* Define the routes for the application.
*
* @return void
*/
public function map(Router $router)
{
// require __DIR__ . '/../Http/routes.php';
}
}

View file

@ -1,13 +0,0 @@
<?php
Route::group(['middleware' => ['web', 'lookup:user', 'auth:user'], 'namespace' => '$MODULE_NAMESPACE$\$STUDLY_NAME$\Http\Controllers'], function()
{
Route::resource('$LOWER_NAME$', '$STUDLY_NAME$Controller');
Route::post('$LOWER_NAME$/bulk', '$STUDLY_NAME$Controller@bulk');
Route::get('api/$LOWER_NAME$', '$STUDLY_NAME$Controller@datatable');
});
Route::group(['middleware' => 'api', 'namespace' => '$MODULE_NAMESPACE$\$STUDLY_NAME$\Http\ApiControllers', 'prefix' => 'api/v1'], function()
{
Route::resource('$LOWER_NAME$', '$STUDLY_NAME$ApiController');
});

View file

@ -1,5 +0,0 @@
<?php
return [
'name' => '$STUDLY_NAME$'
];

View file

@ -1,110 +0,0 @@
<?php
namespace $NAMESPACE$;
use App\Providers\AuthServiceProvider;
use Illuminate\Contracts\Auth\Access\Gate as GateContract;
class $CLASS$ extends AuthServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = false;
/**
* The policy mappings for the application.
*
* @var array
*/
protected $policies = [
\Modules\$STUDLY_NAME$\Models\$STUDLY_NAME$::class => \Modules\$STUDLY_NAME$\Policies\$STUDLY_NAME$Policy::class,
];
/**
* Boot the application events.
*
* @return void
*/
public function boot()
{
parent::boot();
$this->registerTranslations();
$this->registerConfig();
$this->registerViews();
}
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
//
}
/**
* Register config.
*
* @return void
*/
protected function registerConfig()
{
$this->publishes([
__DIR__.'/../$PATH_CONFIG$/config.php' => config_path('$LOWER_NAME$.php'),
]);
$this->mergeConfigFrom(
__DIR__.'/../$PATH_CONFIG$/config.php', '$LOWER_NAME$'
);
}
/**
* Register views.
*
* @return void
*/
public function registerViews()
{
$viewPath = base_path('resources/views/modules/$LOWER_NAME$');
$sourcePath = __DIR__.'/../$PATH_VIEWS$';
$this->publishes([
$sourcePath => $viewPath
]);
$this->loadViewsFrom(array_merge(array_map(function ($path) {
return $path . '/modules/$LOWER_NAME$';
}, \Config::get('view.paths')), [$sourcePath]), '$LOWER_NAME$');
}
/**
* Register translations.
*
* @return void
*/
public function registerTranslations()
{
$langPath = base_path('resources/lang/modules/$LOWER_NAME$');
if (is_dir($langPath)) {
$this->loadTranslationsFrom($langPath, '$LOWER_NAME$');
} else {
$this->loadTranslationsFrom(__DIR__ .'/../$PATH_LANG$', '$LOWER_NAME$');
}
}
/**
* Get the services provided by the provider.
*
* @return array
*/
public function provides()
{
return [];
}
}

View file

@ -1,21 +0,0 @@
<?php
namespace $NAMESPACE$\Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Database\Eloquent\Model;
class $NAME$ extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
Model::unguard();
// $this->call("OthersTableSeeder");
}
}

View file

@ -1,15 +0,0 @@
<?php
/*
|--------------------------------------------------------------------------
| Register Namespaces And Routes
|--------------------------------------------------------------------------
|
| When a module starting, this file will executed automatically. This helps
| to register some namespaces like translator or view. Also this file
| will load the routes file for each module. You may also modify
| this file as you want.
|
*/
require __DIR__ . '/Http/routes.php';

View file

@ -1,35 +0,0 @@
<?php
namespace $NAMESPACE$;
use Modules\$STUDLY_NAME$\Models\$STUDLY_NAME$;
use App\Ninja\Transformers\EntityTransformer;
/**
* @SWG\Definition(definition="$STUDLY_NAME$", @SWG\Xml(name="$STUDLY_NAME$"))
*/
class $STUDLY_NAME$Transformer extends EntityTransformer
{
/**
* @SWG\Property(property="id", type="integer", example=1, readOnly=true)
* @SWG\Property(property="user_id", type="integer", example=1)
* @SWG\Property(property="account_key", type="string", example="123456")
* @SWG\Property(property="updated_at", type="integer", example=1451160233, readOnly=true)
* @SWG\Property(property="archived_at", type="integer", example=1451160233, readOnly=true)
*/
/**
* @param $STUDLY_NAME$ $$LOWER_NAME$
* @return array
*/
public function transform($STUDLY_NAME$ $$LOWER_NAME$)
{
return array_merge($this->getDefaults($$LOWER_NAME$), [
$TRANSFORMER_FIELDS$
'id' => (int) $$LOWER_NAME$->public_id,
'updated_at' => $this->getTimestamp($$LOWER_NAME$->updated_at),
'archived_at' => $this->getTimestamp($$LOWER_NAME$->deleted_at),
]);
}
}

View file

@ -1,28 +0,0 @@
<?php
namespace $NAMESPACE$;
class Update$CLASS$Request extends $CLASS$Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return $this->user()->can('edit', $this->entity());
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
];
}
}

View file

@ -1,57 +0,0 @@
@extends('header')
@section('content')
{!! Former::open($url)
->addClass('col-md-10 col-md-offset-1 warn-on-exit')
->method($method)
->rules([]) !!}
@if ($$LOWER_NAME$)
{!! Former::populate($$LOWER_NAME$) !!}
<div style="display:none">
{!! Former::text('public_id') !!}
</div>
@endif
<div class="row">
<div class="col-md-10 col-md-offset-1">
<div class="panel panel-default">
<div class="panel-body">
$FORM_FIELDS$
</div>
</div>
</div>
</div>
<center class="buttons">
{!! Button::normal(trans('texts.cancel'))
->large()
->asLinkTo(URL::to('/$LOWER_NAME$'))
->appendIcon(Icon::create('remove-circle')) !!}
{!! Button::success(trans('texts.save'))
->submit()
->large()
->appendIcon(Icon::create('floppy-disk')) !!}
</center>
{!! Former::close() !!}
<script type="text/javascript">
$(function() {
$(".warn-on-exit input").first().focus();
})
</script>
@stop

View file

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Module $STUDLY_NAME$</title>
</head>
<body>
@yield('content')
</body>
</html>

View file

@ -1,59 +1,138 @@
<?php <?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Console; namespace App\Console;
use App\Jobs\Cron\AutoBillCron;
use App\Jobs\Cron\RecurringExpensesCron;
use App\Jobs\Cron\RecurringInvoicesCron;
use App\Jobs\Cron\SubscriptionCron;
use App\Jobs\Invoice\InvoiceCheckLateWebhook;
use App\Jobs\Ninja\AdjustEmailQuota;
use App\Jobs\Ninja\BankTransactionSync;
use App\Jobs\Ninja\CompanySizeCheck;
use App\Jobs\Ninja\QueueSize;
use App\Jobs\Ninja\SystemMaintenance;
use App\Jobs\Ninja\TaskScheduler;
use App\Jobs\Quote\QuoteCheckExpired;
use App\Jobs\Subscription\CleanStaleInvoiceOrder;
use App\Jobs\Util\DiskCleanup;
use App\Jobs\Util\ReminderJob;
use App\Jobs\Util\SchedulerCheck;
use App\Jobs\Util\SendFailedEmails;
use App\Jobs\Util\UpdateExchangeRates;
use App\Jobs\Util\VersionCheck;
use App\Models\Account;
use App\Utils\Ninja;
use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Utils;
class Kernel extends ConsoleKernel class Kernel extends ConsoleKernel
{ {
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
'App\Console\Commands\SendRecurringInvoices',
'App\Console\Commands\RemoveOrphanedDocuments',
'App\Console\Commands\ResetData',
'App\Console\Commands\CheckData',
'App\Console\Commands\PruneData',
'App\Console\Commands\CreateTestData',
'App\Console\Commands\CreateLuisData',
'App\Console\Commands\MobileLocalization',
'App\Console\Commands\SendRenewalInvoices',
'App\Console\Commands\ChargeRenewalInvoices',
'App\Console\Commands\SendReminders',
'App\Console\Commands\TestOFX',
'App\Console\Commands\MakeModule',
'App\Console\Commands\MakeClass',
'App\Console\Commands\InitLookup',
'App\Console\Commands\CalculatePayouts',
'App\Console\Commands\UpdateKey',
'App\Console\Commands\ExportMigrations',
];
/** /**
* Define the application's command schedule. * Define the application's command schedule.
* *
* @param \Illuminate\Console\Scheduling\Schedule $schedule * @param Schedule $schedule
*
* @return void * @return void
*/ */
protected function schedule(Schedule $schedule) protected function schedule(Schedule $schedule)
{ {
$logFile = storage_path() . '/logs/cron.log'; /* Check for the latest version of Invoice Ninja */
$schedule->job(new VersionCheck)->daily();
$schedule /* Checks and cleans redundant files */
->command('ninja:send-invoices --force') $schedule->job(new DiskCleanup)->dailyAt('02:10')->withoutOverlapping()->name('disk-cleanup-job')->onOneServer();
->sendOutputTo($logFile)
->withoutOverlapping()
->hourly();
$schedule /* Send reminders */
->command('ninja:send-reminders --force') $schedule->job(new ReminderJob)->hourly()->withoutOverlapping()->name('reminder-job')->onOneServer();
->sendOutputTo($logFile)
->daily(); /* Returns the number of jobs in the queue */
$schedule->job(new QueueSize)->everyFiveMinutes()->withoutOverlapping()->name('queue-size-job')->onOneServer();
/* Checks for large companies and marked them as is_large */
$schedule->job(new CompanySizeCheck)->dailyAt('23:20')->withoutOverlapping()->name('company-size-job')->onOneServer();
/* Pulls in the latest exchange rates */
$schedule->job(new UpdateExchangeRates)->dailyAt('23:30')->withoutOverlapping()->name('exchange-rate-job')->onOneServer();
/* Runs cleanup code for subscriptions */
$schedule->job(new SubscriptionCron)->dailyAt('00:01')->withoutOverlapping()->name('subscription-job')->onOneServer();
/* Sends recurring invoices*/
$schedule->job(new RecurringInvoicesCron)->hourly()->withoutOverlapping()->name('recurring-invoice-job')->onOneServer();
/* Stale Invoice Cleanup*/
$schedule->job(new CleanStaleInvoiceOrder)->hourlyAt(30)->withoutOverlapping()->name('stale-invoice-job')->onOneServer();
/* Sends recurring invoices*/
$schedule->job(new RecurringExpensesCron)->dailyAt('00:10')->withoutOverlapping()->name('recurring-expense-job')->onOneServer();
/* Fires notifications for expired Quotes */
$schedule->job(new QuoteCheckExpired)->dailyAt('05:10')->withoutOverlapping()->name('quote-expired-job')->onOneServer();
/* Fires webhooks for overdue Invoice */
$schedule->job(new InvoiceCheckLateWebhook)->dailyAt('07:00')->withoutOverlapping()->name('invoice-overdue-job')->onOneServer();
/* Performs auto billing */
$schedule->job(new AutoBillCron)->dailyAt('06:20')->withoutOverlapping()->name('auto-bill-job')->onOneServer();
/* Checks the status of the scheduler */
$schedule->job(new SchedulerCheck)->dailyAt('01:10')->withoutOverlapping();
/* Checks for scheduled tasks */
$schedule->job(new TaskScheduler())->hourlyAt(10)->withoutOverlapping()->name('task-scheduler-job')->onOneServer();
/* Performs system maintenance such as pruning the backup table */
$schedule->job(new SystemMaintenance)->sundays()->at('02:30')->withoutOverlapping()->name('system-maintenance-job')->onOneServer();
if (Ninja::isSelfHost()) {
$schedule->call(function () {
Account::whereNotNull('id')->update(['is_scheduler_running' => true]);
})->everyFiveMinutes();
}
/* Run hosted specific jobs */
if (Ninja::isHosted()) {
$schedule->job(new AdjustEmailQuota)->dailyAt('23:30')->withoutOverlapping();
/* Pulls in bank transactions from third party services */
$schedule->job(new BankTransactionSync)->dailyAt('04:10')->withoutOverlapping()->name('bank-trans-sync-job')->onOneServer();
//not used @deprecate
// $schedule->job(new SendFailedEmails)->daily()->withoutOverlapping();
$schedule->command('ninja:check-data --database=db-ninja-01')->dailyAt('02:10')->withoutOverlapping()->name('check-data-db-1-job')->onOneServer();
$schedule->command('ninja:check-data --database=db-ninja-02')->dailyAt('02:20')->withoutOverlapping()->name('check-data-db-2-job')->onOneServer();
$schedule->command('ninja:s3-cleanup')->dailyAt('23:15')->withoutOverlapping()->name('s3-cleanup-job')->onOneServer();
}
if (config('queue.default') == 'database' && Ninja::isSelfHost() && config('ninja.internal_queue_enabled') && ! config('ninja.is_docker')) {
$schedule->command('queue:work database --stop-when-empty --memory=256')->everyMinute()->withoutOverlapping();
$schedule->command('queue:restart')->everyFiveMinutes()->withoutOverlapping();
}
}
/**
* Register the commands for the application.
*
* @return void
*/
protected function commands()
{
$this->load(__DIR__.'/Commands');
require base_path('routes/console.php');
} }
} }

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