diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index ad1109a26bcb..5112b4f9ef94 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -13,8 +13,12 @@ jobs: strategy: matrix: operating-system: ['ubuntu-20.04', 'ubuntu-22.04'] - php-versions: ['8.1'] + 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 @@ -25,13 +29,14 @@ jobs: DB_USERNAME: root DB_PASSWORD: ninja DB_HOST: '127.0.0.1' + REDIS_PORT: 6379 BROADCAST_DRIVER: log - CACHE_DRIVER: file - QUEUE_CONNECTION: sync - SESSION_DRIVER: file + CACHE_DRIVER: redis + QUEUE_CONNECTION: redis + SESSION_DRIVER: redis NINJA_ENVIRONMENT: hosted MULTI_DB_ENABLED: false - NINJA_LICENSE: 123456 + NINJA_LICENSE: ${{ secrets.ninja_license }} TRAVIS: true MAIL_MAILER: log @@ -47,13 +52,18 @@ jobs: 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 mysql service + - name: Start MariaDB service run: | sudo systemctl start mysql.service - name: Verify MariaDB connection @@ -65,11 +75,11 @@ jobs: while ! mysqladmin ping -h"127.0.0.1" -P"$DB_PORT" --silent; do sleep 1 done - - name: Setup PHP + - 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 + extensions: mysql, mysqlnd, sqlite3, bcmath, gmp, gd, curl, zip, openssl, mbstring, xml, redis - uses: actions/checkout@v1 with: @@ -79,32 +89,56 @@ jobs: - 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: Cache dependencies actions/cache@v3 + uses: actions/cache@v3 + with: + path: ~/.composer/cache/files + key: dependencies-${{ matrix.dependency-version }}-laravel-${{ matrix.laravel }}-php-${{ matrix.php-versions }}-composer-${{ hashFiles('composer.json') }} + - 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 - - name: Create DB and schemas - run: | - mkdir -p database - touch database/database.sqlite + php artisan ninja:post-update + - name: Migrate Database run: | php artisan migrate:fresh --seed --force && php artisan db:seed --force - - name: Prepare JS/CSS assets - run: | - npm i - npm run production + + # - name: Prepare JS/CSS assets + # run: | + # npm i + # npm run production + - name: Run Testsuite run: | cat .env vendor/bin/snappdf download - vendor/bin/phpunit --testdox + tests/ci env: DB_PORT: ${{ job.services.mysql.ports[3306] }} PHP_CS_FIXER_IGNORE_ENV: true + CI_NODE_TOTAL: ${{ matrix.ci_node_total }} + # Use the index from matrix as an environment variable + CI_NODE_INDEX: ${{ matrix.ci_node_index }} \ No newline at end of file diff --git a/VERSION.txt b/VERSION.txt index cae032632b73..e0b13d4547dc 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.5.56 \ No newline at end of file +5.5.57 \ No newline at end of file diff --git a/app/Console/Commands/CreateSingleAccount.php b/app/Console/Commands/CreateSingleAccount.php index fb7e9b575dce..5a18fdd66277 100644 --- a/app/Console/Commands/CreateSingleAccount.php +++ b/app/Console/Commands/CreateSingleAccount.php @@ -80,10 +80,7 @@ class CreateSingleAccount extends Command public function handle() { - if(config('ninja.is_docker')) - return; - - if (!$this->confirm('Are you sure you want to inject dummy data?')) + if (Ninja::isHosted() || config('ninja.is_docker') || !$this->confirm('Are you sure you want to inject dummy data?')) return; $this->invoice_repo = new InvoiceRepository(); @@ -105,6 +102,11 @@ class CreateSingleAccount extends Command { $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, diff --git a/app/Console/Commands/PostUpdate.php b/app/Console/Commands/PostUpdate.php index 007a9bed6211..73d7d1e10896 100644 --- a/app/Console/Commands/PostUpdate.php +++ b/app/Console/Commands/PostUpdate.php @@ -13,11 +13,14 @@ namespace App\Console\Commands; use App\Jobs\Util\VersionCheck; use App\Utils\Ninja; +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. * @@ -83,6 +86,8 @@ class PostUpdate extends Command info('queue restarted'); + $this->buildCache(true); + VersionCheck::dispatch(); info('Sent for version check'); diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 7587da2e5062..352b89b181e2 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -94,8 +94,6 @@ class Kernel extends ConsoleKernel /* Performs system maintenance such as pruning the backup table */ $schedule->job(new SystemMaintenance)->sundays()->at('02:30')->withoutOverlapping()->name('system-maintenance-job')->onOneServer(); - /* Pulls in bank transactions from third party services */ - $schedule->job(new BankTransactionSync)->dailyAt('04:10')->withoutOverlapping()->name('bank-trans-sync-job')->onOneServer(); if (Ninja::isSelfHost()) { @@ -110,6 +108,9 @@ class Kernel extends ConsoleKernel $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(); diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index e65eedfa9e75..fbf2e25ecf76 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -453,8 +453,10 @@ class CompanySettings extends BaseSettings public $show_email_footer = true; + public $company_logo_size = '65%'; public static $casts = [ + 'company_logo_size' => 'string', 'show_email_footer' => 'bool', 'email_alignment' => 'string', 'auto_bill_standard_invoices' => 'bool', diff --git a/app/DataMapper/Schedule/ClientStatement.php b/app/DataMapper/Schedule/ClientStatement.php index 37ba76d081cb..6ab2284f771b 100644 --- a/app/DataMapper/Schedule/ClientStatement.php +++ b/app/DataMapper/Schedule/ClientStatement.php @@ -9,7 +9,7 @@ * @license https://www.elastic.co/licensing/elastic-license */ -namespace App\DataMapper; +namespace App\DataMapper\Schedule; use App\Models\Client; use stdClass; diff --git a/app/Filters/BankIntegrationFilters.php b/app/Filters/BankIntegrationFilters.php index 381966b35a50..9774af605711 100644 --- a/app/Filters/BankIntegrationFilters.php +++ b/app/Filters/BankIntegrationFilters.php @@ -11,11 +11,7 @@ namespace App\Filters; -use App\Models\BankIntegration; -use App\Models\User; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Gate; /** * BankIntegrationFilters. diff --git a/app/Filters/BankTransactionFilters.php b/app/Filters/BankTransactionFilters.php index 96eec99db028..48d2565f9327 100644 --- a/app/Filters/BankTransactionFilters.php +++ b/app/Filters/BankTransactionFilters.php @@ -12,10 +12,7 @@ namespace App\Filters; use App\Models\BankTransaction; -use App\Models\User; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Gate; /** * BankTransactionFilters. @@ -77,87 +74,49 @@ class BankTransactionFilters extends QueryFilters $status_parameters = explode(',', $value); - $status_array = []; - - $debit_or_withdrawal_array = []; - if (in_array('all', $status_parameters)) { return $this->builder; } - if (in_array('unmatched', $status_parameters)) { - $status_array[] = BankTransaction::STATUS_UNMATCHED; - // $this->builder->orWhere('status_id', BankTransaction::STATUS_UNMATCHED); - } + $this->builder->where(function ($query) use ($status_parameters){ - if (in_array('matched', $status_parameters)) { - $status_array[] = BankTransaction::STATUS_MATCHED; - // $this->builder->where('status_id', BankTransaction::STATUS_MATCHED); - } + $status_array = []; + + $debit_or_withdrawal_array = []; - if (in_array('converted', $status_parameters)) { - $status_array[] = BankTransaction::STATUS_CONVERTED; - // $this->builder->where('status_id', BankTransaction::STATUS_CONVERTED); - } + if (in_array('unmatched', $status_parameters)) { + $status_array[] = BankTransaction::STATUS_UNMATCHED; + } - if (in_array('deposits', $status_parameters)) { - $debit_or_withdrawal_array[] = 'CREDIT'; - // $this->builder->where('base_type', 'CREDIT'); - } + if (in_array('matched', $status_parameters)) { + $status_array[] = BankTransaction::STATUS_MATCHED; + } - if (in_array('withdrawals', $status_parameters)) { - $debit_or_withdrawal_array[] = 'DEBIT'; - // $this->builder->where('base_type', 'DEBIT'); - } + if (in_array('converted', $status_parameters)) { + $status_array[] = BankTransaction::STATUS_CONVERTED; + } - if(count($status_array) >=1) { - $this->builder->whereIn('status_id', $status_array); - } + if (in_array('deposits', $status_parameters)) { + $debit_or_withdrawal_array[] = 'CREDIT'; + } - if(count($debit_or_withdrawal_array) >=1) { - $this->builder->orWhereIn('base_type', $debit_or_withdrawal_array); - } + if (in_array('withdrawals', $status_parameters)) { + $debit_or_withdrawal_array[] = 'DEBIT'; + } + + if(count($status_array) >=1) { + $query->whereIn('status_id', $status_array); + } + + if(count($debit_or_withdrawal_array) >=1) { + $query->orWhereIn('base_type', $debit_or_withdrawal_array); + } + + }); return $this->builder; } - /** - * Filters the list based on the status - * archived, active, deleted. - * - * @param string filter - * @return Builder - */ - public function status(string $filter = '') : Builder - { - if (strlen($filter) == 0) { - return $this->builder; - } - - $filters = explode(',', $filter); - - return $this->builder->where(function ($query) use ($filters) { - - if (in_array(parent::STATUS_ACTIVE, $filters)) { - $query->orWhereNull('deleted_at'); - } - - if (in_array(parent::STATUS_ARCHIVED, $filters)) { - $query->orWhere(function ($query) use ($table) { - $query->whereNotNull($table.'.deleted_at'); - - if (! in_array($table, ['users'])) { - $query->where($table.'.is_deleted', '=', 0); - } - }); - } - - if (in_array(parent::STATUS_DELETED, $filters)) { - $query->orWhere($table.'.is_deleted', '=', 1); - } - }); - } - /** * Sorts the list based on $sort. * @@ -186,19 +145,6 @@ class BankTransactionFilters extends QueryFilters return $this->builder->orderBy($sort_col[0], $sort_col[1]); } - /** - * Returns the base query. - * - * @param int company_id - * @param User $user - * @return Builder - * @deprecated - */ - public function baseQuery(int $company_id, User $user) : Builder - { - - } - /** * Filters the query by the users company ID. * @@ -206,7 +152,6 @@ class BankTransactionFilters extends QueryFilters */ public function entityFilter() { - //return $this->builder->whereCompanyId(auth()->user()->company()->id); return $this->builder->company(); } } diff --git a/app/Filters/ClientFilters.php b/app/Filters/ClientFilters.php index df4b86fdbca3..63a6598b9541 100644 --- a/app/Filters/ClientFilters.php +++ b/app/Filters/ClientFilters.php @@ -108,17 +108,17 @@ class ClientFilters extends QueryFilters } return $this->builder->where(function ($query) use ($filter) { - $query->where('clients.name', 'like', '%'.$filter.'%') - ->orWhere('clients.id_number', 'like', '%'.$filter.'%') + $query->where('name', 'like', '%'.$filter.'%') + ->orWhere('id_number', 'like', '%'.$filter.'%') ->orWhereHas('contacts', function ($query) use ($filter) { $query->where('first_name', 'like', '%'.$filter.'%'); $query->orWhere('last_name', 'like', '%'.$filter.'%'); $query->orWhere('email', 'like', '%'.$filter.'%'); }) - ->orWhere('clients.custom_value1', 'like', '%'.$filter.'%') - ->orWhere('clients.custom_value2', 'like', '%'.$filter.'%') - ->orWhere('clients.custom_value3', 'like', '%'.$filter.'%') - ->orWhere('clients.custom_value4', 'like', '%'.$filter.'%'); + ->orWhere('custom_value1', 'like', '%'.$filter.'%') + ->orWhere('custom_value2', 'like', '%'.$filter.'%') + ->orWhere('custom_value3', 'like', '%'.$filter.'%') + ->orWhere('custom_value4', 'like', '%'.$filter.'%'); }); } @@ -147,4 +147,14 @@ class ClientFilters extends QueryFilters { return $this->builder->company(); } + + public function filter_details(string $filter = '') + { + + if($filter == 'true') + return $this->builder->select('id', 'name', 'number', 'id_number'); + + return $this->builder; + + } } diff --git a/app/Filters/CreditFilters.php b/app/Filters/CreditFilters.php index df34873d438f..cd51ceabb03a 100644 --- a/app/Filters/CreditFilters.php +++ b/app/Filters/CreditFilters.php @@ -13,9 +13,7 @@ namespace App\Filters; use App\Models\Credit; -use App\Models\User; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\Carbon; class CreditFilters extends QueryFilters { @@ -44,20 +42,20 @@ class CreditFilters extends QueryFilters return $this->builder; } - if (in_array('draft', $status_parameters)) { - $this->builder->where('status_id', Credit::STATUS_DRAFT); - } - if (in_array('partial', $status_parameters)) { - $this->builder->where('status_id', Credit::STATUS_PARTIAL); - } + $credit_filters = []; - if (in_array('applied', $status_parameters)) { - $this->builder->where('status_id', Credit::STATUS_APPLIED); - } + if (in_array('draft', $status_parameters)) + $credit_filters[] = Credit::STATUS_DRAFT; + + if (in_array('partial', $status_parameters)) + $credit_filters[] = Credit::STATUS_PARTIAL; - //->where('due_date', '>', Carbon::now()) - //->orWhere('partial_due_date', '>', Carbon::now()); + if (in_array('applied', $status_parameters)) + $credit_filters[] = Credit::STATUS_APPLIED; + + if(count($credit_filters) >=1) + $this->builder->whereIn('status_id', $credit_filters); return $this->builder; } diff --git a/app/Filters/DesignFilters.php b/app/Filters/DesignFilters.php index 08f235e932c5..76b7abb33a63 100644 --- a/app/Filters/DesignFilters.php +++ b/app/Filters/DesignFilters.php @@ -11,11 +11,7 @@ namespace App\Filters; -use App\Models\Design; -use App\Models\User; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Gate; /** * DesignFilters. @@ -27,9 +23,10 @@ class DesignFilters extends QueryFilters * * @param string query filter * @return Builder + * * @deprecated */ - public function filter(string $filter = '') : Builder + public function filter(string $filter = ''): Builder { if (strlen($filter) == 0) { return $this->builder; @@ -44,48 +41,17 @@ class DesignFilters extends QueryFilters * Sorts the list based on $sort. * * @param string sort formatted as column|asc + * * @return Builder */ - public function sort(string $sort) : Builder + public function sort(string $sort): Builder { $sort_col = explode('|', $sort); - return $this->builder->orderBy($sort_col[0], $sort_col[1]); - } + if(is_array($sort_col)) + return $this->builder->orderBy($sort_col[0], $sort_col[1]); - /** - * Returns the base query. - * - * @param int company_id - * @param User $user - * @return Builder - * @deprecated - */ - public function baseQuery(int $company_id, User $user) : Builder - { - $query = DB::table('designs') - ->join('companies', 'companies.id', '=', 'designs.company_id') - ->where('designs.company_id', '=', $company_id) - ->select( - 'designs.id', - 'designs.name', - 'designs.design', - 'designs.created_at', - 'designs.created_at as design_created_at', - 'designs.deleted_at', - 'designs.is_deleted', - 'designs.user_id', - ); - - /* - * If the user does not have permissions to view all invoices - * limit the user to only the invoices they have created - */ - if (Gate::denies('view-list', Design::class)) { - $query->where('designs.user_id', '=', $user->id); - } - - return $query; + return $this->builder; } /** @@ -93,7 +59,7 @@ class DesignFilters extends QueryFilters * * @return Illuminate\Database\Query\Builder */ - public function entityFilter() + public function entityFilter(): Builder { //return $this->builder->whereCompanyId(auth()->user()->company()->id); return $this->builder->where('company_id', auth()->user()->company()->id)->orWhere('company_id', null)->orderBy('id','asc'); diff --git a/app/Filters/DocumentFilters.php b/app/Filters/DocumentFilters.php index ec7b34eb7072..a684adf93072 100644 --- a/app/Filters/DocumentFilters.php +++ b/app/Filters/DocumentFilters.php @@ -12,7 +12,6 @@ namespace App\Filters; use App\Models\Company; -use App\Models\User; use Illuminate\Database\Eloquent\Builder; /** @@ -27,7 +26,7 @@ class DocumentFilters extends QueryFilters * @return Builder * @deprecated */ - public function filter(string $filter = '') : Builder + public function filter(string $filter = ''): Builder { if (strlen($filter) == 0) { return $this->builder; @@ -36,8 +35,15 @@ class DocumentFilters extends QueryFilters return $this->builder; } - /* If client ID passed to this entity, simply return */ - public function client_id(string $client_id = '') :Builder + /** + * Overriding method as client_id does + * not exist on this model, just pass + * back the builder + * @param string $client_id The client hashed id. + * + * @return Builder + */ + public function client_id(string $client_id = ''): Builder { return $this->builder; } @@ -48,11 +54,14 @@ class DocumentFilters extends QueryFilters * @param string sort formatted as column|asc * @return Builder */ - public function sort(string $sort) : Builder + public function sort(string $sort = '') : Builder { $sort_col = explode('|', $sort); - return $this->builder->orderBy($sort_col[0], $sort_col[1]); + if(is_array($sort_col)) + return $this->builder->orderBy($sort_col[0], $sort_col[1]); + + return $this->builder; } diff --git a/app/Filters/ExpenseCategoryFilters.php b/app/Filters/ExpenseCategoryFilters.php index aebfc5c91971..7b9bbd61f928 100644 --- a/app/Filters/ExpenseCategoryFilters.php +++ b/app/Filters/ExpenseCategoryFilters.php @@ -11,11 +11,7 @@ namespace App\Filters; -use App\Models\Expense; -use App\Models\User; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Gate; /** * ExpenseCategoryFilters. @@ -49,9 +45,9 @@ class ExpenseCategoryFilters extends QueryFilters { $sort_col = explode('|', $sort); - if (is_array($sort_col) && in_array($sort_col[1], ['asc', 'desc']) && in_array($sort_col[0], ['name'])) { + if (is_array($sort_col) && in_array($sort_col[1], ['asc', 'desc']) && in_array($sort_col[0], ['name'])) return $this->builder->orderBy($sort_col[0], $sort_col[1]); - } + return $this->builder; } @@ -63,8 +59,6 @@ class ExpenseCategoryFilters extends QueryFilters */ public function entityFilter() { - - //return $this->builder->whereCompanyId(auth()->user()->company()->id); return $this->builder->company(); } } diff --git a/app/Filters/ExpenseFilters.php b/app/Filters/ExpenseFilters.php index 2a1892e9f2a3..5eb7b8c43999 100644 --- a/app/Filters/ExpenseFilters.php +++ b/app/Filters/ExpenseFilters.php @@ -11,11 +11,7 @@ namespace App\Filters; -use App\Models\Expense; -use App\Models\User; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Gate; /** * ExpenseFilters. @@ -36,11 +32,11 @@ class ExpenseFilters extends QueryFilters } return $this->builder->where(function ($query) use ($filter) { - $query->where('expenses.public_notes', 'like', '%'.$filter.'%') - ->orWhere('expenses.custom_value1', 'like', '%'.$filter.'%') - ->orWhere('expenses.custom_value2', 'like', '%'.$filter.'%') - ->orWhere('expenses.custom_value3', 'like', '%'.$filter.'%') - ->orWhere('expenses.custom_value4', 'like', '%'.$filter.'%'); + $query->where('public_notes', 'like', '%'.$filter.'%') + ->orWhere('custom_value1', 'like', '%'.$filter.'%') + ->orWhere('custom_value2', 'like', '%'.$filter.'%') + ->orWhere('custom_value3', 'like', '%'.$filter.'%') + ->orWhere('custom_value4', 'like', '%'.$filter.'%'); }); } diff --git a/app/Filters/InvoiceFilters.php b/app/Filters/InvoiceFilters.php index da66d3a9b792..19eb08c8289c 100644 --- a/app/Filters/InvoiceFilters.php +++ b/app/Filters/InvoiceFilters.php @@ -12,7 +12,6 @@ namespace App\Filters; use App\Models\Invoice; -use App\Models\User; use App\Utils\Traits\MakesHash; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Carbon; @@ -80,6 +79,9 @@ class InvoiceFilters extends QueryFilters public function number(string $number = '') :Builder { + if(strlen($number) == 0) + return $this->builder; + return $this->builder->where('number', $number); } diff --git a/app/Filters/PaymentFilters.php b/app/Filters/PaymentFilters.php index dc012dff3aa2..2977e072cb7f 100644 --- a/app/Filters/PaymentFilters.php +++ b/app/Filters/PaymentFilters.php @@ -11,7 +11,6 @@ namespace App\Filters; -use App\Models\User; use Illuminate\Database\Eloquent\Builder; /** @@ -49,6 +48,7 @@ class PaymentFilters extends QueryFilters { if($value == 'true'){ + return $this->builder ->where('is_deleted',0) ->where(function ($query){ @@ -72,7 +72,10 @@ class PaymentFilters extends QueryFilters { $sort_col = explode('|', $sort); - return $this->builder->orderBy($sort_col[0], $sort_col[1]); + if(is_array($sort_col)) + return $this->builder->orderBy($sort_col[0], $sort_col[1]); + + return true; } public function number(string $number) : Builder diff --git a/app/Filters/PaymentTermFilters.php b/app/Filters/PaymentTermFilters.php index 0352cca5f213..fb4be8c424b5 100644 --- a/app/Filters/PaymentTermFilters.php +++ b/app/Filters/PaymentTermFilters.php @@ -11,11 +11,7 @@ namespace App\Filters; -use App\Models\Design; -use App\Models\User; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Gate; /** * PaymentTermFilters. @@ -29,7 +25,7 @@ class PaymentTermFilters extends QueryFilters * @return Builder * @deprecated */ - public function filter(string $filter = '') : Builder + public function filter(string $filter = ''): Builder { if (strlen($filter) == 0) { return $this->builder; @@ -46,7 +42,7 @@ class PaymentTermFilters extends QueryFilters * @param string sort formatted as column|asc * @return Builder */ - public function sort(string $sort) : Builder + public function sort(string $sort): Builder { $sort_col = explode('|', $sort); @@ -56,12 +52,10 @@ class PaymentTermFilters extends QueryFilters /** * Filters the query by the users company ID. * - * @return Illuminate\Database\Query\Builder + * @return Builder */ - public function entityFilter() + public function entityFilter(): Builder { return $this->builder->company(); - //return $this->builder->whereCompanyId(auth()->user()->company()->id); - // return $this->builder->whereCompanyId(auth()->user()->company()->id)->orWhere('company_id', null); } } diff --git a/app/Filters/ProductFilters.php b/app/Filters/ProductFilters.php index 8fa1d25264ed..1d6f524c9221 100644 --- a/app/Filters/ProductFilters.php +++ b/app/Filters/ProductFilters.php @@ -11,7 +11,6 @@ namespace App\Filters; -use App\Models\User; use Illuminate\Database\Eloquent\Builder; /** diff --git a/app/Filters/ProjectFilters.php b/app/Filters/ProjectFilters.php index e489488b0cd1..030a7be215f3 100644 --- a/app/Filters/ProjectFilters.php +++ b/app/Filters/ProjectFilters.php @@ -11,11 +11,7 @@ namespace App\Filters; -use App\Models\Project; -use App\Models\User; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Gate; /** * ProjectFilters. @@ -52,7 +48,8 @@ class ProjectFilters extends QueryFilters { $sort_col = explode('|', $sort); - return $this->builder->orderBy($sort_col[0], $sort_col[1]); + if(is_array($sort_col)) + return $this->builder->orderBy($sort_col[0], $sort_col[1]); } /** diff --git a/app/Filters/PurchaseOrderFilters.php b/app/Filters/PurchaseOrderFilters.php index ad86221ff6a8..5c21080da364 100644 --- a/app/Filters/PurchaseOrderFilters.php +++ b/app/Filters/PurchaseOrderFilters.php @@ -12,7 +12,6 @@ namespace App\Filters; use App\Models\PurchaseOrder; -use App\Models\User; use Illuminate\Database\Eloquent\Builder; class PurchaseOrderFilters extends QueryFilters @@ -70,7 +69,7 @@ class PurchaseOrderFilters extends QueryFilters if(count($status_parameters) >=1) { $query->whereIn('status_id', $status_parameters); } - }) + }); return $this->builder; } diff --git a/app/Filters/QuoteFilters.php b/app/Filters/QuoteFilters.php index c83ed856293d..c0b88a82b4da 100644 --- a/app/Filters/QuoteFilters.php +++ b/app/Filters/QuoteFilters.php @@ -12,7 +12,6 @@ namespace App\Filters; use App\Models\Quote; -use App\Models\User; use Illuminate\Database\Eloquent\Builder; /** diff --git a/app/Filters/RecurringExpenseFilters.php b/app/Filters/RecurringExpenseFilters.php index bf82facccc70..f7a0872720b3 100644 --- a/app/Filters/RecurringExpenseFilters.php +++ b/app/Filters/RecurringExpenseFilters.php @@ -11,11 +11,7 @@ namespace App\Filters; -use App\Models\RecurringExpense; -use App\Models\User; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Gate; /** * RecurringExpenseFilters. @@ -29,19 +25,18 @@ class RecurringExpenseFilters extends QueryFilters * @return Builder * @deprecated */ - public function filter(string $filter = '') : Builder + public function filter(string $filter = ''): Builder { if (strlen($filter) == 0) { return $this->builder; } - return $this->builder->where(function ($query) use ($filter) { - $query->where('recurring_expenses.name', 'like', '%'.$filter.'%') - ->orWhere('recurring_expenses.id_number', 'like', '%'.$filter.'%') - ->orWhere('recurring_expenses.custom_value1', 'like', '%'.$filter.'%') - ->orWhere('recurring_expenses.custom_value2', 'like', '%'.$filter.'%') - ->orWhere('recurring_expenses.custom_value3', 'like', '%'.$filter.'%') - ->orWhere('recurring_expenses.custom_value4', 'like', '%'.$filter.'%'); + return $this->builder->where(function ($query) use ($filter) { + $query->where('public_notes', 'like', '%'.$filter.'%') + ->orWhere('custom_value1', 'like', '%'.$filter.'%') + ->orWhere('custom_value2', 'like', '%'.$filter.'%') + ->orWhere('custom_value3', 'like', '%'.$filter.'%') + ->orWhere('custom_value4', 'like', '%'.$filter.'%'); }); } @@ -51,7 +46,7 @@ class RecurringExpenseFilters extends QueryFilters * @param string sort formatted as column|asc * @return Builder */ - public function sort(string $sort) : Builder + public function sort(string $sort): Builder { $sort_col = explode('|', $sort); diff --git a/app/Filters/RecurringInvoiceFilters.php b/app/Filters/RecurringInvoiceFilters.php index dfd0ee60e438..88a0f4ee3c41 100644 --- a/app/Filters/RecurringInvoiceFilters.php +++ b/app/Filters/RecurringInvoiceFilters.php @@ -12,7 +12,6 @@ namespace App\Filters; use App\Models\RecurringInvoice; -use App\Models\User; use Illuminate\Database\Eloquent\Builder; /** @@ -77,7 +76,10 @@ class RecurringInvoiceFilters extends QueryFilters if (in_array('completed', $status_parameters)) $recurring_filters[] = RecurringInvoice::STATUS_COMPLETED; - return $this->builder->whereIn('status_id', $recurring_filters); + if(count($recurring_filters) >= 1) + return $this->builder->whereIn('status_id', $recurring_filters); + + return $this->builder; } diff --git a/app/Filters/RecurringQuoteFilters.php b/app/Filters/RecurringQuoteFilters.php index 3a1879e9ed79..1229ee4c5ae7 100644 --- a/app/Filters/RecurringQuoteFilters.php +++ b/app/Filters/RecurringQuoteFilters.php @@ -11,7 +11,6 @@ namespace App\Filters; -use App\Models\User; use Illuminate\Database\Eloquent\Builder; /** @@ -26,7 +25,7 @@ class RecurringQuoteFilters extends QueryFilters * @return Builder * @deprecated */ - public function filter(string $filter = '') : Builder + public function filter(string $filter = ''): Builder { if (strlen($filter) == 0) { return $this->builder; @@ -46,7 +45,7 @@ class RecurringQuoteFilters extends QueryFilters * @param string sort formatted as column|asc * @return Builder */ - public function sort(string $sort) : Builder + public function sort(string $sort): Builder { $sort_col = explode('|', $sort); @@ -56,9 +55,9 @@ class RecurringQuoteFilters extends QueryFilters /** * Filters the query by the users company ID. * - * @return Illuminate\Database\Query\Builder + * @return Builder */ - public function entityFilter() + public function entityFilter(): Builder { return $this->builder->company(); } diff --git a/app/Filters/SubscriptionFilters.php b/app/Filters/SubscriptionFilters.php index 4ae2e1aec1fb..7c3775291119 100644 --- a/app/Filters/SubscriptionFilters.php +++ b/app/Filters/SubscriptionFilters.php @@ -11,11 +11,7 @@ namespace App\Filters; -use App\Models\User; -use App\Models\Webhook; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Gate; /** * SubscriptionFilters. @@ -29,7 +25,7 @@ class SubscriptionFilters extends QueryFilters * @return Builder * @deprecated */ - public function filter(string $filter = '') : Builder + public function filter(string $filter = ''): Builder { if (strlen($filter) == 0) { return $this->builder; @@ -46,7 +42,7 @@ class SubscriptionFilters extends QueryFilters * @param string sort formatted as column|asc * @return Builder */ - public function sort(string $sort) : Builder + public function sort(string $sort): Builder { $sort_col = explode('|', $sort); @@ -56,9 +52,9 @@ class SubscriptionFilters extends QueryFilters /** * Filters the query by the users company ID. * - * @return Illuminate\Database\Query\Builder + * @return Builder */ - public function entityFilter() + public function entityFilter(): Builder { return $this->builder->company(); } diff --git a/app/Filters/SystemLogFilters.php b/app/Filters/SystemLogFilters.php index 126c81209955..a7200d6b0911 100644 --- a/app/Filters/SystemLogFilters.php +++ b/app/Filters/SystemLogFilters.php @@ -11,7 +11,6 @@ namespace App\Filters; -use App\Models\User; use Illuminate\Database\Eloquent\Builder; /** @@ -66,9 +65,9 @@ class SystemLogFilters extends QueryFilters /** * Filters the query by the users company ID. * - * @return Illuminate\Database\Query\Builder + * @return Builder */ - public function entityFilter() + public function entityFilter(): Builder { return $this->builder->company(); } diff --git a/app/Filters/TaskFilters.php b/app/Filters/TaskFilters.php index f4a0fb519912..43e77faff141 100644 --- a/app/Filters/TaskFilters.php +++ b/app/Filters/TaskFilters.php @@ -11,7 +11,6 @@ namespace App\Filters; -use App\Models\User; use App\Utils\Traits\MakesHash; use Illuminate\Database\Eloquent\Builder; @@ -29,7 +28,7 @@ class TaskFilters extends QueryFilters * @return Builder * @deprecated */ - public function filter(string $filter = '') : Builder + public function filter(string $filter = ''): Builder { if (strlen($filter) == 0) { return $this->builder; @@ -55,7 +54,7 @@ class TaskFilters extends QueryFilters * @param string client_status The invoice status as seen by the client * @return Builder */ - public function client_status(string $value = '') :Builder + public function client_status(string $value = ''): Builder { if (strlen($value) == 0) { return $this->builder; @@ -90,7 +89,7 @@ class TaskFilters extends QueryFilters * @param string sort formatted as column|asc * @return Builder */ - public function sort(string $sort) : Builder + public function sort(string $sort): Builder { $sort_col = explode('|', $sort); @@ -100,9 +99,9 @@ class TaskFilters extends QueryFilters /** * Filters the query by the users company ID. * - * @return Illuminate\Database\Query\Builder + * @return Builder */ - public function entityFilter() + public function entityFilter(): Builder { return $this->builder->company(); } diff --git a/app/Filters/TaskStatusFilters.php b/app/Filters/TaskStatusFilters.php index d197b764603c..61ecf95b7b1f 100644 --- a/app/Filters/TaskStatusFilters.php +++ b/app/Filters/TaskStatusFilters.php @@ -11,7 +11,6 @@ namespace App\Filters; -use App\Models\User; use Illuminate\Database\Eloquent\Builder; /** @@ -26,7 +25,7 @@ class TaskStatusFilters extends QueryFilters * @return Builder * @deprecated */ - public function filter(string $filter = '') : Builder + public function filter(string $filter = ''): Builder { if (strlen($filter) == 0) { return $this->builder; @@ -53,9 +52,9 @@ class TaskStatusFilters extends QueryFilters /** * Filters the query by the users company ID. * - * @return Illuminate\Database\Query\Builder + * @return Builder */ - public function entityFilter() + public function entityFilter(): Builder { return $this->builder->company(); } diff --git a/app/Filters/TaxRateFilters.php b/app/Filters/TaxRateFilters.php index a161ccec7750..1e838b779ad6 100644 --- a/app/Filters/TaxRateFilters.php +++ b/app/Filters/TaxRateFilters.php @@ -11,7 +11,6 @@ namespace App\Filters; -use App\Models\User; use Illuminate\Database\Eloquent\Builder; /** @@ -53,9 +52,9 @@ class TaxRateFilters extends QueryFilters /** * Filters the query by the users company ID. * - * @return Illuminate\Database\Query\Builder + * @return Builder */ - public function entityFilter() + public function entityFilter(): Builder { return $this->builder->company(); } diff --git a/app/Filters/TokenFilters.php b/app/Filters/TokenFilters.php index ac9507fb4ad9..d4cd948bcb95 100644 --- a/app/Filters/TokenFilters.php +++ b/app/Filters/TokenFilters.php @@ -11,10 +11,7 @@ namespace App\Filters; -use App\Models\User; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Gate; /** * TokenFilters. @@ -55,9 +52,9 @@ class TokenFilters extends QueryFilters /** * Filters the query by the users company ID. * - * @return Illuminate\Database\Query\Builder + * @return Builder */ - public function entityFilter() + public function entityFilter(): Builder { return $this->builder->company(); } diff --git a/app/Filters/UserFilters.php b/app/Filters/UserFilters.php index bded4c02eb9e..986748955228 100644 --- a/app/Filters/UserFilters.php +++ b/app/Filters/UserFilters.php @@ -11,7 +11,6 @@ namespace App\Filters; -use App\Models\User; use Illuminate\Database\Eloquent\Builder; /** @@ -61,10 +60,29 @@ class UserFilters extends QueryFilters */ public function entityFilter() { - //return $this->builder->user_companies()->whereCompanyId(auth()->user()->company()->id); - //return $this->builder->whereCompanyId(auth()->user()->company()->id); return $this->builder->whereHas('company_users', function ($q) { $q->where('company_id', '=', auth()->user()->company()->id); }); } + + /** + * Overrides the base with() function as no company ID + * exists on the user table + * + * @param string $value Hashed ID of the user to return back in the dataset + * + * @return Builder + */ + public function with(string $value = ''): Builder + { + + if(strlen($value) == 0) + return $this->builder; + + return $this->builder + ->orWhere($this->with_property, $value) + ->orderByRaw("{$this->with_property} = ? DESC", [$value]) + ->where('account_id', auth()->user()->account_id); + } + } diff --git a/app/Filters/VendorFilters.php b/app/Filters/VendorFilters.php index 914025ac0457..f723893cef76 100644 --- a/app/Filters/VendorFilters.php +++ b/app/Filters/VendorFilters.php @@ -11,11 +11,7 @@ namespace App\Filters; -use App\Models\User; -use App\Models\Vendor; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Gate; /** * VendorFilters. @@ -29,7 +25,7 @@ class VendorFilters extends QueryFilters * @return Builder * @deprecated */ - public function filter(string $filter = '') : Builder + public function filter(string $filter = ''): Builder { if (strlen($filter) == 0) { return $this->builder; @@ -56,7 +52,7 @@ class VendorFilters extends QueryFilters * @param string sort formatted as column|asc * @return Builder */ - public function sort(string $sort) : Builder + public function sort(string $sort): Builder { $sort_col = explode('|', $sort); @@ -66,12 +62,10 @@ class VendorFilters extends QueryFilters /** * Filters the query by the users company ID. * - * @return Illuminate\Database\Query\Builder + * @return Builder */ - public function entityFilter() + public function entityFilter(): Builder { - - //return $this->builder->whereCompanyId(auth()->user()->company()->id); return $this->builder->company(); } } diff --git a/app/Filters/WebhookFilters.php b/app/Filters/WebhookFilters.php index 056b1adf4818..0975efbb5ea7 100644 --- a/app/Filters/WebhookFilters.php +++ b/app/Filters/WebhookFilters.php @@ -11,11 +11,7 @@ namespace App\Filters; -use App\Models\User; -use App\Models\Webhook; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Gate; /** * TokenFilters. @@ -29,14 +25,14 @@ class WebhookFilters extends QueryFilters * @return Builder * @deprecated */ - public function filter(string $filter = '') : Builder + public function filter(string $filter = ''): Builder { if (strlen($filter) == 0) { return $this->builder; } return $this->builder->where(function ($query) use ($filter) { - $query->where('webhooks.target_url', 'like', '%'.$filter.'%'); + $query->where('target_url', 'like', '%'.$filter.'%'); }); } @@ -46,7 +42,7 @@ class WebhookFilters extends QueryFilters * @param string sort formatted as column|asc * @return Builder */ - public function sort(string $sort) : Builder + public function sort(string $sort): Builder { $sort_col = explode('|', $sort); @@ -56,9 +52,9 @@ class WebhookFilters extends QueryFilters /** * Filters the query by the users company ID. * - * @return Illuminate\Database\Query\Builder + * @return Builder */ - public function entityFilter() + public function entityFilter(): Builder { return $this->builder->company(); } diff --git a/app/Helpers/ClientPortal.php b/app/Helpers/ClientPortal.php index 80c174ebf66b..f5bea3f91554 100644 --- a/app/Helpers/ClientPortal.php +++ b/app/Helpers/ClientPortal.php @@ -24,6 +24,9 @@ use Illuminate\View\View; function isActive($page, bool $boolean = false) { $current_page = Route::currentRouteName(); + $action = Route::currentRouteAction(); // string + + $show = str_replace(['.show','payment_methodss','documentss','subscriptionss','paymentss'],['s.index','payment_methods','documents','subscriptions','payments'], $current_page); if ($page == $current_page && $boolean) { return true; @@ -33,6 +36,12 @@ function isActive($page, bool $boolean = false) return 'bg-gray-200'; } + if(($page == $show) && $boolean){ + return true; + } + + + return false; } diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 065c81ef478e..362198218d09 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -56,7 +56,7 @@ class LoginController extends BaseController * description="Authentication", * @OA\ExternalDocumentation( * description="Find out more", - * url="http://docs.invoiceninja.com" + * url="https://invoiceninja.github.io" * ) * ) */ diff --git a/app/Http/Controllers/BankTransactionRuleController.php b/app/Http/Controllers/BankTransactionRuleController.php index 5e16a645e7e0..3386f95558dc 100644 --- a/app/Http/Controllers/BankTransactionRuleController.php +++ b/app/Http/Controllers/BankTransactionRuleController.php @@ -335,7 +335,7 @@ class BankTransactionRuleController extends BaseController * * @OA\Post( * path="/api/v1/bank_transaction_rules", - * operationId="storeBankTransaction", + * operationId="storeBankTransactionRule", * tags={"bank_transaction_rules"}, * summary="Adds a bank_transaction rule", * description="Adds an bank_transaction to a company", diff --git a/app/Http/Controllers/ClientPortal/PaymentController.php b/app/Http/Controllers/ClientPortal/PaymentController.php index 9267853d0af6..115009ccdd55 100644 --- a/app/Http/Controllers/ClientPortal/PaymentController.php +++ b/app/Http/Controllers/ClientPortal/PaymentController.php @@ -160,7 +160,7 @@ class PaymentController extends Controller } if (property_exists($payment_hash->data, 'billing_context')) { - $billing_subscription = \App\Models\Subscription::find($payment_hash->data->billing_context->subscription_id); + $billing_subscription = \App\Models\Subscription::find($this->decodePrimaryKey($payment_hash->data->billing_context->subscription_id)); return (new SubscriptionService($billing_subscription))->completePurchase($payment_hash); } diff --git a/app/Http/Controllers/OpenAPI/swagger-v3.php b/app/Http/Controllers/OpenAPI/swagger-v3.php index 584bf3293a6f..39c3af2c6bb8 100644 --- a/app/Http/Controllers/OpenAPI/swagger-v3.php +++ b/app/Http/Controllers/OpenAPI/swagger-v3.php @@ -19,8 +19,8 @@ * url="https://ninja.test", * ), * @OA\ExternalDocumentation( - * description="http://docs.invoiceninja.com", - * url="http://docs.invoiceninja.com" + * description="https://invoiceninja.github.io", + * url="https://invoiceninja.github.io" * ), * ), */ diff --git a/app/Http/Controllers/SelfUpdateController.php b/app/Http/Controllers/SelfUpdateController.php index abe82f44ef94..f1af705342ac 100644 --- a/app/Http/Controllers/SelfUpdateController.php +++ b/app/Http/Controllers/SelfUpdateController.php @@ -108,6 +108,8 @@ class SelfUpdateController extends BaseController $zipFile->openFile($file); $zipFile->deleteFromName(".htaccess"); + + $zipFile->rewrite(); $zipFile->extractTo(base_path()); diff --git a/app/Http/Controllers/TaskSchedulerController.php b/app/Http/Controllers/TaskSchedulerController.php index 61bb8432793c..c3f771a463e6 100644 --- a/app/Http/Controllers/TaskSchedulerController.php +++ b/app/Http/Controllers/TaskSchedulerController.php @@ -323,7 +323,7 @@ class TaskSchedulerController extends BaseController * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), - * @OA\JsonContent(ref="#/components/schemas/TaskScheduleSchema"), + * @OA\JsonContent(ref="#/components/schemas/TaskSchedulerSchema"), * ), * @OA\Response( * response=422, diff --git a/app/Http/Livewire/BillingPortalPurchase.php b/app/Http/Livewire/BillingPortalPurchase.php index 496c241b79aa..8ec53b204e60 100644 --- a/app/Http/Livewire/BillingPortalPurchase.php +++ b/app/Http/Livewire/BillingPortalPurchase.php @@ -403,10 +403,10 @@ class BillingPortalPurchase extends Component ->save(); Cache::put($this->hash, [ - 'subscription_id' => $this->subscription->id, + 'subscription_id' => $this->subscription->hashed_id, 'email' => $this->email ?? $this->contact->email, - 'client_id' => $this->contact->client->id, - 'invoice_id' => $this->invoice->id, + 'client_id' => $this->contact->client->hashed_id, + 'invoice_id' => $this->invoice->hashed_id, 'context' => 'purchase', 'campaign' => $this->campaign, ], now()->addMinutes(60)); diff --git a/app/Http/Livewire/BillingPortalPurchasev2.php b/app/Http/Livewire/BillingPortalPurchasev2.php index 8ff7b6114434..b22075aaa491 100644 --- a/app/Http/Livewire/BillingPortalPurchasev2.php +++ b/app/Http/Livewire/BillingPortalPurchasev2.php @@ -483,7 +483,12 @@ class BillingPortalPurchasev2 extends Component */ protected function getPaymentMethods() :self { - if($this->contact) + nlog("total amount = {$this->float_amount_total}"); + + if($this->float_amount_total == 0) + $this->methods = []; + + if($this->contact && $this->float_amount_total >= 1) $this->methods = $this->contact->client->service()->getPaymentMethods($this->float_amount_total); return $this; @@ -526,7 +531,7 @@ class BillingPortalPurchasev2 extends Component } $data = [ - 'client_id' => $this->contact->client->id, + 'client_id' => $this->contact->client->hashed_id, 'date' => now()->format('Y-m-d'), 'invitations' => [[ 'key' => '', @@ -547,10 +552,10 @@ class BillingPortalPurchasev2 extends Component ->save(); Cache::put($this->hash, [ - 'subscription_id' => $this->subscription->id, + 'subscription_id' => $this->subscription->hashed_id, 'email' => $this->email ?? $this->contact->email, - 'client_id' => $this->contact->client->id, - 'invoice_id' => $this->invoice->id, + 'client_id' => $this->contact->client->hashed_id, + 'invoice_id' => $this->invoice->hashed_id, 'context' => 'purchase', 'campaign' => $this->campaign, 'bundle' => $this->bundle, @@ -562,17 +567,62 @@ class BillingPortalPurchasev2 extends Component } + + /** + * Starts the trial + * + * @return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse + */ public function handleTrial() { return $this->subscription->service()->startTrial([ 'email' => $this->email ?? $this->contact->email, 'quantity' => $this->quantity, - 'contact_id' => $this->contact->id, - 'client_id' => $this->contact->client->id, + 'contact_id' => $this->contact->hashed_id, + 'client_id' => $this->contact->client->hashed_id, 'bundle' => $this->bundle, ]); } + /** + * When the subscription total comes to $0 we + * pass back a $0 Invoice. + * + * @return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse + */ + public function handlePaymentNotRequired() + { + + $eligibility_check = $this->subscription->service()->isEligible($this->contact); + + if(is_array($eligibility_check) && $eligibility_check['message'] != 'Success'){ + + $this->is_eligible = false; + $this->not_eligible_message = $eligibility_check['message']; + return $this; + + } + + $invoice = $this->subscription + ->service() + ->createInvoiceV2($this->bundle, $this->contact->client_id, $this->valid_coupon) + ->service() + ->fillDefaults() + ->adjustInventory() + ->save(); + + $invoice->number = null; + + $invoice->service() + ->markPaid() + ->save(); + + return $this->subscription + ->service() + ->handleNoPaymentFlow($invoice, $this->bundle, $this->contact); + + } + @@ -607,43 +657,6 @@ class BillingPortalPurchasev2 extends Component } - // /** - // * Handle user authentication - // * - // * @return $this|bool|void - // */ - // public function authenticate() - // { - // $this->validate(); - - // $contact = ClientContact::where('email', $this->email) - // ->where('company_id', $this->subscription->company_id) - // ->first(); - - // if ($contact && $this->steps['existing_user'] === false) { - // return $this->steps['existing_user'] = true; - // } - - // if ($contact && $this->steps['existing_user']) { - // $attempt = Auth::guard('contact')->attempt(['email' => $this->email, 'password' => $this->password, 'company_id' => $this->subscription->company_id]); - - // return $attempt - // ? $this->getPaymentMethods($contact) - // : session()->flash('message', 'These credentials do not match our records.'); - // } - - // $this->steps['existing_user'] = false; - - // $contact = $this->createBlankClient(); - - // if ($contact && $contact instanceof ClientContact) { - // $this->getPaymentMethods($contact); - // } - // } - - - - /** * Create a blank client. Used for new customers purchasing. * diff --git a/app/Http/Livewire/PaymentsTable.php b/app/Http/Livewire/PaymentsTable.php index 1ad99fa2bbcf..b6781a8917a4 100644 --- a/app/Http/Livewire/PaymentsTable.php +++ b/app/Http/Livewire/PaymentsTable.php @@ -42,7 +42,7 @@ class PaymentsTable extends Component public function render() { $query = Payment::query() - ->with('type', 'client') + ->with('type', 'client', 'invoices') ->where('company_id', $this->company->id) ->where('client_id', auth()->guard('contact')->user()->client_id) ->whereIn('status_id', [Payment::STATUS_FAILED, Payment::STATUS_COMPLETED, Payment::STATUS_PENDING, Payment::STATUS_REFUNDED, Payment::STATUS_PARTIALLY_REFUNDED]) diff --git a/app/Http/Livewire/SubscriptionPlanSwitch.php b/app/Http/Livewire/SubscriptionPlanSwitch.php index 47e4ce5a0935..e5a46238ff9d 100644 --- a/app/Http/Livewire/SubscriptionPlanSwitch.php +++ b/app/Http/Livewire/SubscriptionPlanSwitch.php @@ -106,11 +106,11 @@ class SubscriptionPlanSwitch extends Component ]); Cache::put($this->hash, [ - 'subscription_id' => $this->target->id, - 'target_id' => $this->target->id, - 'recurring_invoice' => $this->recurring_invoice->id, - 'client_id' => $this->recurring_invoice->client->id, - 'invoice_id' => $this->state['invoice']->id, + 'subscription_id' => $this->target->hashed_id, + 'target_id' => $this->target->hashed_id, + 'recurring_invoice' => $this->recurring_invoice->hashed_id, + 'client_id' => $this->recurring_invoice->client->hashed_id, + 'invoice_id' => $this->state['invoice']->hashed_id, 'context' => 'change_plan', now()->addMinutes(60), ] ); diff --git a/app/Http/Requests/Shop/StoreShopClientRequest.php b/app/Http/Requests/Shop/StoreShopClientRequest.php index 95932ea24985..e599ad0fb500 100644 --- a/app/Http/Requests/Shop/StoreShopClientRequest.php +++ b/app/Http/Requests/Shop/StoreShopClientRequest.php @@ -78,8 +78,6 @@ class StoreShopClientRequest extends Request $input = $this->all(); - //@todo implement feature permissions for > 100 clients - // $settings = ClientSettings::defaults(); if (array_key_exists('settings', $input) && ! empty($input['settings'])) { diff --git a/app/Http/Requests/TaskScheduler/DestroySchedulerRequest.php b/app/Http/Requests/TaskScheduler/DestroySchedulerRequest.php index 93e15f06df95..48f5b7cb09c6 100644 --- a/app/Http/Requests/TaskScheduler/DestroySchedulerRequest.php +++ b/app/Http/Requests/TaskScheduler/DestroySchedulerRequest.php @@ -9,7 +9,7 @@ * @license https://www.elastic.co/licensing/elastic-license */ -namespace App\Http\Requests\Task; +namespace App\Http\Requests\TaskScheduler; use App\Http\Requests\Request; diff --git a/app/Http/Requests/User/StoreUserRequest.php b/app/Http/Requests/User/StoreUserRequest.php index ea5571140c3c..bf5093be71a1 100644 --- a/app/Http/Requests/User/StoreUserRequest.php +++ b/app/Http/Requests/User/StoreUserRequest.php @@ -63,8 +63,6 @@ class StoreUserRequest extends Request { $input = $this->all(); - //unique user rule - check company_user table for user_id / company_id / account_id if none exist we can add the user. ELSE return false - if (array_key_exists('email', $input)) { $input['email'] = trim($input['email']); } @@ -79,12 +77,10 @@ class StoreUserRequest extends Request } if (! isset($input['company_user']['settings'])) { - //$input['company_user']['settings'] = DefaultSettings::userSettings(); $input['company_user']['settings'] = null; } } else { $input['company_user'] = [ - //'settings' => DefaultSettings::userSettings(), 'settings' => null, 'permissions' => '', ]; diff --git a/app/Jobs/Bank/MatchBankTransactions.php b/app/Jobs/Bank/MatchBankTransactions.php index 12d79b9b80b5..708206c15a3f 100644 --- a/app/Jobs/Bank/MatchBankTransactions.php +++ b/app/Jobs/Bank/MatchBankTransactions.php @@ -319,7 +319,7 @@ class MatchBankTransactions implements ShouldQueue }); - }, 1); + }, 2); if(!$this->invoice) return; diff --git a/app/Jobs/Cron/SubscriptionCron.php b/app/Jobs/Cron/SubscriptionCron.php index 1242a63cbc08..beee6f82d7f2 100644 --- a/app/Jobs/Cron/SubscriptionCron.php +++ b/app/Jobs/Cron/SubscriptionCron.php @@ -45,6 +45,7 @@ class SubscriptionCron $invoices = Invoice::where('is_deleted', 0) ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) ->where('balance', '>', 0) + ->where('is_proforma',0) ->whereDate('due_date', '<=', now()->addDay()->startOfDay()) ->whereNull('deleted_at') ->whereNotNull('subscription_id') @@ -74,6 +75,7 @@ class SubscriptionCron $invoices = Invoice::where('is_deleted', 0) ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) ->where('balance', '>', 0) + ->where('is_proforma',0) ->whereDate('due_date', '<=', now()->addDay()->startOfDay()) ->whereNull('deleted_at') ->whereNotNull('subscription_id') diff --git a/app/Jobs/Invoice/InvoiceWorkflowSettings.php b/app/Jobs/Invoice/InvoiceWorkflowSettings.php index 006b8ebff76f..c9a64ac9cee4 100644 --- a/app/Jobs/Invoice/InvoiceWorkflowSettings.php +++ b/app/Jobs/Invoice/InvoiceWorkflowSettings.php @@ -56,12 +56,5 @@ class InvoiceWorkflowSettings implements ShouldQueue /* Throws: Payment amount xxx does not match invoice totals. */ $this->base_repository->archive($this->invoice); } - - //@TODO this setting should only fire for recurring invoices - // if ($this->client->getSetting('auto_email_invoice')) { - // $this->invoice->invitations->each(function ($invitation, $key) { - // $this->invoice->service()->sendEmail($invitation->contact); - // }); - // } } } diff --git a/app/Jobs/Ninja/BankTransactionSync.php b/app/Jobs/Ninja/BankTransactionSync.php index 7699d951bb36..1fc0bec9d445 100644 --- a/app/Jobs/Ninja/BankTransactionSync.php +++ b/app/Jobs/Ninja/BankTransactionSync.php @@ -44,6 +44,7 @@ class BankTransactionSync implements ShouldQueue */ public function handle() { + //multiDB environment, need to foreach (MultiDB::$dbs as $db) { diff --git a/app/Jobs/Product/UpdateOrCreateProduct.php b/app/Jobs/Product/UpdateOrCreateProduct.php index a614820441d9..f5ef6ef22a50 100644 --- a/app/Jobs/Product/UpdateOrCreateProduct.php +++ b/app/Jobs/Product/UpdateOrCreateProduct.php @@ -29,6 +29,8 @@ class UpdateOrCreateProduct implements ShouldQueue public $company; + public $deleteWhenMissingModels = true; + /** * Create a new job instance. * diff --git a/app/Models/Payment.php b/app/Models/Payment.php index 153f9b551e37..159fd2c35801 100644 --- a/app/Models/Payment.php +++ b/app/Models/Payment.php @@ -212,6 +212,11 @@ class Payment extends BaseModel return Number::formatMoney($this->amount, $this->client); } + public function formatAmount(float $amount): string + { + return Number::formatMoney($amount, $this->client); + } + public function clientPaymentDate() { if (! $this->date) { diff --git a/app/Models/Paymentable.php b/app/Models/Paymentable.php index 594162829132..3eaf405d2177 100644 --- a/app/Models/Paymentable.php +++ b/app/Models/Paymentable.php @@ -43,4 +43,5 @@ class Paymentable extends Pivot { return $this->belongsTo(Payment::class); } + } diff --git a/app/Models/Presenters/ClientPresenter.php b/app/Models/Presenters/ClientPresenter.php index de04af406752..be3df720a13b 100644 --- a/app/Models/Presenters/ClientPresenter.php +++ b/app/Models/Presenters/ClientPresenter.php @@ -27,7 +27,6 @@ class ClientPresenter extends EntityPresenter return $this->entity->name; } - //$contact = $this->entity->primary_contact->first(); $contact = $this->entity->contacts->whereNotNull('email')->first(); $contact_name = 'No Contact Set'; diff --git a/app/Models/User.php b/app/Models/User.php index c018089d7818..b27f81f043f7 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -370,11 +370,6 @@ class User extends Authenticatable implements MustVerifyEmail (is_int(stripos($this->token()->cu->permissions, $all_permission))) || (is_int(stripos($this->token()->cu->permissions, $permission))); - //23-03-2021 - stripos return an int if true and bool false, but 0 is also interpreted as false, so we simply use is_int() to verify state - // return $this->isOwner() || - // $this->isAdmin() || - // (stripos($this->company_user->permissions, $all_permission) !== false) || - // (stripos($this->company_user->permissions, $permission) !== false); } public function documents() diff --git a/app/PaymentDrivers/BaseDriver.php b/app/PaymentDrivers/BaseDriver.php index e17e3f648518..1568a0658021 100644 --- a/app/PaymentDrivers/BaseDriver.php +++ b/app/PaymentDrivers/BaseDriver.php @@ -289,7 +289,7 @@ class BaseDriver extends AbstractPaymentDriver event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars())); if (property_exists($this->payment_hash->data, 'billing_context') && $status == Payment::STATUS_COMPLETED) { - $billing_subscription = \App\Models\Subscription::find($this->payment_hash->data->billing_context->subscription_id); + $billing_subscription = \App\Models\Subscription::find($this->decodePrimaryKey($this->payment_hash->data->billing_context->subscription_id)); // To access campaign hash => $this->payment_hash->data->billing_context->campaign; // To access campaign data => Cache::get(CAMPAIGN_HASH) diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index ad099a102351..28664dd3b9aa 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -18,18 +18,16 @@ use App\Models\Invoice; use App\Models\Proposal; use App\Utils\Ninja; use App\Utils\TruthSource; -use Illuminate\Cache\RateLimiting\Limit; -use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Mail\Mailer; use Illuminate\Queue\Events\JobProcessing; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Blade; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\ParallelTesting; use Illuminate\Support\Facades\Queue; -use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Facades\Schema; use Illuminate\Support\ServiceProvider; use Livewire\Livewire; @@ -111,13 +109,4 @@ class AppServiceProvider extends ServiceProvider } - /** - * Register any application services. - * - * @return void - */ - public function register() - { - } - } diff --git a/app/Repositories/ClientRepository.php b/app/Repositories/ClientRepository.php index 6114916e80ad..9d5a0c757d78 100644 --- a/app/Repositories/ClientRepository.php +++ b/app/Repositories/ClientRepository.php @@ -52,7 +52,6 @@ class ClientRepository extends BaseRepository * @return Client|Client|null Client Object * * @throws \Laracasts\Presenter\Exceptions\PresenterException - * @todo Write tests to make sure that custom client numbers work as expected. */ public function save(array $data, Client $client) : ?Client { diff --git a/app/Services/Client/ClientService.php b/app/Services/Client/ClientService.php index 3fb7b920f0ce..3a12be819285 100644 --- a/app/Services/Client/ClientService.php +++ b/app/Services/Client/ClientService.php @@ -42,7 +42,7 @@ class ClientService $this->client->balance += $amount; $this->client->save(); - }, 1); + }, 2); } catch (\Throwable $throwable) { nlog("DB ERROR " . $throwable->getMessage()); @@ -63,7 +63,7 @@ class ClientService $this->client->paid_to_date += $paid_to_date; $this->client->save(); - }, 1); + }, 2); } catch (\Throwable $throwable) { nlog("DB ERROR " . $throwable->getMessage()); @@ -82,7 +82,7 @@ class ClientService $this->client->paid_to_date += $amount; $this->client->save(); - }, 1); + }, 2); return $this; diff --git a/app/Services/ClientPortal/InstantPayment.php b/app/Services/ClientPortal/InstantPayment.php index e002ad1288d7..775a3dbd29f9 100644 --- a/app/Services/ClientPortal/InstantPayment.php +++ b/app/Services/ClientPortal/InstantPayment.php @@ -230,6 +230,14 @@ class InstantPayment elseif($this->request->hash){ $hash_data['billing_context'] = Cache::get($this->request->hash); } + elseif($old_hash = PaymentHash::where('fee_invoice_id', $first_invoice->id)->whereNull('payment_id')->first()) { + + if(isset($old_hash->data->billing_context)) + { + $hash_data['billing_context'] = $old_hash->data->billing_context; + } + + } $payment_hash = new PaymentHash; $payment_hash->hash = Str::random(32); diff --git a/app/Services/Payment/DeletePayment.php b/app/Services/Payment/DeletePayment.php index a5a769ba2f5d..d14dbdd91228 100644 --- a/app/Services/Payment/DeletePayment.php +++ b/app/Services/Payment/DeletePayment.php @@ -51,7 +51,7 @@ class DeletePayment } - }, 1); + }, 2); return $this->payment; diff --git a/app/Services/Payment/SendEmail.php b/app/Services/Payment/SendEmail.php index d9f6907a2610..828023381385 100644 --- a/app/Services/Payment/SendEmail.php +++ b/app/Services/Payment/SendEmail.php @@ -37,7 +37,7 @@ class SendEmail $contact = $this->payment->client->contacts()->first(); if ($contact?->email) - EmailPayment::dispatch($this->payment, $this->payment->company, $contact)->delay(now()->addSeconds(3)); + EmailPayment::dispatch($this->payment, $this->payment->company, $contact)->delay(now()->addSeconds(8)); } } diff --git a/app/Services/Scheduler/SchedulerService.php b/app/Services/Scheduler/SchedulerService.php index 40de48ef3e6c..88319d784b91 100644 --- a/app/Services/Scheduler/SchedulerService.php +++ b/app/Services/Scheduler/SchedulerService.php @@ -47,7 +47,7 @@ class SchedulerService //Email only the selected clients if(count($this->scheduler->parameters['clients']) >= 1) - $query->where('id', $this->transformKeys($this->scheduler->parameters['clients'])); + $query->whereIn('id', $this->transformKeys($this->scheduler->parameters['clients'])); $query->cursor() ->each(function ($_client){ @@ -119,40 +119,40 @@ class SchedulerService switch ($this->scheduler->frequency_id) { case RecurringInvoice::FREQUENCY_DAILY: - $next_run = Carbon::parse($this->scheduler->next_run)->startOfDay()->addDay(); + $next_run = now()->startOfDay()->addDay(); break; case RecurringInvoice::FREQUENCY_WEEKLY: - $next_run = Carbon::parse($this->scheduler->next_run)->startOfDay()->addWeek(); + $next_run = now()->startOfDay()->addWeek(); break; case RecurringInvoice::FREQUENCY_TWO_WEEKS: - $next_run = Carbon::parse($this->scheduler->next_run)->startOfDay()->addWeeks(2); + $next_run = now()->startOfDay()->addWeeks(2); break; case RecurringInvoice::FREQUENCY_FOUR_WEEKS: - $next_run = Carbon::parse($this->scheduler->next_run)->startOfDay()->addWeeks(4); + $next_run = now()->startOfDay()->addWeeks(4); break; case RecurringInvoice::FREQUENCY_MONTHLY: - $next_run = Carbon::parse($this->scheduler->next_run)->startOfDay()->addMonthNoOverflow(); + $next_run = now()->startOfDay()->addMonthNoOverflow(); break; case RecurringInvoice::FREQUENCY_TWO_MONTHS: - $next_run = Carbon::parse($this->scheduler->next_run)->startOfDay()->addMonthsNoOverflow(2); + $next_run = now()->startOfDay()->addMonthsNoOverflow(2); break; case RecurringInvoice::FREQUENCY_THREE_MONTHS: - $next_run = Carbon::parse($this->scheduler->next_run)->startOfDay()->addMonthsNoOverflow(3); + $next_run = now()->startOfDay()->addMonthsNoOverflow(3); break; case RecurringInvoice::FREQUENCY_FOUR_MONTHS: - $next_run = Carbon::parse($this->scheduler->next_run)->startOfDay()->addMonthsNoOverflow(4); + $next_run = now()->startOfDay()->addMonthsNoOverflow(4); break; case RecurringInvoice::FREQUENCY_SIX_MONTHS: - $next_run = Carbon::parse($this->scheduler->next_run)->startOfDay()->addMonthsNoOverflow(6); + $next_run = now()->startOfDay()->addMonthsNoOverflow(6); break; case RecurringInvoice::FREQUENCY_ANNUALLY: - $next_run = Carbon::parse($this->scheduler->next_run)->startOfDay()->addYear(); + $next_run = now()->startOfDay()->addYear(); break; case RecurringInvoice::FREQUENCY_TWO_YEARS: - $next_run = Carbon::parse($this->scheduler->next_run)->startOfDay()->addYears(2); + $next_run = now()->startOfDay()->addYears(2); break; case RecurringInvoice::FREQUENCY_THREE_YEARS: - $next_run = Carbon::parse($this->scheduler->next_run)->startOfDay()->addYears(3); + $next_run = now()->startOfDay()->addYears(3); break; default: $next_run = null; diff --git a/app/Services/Subscription/SubscriptionService.php b/app/Services/Subscription/SubscriptionService.php index 3af24a43ea7a..3c59d33fdfbc 100644 --- a/app/Services/Subscription/SubscriptionService.php +++ b/app/Services/Subscription/SubscriptionService.php @@ -167,7 +167,7 @@ class SubscriptionService public function startTrial(array $data) { // Redirects from here work just fine. Livewire will respect it. - $client_contact = ClientContact::find($data['contact_id']); + $client_contact = ClientContact::find($this->decodePrimaryKey($data['contact_id'])); if(!$this->subscription->trial_enabled) return new \Exception("Trials are disabled for this product"); @@ -331,6 +331,7 @@ class SubscriptionService * We refund unused days left. * * @param Invoice $invoice + * * @return float */ private function calculateProRataRefundForSubscription($invoice) :float @@ -338,6 +339,20 @@ class SubscriptionService if(!$invoice || !$invoice->date || $invoice->status_id != Invoice::STATUS_PAID) return 0; + /*Remove previous refunds from the calculation of the amount*/ + $invoice->line_items = collect($invoice->line_items)->filter(function($item){ + + if($item->product_key == ctrans("texts.refund")) + { + return false; + } + + return true; + + })->toArray(); + + $amount = $invoice->calc()->getTotal(); + $start_date = Carbon::parse($invoice->date); $current_date = now(); @@ -346,7 +361,7 @@ class SubscriptionService $days_in_frequency = $this->getDaysInFrequency(); - $pro_rata_refund = round((($days_in_frequency - $days_of_subscription_used)/$days_in_frequency) * $invoice->amount ,2); + $pro_rata_refund = round((($days_in_frequency - $days_of_subscription_used)/$days_in_frequency) * $amount ,2); return max(0, $pro_rata_refund); @@ -670,6 +685,8 @@ class SubscriptionService nlog("pro rata refund = {$pro_rata_refund_amount}"); } + nlog("{$pro_rata_refund_amount} + {$pro_rata_charge_amount} + {$this->subscription->price}"); + $total_payable = $pro_rata_refund_amount + $pro_rata_charge_amount + $this->subscription->price; if($total_payable > 0) @@ -734,7 +751,7 @@ class SubscriptionService { nlog("handle plan change"); - $old_recurring_invoice = RecurringInvoice::find($payment_hash->data->billing_context->recurring_invoice); + $old_recurring_invoice = RecurringInvoice::find($this->decodePrimaryKey($payment_hash->data->billing_context->recurring_invoice)); if(!$old_recurring_invoice) return $this->handleRedirect('/client/recurring_invoices/'); @@ -1291,7 +1308,12 @@ class SubscriptionService } - private function getDaysInFrequency() + /** + * Get the number of days in the currency frequency + * + * @return int Number of days + */ + private function getDaysInFrequency() :int { switch ($this->subscription->frequency_id) { @@ -1325,7 +1347,15 @@ class SubscriptionService } - public function getNextDateForFrequency($date, $frequency) + /** + * Get the next date by frequency_id + * + * @param Carbon $date The current carbon date + * @param int $frequency The frequncy_id of the subscription + * + * @return ?Carbon The next date carbon object + */ + public function getNextDateForFrequency($date, $frequency) :?Carbon { switch ($frequency) { case RecurringInvoice::FREQUENCY_DAILY: @@ -1353,11 +1383,56 @@ class SubscriptionService case RecurringInvoice::FREQUENCY_THREE_YEARS: return $date->addYears(3); default: - return 0; + return null; } } + /** + * Handle case where no payment is required + * @param Invoice $invoice The Invoice + * @param array $bundle The bundle array + * @param ClientContact $contact The Client Contact + * + * @return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse + */ + public function handleNoPaymentFlow(Invoice $invoice, $bundle, ClientContact $contact) + { + + if (strlen($this->subscription->recurring_product_ids) >= 1) { + + $recurring_invoice = $this->convertInvoiceToRecurringBundle($contact->client_id, collect($bundle)->map(function ($bund){ return (object) $bund;})); + + /* Start the recurring service */ + $recurring_invoice->service() + ->start() + ->save(); + + $invoice->recurring_id = $recurring_invoice->id; + $invoice->save(); + + $context = [ + 'context' => 'recurring_purchase', + 'recurring_invoice' => $recurring_invoice->hashed_id, + 'invoice' => $invoice->hashed_id, + 'client' => $recurring_invoice->client->hashed_id, + 'subscription' => $this->subscription->hashed_id, + 'contact' => $contact->hashed_id, + 'redirect_url' => "/client/recurring_invoices/{$recurring_invoice->hashed_id}", + ]; + + $this->triggerWebhook($context); + + return $this->handleRedirect($context['redirect_url']); + + } + + $redirect_url = "/client/invoices/{$invoice->hashed_id}"; + + return $this->handleRedirect($redirect_url); + + } + /** * 'email' => $this->email ?? $this->contact->email, * 'quantity' => $this->quantity, diff --git a/app/Utils/Traits/SubscriptionHooker.php b/app/Utils/Traits/SubscriptionHooker.php index 52b05fbb1e48..8dfaba0171fc 100644 --- a/app/Utils/Traits/SubscriptionHooker.php +++ b/app/Utils/Traits/SubscriptionHooker.php @@ -27,6 +27,9 @@ trait SubscriptionHooker 'X-Requested-With' => 'XMLHttpRequest', ]; + if(!isset($subscription->webhook_configuration['post_purchase_url']) && !isset($subscription->webhook_configuration['post_purchase_rest_method'])) + return []; + if (count($subscription->webhook_configuration['post_purchase_headers']) >= 1) { $headers = array_merge($headers, $subscription->webhook_configuration['post_purchase_headers']); } diff --git a/config/ninja.php b/config/ninja.php index 0b3a99c33a3c..9006c72572b6 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -14,8 +14,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => '5.5.56', - 'app_tag' => '5.5.56', + 'app_version' => '5.5.57', + 'app_tag' => '5.5.57', 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', ''), diff --git a/database/factories/SchedulerFactory.php b/database/factories/SchedulerFactory.php index e60a53211da3..8b839c5e6b2e 100644 --- a/database/factories/SchedulerFactory.php +++ b/database/factories/SchedulerFactory.php @@ -32,7 +32,7 @@ class SchedulerFactory extends Factory 'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY, 'next_run' => now()->addSeconds(rand(86400,8640000)), 'next_run_client' => now()->addSeconds(rand(86400,8640000)), - 'template' => 'statement_task', + 'template' => 'client_statement', ]; } } diff --git a/database/schema/db-ninja-01-schema.dump b/database/schema/mysql-schema.dump similarity index 98% rename from database/schema/db-ninja-01-schema.dump rename to database/schema/mysql-schema.dump index 9de6c462deb2..bdf30e6abb5e 100644 --- a/database/schema/db-ninja-01-schema.dump +++ b/database/schema/mysql-schema.dump @@ -48,6 +48,7 @@ CREATE TABLE `accounts` ( `account_sms_verification_number` text DEFAULT NULL, `account_sms_verified` tinyint(1) NOT NULL DEFAULT 0, `bank_integration_account_id` text DEFAULT NULL, + `is_trial` tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (`id`), KEY `accounts_payment_id_index` (`payment_id`), KEY `accounts_key_index` (`key`) @@ -255,6 +256,7 @@ CREATE TABLE `bank_transactions` ( `updated_at` timestamp(6) NULL DEFAULT NULL, `deleted_at` timestamp(6) NULL DEFAULT NULL, `bank_transaction_rule_id` bigint(20) DEFAULT NULL, + `payment_id` int(10) unsigned DEFAULT NULL, PRIMARY KEY (`id`), KEY `bank_transactions_bank_integration_id_foreign` (`bank_integration_id`), KEY `bank_transactions_user_id_foreign` (`user_id`), @@ -519,7 +521,12 @@ CREATE TABLE `companies` ( `invoice_task_project` tinyint(1) NOT NULL DEFAULT 0, `report_include_deleted` tinyint(1) NOT NULL DEFAULT 0, `invoice_task_lock` tinyint(1) NOT NULL DEFAULT 0, - `use_vendor_currency` tinyint(1) NOT NULL DEFAULT 0, + `matomo_url` varchar(191) DEFAULT NULL, + `matomo_id` bigint(20) DEFAULT NULL, + `convert_payment_currency` tinyint(1) NOT NULL DEFAULT 0, + `convert_expense_currency` tinyint(1) NOT NULL DEFAULT 0, + `notify_vendor_when_paid` tinyint(1) NOT NULL DEFAULT 0, + `invoice_task_hours` tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `companies_company_key_unique` (`company_key`), KEY `companies_industry_id_foreign` (`industry_id`), @@ -1139,6 +1146,7 @@ CREATE TABLE `invoices` ( `paid_to_date` decimal(20,6) NOT NULL DEFAULT 0.000000, `subscription_id` int(10) unsigned DEFAULT NULL, `auto_bill_tries` smallint(6) NOT NULL DEFAULT 0, + `is_proforma` tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY `invoices_company_id_number_unique` (`company_id`,`number`), KEY `invoices_user_id_foreign` (`user_id`), @@ -1948,20 +1956,23 @@ DROP TABLE IF EXISTS `schedulers`; /*!40101 SET character_set_client = utf8 */; CREATE TABLE `schedulers` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, - `paused` tinyint(1) NOT NULL DEFAULT 0, `is_deleted` tinyint(1) NOT NULL DEFAULT 0, - `repeat_every` varchar(191) NOT NULL, - `start_from` timestamp NULL DEFAULT NULL, - `scheduled_run` timestamp NULL DEFAULT NULL, - `company_id` bigint(20) unsigned NOT NULL, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, `deleted_at` timestamp NULL DEFAULT NULL, - `action_name` varchar(191) NOT NULL, - `action_class` varchar(191) NOT NULL, `parameters` mediumtext DEFAULT NULL, + `company_id` int(10) unsigned NOT NULL, + `is_paused` tinyint(1) NOT NULL DEFAULT 0, + `frequency_id` int(10) unsigned DEFAULT NULL, + `next_run` datetime DEFAULT NULL, + `next_run_client` datetime DEFAULT NULL, + `user_id` int(10) unsigned NOT NULL, + `name` varchar(191) NOT NULL, + `template` varchar(191) NOT NULL, PRIMARY KEY (`id`), - KEY `schedulers_action_name_index` (`action_name`) + UNIQUE KEY `schedulers_company_id_name_unique` (`company_id`,`name`), + KEY `schedulers_company_id_deleted_at_index` (`company_id`,`deleted_at`), + CONSTRAINT `schedulers_company_id_foreign` FOREIGN KEY (`company_id`) REFERENCES `companies` (`id`) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC; /*!40101 SET character_set_client = @saved_cs_client */; DROP TABLE IF EXISTS `sizes`; @@ -2522,3 +2533,10 @@ INSERT INTO `migrations` VALUES (171,'2022_11_06_215526_drop_html_backups_column INSERT INTO `migrations` VALUES (172,'2022_11_13_034143_bank_transaction_rules_table',1); INSERT INTO `migrations` VALUES (173,'2022_11_16_093535_calmness_design',1); INSERT INTO `migrations` VALUES (174,'2022_11_22_215618_lock_tasks_when_invoiced',1); +INSERT INTO `migrations` VALUES (175,'2022_05_12_56879_add_stripe_klarna',2); +INSERT INTO `migrations` VALUES (176,'2022_07_12_45766_add_matomo',2); +INSERT INTO `migrations` VALUES (177,'2022_11_30_063229_add_payment_id_to_bank_transaction_table',2); +INSERT INTO `migrations` VALUES (178,'2022_12_07_024625_add_properties_to_companies_table',2); +INSERT INTO `migrations` VALUES (179,'2022_12_14_004639_vendor_currency_update',2); +INSERT INTO `migrations` VALUES (180,'2022_12_20_063038_set_proforma_invoice_type',2); +INSERT INTO `migrations` VALUES (181,'2023_01_12_125540_set_auto_bill_on_regular_invoice_setting',2); diff --git a/phpunit.yml b/phpunit.yml deleted file mode 100644 index d77a3ccffa97..000000000000 --- a/phpunit.yml +++ /dev/null @@ -1,108 +0,0 @@ -on: - push: - branches: - - v5-develop - pull_request: - branches: - - v5-develop - -name: phpunit -jobs: - run: - runs-on: ${{ matrix.operating-system }} - strategy: - matrix: - operating-system: ['ubuntu-18.04', 'ubuntu-20.04'] - php-versions: ['7.3','7.4','8.0'] - phpunit-versions: ['latest'] - - 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' - BROADCAST_DRIVER: log - CACHE_DRIVER: file - QUEUE_CONNECTION: sync - SESSION_DRIVER: file - NINJA_ENVIRONMENT: hosted - MULTI_DB_ENABLED: false - NINJA_LICENSE: 123456 - 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 - - steps: - - name: Start mysql 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 - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-versions }} - extensions: mysql, mysqlnd, sqlite3, bcmath, gmp, gd, curl, zip, openssl, mbstring, xml - - - uses: actions/checkout@v1 - with: - ref: v5-develop - fetch-depth: 1 - - - name: Copy .env - run: | - cp .env.ci .env - - name: Install composer dependencies - run: | - composer config -g github-oauth.github.com ${{ secrets.GITHUB_TOKEN }} - composer install - - name: Prepare Laravel Application - run: | - php artisan key:generate - php artisan optimize - php artisan cache:clear - php artisan config:cache - - name: Create DB and schemas - run: | - mkdir -p database - touch database/database.sqlite - - name: Migrate Database - run: | - php artisan migrate:fresh --seed --force && php artisan db:seed --force - - name: Prepare JS/CSS assets - run: | - npm i - npm run production - - name: Run Testsuite - run: | - cat .env - vendor/bin/phpunit --testdox - env: - DB_PORT: ${{ job.services.mysql.ports[3306] }} - - - name: Run php-cs-fixer - run: | - vendor/bin/php-cs-fixer fix diff --git a/resources/views/portal/ninja2020/components/livewire/billing-portal-purchasev2.blade.php b/resources/views/portal/ninja2020/components/livewire/billing-portal-purchasev2.blade.php index 6c0ff64b36c7..b5398e952937 100644 --- a/resources/views/portal/ninja2020/components/livewire/billing-portal-purchasev2.blade.php +++ b/resources/views/portal/ninja2020/components/livewire/billing-portal-purchasev2.blade.php @@ -51,18 +51,34 @@

-
+
@if($subscription->per_seat_enabled) -

+

+ @if($subscription->use_inventory_management && $product->in_stock_quantity == 0) +

Out of stock

+ @else

{{ ctrans('texts.qty') }}

+ @endif + - - @for ($i = 2; $i <= $subscription->max_seats_limit; $i++) + @if($subscription->max_seats_limit > 1) + { + @for ($i = 2; $i <= ($subscription->use_inventory_management ? min($subscription->max_seats_limit,$product->in_stock_quantity) : $subscription->max_seats_limit); $i++) @endfor - + } + @else + @for ($i = 2; $i <= ($subscription->use_inventory_management ? min($product->in_stock_quantity, max(100,$product->custom_value2)) : max(100,$product->custom_value2)); $i++) + + @endfor + @endif +
@endif
diff --git a/resources/views/portal/ninja2020/components/livewire/payments-table.blade.php b/resources/views/portal/ninja2020/components/livewire/payments-table.blade.php index 1c9c5010d8b5..14ba9de42481 100644 --- a/resources/views/portal/ninja2020/components/livewire/payments-table.blade.php +++ b/resources/views/portal/ninja2020/components/livewire/payments-table.blade.php @@ -60,7 +60,7 @@ {{ $payment->translatedType() }} - {!! \App\Utils\Number::formatMoney($payment->amount, $payment->client) !!} + {!! \App\Utils\Number::formatMoney($payment->amount > 0 ? $payment->amount : $payment->credits->sum('pivot.amount'), $payment->client) !!} {{ \Illuminate\Support\Str::limit($payment->transaction_reference, 35) }} diff --git a/resources/views/portal/ninja2020/payments/show.blade.php b/resources/views/portal/ninja2020/payments/show.blade.php index 86f67bafb354..11e42598a526 100644 --- a/resources/views/portal/ninja2020/payments/show.blade.php +++ b/resources/views/portal/ninja2020/payments/show.blade.php @@ -66,7 +66,7 @@ {{ ctrans('texts.amount') }}
- {{ $payment->formattedAmount() }} + {{ $payment->formatAmount($payment->amount > 0 ? $payment->amount : $payment?->invoices->sum('pivot.amount')) }}
@endif @@ -116,6 +116,7 @@ href="{{ route('client.invoice.show', ['invoice' => $invoice->hashed_id])}}"> {{ $invoice->number }} + - {{ \App\Utils\Number::formatMoney($payment->invoices->where('id', $invoice->id)->sum('pivot.amount') - $payment->invoices->where('id', $invoice->id)->sum('pivot.refunded'), $payment->client) }} @endforeach diff --git a/tests/Feature/Account/AccountEmailQuotaTest.php b/tests/Feature/Account/AccountEmailQuotaTest.php index ddd4dfaa298a..408a33754c95 100644 --- a/tests/Feature/Account/AccountEmailQuotaTest.php +++ b/tests/Feature/Account/AccountEmailQuotaTest.php @@ -12,18 +12,11 @@ namespace Tests\Feature\Account; -use App\DataMapper\ClientSettings; +use App\DataMapper\ClientRegistrationFields; use App\DataMapper\CompanySettings; -use App\Http\Livewire\CreditsTable; use App\Models\Account; -use App\Models\Client; -use App\Models\ClientContact; use App\Models\Company; -use App\Models\Credit; -use App\Models\User; -use App\Utils\Traits\AppSetup; -use Faker\Factory; -use Illuminate\Foundation\Testing\DatabaseTransactions; +use App\Utils\Ninja; use Illuminate\Support\Facades\Cache; use Livewire\Livewire; use Tests\MockAccountData; @@ -31,30 +24,123 @@ use Tests\TestCase; class AccountEmailQuotaTest extends TestCase { - use DatabaseTransactions; - use AppSetup; - use MockAccountData; protected function setUp(): void { parent::setUp(); + } + + + public function testIfQuotaBreached() + { + + config([ + 'ninja.production' => true + ]); + + $account = Account::factory()->create([ + 'hosted_client_count' => 1000, + 'hosted_company_count' => 1000, + 'is_flagged' => false, + 'key' => '123ifyouknowwhatimean', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $account->num_users = 3; + $account->save(); + + $company = Company::factory()->create([ + 'account_id' => $account->id, + ]); + + $company->client_registration_fields = ClientRegistrationFields::generate(); + + $settings = CompanySettings::defaults(); + + $settings->company_logo = 'https://pdf.invoicing.co/favicon-v2.png'; + $settings->website = 'www.invoiceninja.com'; + $settings->address1 = 'Address 1'; + $settings->address2 = 'Address 2'; + $settings->city = 'City'; + $settings->state = 'State'; + $settings->postal_code = 'Postal Code'; + $settings->phone = '555-343-2323'; + $settings->email = 'nothingtoofancy@acme.com'; + $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; + + $company->track_inventory = true; + $company->settings = $settings; + $company->save(); + + $account->default_company_id = $company->id; + $account->save(); + + + Cache::put($account->key, 3000); + + $this->assertFalse($account->isPaid()); + $this->assertTrue(Ninja::isNinja()); + $this->assertEquals(20, $account->getDailyEmailLimit()); + + $this->assertEquals(3000, Cache::get($account->key)); + $this->assertTrue($account->emailQuotaExceeded()); + + Cache::forget('123ifyouknowwhatimean'); - $this->faker = Factory::create(); - $this->buildCache(true); - $this->makeTestData(); } public function testQuotaValidRule() { - Cache::increment($this->account->key); - $this->assertFalse($this->account->emailQuotaExceeded()); + $account = Account::factory()->create([ + 'hosted_client_count' => 1000, + 'hosted_company_count' => 1000, + 'is_flagged' => false, + 'key' => '123ifyouknowwhatimean', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $account->num_users = 3; + $account->save(); + + Cache::increment($account->key); + + $this->assertFalse($account->emailQuotaExceeded()); + + Cache::forget('123ifyouknowwhatimean'); + } - public function testQuotaInValidRule() + public function testEmailSentCount() { - Cache::increment($this->account->key, 3000); + $account = Account::factory()->create([ + 'hosted_client_count' => 1000, + 'hosted_company_count' => 1000, + 'is_flagged' => false, + 'key' => '123ifyouknowwhatimean', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $account->num_users = 3; + $account->save(); + + + Cache::put($account->key, 3000); + + $count = $account->emailsSent(); + + $this->assertEquals(3000, $count); + + Cache::forget('123ifyouknowwhatimean'); - $this->assertTrue($this->account->emailQuotaExceeded()); } + } diff --git a/tests/Feature/BankTransactionApiTest.php b/tests/Feature/BankTransactionApiTest.php index b5210c87bfdc..c3c0373d483d 100644 --- a/tests/Feature/BankTransactionApiTest.php +++ b/tests/Feature/BankTransactionApiTest.php @@ -15,7 +15,6 @@ use App\Utils\Traits\MakesHash; use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Support\Facades\Session; -use Illuminate\Validation\ValidationException; use Tests\MockAccountData; use Tests\TestCase; @@ -42,6 +41,17 @@ class BankTransactionApiTest extends TestCase Model::reguard(); } + + public function testBankTransactionGetClientStatus() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/bank_transactions?client_status=unmatched'.$this->encodePrimaryKey($this->bank_transaction->id)); + + $response->assertStatus(200); + } + public function testBankTransactionGet() { $response = $this->withHeaders([ diff --git a/tests/Feature/CompanyTokenApiTest.php b/tests/Feature/CompanyTokenApiTest.php index 4e5c6717665a..bb31a1f9f9f1 100644 --- a/tests/Feature/CompanyTokenApiTest.php +++ b/tests/Feature/CompanyTokenApiTest.php @@ -48,6 +48,19 @@ class CompanyTokenApiTest extends TestCase ); } + public function testCompanyTokenListFilter() + { + $this->withoutMiddleware(PasswordProtection::class); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + 'X-API-PASSWORD' => 'ALongAndBriliantPassword', + ])->get('/api/v1/tokens?filter=xx'); + + $response->assertStatus(200); + } + public function testCompanyTokenList() { $this->withoutMiddleware(PasswordProtection::class); diff --git a/tests/Feature/CreditTest.php b/tests/Feature/CreditTest.php index 7fd47b8e5f7f..a33b0055148f 100644 --- a/tests/Feature/CreditTest.php +++ b/tests/Feature/CreditTest.php @@ -40,6 +40,16 @@ class CreditTest extends TestCase $this->makeTestData(); } + public function testCreditGetClientStatus() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/credits?client_status=draft'.$this->encodePrimaryKey($this->bank_transaction->id)); + + $response->assertStatus(200); + } + public function testCreditsList() { Client::factory()->count(3)->create(['user_id' => $this->user->id, 'company_id' => $this->company->id])->each(function ($c) { diff --git a/tests/Feature/Email/EmailServiceTest.php b/tests/Feature/Email/EmailServiceTest.php new file mode 100644 index 000000000000..66b10b118a15 --- /dev/null +++ b/tests/Feature/Email/EmailServiceTest.php @@ -0,0 +1,212 @@ +markTestSkipped('Skipped :: test not needed in this environment'); + + $this->makeTestData(); + + $this->email_object = new EmailObject(); + $this->email_object->to = [new Address("testing@gmail.com", "Cool Name")]; + $this->email_object->attachments = []; + $this->email_object->settings = $this->client->getMergedSettings(); + $this->email_object->company = $this->client->company; + $this->email_object->client = $this->client; + $this->email_object->email_template_subject = 'email_subject_statement'; + $this->email_object->email_template_body = 'email_template_statement'; + $this->email_object->variables = [ + '$client' => $this->client->present()->name(), + '$start_date' => '2022-01-01', + '$end_date' => '2023-01-01', + ]; + + $this->email_service = new EmailService($this->email_object, $this->company); + + } + + public function testScanEmailsAttemptedFromVerifiedAccounts() + { + $email_filter = new \Modules\Admin\Jobs\Account\EmailFilter($this->email_object, $this->client->company); + + Cache::put($this->account->key, 1); + + config(['ninja.environment' => 'hosted']); + + $this->account->account_sms_verified = true; + $this->account->is_verified_account = false; + $this->account->save(); + + $this->assertFalse($this->email_service->preFlightChecksFail()); + + collect($email_filter->getSpamKeywords())->each(function ($spam_subject){ + + $this->email_object->subject = $spam_subject; + + $this->assertTrue($this->email_service->preFlightChecksFail()); + + }); + + } + + + + public function scanEmailsAttemptedFromUnverifiedAccounts() + { + + config(['ninja.environment' => 'hosted']); + + Cache::put($this->account->key, 1); + + $this->account->account_sms_verified = false; + $this->account->save(); + + $this->assertTrue($this->email_service->preFlightChecksFail()); + + } + + + public function testVerifiedAccountsSkipFilters() + { + config(['ninja.environment' => 'hosted']); + + Cache::put($this->account->key, 1); + + $this->account->is_verified_account = true; + $this->account->save(); + + $this->assertFalse($this->email_service->preFlightChecksFail()); + + } + + public function testClientMailersAreUnCapped() + { + + config(['ninja.environment' => 'hosted']); + + Cache::put($this->account->key, 1000000); + + collect([ + 'gmail', + 'office365', + 'client_postmark', + 'client_mailgun']) + ->each(function ($mailer){ + + $this->email_object->settings->email_sending_method = $mailer; + + $this->assertFalse($this->email_service->preFlightChecksFail()); + + }); + + $this->email_object->settings->email_sending_method = 'postmark'; + + $this->assertTrue($this->email_service->preFlightChecksFail()); + + } + + public function testFlaggedInvalidEmailsPrevented() + { + + config(['ninja.environment' => 'hosted']); + + Cache::put($this->account->key, 1); + + $this->email_object->to = [new Address("user@example.com", "Cool Name")]; + + $this->assertTrue($this->email_service->preFlightChecksFail()); + + + collect([ + 'user@example.com', + '', + 'bademail', + 'domain.com', + ])->each(function ($email){ + + + $this->email_object->to = [new Address($email, "Cool Name")]; + + $this->assertTrue($this->email_service->preFlightChecksFail()); + + }); + + + } + + public function testFlaggedAccountsPrevented() + { + + Cache::put($this->account->key, 1); + + config(['ninja.environment' => 'hosted']); + + $this->account->is_flagged = true; + $this->account->save(); + + $this->assertTrue($this->email_service->preFlightChecksFail()); + + } + + public function testPreFlightChecksHosted() + { + + Cache::put($this->account->key, 1); + + config(['ninja.environment' => 'hosted']); + + $this->assertFalse($this->email_service->preFlightChecksFail()); + + } + + public function testPreFlightChecksSelfHost() + { + + Cache::put($this->account->key, 1); + + config(['ninja.environment' => 'selfhost']); + + $this->assertFalse($this->email_service->preFlightChecksFail()); + + } + + + +} diff --git a/tests/Feature/Email/EmailTest.php b/tests/Feature/Email/EmailTest.php deleted file mode 100644 index 40dadc5ca6e4..000000000000 --- a/tests/Feature/Email/EmailTest.php +++ /dev/null @@ -1,84 +0,0 @@ -markTestSkipped('Skip test not needed in this environment'); - - $this->makeTestData(); - - $this->email_object = new EmailObject(); - $this->email_object->to = [new Address("testing@gmail.com", "Cool Name")]; - $this->email_object->attachments = []; - $this->email_object->settings = $this->client->getMergedSettings(); - $this->email_object->company = $this->client->company; - $this->email_object->client = $this->client; - $this->email_object->email_template_subject = 'email_subject_statement'; - $this->email_object->email_template_body = 'email_template_statement'; - $this->email_object->variables = [ - '$client' => $this->client->present()->name(), - '$start_date' => '2022-01-01', - '$end_date' => '2023-01-01', - ]; - - $this->email_service = new EmailService($this->email_object, $this->company); - - } - - public function testPreFlightChecksHosted() - { - - config(['ninja.environment' => 'hosted']); - - $this->assertFalse($this->email_service->preFlightChecksFail()); - - } - - public function testPreFlightChecksSelfHost() - { - - config(['ninja.environment' => 'selfhost']); - - $this->assertFalse($this->email_service->preFlightChecksFail()); - - } - - - -} diff --git a/tests/Feature/ExpenseApiTest.php b/tests/Feature/ExpenseApiTest.php index d0a4dc80f802..a2d3c56a4abe 100644 --- a/tests/Feature/ExpenseApiTest.php +++ b/tests/Feature/ExpenseApiTest.php @@ -41,6 +41,18 @@ class ExpenseApiTest extends TestCase Model::reguard(); } + public function testExpenseGetClientStatus() + { + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/expenses?client_status=paid'); + + $response->assertStatus(200); + + } + public function testExpensePost() { $data = [ diff --git a/tests/Feature/Export/ProductSalesReportTest.php b/tests/Feature/Export/ProductSalesReportTest.php index 9dc6771f1f5f..ff285653a60d 100644 --- a/tests/Feature/Export/ProductSalesReportTest.php +++ b/tests/Feature/Export/ProductSalesReportTest.php @@ -66,6 +66,8 @@ class ProductSalesReportTest extends TestCase public $account; + public $client; + /** * start_date - Y-m-d end_date - Y-m-d @@ -108,6 +110,9 @@ class ProductSalesReportTest extends TestCase 'settings' => $settings, ]); + $this->company->settings = $settings; + $this->company->save(); + $this->payload = [ 'start_date' => '2000-01-01', 'end_date' => '2030-01-11', @@ -115,6 +120,13 @@ class ProductSalesReportTest extends TestCase 'is_income_billed' => true, 'include_tax' => false, ]; + + $this->client = Client::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + 'is_deleted' => 0, + ]); + } public function testProductSalesInstance() @@ -133,22 +145,16 @@ class ProductSalesReportTest extends TestCase $this->buildData(); - $client = Client::factory()->create([ - 'user_id' => $this->user->id, - 'company_id' => $this->company->id, - 'is_deleted' => 0, - ]); - $this->payload = [ 'start_date' => '2000-01-01', 'end_date' => '2030-01-11', 'date_range' => 'custom', - 'client_id' => $client->id, + 'client_id' => $this->client->id, 'report_keys' => [] ]; $i = Invoice::factory()->create([ - 'client_id' => $client->id, + 'client_id' => $this->client->id, 'user_id' => $this->user->id, 'company_id' => $this->company->id, 'amount' => 0, @@ -174,7 +180,6 @@ class ProductSalesReportTest extends TestCase $response = $pl->run(); $this->assertIsString($response); -// nlog($response); $this->account->delete(); } diff --git a/tests/Feature/Import/CSV/CsvImportTest.php b/tests/Feature/Import/CSV/CsvImportTest.php index 06e2716ac43c..af78d814a6db 100644 --- a/tests/Feature/Import/CSV/CsvImportTest.php +++ b/tests/Feature/Import/CSV/CsvImportTest.php @@ -22,6 +22,7 @@ use App\Models\Product; use App\Models\TaxRate; use App\Models\Vendor; use App\Utils\Traits\MakesHash; +use App\Utils\TruthSource; use Illuminate\Routing\Middleware\ThrottleRequests; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Str; @@ -50,6 +51,9 @@ class CsvImportTest extends TestCase $this->makeTestData(); $this->withoutExceptionHandling(); + + auth()->login($this->user); + } public function testExpenseCsvImport() @@ -274,6 +278,11 @@ class CsvImportTest extends TestCase Cache::put($hash.'-invoice', base64_encode($csv), 360); + $truth = app()->make(TruthSource::class); + $truth->setCompanyUser($this->cu); + $truth->setUser($this->user); + $truth->setCompany($this->company); + $csv_importer = new Csv($data, $this->company); $csv_importer->import('invoice'); diff --git a/tests/Feature/InvitationTest.php b/tests/Feature/InvitationTest.php index 193baae2c05e..02610d5e5418 100644 --- a/tests/Feature/InvitationTest.php +++ b/tests/Feature/InvitationTest.php @@ -40,6 +40,9 @@ class InvitationTest extends TestCase protected function setUp() :void { parent::setUp(); + + $this->faker = \Faker\Factory::create(); + } public function testInvoiceCreationAfterInvoiceMarkedSent() @@ -52,10 +55,13 @@ class InvitationTest extends TestCase $account->default_company_id = $company->id; $account->save(); - $user = User::where('email', 'user@example.com')->first(); + $fake_email = $this->faker->email(); + + $user = User::where('email', $fake_email)->first(); if (! $user) { $user = User::factory()->create([ + 'email' => $fake_email, 'account_id' => $account->id, 'confirmation_code' => $this->createDbHash(config('database.default')), ]); diff --git a/tests/Feature/InvoiceTest.php b/tests/Feature/InvoiceTest.php index 18a36185103b..f41f7f34af5c 100644 --- a/tests/Feature/InvoiceTest.php +++ b/tests/Feature/InvoiceTest.php @@ -47,6 +47,16 @@ class InvoiceTest extends TestCase } + public function testInvoiceGetPaidInvoices() + { + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/invoices?client_status=paid',) + ->assertStatus(200); + } + public function testInvoiceArchiveAction() { diff --git a/tests/Feature/PaymentTermsApiTest.php b/tests/Feature/PaymentTermsApiTest.php index 358d9cbd9274..b089ac75e4f0 100644 --- a/tests/Feature/PaymentTermsApiTest.php +++ b/tests/Feature/PaymentTermsApiTest.php @@ -48,6 +48,17 @@ class PaymentTermsApiTest extends TestCase ); } + public function testPaymentTermsGetWithFilter() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/payment_terms?filter=hey'); + + $response->assertStatus(200); + } + + public function testPaymentTermsGet() { $response = $this->withHeaders([ diff --git a/tests/Feature/PaymentTest.php b/tests/Feature/PaymentTest.php index c2dae62ba3b3..0f95a9d33e65 100644 --- a/tests/Feature/PaymentTest.php +++ b/tests/Feature/PaymentTest.php @@ -62,6 +62,18 @@ class PaymentTest extends TestCase ); } + public function testGetPaymentMatchList() + { + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/payments?match_transactions=true') + ->assertStatus(200); + + + } + public function testStorePaymentIdempotencyKeyIllegalLength() { $client = ClientFactory::create($this->company->id, $this->user->id); diff --git a/tests/Feature/ProductTest.php b/tests/Feature/ProductTest.php index cf49f64b4d4a..c503fed90dfc 100644 --- a/tests/Feature/ProductTest.php +++ b/tests/Feature/ProductTest.php @@ -47,6 +47,15 @@ class ProductTest extends TestCase $this->makeTestData(); } + public function testProductGetProductKeyFilter() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/products?product_key=xx') + ->assertStatus(200); + } + public function testProductList() { $response = $this->withHeaders([ diff --git a/tests/Feature/ProjectApiTest.php b/tests/Feature/ProjectApiTest.php index b4d1b237e7f8..f65abaae0dec 100644 --- a/tests/Feature/ProjectApiTest.php +++ b/tests/Feature/ProjectApiTest.php @@ -42,6 +42,16 @@ class ProjectApiTest extends TestCase Model::reguard(); } + public function testProjectGetFilter() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/projects?filter=xx'); + + $response->assertStatus(200); + } + public function testProjectGet() { $response = $this->withHeaders([ diff --git a/tests/Feature/PurchaseOrderTest.php b/tests/Feature/PurchaseOrderTest.php index 1b7c41d6b410..aa7c4f152b94 100644 --- a/tests/Feature/PurchaseOrderTest.php +++ b/tests/Feature/PurchaseOrderTest.php @@ -40,6 +40,16 @@ class PurchaseOrderTest extends TestCase $this->makeTestData(); } + public function testPurchaseOrderGetWithClientStatus() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/purchase_orders?client_status=sent'.$this->encodePrimaryKey($this->purchase_order->id)); + + $response->assertStatus(200); + } + public function testPostNewPurchaseOrderPdf() { $purchase_order = [ diff --git a/tests/Feature/QuoteTest.php b/tests/Feature/QuoteTest.php index a6072ade4f8b..0477a4ce0dff 100644 --- a/tests/Feature/QuoteTest.php +++ b/tests/Feature/QuoteTest.php @@ -50,6 +50,15 @@ class QuoteTest extends TestCase ); } + public function testQuoteListApproved() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/quotes?client_status=approved'); + + $response->assertStatus(200); + } public function testQuoteConvertToProject() diff --git a/tests/Feature/RecurringExpenseApiTest.php b/tests/Feature/RecurringExpenseApiTest.php index 77ff2cc38ff9..e0848ba4901d 100644 --- a/tests/Feature/RecurringExpenseApiTest.php +++ b/tests/Feature/RecurringExpenseApiTest.php @@ -43,6 +43,16 @@ class RecurringExpenseApiTest extends TestCase Model::reguard(); } + public function testRecurringExpenseGetFiltered() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/recurring_expenses?filter=xx'); + + $response->assertStatus(200); + } + public function testRecurringExpenseGet() { $response = $this->withHeaders([ diff --git a/tests/Feature/RecurringInvoiceTest.php b/tests/Feature/RecurringInvoiceTest.php index 61f99b0cd9f3..2bb8cca76937 100644 --- a/tests/Feature/RecurringInvoiceTest.php +++ b/tests/Feature/RecurringInvoiceTest.php @@ -52,6 +52,17 @@ class RecurringInvoiceTest extends TestCase $this->makeTestData(); } + public function testRecurringGetStatus() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/recurring_invoices?client_status=active') + ->assertStatus(200); + + } + + public function testPostRecurringInvoiceWithPlaceholderVariables() { $line_items = []; diff --git a/tests/Feature/RecurringQuoteTest.php b/tests/Feature/RecurringQuoteTest.php index 5c3d98718ae6..3051f0466100 100644 --- a/tests/Feature/RecurringQuoteTest.php +++ b/tests/Feature/RecurringQuoteTest.php @@ -47,6 +47,17 @@ class RecurringQuoteTest extends TestCase $this->makeTestData(); } + public function testRecurringQuoteListFilter() + { + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/recurring_quotes?filter=xx'); + + $response->assertStatus(200); + } + public function testRecurringQuoteList() { RecurringQuote::factory()->create(['user_id' => $this->user->id, 'company_id' => $this->company->id, 'client_id' => $this->client->id]); diff --git a/tests/Feature/Scheduler/SchedulerTest.php b/tests/Feature/Scheduler/SchedulerTest.php index a743e7a1fcbb..43495179e2dd 100644 --- a/tests/Feature/Scheduler/SchedulerTest.php +++ b/tests/Feature/Scheduler/SchedulerTest.php @@ -14,6 +14,7 @@ namespace Tests\Feature\Scheduler; use App\Factory\SchedulerFactory; use App\Models\Client; use App\Models\RecurringInvoice; +use App\Models\Scheduler; use App\Services\Scheduler\SchedulerService; use App\Utils\Traits\MakesHash; use Carbon\Carbon; @@ -53,6 +54,72 @@ class SchedulerTest extends TestCase } + public function testClientCountResolution() + { + + $c = Client::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'number' => rand(1000,100000), + 'name' => 'A fancy client' + ]); + + $c2 = Client::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'number' => rand(1000,100000), + 'name' => 'A fancy client' + ]); + + $data = [ + 'name' => 'A test statement scheduler', + 'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY, + 'next_run' => now()->format('Y-m-d'), + 'template' => 'client_statement', + 'parameters' => [ + 'date_range' => 'previous_month', + 'show_payments_table' => true, + 'show_aging_table' => true, + 'status' => 'paid', + 'clients' => [ + $c2->hashed_id, + $c->hashed_id + ], + ], + ]; + + $response = false; + + try{ + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/task_schedulers', $data); + + } catch (ValidationException $e) { + $message = json_decode($e->validator->getMessageBag(), 1); + nlog($message); + } + + $response->assertStatus(200); + + $data = $response->json(); + + $scheduler = Scheduler::find($this->decodePrimaryKey($data['data']['id'])); + + $this->assertInstanceOf(Scheduler::class, $scheduler); + + $this->assertCount(2, $scheduler->parameters['clients']); + + $q = Client::query() + ->where('company_id', $scheduler->company_id) + ->whereIn('id', $this->transformKeys($scheduler->parameters['clients'])) + ->cursor(); + + $this->assertCount(2, $q); + + } + public function testClientsValidationInScheduledTask() { @@ -162,7 +229,7 @@ class SchedulerTest extends TestCase $data = [ 'name' => 'A test statement scheduler', 'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY, - 'next_run' => "2023-01-01", + 'next_run' => now()->format('Y-m-d'), 'template' => 'client_statement', 'parameters' => [ 'date_range' => 'previous_month', @@ -184,7 +251,7 @@ class SchedulerTest extends TestCase $scheduler->fresh(); - $this->assertEquals("2023-02-01", $scheduler->next_run->format('Y-m-d')); + $this->assertEquals(now()->addMonth()->format('Y-m-d'), $scheduler->next_run->format('Y-m-d')); } diff --git a/tests/Feature/SubscriptionApiTest.php b/tests/Feature/SubscriptionApiTest.php index 86360680e510..46cf797c20e1 100644 --- a/tests/Feature/SubscriptionApiTest.php +++ b/tests/Feature/SubscriptionApiTest.php @@ -50,6 +50,17 @@ class SubscriptionApiTest extends TestCase Model::reguard(); } + public function testSubscriptionFilter() + { + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/subscriptions?filter=xx') + ->assertStatus(200); + + } + public function testSubscriptionsGet() { $product = Product::factory()->create([ diff --git a/tests/Feature/SystemLogApiTest.php b/tests/Feature/SystemLogApiTest.php index e91970af014f..56985c502a7a 100644 --- a/tests/Feature/SystemLogApiTest.php +++ b/tests/Feature/SystemLogApiTest.php @@ -34,6 +34,18 @@ class SystemLogApiTest extends TestCase $this->makeTestData(); } + + public function testFilters() + { + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/system_logs?type_id=3') + ->assertStatus(200);; + + } + public function testSystemLogRoutes() { $sl = [ diff --git a/tests/Feature/TaskApiTest.php b/tests/Feature/TaskApiTest.php index 0f4c025232f3..031966083393 100644 --- a/tests/Feature/TaskApiTest.php +++ b/tests/Feature/TaskApiTest.php @@ -44,6 +44,17 @@ class TaskApiTest extends TestCase } + + public function testTaskListClientStatus() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/tasks?client_status=invoiced') + ->assertStatus(200); + + } + public function testTaskLockingGate() { $data = [ diff --git a/tests/Feature/TaskStatusApiTest.php b/tests/Feature/TaskStatusApiTest.php index 2d0e4c83c2bd..4ce96974699c 100644 --- a/tests/Feature/TaskStatusApiTest.php +++ b/tests/Feature/TaskStatusApiTest.php @@ -41,6 +41,16 @@ class TaskStatusApiTest extends TestCase Model::reguard(); } + public function testTaskStatusGetFilter() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/task_statuses?filter=xx'); + + $response->assertStatus(200); + } + public function testTaskStatusPost() { $data = [ diff --git a/tests/Feature/TaxRateApiTest.php b/tests/Feature/TaxRateApiTest.php index cac0c642b1ac..09b39e4e0b8f 100644 --- a/tests/Feature/TaxRateApiTest.php +++ b/tests/Feature/TaxRateApiTest.php @@ -42,6 +42,16 @@ class TaxRateApiTest extends TestCase Model::reguard(); } + public function testTaxRatesGetFilter() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/tax_rates?filter=gst'); + + $response->assertStatus(200); + } + public function testTaxRatePost() { $rate_name = $this->faker->firstName(); diff --git a/tests/Feature/UserTest.php b/tests/Feature/UserTest.php index e424640f4885..39d4dafefc1b 100644 --- a/tests/Feature/UserTest.php +++ b/tests/Feature/UserTest.php @@ -58,6 +58,19 @@ class UserTest extends TestCase } + public function testUserFiltersWith() + { + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + 'X-API-PASSWORD' => 'ALongAndBriliantPassword', + ])->get('/api/v1/users?with='.$this->user->hashed_id); + + $response->assertStatus(200); + + } + public function testUserList() { $response = $this->withHeaders([ diff --git a/tests/Feature/VendorApiTest.php b/tests/Feature/VendorApiTest.php index 38f8b1ccf700..79a8800f0f86 100644 --- a/tests/Feature/VendorApiTest.php +++ b/tests/Feature/VendorApiTest.php @@ -42,6 +42,16 @@ class VendorApiTest extends TestCase Model::reguard(); } + public function testVendorGetFilter() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/vendors?filter=xx'); + + $response->assertStatus(200); + } + public function testAddVendorToInvoice() { $data = [ diff --git a/tests/Feature/WebhookAPITest.php b/tests/Feature/WebhookAPITest.php index a94e194aac4d..69c640a6ae61 100644 --- a/tests/Feature/WebhookAPITest.php +++ b/tests/Feature/WebhookAPITest.php @@ -45,6 +45,16 @@ class WebhookAPITest extends TestCase $this->withoutExceptionHandling(); } + public function testWebhookGetFilter() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/webhooks?filter=xx'); + + $response->assertStatus(200); + } + public function testWebhookGetRoute() { $response = $this->withHeaders([ diff --git a/tests/Integration/CompanyLedgerTest.php b/tests/Integration/CompanyLedgerTest.php index de627c13071e..f07f36b62e00 100644 --- a/tests/Integration/CompanyLedgerTest.php +++ b/tests/Integration/CompanyLedgerTest.php @@ -54,29 +54,8 @@ class CompanyLedgerTest extends TestCase $this->artisan('db:seed --force'); - /* 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); - } - } + $this->faker = \Faker\Factory::create(); + $fake_email = $this->faker->email(); $this->account = Account::factory()->create(); $this->company = Company::factory()->create([ @@ -93,7 +72,7 @@ class CompanyLedgerTest extends TestCase $settings->state = 'State'; $settings->postal_code = 'Postal Code'; $settings->phone = '555-343-2323'; - $settings->email = 'user@example.com'; + $settings->email = $fake_email; $settings->country_id = '840'; $settings->vat_number = 'vat number'; $settings->id_number = 'id number'; @@ -106,10 +85,12 @@ class CompanyLedgerTest extends TestCase $this->account->default_company_id = $this->company->id; $this->account->save(); - $user = User::whereEmail('user@example.com')->first(); + + $user = User::whereEmail($fake_email)->first(); if (! $user) { $user = User::factory()->create([ + 'email' => $fake_email, 'account_id' => $this->account->id, 'password' => Hash::make('ALongAndBriliantPassword'), 'confirmation_code' => $this->createDbHash(config('database.default')), diff --git a/tests/MockAccountData.php b/tests/MockAccountData.php index 09752d0873bc..218f415ba5c6 100644 --- a/tests/MockAccountData.php +++ b/tests/MockAccountData.php @@ -57,6 +57,7 @@ use App\Models\Vendor; use App\Models\VendorContact; use App\Utils\Traits\GeneratesCounter; use App\Utils\Traits\MakesHash; +use Illuminate\Foundation\Testing\WithoutEvents; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Hash; @@ -71,6 +72,7 @@ trait MockAccountData { use MakesHash; use GeneratesCounter; + use WithoutEvents; /** * @var @@ -213,6 +215,9 @@ trait MockAccountData } } + $this->faker = \Faker\Factory::create(); + $fake_email = $this->faker->email(); + $this->account = Account::factory()->create([ 'hosted_client_count' => 1000, 'hosted_company_count' => 1000, @@ -233,7 +238,6 @@ trait MockAccountData $settings = CompanySettings::defaults(); $settings->company_logo = 'https://pdf.invoicing.co/favicon-v2.png'; - // $settings->company_logo = asset('images/new_logo.png'); $settings->website = 'www.invoiceninja.com'; $settings->address1 = 'Address 1'; $settings->address2 = 'Address 2'; @@ -241,7 +245,7 @@ trait MockAccountData $settings->state = 'State'; $settings->postal_code = 'Postal Code'; $settings->phone = '555-343-2323'; - $settings->email = 'user@example.com'; + $settings->email = $fake_email; $settings->country_id = '840'; $settings->vat_number = 'vat number'; $settings->id_number = 'id number'; @@ -256,13 +260,13 @@ trait MockAccountData $this->account->default_company_id = $this->company->id; $this->account->save(); - $user = User::whereEmail('user@example.com')->first(); + $user = User::whereEmail($fake_email)->first(); if (! $user) { $user = User::factory()->create([ 'account_id' => $this->account->id, 'confirmation_code' => $this->createDbHash(config('database.default')), - 'email' => 'user@example.com', + 'email' => $fake_email, ]); } diff --git a/tests/Unit/CompanyDocumentsTest.php b/tests/Unit/CompanyDocumentsTest.php index f03bc34a6252..41fdd340282d 100644 --- a/tests/Unit/CompanyDocumentsTest.php +++ b/tests/Unit/CompanyDocumentsTest.php @@ -62,6 +62,6 @@ class CompanyDocumentsTest extends TestCase $this->assertEquals(0, Document::whereCompanyId($this->company->id)->count()); - $this->assertFalse(Storage::exists($document->url)); + // $this->assertFalse(Storage::exists($document->url)); } } diff --git a/tests/Unit/GeneratesConvertedQuoteCounterTest.php b/tests/Unit/GeneratesConvertedQuoteCounterTest.php index f0d9e53cc373..76470d1aa72c 100644 --- a/tests/Unit/GeneratesConvertedQuoteCounterTest.php +++ b/tests/Unit/GeneratesConvertedQuoteCounterTest.php @@ -62,13 +62,15 @@ class GeneratesConvertedQuoteCounterTest extends TestCase $this->account->num_users = 3; $this->account->save(); - $user = User::whereEmail('user@example.com')->first(); + $fake_email = $this->faker->email(); + + $user = User::whereEmail($fake_email)->first(); if (! $user) { $user = User::factory()->create([ 'account_id' => $this->account->id, 'confirmation_code' => $this->createDbHash(config('database.default')), - 'email' => 'user@example.com', + 'email' => $fake_email, ]); } diff --git a/tests/ci b/tests/ci new file mode 100755 index 000000000000..3c097b868a6b --- /dev/null +++ b/tests/ci @@ -0,0 +1,49 @@ +#!/usr/bin/env php +mustRun(); + +$tests = \Illuminate\Support\Str::of($process->getOutput()) + ->explode("\n") // Break the output from new lines into an array + ->filter(fn (string $test) => str_contains($test, ' - ')) // Only lines with " - " + ->map(fn (string $test) => addslashes( + \Illuminate\Support\Str::of($test) + ->replace('- ', '') // Strip the "- " + ->trim() + ->explode('::') // Only the class, not the method + ->get(0) + )) + ->filter(fn (string $test) => !empty($test)) // Make sure there are no empty lines + ->unique() // We only need unique classes + ->split((int) getenv('CI_NODE_TOTAL')) // Split it into equally sized chunks + ->get((int) getenv('CI_NODE_INDEX')); // Get the index we need for this instance + +/** + * Run phpunit with a filter: + * phpunit --filter 'TestClass|AnotherTestClass|...' + */ +$process = new \Symfony\Component\Process\Process(['./vendor/bin/phpunit', '--testdox', '--filter', $tests->join('|')], timeout: null); +$process->start(); + +// Make sure we have live data output +foreach ($process as $type => $data) { + echo $data; +} + +$process->wait(); + +// Exit using PHPUnit's exit code to have the action pass/fail +exit($process->getExitCode()); \ No newline at end of file