Merge pull request #8174 from turbo124/v5-stable

v5.5.57
This commit is contained in:
David Bomba 2023-01-19 14:53:35 +11:00 committed by GitHub
commit 4632011d39
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
125 changed files with 108775 additions and 107734 deletions

View file

@ -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 }}

View file

@ -1 +1 @@
5.5.56
5.5.57

View file

@ -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,

View file

@ -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');

View file

@ -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();

View file

@ -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',

View file

@ -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;

View file

@ -26,7 +26,9 @@ class SchedulerFactory
$scheduler->is_paused = false;
$scheduler->is_deleted = false;
$scheduler->template = '';
$scheduler->next_run = now()->format('Y-m-d');
$scheduler->next_run_client = now()->format('Y-m-d');
return $scheduler;
}
}

View file

@ -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.

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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');

View file

@ -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;
}

View file

@ -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();
}
}

View file

@ -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.'%');
});
}

View file

@ -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);
}

View file

@ -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

View file

@ -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);
}
}

View file

@ -11,7 +11,6 @@
namespace App\Filters;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
/**

View file

@ -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]);
}
/**

View file

@ -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;
}

View file

@ -12,7 +12,6 @@
namespace App\Filters;
use App\Models\Quote;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
/**

View file

@ -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);

View file

@ -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;
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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();
}

View file

@ -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;
}

View file

@ -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"
* )
* )
*/

View file

@ -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",

View file

@ -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);
}

View file

@ -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"
* ),
* ),
*/

View file

@ -108,6 +108,8 @@ class SelfUpdateController extends BaseController
$zipFile->openFile($file);
$zipFile->deleteFromName(".htaccess");
$zipFile->rewrite();
$zipFile->extractTo(base_path());

View file

@ -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,

View file

@ -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));

View file

@ -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.
*

View file

@ -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])

View file

@ -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), ]
);

View file

@ -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'])) {

View file

@ -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;

View file

@ -12,10 +12,14 @@
namespace App\Http\Requests\TaskScheduler;
use App\Http\Requests\Request;
use App\Http\ValidationRules\Scheduler\ValidClientIds;
use App\Models\Client;
use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule;
class StoreSchedulerRequest extends Request
{
use MakesHash;
/**
* Determine if the user is authorized to make this request.
*
@ -33,12 +37,27 @@ class StoreSchedulerRequest extends Request
'name' => ['bail', 'required', Rule::unique('schedulers')->where('company_id', auth()->user()->company()->id)],
'is_paused' => 'bail|sometimes|boolean',
'frequency_id' => 'bail|required|integer|digits_between:1,12',
'next_run' => 'bail|required|date:Y-m-d',
'next_run' => 'bail|required|date:Y-m-d|after_or_equal:today',
'next_run_client' => 'bail|sometimes|date:Y-m-d',
'template' => 'bail|required|string',
'parameters' => 'bail|array',
'parameters.clients' => ['bail','sometimes', 'array', new ValidClientIds()],
];
return $rules;
}
public function prepareForValidation()
{
$input = $this->all();
if (array_key_exists('next_run', $input) && is_string($input['next_run']))
$this->merge(['next_run_client' => $input['next_run']]);
return $input;
}
}

View file

@ -32,7 +32,8 @@ class UpdateSchedulerRequest extends Request
'name' => ['bail', 'sometimes', Rule::unique('schedulers')->where('company_id', auth()->user()->company()->id)->ignore($this->task_scheduler->id)],
'is_paused' => 'bail|sometimes|boolean',
'frequency_id' => 'bail|required|integer|digits_between:1,12',
'next_run' => 'bail|required|date:Y-m-d',
'next_run' => 'bail|required|date:Y-m-d|after_or_equal:today',
'next_run_client' => 'bail|sometimes|date:Y-m-d',
'template' => 'bail|required|string',
'parameters' => 'bail|array',
];
@ -40,4 +41,17 @@ class UpdateSchedulerRequest extends Request
return $rules;
}
public function prepareForValidation()
{
$input = $this->all();
if (array_key_exists('next_run', $input) && is_string($input['next_run']))
$this->merge(['next_run_client' => $input['next_run']]);
return $input;
}
}

View file

@ -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' => '',
];

View file

@ -0,0 +1,43 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\ValidationRules\Scheduler;
use App\Models\Client;
use App\Utils\Traits\MakesHash;
use Illuminate\Contracts\Validation\Rule;
/**
* Class ValidClientIds.
*/
class ValidClientIds implements Rule
{
use MakesHash;
/**
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
return Client::where('company_id', auth()->user()->company()->id)->whereIn('id', $this->transformKeys($value))->count() == count($value);
}
/**
* @return string
*/
public function message()
{
return 'Invalid client ids';
}
}

View file

@ -319,7 +319,7 @@ class MatchBankTransactions implements ShouldQueue
});
}, 1);
}, 2);
if(!$this->invoice)
return;

View file

@ -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')

View file

@ -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);
// });
// }
}
}

View file

@ -44,6 +44,7 @@ class BankTransactionSync implements ShouldQueue
*/
public function handle()
{
//multiDB environment, need to
foreach (MultiDB::$dbs as $db)
{

View file

@ -29,6 +29,8 @@ class UpdateOrCreateProduct implements ShouldQueue
public $company;
public $deleteWhenMissingModels = true;
/**
* Create a new job instance.
*

View file

@ -649,6 +649,24 @@ class Company extends BaseModel
return $data;
}
public function timezone_offset()
{
$offset = 0;
$entity_send_time = $this->getSetting('entity_send_time');
if ($entity_send_time == 0) {
return 0;
}
$timezone = $this->timezone();
$offset -= $timezone->utc_offset;
$offset += ($entity_send_time * 3600);
return $offset;
}
public function translate_entity()
{
return ctrans('texts.company');
@ -666,4 +684,5 @@ class Company extends BaseModel
return $item->id == $this->getSetting('date_format_id');
})->first()->format;
}
}

View file

@ -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) {

View file

@ -43,4 +43,5 @@ class Paymentable extends Pivot
{
return $this->belongsTo(Payment::class);
}
}

View file

@ -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';

View file

@ -257,13 +257,6 @@ class RecurringInvoice extends BaseModel
}
}
/*
As we are firing at UTC+0 if our offset is negative it is technically firing the day before so we always need
to add ON a day - a day = 86400 seconds
*/
// if($offset < 0)
// $offset += 86400;
switch ($this->frequency_id) {
case self::FREQUENCY_DAILY:
return Carbon::parse($this->next_send_date_client)->startOfDay()->addDay()->addSeconds($offset);

View file

@ -38,7 +38,7 @@ class Scheduler extends BaseModel
'name',
'frequency_id',
'next_run',
'scheduled_run',
'next_run_client',
'template',
'is_paused',
'parameters',
@ -46,6 +46,7 @@ class Scheduler extends BaseModel
protected $casts = [
'next_run' => 'datetime',
'next_run_client' => 'datetime',
'created_at' => 'timestamp',
'updated_at' => 'timestamp',
'deleted_at' => 'timestamp',
@ -66,7 +67,7 @@ class Scheduler extends BaseModel
return new SchedulerService($this);
}
public function company(): \Illuminate\Database\Eloquent\Relations\BelongsTo
public function company()
{
return $this->belongsTo(Company::class);
}

View file

@ -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()

View file

@ -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)

View file

@ -18,16 +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;
@ -102,15 +102,11 @@ class AppServiceProvider extends ServiceProvider
return $this;
});
}
/**
* Register any application services.
*
* @return void
*/
public function register()
{
ParallelTesting::setUpTestDatabase(function ($database, $token) {
Artisan::call('db:seed');
});
}
}

View file

@ -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
{

View file

@ -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;

View file

@ -27,6 +27,7 @@ use App\Utils\Number;
use App\Utils\PhantomJS\Phantom;
use App\Utils\Traits\Pdf\PdfMaker as PdfMakerTrait;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
class Statement
{
@ -117,7 +118,7 @@ class Statement
}
if (\is_null($this->entity)) {
\DB::connection(config('database.default'))->beginTransaction();
DB::connection(config('database.default'))->beginTransaction();
$this->rollback = true;

View file

@ -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);

View file

@ -51,7 +51,7 @@ class DeletePayment
}
}, 1);
}, 2);
return $this->payment;

View file

@ -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));
}
}

View file

@ -12,9 +12,11 @@
namespace App\Services\Scheduler;
use App\Models\Client;
use App\Models\RecurringInvoice;
use App\Models\Scheduler;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
use Carbon\Carbon;
class SchedulerService
{
@ -45,24 +47,31 @@ 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){
$this->client = $_client;
$statement_properties = $this->calculateStatementProperties();
//work out the date range
$pdf = $_client->service()->statement($statement_properties,true);
$statement_properties = $this->calculateStatementProperties();
//calculate next run dates;
$_client->service()->statement($statement_properties,true);
});
//calculate next run dates;
$this->calculateNextRun();
}
private function calculateStatementProperties()
/**
* Hydrates the array needed to generate the statement
*
* @return array The statement options array
*/
private function calculateStatementProperties(): array
{
$start_end = $this->calculateStartAndEndDates();
@ -76,7 +85,12 @@ class SchedulerService
}
private function calculateStartAndEndDates()
/**
* Start and end date of the statement
*
* @return array [$start_date, $end_date];
*/
private function calculateStartAndEndDates(): array
{
return match ($this->scheduler->parameters['date_range']) {
'this_month' => [now()->firstOfMonth()->format('Y-m-d'), now()->lastOfMonth()->format('Y-m-d')],
@ -91,6 +105,67 @@ class SchedulerService
}
/**
* Sets the next run date of the scheduled task
*
*/
private function calculateNextRun()
{
if (! $this->scheduler->next_run) {
return null;
}
$offset = $this->scheduler->company->timezone_offset();
switch ($this->scheduler->frequency_id) {
case RecurringInvoice::FREQUENCY_DAILY:
$next_run = now()->startOfDay()->addDay();
break;
case RecurringInvoice::FREQUENCY_WEEKLY:
$next_run = now()->startOfDay()->addWeek();
break;
case RecurringInvoice::FREQUENCY_TWO_WEEKS:
$next_run = now()->startOfDay()->addWeeks(2);
break;
case RecurringInvoice::FREQUENCY_FOUR_WEEKS:
$next_run = now()->startOfDay()->addWeeks(4);
break;
case RecurringInvoice::FREQUENCY_MONTHLY:
$next_run = now()->startOfDay()->addMonthNoOverflow();
break;
case RecurringInvoice::FREQUENCY_TWO_MONTHS:
$next_run = now()->startOfDay()->addMonthsNoOverflow(2);
break;
case RecurringInvoice::FREQUENCY_THREE_MONTHS:
$next_run = now()->startOfDay()->addMonthsNoOverflow(3);
break;
case RecurringInvoice::FREQUENCY_FOUR_MONTHS:
$next_run = now()->startOfDay()->addMonthsNoOverflow(4);
break;
case RecurringInvoice::FREQUENCY_SIX_MONTHS:
$next_run = now()->startOfDay()->addMonthsNoOverflow(6);
break;
case RecurringInvoice::FREQUENCY_ANNUALLY:
$next_run = now()->startOfDay()->addYear();
break;
case RecurringInvoice::FREQUENCY_TWO_YEARS:
$next_run = now()->startOfDay()->addYears(2);
break;
case RecurringInvoice::FREQUENCY_THREE_YEARS:
$next_run = now()->startOfDay()->addYears(3);
break;
default:
$next_run = null;
}
$this->scheduler->next_run_client = $next_run ?: null;
$this->scheduler->next_run = $next_run ? $next_run->copy()->addSeconds($offset) : null;
$this->scheduler->save();
}
//handle when the scheduler has been paused.
}

View file

@ -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,

View file

@ -24,7 +24,7 @@ class SchedulerTransformer extends EntityTransformer
'id' => $this->encodePrimaryKey($scheduler->id),
'name' => (string) $scheduler->name,
'frequency_id' => (string) $scheduler->frequency_id,
'next_run' => $scheduler->next_run,
'next_run' => $scheduler->next_run_client->format('Y-m-d'),
'template' => (string) $scheduler->template,
'is_paused' => (bool) $scheduler->is_paused,
'is_deleted' => (bool) $scheduler->is_deleted,

View file

@ -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']);
}

View file

@ -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', ''),

View file

@ -31,7 +31,8 @@ class SchedulerFactory extends Factory
'parameters' => [],
'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY,
'next_run' => now()->addSeconds(rand(86400,8640000)),
'template' => 'statement_task',
'next_run_client' => now()->addSeconds(rand(86400,8640000)),
'template' => 'client_statement',
];
}
}

View file

@ -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);

View file

@ -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

View file

@ -10,12 +10,12 @@ const RESOURCES = {
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
"version.json": "864f04cd6ffe2879cdcfde94b20a735f",
"main.dart.js": "5d0e9380404bd3ea5a1b74acbd7a863c",
"main.dart.js": "7b89a717d47c6b9b1fd32bf242af8615",
"canvaskit/canvaskit.js": "2bc454a691c631b07a9307ac4ca47797",
"canvaskit/profiling/canvaskit.js": "38164e5a72bdad0faa4ce740c9b8e564",
"canvaskit/profiling/canvaskit.wasm": "95a45378b69e77af5ed2bc72b2209b94",
"canvaskit/canvaskit.wasm": "bf50631470eb967688cca13ee181af62",
"/": "f1f8f2f37805da24445bba0c16aa56e2",
"/": "d10f0927ab0b41ee5677cd2158df0eba",
"assets/AssetManifest.json": "759f9ef9973f7e26c2a51450b55bb9fa",
"assets/packages/intl_phone_field/assets/flags/pf.png": "1ae72c24380d087cbe2d0cd6c3b58821",
"assets/packages/intl_phone_field/assets/flags/fi.png": "3ccd69a842e55183415b7ea2c04b15c8",

107686
public/main.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

106120
public/main.foss.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -51,18 +51,34 @@
</div>
<p class="mt-1 text-sm text-gray-500"></p>
</div>
<div class="flex content-end text-sm mt-1">
<div class="flex justify-between text-sm mt-1">
@if($subscription->per_seat_enabled)
<p class="text-gray-500 w-full"></p>
<p class="text-gray-500 w-3/4"></p>
<div class="flex place-content-end">
@if($subscription->use_inventory_management && $product->in_stock_quantity == 0)
<p class="text-sm font-light text-red-500 text-right mr-2 mt-2">Out of stock</p>
@else
<p class="text-sm font-light text-gray-700 text-right mr-2 mt-2">{{ ctrans('texts.qty') }}</p>
@endif
<select wire:model.debounce.300ms="data.{{ $index }}.recurring_qty" class="rounded-md border-gray-300 shadow-sm sm:text-sm"
@if($subscription->use_inventory_management && $product->in_stock_quantity == 0)
disabled
@endif
>
<option value="1" selected="selected">1</option>
<select wire:model.debounce.300ms="data.{{ $index }}.recurring_qty" class="rounded-md border-gray-300 shadow-sm sm:text-sm">
<option value="1" selected="selected">1</option>
@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++)
<option value="{{$i}}">{{$i}}</option>
@endfor
</select>
}
@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++)
<option value="{{$i}}">{{$i}}</option>
@endfor
@endif
</select>
</div>
@endif
</div>

View file

@ -60,7 +60,7 @@
{{ $payment->translatedType() }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm leading-5 text-gray-500">
{!! \App\Utils\Number::formatMoney($payment->amount, $payment->client) !!}
{!! \App\Utils\Number::formatMoney($payment->amount > 0 ? $payment->amount : $payment->credits->sum('pivot.amount'), $payment->client) !!}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm leading-5 text-gray-500">
{{ \Illuminate\Support\Str::limit($payment->transaction_reference, 35) }}

View file

@ -66,7 +66,7 @@
{{ ctrans('texts.amount') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $payment->formattedAmount() }}
{{ $payment->formatAmount($payment->amount > 0 ? $payment->amount : $payment?->invoices->sum('pivot.amount')) }}
</dd>
</div>
@endif
@ -116,6 +116,7 @@
href="{{ route('client.invoice.show', ['invoice' => $invoice->hashed_id])}}">
{{ $invoice->number }}
</a>
- {{ \App\Utils\Number::formatMoney($payment->invoices->where('id', $invoice->id)->sum('pivot.amount') - $payment->invoices->where('id', $invoice->id)->sum('pivot.refunded'), $payment->client) }}
</div>
</div>
@endforeach

View file

@ -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());
}
}

View file

@ -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([

View file

@ -47,6 +47,42 @@ class CompanyTest extends TestCase
$this->makeTestData();
}
public function testUpdateCompanyPropertyInvoiceTaskHours()
{
$company_update = [
'invoice_task_hours' => true
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/companies/'.$this->encodePrimaryKey($this->company->id), $company_update)
->assertStatus(200);
$arr = $response->json();
$this->assertTrue($arr['data']['invoice_task_hours']);
$company_update = [
'invoice_task_hours' => false
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/companies/'.$this->encodePrimaryKey($this->company->id), $company_update)
->assertStatus(200);
$arr = $response->json();
$this->assertFalse($arr['data']['invoice_task_hours']);
}
public function testCompanyList()
{
$this->withoutMiddleware(PasswordProtection::class);

View file

@ -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);

View file

@ -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) {

View file

@ -0,0 +1,212 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Feature\Email;
use App\Services\Email\EmailObject;
use App\Services\Email\EmailService;
use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\MakesHash;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Tests\MockAccountData;
use Tests\TestCase;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Support\Facades\Cache;
/**
* @test
* @covers App\Services\Email\EmailService
*/
class EmailServiceTest extends TestCase
{
use MakesHash;
use GeneratesCounter;
use MockAccountData;
public EmailService $email_service;
public EmailObject $email_object;
protected function setUp() :void
{
parent::setUp();
if(!class_exists(\Modules\Admin\Jobs\Account\EmailFilter::class))
$this->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());
}
}

View file

@ -1,84 +0,0 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace Tests\Feature;
use App\Services\Email\EmailObject;
use App\Services\Email\EmailService;
use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\MakesHash;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Tests\MockAccountData;
use Tests\TestCase;
use Illuminate\Mail\Mailables\Address;
/**
* @test
* @covers App\Services\Email\EmailService
*/
class EmailTest extends TestCase
{
use MakesHash;
use GeneratesCounter;
use MockAccountData;
public EmailService $email_service;
public EmailObject $email_object;
protected function setUp() :void
{
parent::setUp();
if(!class_exists(\Modules\Admin\Jobs\Account\EmailFilter::class))
$this->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());
}
}

View file

@ -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 = [

View file

@ -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();
}

View file

@ -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');

View file

@ -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')),
]);

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