Hosted platform rules (#3650)

* Filter properties which can be saved on free accounts

* Self Updater

* Fixes for tests

* Refactor for settings

* Working on feature permissions - Settings Saver

* Fixes for events on self-updater

* Working on Self Updater

* Working on free /pro settings saver

* Implement free/pro/enterprise saving for settings

* Update company request

* Implement settings saver for hosted platform for clients and group level settings

* Implement quotas for hosted version

* Validation rules for hosted platform"
This commit is contained in:
David Bomba 2020-04-21 15:16:45 +10:00 committed by GitHub
parent b285067adb
commit 280e42d366
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 380 additions and 142 deletions

View File

@ -396,6 +396,43 @@ class CompanySettings extends BaseSettings
'portal_custom_js' => 'string',
];
public static $free_plan_casts = [
'currency_id' => 'string',
'company_gateway_ids' => 'string',
'address1' => 'string',
'address2' => 'string',
'city' => 'string',
'company_logo' => 'string',
'country_id' => 'string',
'custom_value1' => 'string',
'custom_value2' => 'string',
'custom_value3' => 'string',
'custom_value4' => 'string',
'inclusive_taxes' => 'bool',
'name' => 'string',
'payment_terms' => 'integer',
'payment_type_id' => 'string',
'phone' => 'string',
'postal_code' => 'string',
'state' => 'string',
'email' => 'string',
'vat_number' => 'string',
'id_number' => 'string',
'tax_name1' => 'string',
'tax_name2' => 'string',
'tax_name3' => 'string',
'tax_rate1' => 'float',
'tax_rate2' => 'float',
'tax_rate3' => 'float',
'timezone_id' => 'string',
'date_format_id' => 'string',
'military_time' => 'bool',
'language_id' => 'string',
'show_currency_code' => 'bool',
'website' => 'string',
];
/**
* Array of variables which
* cannot be modified client side
@ -406,6 +443,12 @@ class CompanySettings extends BaseSettings
// 'quote_number_counter',
];
public static $string_casts = [
'invoice_design_id',
'quote_design_id',
'credit_design_id',
];
/**
* Cast object values and return entire class
* prevents missing properties from not being returned
@ -430,7 +473,9 @@ class CompanySettings extends BaseSettings
unset($data->casts);
unset($data->protected_fields);
unset($data->free_plan_casts);
unset($data->string_casts);
$data->timezone_id = (string) config('ninja.i18n.timezone_id');
$data->currency_id = (string) config('ninja.i18n.currency_id');
$data->language_id = (string) config('ninja.i18n.language_id');

View File

@ -66,15 +66,23 @@ class SelfUpdateController extends BaseController
return response()->json(['message' => 'Self update not available on this system.'], 403);
}
info("is new version available = ". $updater->source()->isNewVersionAvailable());
// Get the new version available
$versionAvailable = $updater->source()->getVersionAvailable();
info($versionAvailable);
// Create a release
$release = $updater->source()->fetch($versionAvailable);
info(print_r($release,1));
// Run the update process
$res = $updater->source()->update($release);
info(print_r($res,1));
return response()->json(['message'=>$res], 200);
}
}

View File

@ -13,6 +13,7 @@ namespace App\Http\Requests\Client;
use App\DataMapper\ClientSettings;
use App\Http\Requests\Request;
use App\Http\ValidationRules\Ninja\CanStoreClientsRule;
use App\Http\ValidationRules\ValidClientGroupSettingsRule;
use App\Models\Client;
use App\Models\GroupSetting;
@ -53,15 +54,8 @@ class StoreClientRequest extends Request
//'regex:/[@$!%*#?&.]/', // must contain a special character
];
// $contacts = request('contacts');
// if (is_array($contacts)) {
// for ($i = 0; $i < count($contacts); $i++) {
// //$rules['contacts.' . $i . '.email'] = 'nullable|email|distinct';
// }
// }
if(auth()->user()->company()->account->isFreeHostedClient())
$rules['hosted_clients'] = new CanStoreClientsRule($this->company_id);
return $rules;
}

View File

@ -12,10 +12,12 @@
namespace App\Http\Requests\Client;
use App\DataMapper\ClientSettings;
use App\DataMapper\CompanySettings;
use App\Http\Requests\Request;
use App\Http\ValidationRules\IsDeletedRule;
use App\Http\ValidationRules\ValidClientGroupSettingsRule;
use App\Http\ValidationRules\ValidSettingsRule;
use App\Utils\Ninja;
use App\Utils\Traits\ChecksEntityStatus;
use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule;
@ -103,6 +105,41 @@ class UpdateClientRequest extends Request
}
}
}
if(array_key_exists('settings', $input))
$input['settings'] = $this->filterSaveableSettings($input['settings']);
$this->replace($input);
}
/**
* For the hosted platform, we restrict the feature settings.
*
* This method will trim the company settings object
* down to the free plan setting properties which
* are saveable
*
* @param object $settings
* @return object $settings
*/
private function filterSaveableSettings($settings)
{
$account = $this->client->company->account;
if(!$account->isFreeHostedClient())
return $settings;
$saveable_casts = CompanySettings::$free_plan_casts;
foreach($settings as $key => $value){
if(!array_key_exists($key, $saveable_casts))
unset($settings->{$key});
}
return $settings;
}
}

View File

@ -66,13 +66,6 @@ class StoreCompanyRequest extends Request
}
}
// $company_settings->invoice_design_id = $this->encodePrimaryKey(1);
// $company_settings->quote_design_id = $this->encodePrimaryKey(1);
// $company_settings->credit_design_id = $this->encodePrimaryKey(1);
// $input['settings'] = $company_settings;
$this->replace($input);
}
}

View File

@ -14,6 +14,7 @@ namespace App\Http\Requests\Company;
use App\DataMapper\CompanySettings;
use App\Http\Requests\Request;
use App\Http\ValidationRules\ValidSettingsRule;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
@ -50,10 +51,51 @@ class UpdateCompanyRequest extends Request
$rules['portal_domain'] = 'nullable|alpha_num';
}
if($this->company->account->isPaidHostedClient())
return $settings;
return $rules;
}
protected function prepareForValidation()
{
$input = $this->all();
if(array_key_exists('settings', $input))
$input['settings'] = $this->filterSaveableSettings($input['settings']);
$this->replace($input);
}
/**
* For the hosted platform, we restrict the feature settings.
*
* This method will trim the company settings object
* down to the free plan setting properties which
* are saveable
*
* @param object $settings
* @return object $settings
*/
private function filterSaveableSettings($settings)
{
$account = $this->company->account;
if(!$account->isFreeHostedClient())
return $settings;
$saveable_casts = CompanySettings::$free_plan_casts;
foreach($settings as $key => $value){
if(!array_key_exists($key, $saveable_casts))
unset($settings->{$key});
}
return $settings;
}
}

View File

@ -11,8 +11,10 @@
namespace App\Http\Requests\GroupSetting;
use App\DataMapper\CompanySettings;
use App\Http\Requests\Request;
use App\Http\ValidationRules\ValidClientGroupSettingsRule;
use App\Utils\Ninja;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;
@ -40,6 +42,40 @@ class UpdateGroupSettingRequest extends Request
{
$input = $this->all();
if(array_key_exists('settings', $input))
$input['settings'] = $this->filterSaveableSettings($input['settings']);
$this->replace($input);
}
/**
* For the hosted platform, we restrict the feature settings.
*
* This method will trim the company settings object
* down to the free plan setting properties which
* are saveable
*
* @param object $settings
* @return object $settings
*/
private function filterSaveableSettings($settings)
{
$account = $this->group_setting->company->account;
if(!$account->isFreeHostedClient())
return $settings;
$saveable_casts = CompanySettings::$free_plan_casts;
foreach($settings as $key => $value){
if(!array_key_exists($key, $saveable_casts))
unset($settings->{$key});
}
return $settings;
}
}

View File

@ -0,0 +1,49 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\ValidationRules\Ninja;
use App\Models\Company;
use Illuminate\Contracts\Validation\Rule;
/**
* Class CanAddUserRule
* @package App\Http\ValidationRules
*/
class CanAddUserRule implements Rule
{
public $account;
public function __construct($account)
{
$this->account = $account;
}
/**
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
}
/**
* @return string
*/
public function message()
{
return ctrans('texts.limit_users', ['limit' => 1]);
}
}

View File

@ -0,0 +1,51 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\ValidationRules\Ninja;
use App\Models\Company;
use Illuminate\Contracts\Validation\Rule;
/**
* Class CanStoreClientsRule
* @package App\Http\ValidationRules
*/
class CanStoreClientsRule implements Rule
{
public $company_id;
public function __construct($company_id)
{
$this->company_id = $company_id;
}
/**
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
$company = Company::find($this->company_id);
return $company->clients->count() < config('ninja.quotas.free.clients');
}
/**
* @return string
*/
public function message()
{
return ctrans('texts.limit_clients', ['count' => config('ninja.quotas.free.clients')]);
}
}

View File

@ -35,6 +35,6 @@ class CreateInvoicePdf implements ShouldQueue
*/
public function handle($event)
{
PdfCreator::dispatch($invoice->invitations->first());
PdfCreator::dispatch($event->invoice->invitations->first());
}
}

View File

@ -194,6 +194,15 @@ class Account extends BaseModel
return $this->plan == 'pro' || $this->plan == 'enterprise';
}
public function isFreeHostedClient()
{
if (! Ninja::isNinja()) {
return false;
}
return $this->plan == 'free';
}
public function isTrial()
{
if (! Ninja::isNinja()) {

View File

@ -64,12 +64,6 @@ class EventServiceProvider extends ServiceProvider
* @var array
*/
protected $listen = [
\Codedge\Updater\Events\UpdateAvailable::class => [
\Codedge\Updater\Listeners\SendUpdateAvailableNotification::class
], // [3]
\Codedge\Updater\Events\UpdateSucceeded::class => [
\Codedge\Updater\Listeners\SendUpdateSucceededNotification::class
],
UserWasCreated::class => [
SendVerificationNotification::class,
],

View File

@ -12,10 +12,16 @@
namespace App\Utils\Traits;
use App\DataMapper\CompanySettings;
use App\Utils\Traits\SettingsSaver;
/**
* Class ClientGroupSettingsSaver
*
* Whilst it may appear that this CompanySettingsSaver and ClientGroupSettingsSaver
* could be duplicates, they are not.
*
* Each requires their own approach to saving and attempts to
* merge the two code paths should be avoided.
*
* @package App\Utils\Traits
*/
trait ClientGroupSettingsSaver
@ -42,7 +48,6 @@ trait ClientGroupSettingsSaver
unset($settings[$field]);
}
/**
* for clients and group settings, if a field is not set or is set to a blank value,
* we unset it from the settings object
@ -90,7 +95,7 @@ trait ClientGroupSettingsSaver
}
foreach ($casts as $key => $value) {
if (in_array($key, SettingsSaver::$string_casts)) {
if (in_array($key, CompanySettings::$string_casts)) {
$value = "string";
if (!property_exists($settings, $key)) {

View File

@ -12,10 +12,18 @@
namespace App\Utils\Traits;
use App\DataMapper\CompanySettings;
use App\Utils\Traits\SettingsSaver;
use App\Models\Company;
use App\Utils\Ninja;
/**
* Class CompanySettingsSaver
*
* Whilst it may appear that this CompanySettingsSaver and ClientGroupSettingsSaver
* could be duplicates, they are not.
*
* Each requires their own approach to saving and attempts to
* merge the two code paths should be avoided.
*
* @package App\Utils\Traits
*/
trait CompanySettingsSaver
@ -44,12 +52,13 @@ trait CompanySettingsSaver
$settings = $this->checkSettingType($settings);
$company_settings = CompanySettings::defaults();
//Iterate and set NEW settings
foreach ($settings as $key => $value) {
foreach ($settings as $key => $value) {
if (is_null($settings->{$key})) {
$company_settings->{$key} = '';
} else {
$company_settings->{$key} = $value;
$company_settings->{$key} = $value;
}
}
@ -72,14 +81,11 @@ trait CompanySettingsSaver
$settings = (object)$settings;
$casts = CompanySettings::$casts;
// if(property_exists($settings, 'pdf_variables'))
// unset($settings->pdf_variables);
ksort($casts);
foreach ($casts as $key => $value) {
if (in_array($key, SettingsSaver::$string_casts)) {
if (in_array($key, CompanySettings::$string_casts)) {
$value = "string";
if (!property_exists($settings, $key)) {
@ -109,8 +115,6 @@ trait CompanySettingsSaver
if (!property_exists($settings, $key) || is_null($settings->{$key}) || !isset($settings->{$key}) || $settings->{$key} == '') {
continue;
}
/*Catch all filter */
if (!$this->checkAttribute($value, $settings->{$key})) {
@ -139,7 +143,7 @@ trait CompanySettingsSaver
$casts = CompanySettings::$casts;
foreach ($casts as $key => $value) {
if (in_array($key, SettingsSaver::$string_casts)) {
if (in_array($key, CompanySettings::$string_casts)) {
$value = "string";
if (!property_exists($settings, $key)) {
@ -230,4 +234,12 @@ trait CompanySettingsSaver
return false;
}
}
private function getAccountFromEntity($entity)
{
if($entity instanceof Company)
return $entity->account;
return $entity->company->account;
}
}

View File

@ -19,43 +19,6 @@ use App\DataMapper\CompanySettings;
*/
trait SettingsSaver
{
public static $string_casts = [
'invoice_design_id',
'quote_design_id',
'credit_design_id',
];
/**
* Saves a setting object
*
* Works for groups|clients|companies
* @param array $settings The request input settings array
* @param object $entity The entity which the settings belongs to
* @return void
*/
public function saveSettings($settings, $entity)
{
if (!$settings) {
return;
}
$entity_settings = $this->settings;
//unset protected properties.
foreach (CompanySettings::$protected_fields as $field) {
unset($settings[$field]);
}
$settings = $this->checkSettingType($settings);
//iterate through set properties with new values;
foreach ($settings as $key => $value) {
$entity_settings->{$key} = $value;
}
$entity->settings = $entity_settings;
$entity->save();
}
/**
* Used for custom validation of inbound
* settings request.
@ -70,13 +33,10 @@ trait SettingsSaver
$settings = (object)$settings;
$casts = CompanySettings::$casts;
// if(property_exists($settings, 'pdf_variables'))
// unset($settings->pdf_variables);
ksort($casts);
foreach ($casts as $key => $value) {
if (in_array($key, self::$string_casts)) {
if (in_array($key, CompanySettings::$string_casts)) {
$value = "string";
if (!property_exists($settings, $key)) {
continue;
@ -115,68 +75,6 @@ trait SettingsSaver
return true;
}
/**
* Checks the settings object for
* correct property types.
*
* The method will drop invalid types from
* the object and will also settype() the property
* so that it can be saved cleanly
*
* @param array $settings The settings request() array
* @return object stdClass object
*/
private function checkSettingType($settings) : \stdClass
{
$settings = (object)$settings;
/* Because of the object casting we cannot check pdf_variables */
if (property_exists($settings, 'pdf_variables')) {
unset($settings->pdf_variables);
}
$casts = CompanySettings::$casts;
foreach ($casts as $key => $value) {
if (substr($key, -3) == '_id' || substr($key, -14) == 'number_counter') {
$value = "integer";
if (!property_exists($settings, $key)) {
continue;
} elseif ($this->checkAttribute($value, $settings->{$key})) {
if (substr($key, -3) == '_id') {
settype($settings->{$key}, 'string');
} else {
settype($settings->{$key}, $value);
}
} else {
unset($settings->{$key});
}
continue;
}
/* Handles unset settings or blank strings */
if (!property_exists($settings, $key) || is_null($settings->{$key}) || !isset($settings->{$key}) || $settings->{$key} == '') {
continue;
}
/*Catch all filter */
if ($this->checkAttribute($value, $settings->{$key})) {
if ($value == 'string' && is_null($settings->{$key})) {
$settings->{$key} = '';
}
settype($settings->{$key}, $value);
} else {
unset($settings->{$key});
}
}
return $settings;
}
/**
* Type checks a object property.
* @param string $key The type

View File

@ -134,4 +134,16 @@ return [
'global' => 'ninja2020',
'portal' => 'ninja2020',
],
'quotas' => [
'free' => [
'clients' => 50,
'daily_emails' => 50,
],
'pro' => [
'daily_emails' => 100,
],
'enterprise' => [
'daily_emails' => 200,
]
]
];

View File

@ -144,7 +144,8 @@ return [
'log' => 1,
'reset' => false,
// etc.
] ],
]
],
],
],

View File

@ -0,0 +1,52 @@
<?php
namespace Tests\Unit;
use App\DataMapper\CompanySettings;
use Tests\TestCase;
/**
* @test
* @covers App\Http\Requests\Company\UpdateCompanyRequest
*/
class CompanySettingsSaveableTest extends TestCase
{
public function setUp() :void
{
parent::setUp();
}
public function testSettingsSaverWithFreePlan()
{
$filtered = $this->filterSaver(CompanySettings::defaults());
$this->assertTrue(property_exists($filtered, 'timezone_id'));
$this->assertTrue(property_exists(CompanySettings::defaults(), 'timezone_id'));
$this->assertTrue(property_exists(CompanySettings::defaults(), 'auto_archive_invoice'));
$this->assertFalse(property_exists($filtered, 'auto_archive_invoice'));
}
private function filterSaver($settings)
{
$saveable_cast = CompanySettings::$free_plan_casts;
foreach($settings as $key => $value){
if(!array_key_exists($key, $saveable_cast))
unset($settings->{$key});
}
return $settings;
}
}