Enhancements to API (#3088)

* working on email throttling

* Fixes for invitaiton links

* pass custom fields as object

* Add user agent to company token

* Update company token transformer

* Remove prefix setting from CompanySettings

* Implement user agent on company token & provide better error handling for undefined relationships includes

* Fix bulk actions

* Working on updating/creating a company user

* Fixes for tests
This commit is contained in:
David Bomba 2019-11-21 19:38:57 +11:00 committed by GitHub
parent 170340cdfa
commit 69efd4d574
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 186 additions and 121 deletions

View File

@ -71,37 +71,28 @@ class CompanySettings extends BaseSettings
/* Counters */ /* Counters */
public $invoice_number_pattern = ''; public $invoice_number_pattern = '';
public $invoice_number_counter = 1; public $invoice_number_counter = 1;
public $invoice_number_prefix = '';
public $quote_number_prefix = '';
public $quote_number_pattern = ''; public $quote_number_pattern = '';
public $quote_number_counter = 1; public $quote_number_counter = 1;
public $client_number_prefix = '';
public $client_number_pattern = ''; public $client_number_pattern = '';
public $client_number_counter = 1; public $client_number_counter = 1;
public $credit_number_prefix = '';
public $credit_number_pattern = ''; public $credit_number_pattern = '';
public $credit_number_counter = 1; public $credit_number_counter = 1;
public $task_number_prefix = '';
public $task_number_pattern = ''; public $task_number_pattern = '';
public $task_number_counter = 1; public $task_number_counter = 1;
public $expense_number_prefix = '';
public $expense_number_pattern = ''; public $expense_number_pattern = '';
public $expense_number_counter = 1; public $expense_number_counter = 1;
public $vendor_number_prefix = '';
public $vendor_number_pattern = ''; public $vendor_number_pattern = '';
public $vendor_number_counter = 1; public $vendor_number_counter = 1;
public $ticket_number_prefix = '';
public $ticket_number_pattern = ''; public $ticket_number_pattern = '';
public $ticket_number_counter = 1; public $ticket_number_counter = 1;
public $payment_number_prefix = '';
public $payment_number_pattern = ''; public $payment_number_pattern = '';
public $payment_number_counter = 1; public $payment_number_counter = 1;
@ -260,19 +251,14 @@ class CompanySettings extends BaseSettings
'embed_documents' => 'bool', 'embed_documents' => 'bool',
'all_pages_header' => 'bool', 'all_pages_header' => 'bool',
'all_pages_footer' => 'bool', 'all_pages_footer' => 'bool',
'task_number_prefix' => 'string',
'task_number_pattern' => 'string', 'task_number_pattern' => 'string',
'task_number_counter' => 'int', 'task_number_counter' => 'int',
'expense_number_prefix' => 'string',
'expense_number_pattern' => 'string', 'expense_number_pattern' => 'string',
'expense_number_counter' => 'int', 'expense_number_counter' => 'int',
'vendor_number_prefix' => 'string',
'vendor_number_pattern' => 'string', 'vendor_number_pattern' => 'string',
'vendor_number_counter' => 'int', 'vendor_number_counter' => 'int',
'ticket_number_prefix' => 'string',
'ticket_number_pattern' => 'string', 'ticket_number_pattern' => 'string',
'ticket_number_counter' => 'int', 'ticket_number_counter' => 'int',
'payment_number_prefix' => 'string',
'payment_number_pattern' => 'string', 'payment_number_pattern' => 'string',
'payment_number_counter' => 'int', 'payment_number_counter' => 'int',
'reply_to_email' => 'string', 'reply_to_email' => 'string',
@ -287,10 +273,8 @@ class CompanySettings extends BaseSettings
'city' => 'string', 'city' => 'string',
'company_logo' => 'string', 'company_logo' => 'string',
'country_id' => 'string', 'country_id' => 'string',
'client_number_prefix' => 'string',
'client_number_pattern' => 'string', 'client_number_pattern' => 'string',
'client_number_counter' => 'integer', 'client_number_counter' => 'integer',
'credit_number_prefix' => 'string',
'credit_number_pattern' => 'string', 'credit_number_pattern' => 'string',
'credit_number_counter' => 'integer', 'credit_number_counter' => 'integer',
'currency_id' => 'string', 'currency_id' => 'string',
@ -320,7 +304,6 @@ class CompanySettings extends BaseSettings
'email_template_reminder_endless' => 'string', 'email_template_reminder_endless' => 'string',
'enable_client_portal_password' => 'bool', 'enable_client_portal_password' => 'bool',
'inclusive_taxes' => 'bool', 'inclusive_taxes' => 'bool',
'invoice_number_prefix' => 'string',
'invoice_number_pattern' => 'string', 'invoice_number_pattern' => 'string',
'invoice_number_counter' => 'integer', 'invoice_number_counter' => 'integer',
'invoice_design_id' => 'string', 'invoice_design_id' => 'string',
@ -336,7 +319,6 @@ class CompanySettings extends BaseSettings
'phone' => 'string', 'phone' => 'string',
'postal_code' => 'string', 'postal_code' => 'string',
'quote_design_id' => 'string', 'quote_design_id' => 'string',
'quote_number_prefix' => 'string',
'quote_number_pattern' => 'string', 'quote_number_pattern' => 'string',
'quote_number_counter' => 'integer', 'quote_number_counter' => 'integer',
'quote_terms' => 'string', 'quote_terms' => 'string',

View File

@ -22,6 +22,7 @@ use Illuminate\Validation\ValidationException;
use Symfony\Component\Debug\Exception\FatalThrowableError; use Symfony\Component\Debug\Exception\FatalThrowableError;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException; use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Illuminate\Database\Eloquent\RelationNotFoundException;
class Handler extends ExceptionHandler class Handler extends ExceptionHandler
{ {
@ -70,15 +71,15 @@ class Handler extends ExceptionHandler
public function render($request, Exception $exception) public function render($request, Exception $exception)
{ {
if ($exception instanceof ModelNotFoundException) if ($exception instanceof ModelNotFoundException && $request->expectsJson())
{ {
return response()->json(['message'=>'Record not found'],400); return response()->json(['message'=>'Record not found'],400);
} }
else if($exception instanceof ThrottleRequestsException) else if($exception instanceof ThrottleRequestsException && $request->expectsJson())
{ {
return response()->json(['message'=>'Too many requests'],429); return response()->json(['message'=>'Too many requests'],429);
} }
else if($exception instanceof FatalThrowableError) else if($exception instanceof FatalThrowableError && $request->expectsJson())
{ {
return response()->json(['message'=>'Fatal error'], 500); return response()->json(['message'=>'Fatal error'], 500);
} }
@ -95,15 +96,18 @@ class Handler extends ExceptionHandler
'message' => ctrans('texts.token_expired'), 'message' => ctrans('texts.token_expired'),
'message-type' => 'danger']); 'message-type' => 'danger']);
} }
else if ($exception instanceof NotFoundHttpException) { else if ($exception instanceof NotFoundHttpException && $request->expectsJson()) {
return response()->json(['message'=>'Route does not exist'],404); return response()->json(['message'=>'Route does not exist'],404);
} }
else if($exception instanceof MethodNotAllowedHttpException){ else if($exception instanceof MethodNotAllowedHttpException && $request->expectsJson()){
return response()->json(['message'=>'Method not support for this route'],404); return response()->json(['message'=>'Method not support for this route'],404);
} }
else if ($exception instanceof ValidationException && $request->expectsJson()) { else if ($exception instanceof ValidationException && $request->expectsJson()) {
return response()->json(['message' => 'The given data was invalid.', 'errors' => $exception->validator->getMessageBag()], 422); return response()->json(['message' => 'The given data was invalid.', 'errors' => $exception->validator->getMessageBag()], 422);
} }
else if ($exception instanceof RelationNotFoundException && $request->expectsJson()) {
return response()->json(['message' => $exception->getMessage()], 400);
}
return parent::render($request, $exception); return parent::render($request, $exception);

View File

@ -28,6 +28,7 @@ class CompanyFactory
$company->company_key = $this->createHash(); $company->company_key = $this->createHash();
$company->settings = CompanySettings::defaults(); $company->settings = CompanySettings::defaults();
$company->db = config('database.default'); $company->db = config('database.default');
$company->custom_fields = (object) ['custom1' => '1', 'custom2' => '2', 'custom3'=>3];
$company->domain = ''; $company->domain = '';
return $company; return $company;

View File

@ -0,0 +1,30 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2019. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Factory;
use App\Models\CompanyUser;
class CompanyUserFactory
{
public static function create($user_id, $company_id, $account_id) :CompanyUser
{
$company_user = new CompanyUser;
$company_user->user_id = $user_id;
$company_user->company_id = $company_id;
$company_user->account_id = $account_id;
return $company_user;
}
}

View File

@ -139,6 +139,7 @@ class AccountController extends BaseController
$account = CreateAccount::dispatchNow($request->all()); $account = CreateAccount::dispatchNow($request->all());
$ct = CompanyUser::whereUserId(auth()->user()->id); $ct = CompanyUser::whereUserId(auth()->user()->id);
return $this->listResponse($ct); return $this->listResponse($ct);
} }

View File

@ -32,7 +32,7 @@ class InvitationController extends Controller
public function router(string $entity, string $invitation_key) public function router(string $entity, string $invitation_key)
{ {
$key = $entity.'_id'; $key = $entity.'_id';
$entity_obj = ucfirst($entity).'Invitation'; $entity_obj = 'App\Models\\'.ucfirst($entity).'Invitation';
$invitation = $entity_obj::whereRaw("BINARY `key`= ?", [$invitation_key])->first(); $invitation = $entity_obj::whereRaw("BINARY `key`= ?", [$invitation_key])->first();
@ -40,6 +40,8 @@ class InvitationController extends Controller
if((bool)$invitation->contact->client->getSetting('enable_client_portal_password') !== false) if((bool)$invitation->contact->client->getSetting('enable_client_portal_password') !== false)
$this->middleware('auth:contact'); $this->middleware('auth:contact');
else
auth()->guard('contact')->login($invitation->contact, false);
$invitation->markViewed(); $invitation->markViewed();

View File

@ -228,7 +228,7 @@ class CompanyController extends BaseController
/* /*
* Create token * Create token
*/ */
$company_token = CreateCompanyToken::dispatchNow($company, auth()->user()); $company_token = CreateCompanyToken::dispatchNow($company, auth()->user(), request()->server('HTTP_USER_AGENT'));
$this->entity_transformer = CompanyUserTransformer::class; $this->entity_transformer = CompanyUserTransformer::class;
$this->entity_type = CompanyUser::class; $this->entity_type = CompanyUser::class;

View File

@ -519,7 +519,7 @@ class InvoiceController extends BaseController
$ids = request()->input('ids'); $ids = request()->input('ids');
$invoices = Invoice::withTrashed()->find($ids); $invoices = Invoice::withTrashed()->find($this->transformKeys($ids));
$invoices->each(function ($invoice, $key) use($action){ $invoices->each(function ($invoice, $key) use($action){
@ -528,8 +528,7 @@ class InvoiceController extends BaseController
}); });
//todo need to return the updated dataset return $this->listResponse(Invoice::withTrashed()->whereIn('id', $this->transformKeys($ids)));
return $this->listResponse(Invoice::withTrashed()->whereIn('id', $ids));
} }

View File

@ -7,9 +7,11 @@
* @OA\Property(property="size_id", type="string", example="1", description="The company size ID"), * @OA\Property(property="size_id", type="string", example="1", description="The company size ID"),
* @OA\Property(property="industry_id", type="string", example="1", description="The company industry ID"), * @OA\Property(property="industry_id", type="string", example="1", description="The company industry ID"),
* @OA\Property(property="portal_mode", type="string", example="subdomain", description="Determines the client facing urls ie: subdomain,domain,iframe"), * @OA\Property(property="portal_mode", type="string", example="subdomain", description="Determines the client facing urls ie: subdomain,domain,iframe"),
* @OA\Property(property="domain", type="string", example="http://acmeco.invoicing.co", description="Determines the client facing url "),
* @OA\Property(property="portal_domain", type="string", example="https://subdomain.invoicing.co", description="The fully qualified domain for client facing URLS"), * @OA\Property(property="portal_domain", type="string", example="https://subdomain.invoicing.co", description="The fully qualified domain for client facing URLS"),
* @OA\Property(property="enabled_tax_rates", type="integer", example="1", description="Number of taxes rates used per entity"), * @OA\Property(property="enabled_tax_rates", type="integer", example="1", description="Number of taxes rates used per entity"),
* @OA\Property(property="fill_products", type="boolean", example=true, description="Toggles filling a product description based on product key"), * @OA\Property(property="fill_products", type="boolean", example=true, description="Toggles filling a product description based on product key"),
* @OA\Property(property="enable_invoice_quantity", type="boolean", example=true, description="Toggles filling a product description based on product key"),
* @OA\Property(property="convert_products", type="boolean", example=true, description="___________"), * @OA\Property(property="convert_products", type="boolean", example=true, description="___________"),
* @OA\Property(property="update_products", type="boolean", example=true, description="Toggles updating a product description which description changes"), * @OA\Property(property="update_products", type="boolean", example=true, description="Toggles updating a product description which description changes"),
* @OA\Property(property="custom_fields", type="object", description="Custom fields map"), * @OA\Property(property="custom_fields", type="object", description="Custom fields map"),

View File

@ -35,32 +35,23 @@
* @OA\Property(property="auto_convert_quote", type="boolean", example=true, description="____________"), * @OA\Property(property="auto_convert_quote", type="boolean", example=true, description="____________"),
* @OA\Property(property="inclusive_taxes", type="boolean", example=true, description="____________"), * @OA\Property(property="inclusive_taxes", type="boolean", example=true, description="____________"),
* @OA\Property(property="translations", type="object", example="", description="JSON payload of customized translations"), * @OA\Property(property="translations", type="object", example="", description="JSON payload of customized translations"),
* @OA\Property(property="task_number_prefix", type="string", example="R", description="This string is prepended to the task number"),
* @OA\Property(property="task_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the task number pattern"), * @OA\Property(property="task_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the task number pattern"),
* @OA\Property(property="task_number_counter", type="integer", example="1", description="____________"), * @OA\Property(property="task_number_counter", type="integer", example="1", description="____________"),
* @OA\Property(property="expense_number_prefix", type="string", example="R", description="This string is prepended to the expense number"),
* @OA\Property(property="expense_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the expense number pattern"), * @OA\Property(property="expense_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the expense number pattern"),
* @OA\Property(property="expense_number_counter", type="integer", example="1", description="____________"), * @OA\Property(property="expense_number_counter", type="integer", example="1", description="____________"),
* @OA\Property(property="vendor_number_prefix", type="string", example="R", description="This string is prepended to the vendor number"),
* @OA\Property(property="vendor_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the vendor number pattern"), * @OA\Property(property="vendor_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the vendor number pattern"),
* @OA\Property(property="vendor_number_counter", type="integer", example="1", description="____________"), * @OA\Property(property="vendor_number_counter", type="integer", example="1", description="____________"),
* @OA\Property(property="ticket_number_prefix", type="string", example="R", description="This string is prepended to the ticket number"),
* @OA\Property(property="ticket_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the ticket number pattern"), * @OA\Property(property="ticket_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the ticket number pattern"),
* @OA\Property(property="ticket_number_counter", type="integer", example="1", description="____________"), * @OA\Property(property="ticket_number_counter", type="integer", example="1", description="____________"),
* *
* @OA\Property(property="payment_number_prefix", type="string", example="R", description="This string is prepended to the payment number"),
* @OA\Property(property="payment_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the payment number pattern"), * @OA\Property(property="payment_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the payment number pattern"),
* @OA\Property(property="payment_number_counter", type="integer", example="1", description="____________"), * @OA\Property(property="payment_number_counter", type="integer", example="1", description="____________"),
* @OA\Property(property="invoice_number_prefix", type="string", example="R", description="This string is prepended to the invoice number"),
* @OA\Property(property="invoice_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the invoice number pattern"), * @OA\Property(property="invoice_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the invoice number pattern"),
* @OA\Property(property="invoice_number_counter", type="integer", example="1", description="____________"), * @OA\Property(property="invoice_number_counter", type="integer", example="1", description="____________"),
* @OA\Property(property="quote_number_prefix", type="string", example="R", description="This string is prepended to the quote number"),
* @OA\Property(property="quote_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the quote number pattern"), * @OA\Property(property="quote_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the quote number pattern"),
* @OA\Property(property="quote_number_counter", type="integer", example="1", description="____________"), * @OA\Property(property="quote_number_counter", type="integer", example="1", description="____________"),
* @OA\Property(property="client_number_prefix", type="string", example="R", description="This string is prepended to the client number"),
* @OA\Property(property="client_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the client number pattern"), * @OA\Property(property="client_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the client number pattern"),
* @OA\Property(property="client_number_counter", type="integer", example="1", description="____________"), * @OA\Property(property="client_number_counter", type="integer", example="1", description="____________"),
* @OA\Property(property="credit_number_prefix", type="string", example="R", description="This string is prepended to the credit number"),
* @OA\Property(property="credit_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the credit number pattern"), * @OA\Property(property="credit_number_pattern", type="string", example="{$year}-{$counter}", description="Allows customisation of the credit number pattern"),
* @OA\Property(property="credit_number_counter", type="integer", example="1", description="____________"), * @OA\Property(property="credit_number_counter", type="integer", example="1", description="____________"),
* @OA\Property(property="recurring_invoice_number_prefix", type="string", example="R", description="This string is prepended to the recurring invoice number"), * @OA\Property(property="recurring_invoice_number_prefix", type="string", example="R", description="This string is prepended to the recurring invoice number"),

View File

@ -554,7 +554,7 @@ class PaymentController extends BaseController
$ids = request()->input('ids'); $ids = request()->input('ids');
$payments = Payment::withTrashed()->find($ids); $payments = Payment::withTrashed()->find($this->transformKeys($ids));
$payments->each(function ($payment, $key) use($action){ $payments->each(function ($payment, $key) use($action){
@ -563,8 +563,7 @@ class PaymentController extends BaseController
}); });
//todo need to return the updated dataset return $this->listResponse(Payment::withTrashed()->whereIn('id', $this->transformKeys($ids)));
return $this->listResponse(Payment::withTrashed()->whereIn('id', $ids));
} }

View File

@ -470,17 +470,16 @@ class ProductController extends BaseController
$ids = request()->input('ids'); $ids = request()->input('ids');
$products = Product::withTrashed()->find($ids); $products = Product::withTrashed()->find($this->transformKeys($ids));
$products->each(function ($product, $key) use($action){ $products->each(function ($product, $key) use($action){
if(auth()->user()->can('edit', $product)) if(auth()->user()->can('edit', $product))
ActionEntity::dispatchNow($product, $action); $this->product_repo->{$action}($product);
}); });
//todo need to return the updated dataset return $this->listResponse(Product::withTrashed()->whereIn('id', $this->transformKeys($ids)));
return response()->json([], 200);
} }
} }

View File

@ -507,17 +507,16 @@ class QuoteController extends BaseController
$ids = request()->input('ids'); $ids = request()->input('ids');
$quotes = Quote::withTrashed()->find($ids); $quotes = Quote::withTrashed()->find($this->transformKeys($ids));
$quotes->each(function ($quote, $key) use($action){ $quotes->each(function ($quote, $key) use($action){
if(auth()->user()->can('edit', $quote)) if(auth()->user()->can('edit', $quote))
$this->quote_repo->{$action}($quote); $this->product_repo->{$action}($quote);
}); });
//todo need to return the updated dataset return $this->listResponse(Quote::withTrashed()->whereIn('id', $this->transformKeys($ids)));
return $this->listResponse(Quote::withTrashed()->whereIn('id', $ids));
} }

View File

@ -517,7 +517,7 @@ class RecurringInvoiceController extends BaseController
$ids = request()->input('ids'); $ids = request()->input('ids');
$recurring_invoices = RecurringInvoice::withTrashed()->find($ids); $recurring_invoices = RecurringInvoice::withTrashed()->find($this->transformKeys($ids));
$recurring_invoices->each(function ($recurring_invoice, $key) use($action){ $recurring_invoices->each(function ($recurring_invoice, $key) use($action){
@ -526,8 +526,7 @@ class RecurringInvoiceController extends BaseController
}); });
//todo need to return the updated dataset return $this->listResponse(RecurringInvoice::withTrashed()->whereIn('id', $this->transformKeys($ids)));
return $this->listResponse(RecurringInvoice::withTrashed()->whereIn('id', $ids));
} }
@ -610,7 +609,7 @@ class RecurringInvoiceController extends BaseController
// return $this->itemResponse($recurring_invoice); // return $this->itemResponse($recurring_invoice);
break; break;
case 'clone_to_quote': case 'clone_to_quote':
// $quote = CloneRecurringInvoiceToQuoteFactory::create($recurring_invoice, auth()->user()->id); // $recurring_invoice = CloneRecurringInvoiceToQuoteFactory::create($recurring_invoice, auth()->user()->id);
// todo build the quote transformer and return response here // todo build the quote transformer and return response here
break; break;
case 'history': case 'history':

View File

@ -515,7 +515,7 @@ class RecurringQuoteController extends BaseController
$ids = request()->input('ids'); $ids = request()->input('ids');
$recurring_quotes = RecurringQuote::withTrashed()->find($ids); $recurring_quotes = RecurringQuote::withTrashed()->find($this->transformKeys($ids));
$recurring_quotes->each(function ($recurring_quote, $key) use($action){ $recurring_quotes->each(function ($recurring_quote, $key) use($action){
@ -524,8 +524,7 @@ class RecurringQuoteController extends BaseController
}); });
//todo need to return the updated dataset return $this->listResponse(RecurringQuote::withTrashed()->whereIn('id', $this->transformKeys($ids)));
return $this->listResponse(RecurringQuote::withTrashed()->whereIn('id', $ids));
} }

View File

@ -213,7 +213,7 @@ class UserController extends BaseController
'settings' => $request->input('settings'), 'settings' => $request->input('settings'),
]); ]);
CreateCompanyToken::dispatchNow($company, $user); CreateCompanyToken::dispatchNow($company, $user, request()->server('HTTP_USER_AGENT'));
$user->load('companies'); $user->load('companies');
@ -514,9 +514,7 @@ class UserController extends BaseController
$ids = request()->input('ids'); $ids = request()->input('ids');
$ids = $this->transformKeys($ids); $users = User::withTrashed()->find($this->transformKeys($ids));
$users = User::withTrashed()->find($ids);
$users->each(function ($user, $key) use($action){ $users->each(function ($user, $key) use($action){
@ -525,8 +523,7 @@ class UserController extends BaseController
}); });
//todo need to return the updated dataset return $this->listResponse(User::withTrashed()->whereIn('id', $this->transformKeys($ids)));
return $this->listResponse(User::withTrashed()->whereIn('id', $ids));
} }

View File

@ -27,7 +27,7 @@ class StoreUserRequest extends Request
public function authorize() : bool public function authorize() : bool
{ {
return auth()->user()->can('create', User::class); return auth()->user()->isAdmin();
} }
@ -40,7 +40,6 @@ class StoreUserRequest extends Request
'first_name' => 'required|string|max:100', 'first_name' => 'required|string|max:100',
'last_name' => 'required|string:max:100', 'last_name' => 'required|string:max:100',
'email' => new NewUniqueUserRule(), 'email' => new NewUniqueUserRule(),
'is_admin' => 'required',
]; ];
} }
@ -49,16 +48,34 @@ class StoreUserRequest extends Request
{ {
$input = $this->all(); $input = $this->all();
if(!isset($input['is_admin'])) if(isset($input['company_user']))
$input['is_admin'] = null; {
if(!isset($input['permissions'])) if(!isset($input['company_user']['permissions']))
$input['permissions'] = json_encode([]); $input['company_user']['permissions'] = '';
if(!isset($input['settings'])) if(!isset($input['company_user']['settings']))
$input['settings'] = json_encode(DefaultSettings::userSettings()); $input['company_user']['settings'] = json_encode(DefaultSettings::userSettings());
}
else{
$input['company_user'] = [
'settings' => json_encode(DefaultSettings::userSettings()),
'permissions' => '',
];
}
$this->replace($input); $this->replace($input);
return $this->all();
}
public function messages()
{
return [
'company_user' => 'T',
]
} }

View File

@ -80,7 +80,7 @@ class CreateAccount
/* /*
* Create token * Create token
*/ */
$company_token = CreateCompanyToken::dispatchNow($company, $user); $company_token = CreateCompanyToken::dispatchNow($company, $user, $this->request['user_agent']);
/* /*
* Login user * Login user
*/ */

View File

@ -29,16 +29,19 @@ class CreateCompanyToken implements ShouldQueue
protected $user; protected $user;
protected $user_agent;
/** /**
* Create a new job instance. * Create a new job instance.
* *
* @return void * @return void
*/ */
public function __construct(Company $company, User $user) public function __construct(Company $company, User $user, string $user_agent)
{ {
$this->company = $company; $this->company = $company;
$this->user = $user; $this->user = $user;
$this->user_agent = $user_agent;
} }
/** /**
@ -55,6 +58,7 @@ class CreateCompanyToken implements ShouldQueue
'token' => Str::random(64), 'token' => Str::random(64),
'name' => $this->user->first_name. ' '. $this->user->last_name, 'name' => $this->user->first_name. ' '. $this->user->last_name,
'company_id' => $this->company->id, 'company_id' => $this->company->id,
'user_agent' => $this->user_agent,
]); ]);
return $ct; return $ct;

View File

@ -70,7 +70,7 @@ class CreateUser
'is_owner' => $this->company_owner, 'is_owner' => $this->company_owner,
'is_admin' => 1, 'is_admin' => 1,
'is_locked' => 0, 'is_locked' => 0,
'permissions' => json_encode([]), 'permissions' => '',
'settings' => json_encode(DefaultSettings::userSettings()), 'settings' => json_encode(DefaultSettings::userSettings()),
]); ]);

View File

@ -178,11 +178,12 @@ class MultiDB
public static function findAndSetDbByInvitation($entity, $invitation_key) public static function findAndSetDbByInvitation($entity, $invitation_key)
{ {
$entity.'Invitation';
$class = 'App\Models\\'.ucfirst($entity).'Invitation';
foreach (self::$dbs as $db) foreach (self::$dbs as $db)
{ {
if($invite = $entity::on($db)->whereKey($invitation_key)->first()) if($invite = $class::on($db)->whereRaw("BINARY `key`= ?",[$invitation_key])->first())
{ {
self::setDb($db); self::setDb($db);
return true; return true;

View File

@ -59,6 +59,7 @@ class Client extends BaseModel
]; ];
protected $fillable = [ protected $fillable = [
'currency_id',
'name', 'name',
'website', 'website',
'private_notes', 'private_notes',

View File

@ -53,6 +53,17 @@ class Company extends BaseModel
'enable_product_cost', 'enable_product_cost',
'enable_product_quantity', 'enable_product_quantity',
'default_quantity', 'default_quantity',
'enable_invoice_quantity',
'enabled_tax_rates',
'portal_mode',
'portal_domain',
'convert_products',
'update_products',
'custom_surcharge_taxes1',
'custom_surcharge_taxes2',
'custom_surcharge_taxes3',
'custom_surcharge_taxes4',
]; ];
protected $hidden = [ protected $hidden = [

View File

@ -23,12 +23,20 @@ class CompanyUser extends Pivot
* @var array * @var array
*/ */
protected $casts = [ protected $casts = [
'permissions' => 'object',
'updated_at' => 'timestamp', 'updated_at' => 'timestamp',
'created_at' => 'timestamp', 'created_at' => 'timestamp',
'deleted_at' => 'timestamp', 'deleted_at' => 'timestamp',
]; ];
protected $fillable = [
'account_id',
'permissions',
'settings',
'is_admin',
'is_owner',
'is_locked'
];
public function account() public function account()
{ {
return $this->belongsTo(Account::class); return $this->belongsTo(Account::class);

View File

@ -73,7 +73,7 @@ class ClientRepository extends BaseRepository
$contacts = $this->contact_repo->save($data['contacts'], $client); $contacts = $this->contact_repo->save($data['contacts'], $client);
if($data['name'] == '') if(empty($data['name']))
$data['name'] = $client->present()->name(); $data['name'] = $client->present()->name();

View File

@ -12,6 +12,8 @@
namespace App\Repositories; namespace App\Repositories;
use App\Models\User; use App\Models\User;
use App\Models\CompanyUser;
use App\Factory\CompanyUserFactory;
use Illuminate\Http\Request; use Illuminate\Http\Request;
/** /**
@ -50,9 +52,24 @@ class UserRepository extends BaseRepository
{ {
$user->fill($data); $user->fill($data);
$user->save(); $user->save();
if($data['company_user'])
{
$company = auth()->user()->company();
$account_id = $company->account->id;
$cu = CompanyUser::whereUserId($user->id)->whereCompanyId($company->id)->first();
if(!$cu)
$cu = CompanyUserFactory::create($user->id, $company->id, $account_id);
$cu->fill($data['company_user']);
$cu->save();
}
return $user; return $user;
} }

View File

@ -44,6 +44,7 @@ class CompanyTokenTransformer extends EntityTransformer
return [ return [
'token' => $company_token->token, 'token' => $company_token->token,
'name' => $company_token->name ?: '', 'name' => $company_token->name ?: '',
'user_agent' => $company_token->user_agent ?: 'Unidentified',
]; ];
} }

View File

@ -68,6 +68,8 @@ class CompanyTransformer extends EntityTransformer
*/ */
public function transform(Company $company) public function transform(Company $company)
{ {
$std = new \stdClass;
return [ return [
'id' => (string)$this->encodePrimaryKey($company->id), 'id' => (string)$this->encodePrimaryKey($company->id),
'company_key' => (string)$company->company_key ?: '', 'company_key' => (string)$company->company_key ?: '',
@ -81,7 +83,7 @@ class CompanyTransformer extends EntityTransformer
'enable_product_cost' => (bool)$company->enable_product_cost, 'enable_product_cost' => (bool)$company->enable_product_cost,
'enable_product_quantity' => (bool)$company->enable_product_quantity, 'enable_product_quantity' => (bool)$company->enable_product_quantity,
'default_quantity' => (bool)$company->default_quantity, 'default_quantity' => (bool)$company->default_quantity,
'custom_fields' => (string) $company->custom_fields, 'custom_fields' => $company->custom_fields ?: $std,
'size_id' => (string) $company->size_id ?: '', 'size_id' => (string) $company->size_id ?: '',
'industry_id' => (string) $company->industry_id ?: '', 'industry_id' => (string) $company->industry_id ?: '',
'first_month_of_year' => (string) $company->first_month_of_year ?: '', 'first_month_of_year' => (string) $company->first_month_of_year ?: '',

View File

@ -61,10 +61,9 @@ trait GeneratesCounter
//Return a valid counter //Return a valid counter
$pattern = $client->getSetting('invoice_number_pattern'); $pattern = $client->getSetting('invoice_number_pattern');
$prefix = $client->getSetting('invoice_number_prefix');
$padding = $client->getSetting('counter_padding'); $padding = $client->getSetting('counter_padding');
$invoice_number = $this->checkEntityNumber(Invoice::class, $client, $counter, $padding, $prefix, $pattern); $invoice_number = $this->checkEntityNumber(Invoice::class, $client, $counter, $padding, $pattern);
$this->incrementCounter($counter_entity, 'invoice_number_counter'); $this->incrementCounter($counter_entity, 'invoice_number_counter');
@ -126,9 +125,9 @@ trait GeneratesCounter
//Return a valid counter //Return a valid counter
$pattern = ''; $pattern = '';
$prefix = $client->company->settings->recurring_invoice_number_prefix; $padding = $client->getSetting('counter_padding');
$padding = $client->company->settings->counter_padding; $invoice_number = $this->checkEntityNumber(Invoice::class, $client, $counter, $padding, $pattern);
$invoice_number = $this->checkEntityNumber(Invoice::class, $client, $counter, $padding, $prefix, $pattern); $invoice_number = $this->prefixCounter($invoice_number, $client->getSetting('recurring_number_prefix'));
//increment the correct invoice_number Counter (company vs client) //increment the correct invoice_number Counter (company vs client)
if($is_client_counter) if($is_client_counter)
@ -155,7 +154,7 @@ trait GeneratesCounter
$counter = $client->getSetting('client_number_counter' ); $counter = $client->getSetting('client_number_counter' );
$setting_entity = $client->getSettingEntity('client_number_counter'); $setting_entity = $client->getSettingEntity('client_number_counter');
$client_number = $this->checkEntityNumber(Client::class, $client, $counter, $client->getSetting('counter_padding'), $client->getSetting('client_number_prefix'), $client->getSetting('client_number_pattern')); $client_number = $this->checkEntityNumber(Client::class, $client, $counter, $client->getSetting('counter_padding'), $client->getSetting('client_number_pattern'));
$this->incrementCounter($setting_entity, 'client_number_counter'); $this->incrementCounter($setting_entity, 'client_number_counter');
@ -183,11 +182,10 @@ trait GeneratesCounter
* @param Collection $entity The entity ie App\Models\Client, Invoice, Quote etc * @param Collection $entity The entity ie App\Models\Client, Invoice, Quote etc
* @param integer $counter The counter * @param integer $counter The counter
* @param integer $padding The padding * @param integer $padding The padding
* @param string $prefix The prefix
* *
* @return string The padded and prefixed invoice number * @return string The padded and prefixed invoice number
*/ */
private function checkEntityNumber($class, $client, $counter, $padding, $prefix, $pattern) private function checkEntityNumber($class, $client, $counter, $padding, $pattern)
{ {
$check = false; $check = false;
@ -195,9 +193,6 @@ trait GeneratesCounter
$number = $this->padCounter($counter, $padding); $number = $this->padCounter($counter, $padding);
if(isset($prefix) && strlen($prefix) >= 1)
$number = $this->prefixCounter($number, $prefix);
else
$number = $this->applyNumberPattern($client, $number, $pattern); $number = $this->applyNumberPattern($client, $number, $pattern);
if($class == Invoice::class || $class == RecurringInvoice::class) if($class == Invoice::class || $class == RecurringInvoice::class)

View File

@ -52,14 +52,14 @@ trait Inviteable
switch ($this->company->portal_mode) { switch ($this->company->portal_mode) {
case 'subdomain': case 'subdomain':
return $domain . $entity_type .'/'. $this->key; return $domain .'client/'. $entity_type .'/'. $this->key;
break; break;
case 'iframe': case 'iframe':
return $domain . $entity_type .'/'. $this->key; return $domain .'client/'. $entity_type .'/'. $this->key;
//return $domain . $entity_type .'/'. $this->contact->client->client_hash .'/'. $this->key; //return $domain . $entity_type .'/'. $this->contact->client->client_hash .'/'. $this->key;
break; break;
case 'domain': case 'domain':
return $domain . $entity_type .'/'. $this->key; return $domain .'client/'. $entity_type .'/'. $this->key;
break; break;
} }

View File

@ -10,6 +10,8 @@ $factory->define(App\Models\Company::class, function (Faker $faker) {
'ip' => $faker->ipv4, 'ip' => $faker->ipv4,
'db' => config('database.default'), 'db' => config('database.default'),
'settings' => CompanySettings::defaults(), 'settings' => CompanySettings::defaults(),
'custom_fields' => (object) ['custom1' => '1', 'custom2' => '2', 'custom3'=>3],
// 'address1' => $faker->secondaryAddress, // 'address1' => $faker->secondaryAddress,
// 'address2' => $faker->address, // 'address2' => $faker->address,
// 'city' => $faker->city, // 'city' => $faker->city,

View File

@ -152,6 +152,7 @@ class CreateUsersTable extends Migration
$table->boolean('custom_surcharge_taxes2')->default(false); $table->boolean('custom_surcharge_taxes2')->default(false);
$table->boolean('custom_surcharge_taxes3')->default(false); $table->boolean('custom_surcharge_taxes3')->default(false);
$table->boolean('custom_surcharge_taxes4')->default(false); $table->boolean('custom_surcharge_taxes4')->default(false);
$table->boolean('enable_invoice_quantity')->default(true);
$table->boolean('show_product_cost')->default(false); $table->boolean('show_product_cost')->default(false);
$table->unsignedInteger('enabled_tax_rates')->default(1); $table->unsignedInteger('enabled_tax_rates')->default(1);
@ -195,6 +196,7 @@ class CreateUsersTable extends Migration
$table->boolean('is_owner')->default(false); $table->boolean('is_owner')->default(false);
$table->boolean('is_admin')->default(false); $table->boolean('is_admin')->default(false);
$table->boolean('is_locked')->default(false); // locks user out of account $table->boolean('is_locked')->default(false); // locks user out of account
$table->timestamps(6);
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade'); $table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');
$table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');
@ -271,6 +273,7 @@ class CreateUsersTable extends Migration
$table->unsignedInteger('user_id'); $table->unsignedInteger('user_id');
$table->string('token')->nullable(); $table->string('token')->nullable();
$table->string('name')->nullable(); $table->string('name')->nullable();
$table->string('user_agent')->nullable();
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade'); $table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');
$table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade');

View File

@ -66,7 +66,7 @@ class PaymentLibrariesSeeder extends Seeder
['name' => 'GoCardless', 'provider' => 'GoCardlessV2\Redirect', 'sort_order' => 9, 'is_offsite' => true, 'key' => 'b9886f9257f0c6ee7c302f1c74475f6c', 'fields' => '{"accessToken":"","webhookSecret":"","testMode":true}'], ['name' => 'GoCardless', 'provider' => 'GoCardlessV2\Redirect', 'sort_order' => 9, 'is_offsite' => true, 'key' => 'b9886f9257f0c6ee7c302f1c74475f6c', 'fields' => '{"accessToken":"","webhookSecret":"","testMode":true}'],
['name' => 'PagSeguro', 'provider' => 'PagSeguro', 'key' => 'ef498756b54db63c143af0ec433da803', 'fields' => '{"email":"","token":"","sandbox":false}'], ['name' => 'PagSeguro', 'provider' => 'PagSeguro', 'key' => 'ef498756b54db63c143af0ec433da803', 'fields' => '{"email":"","token":"","sandbox":false}'],
['name' => 'PAYMILL', 'provider' => 'Paymill', 'key' => 'ca52f618a39367a4c944098ebf977e1c', 'fields' => '{"apiKey":""}'], ['name' => 'PAYMILL', 'provider' => 'Paymill', 'key' => 'ca52f618a39367a4c944098ebf977e1c', 'fields' => '{"apiKey":""}'],
['name' => 'Custom', 'provider' => 'Custom2', 'is_offsite' => true, 'sort_order' => 21, 'key' => '54faab2ab6e3223dbe848b1686490baa', 'fields' => '{"text":"","name":""}'], ['name' => 'Custom', 'provider' => 'Custom', 'is_offsite' => true, 'sort_order' => 21, 'key' => '54faab2ab6e3223dbe848b1686490baa', 'fields' => '{"name":"","text":""}'],
]; ];
foreach ($gateways as $gateway) { foreach ($gateways as $gateway) {

View File

@ -93,7 +93,7 @@ class RandomDataSeeder extends Seeder
]); ]);
factory(\App\Models\Client::class, 20)->create(['user_id' => $user->id, 'company_id' => $company->id])->each(function ($c) use ($user, $company){ factory(\App\Models\Client::class, 10)->create(['user_id' => $user->id, 'company_id' => $company->id])->each(function ($c) use ($user, $company){
factory(\App\Models\ClientContact::class,1)->create([ factory(\App\Models\ClientContact::class,1)->create([
'user_id' => $user->id, 'user_id' => $user->id,
@ -102,7 +102,7 @@ class RandomDataSeeder extends Seeder
'is_primary' => 1 'is_primary' => 1
]); ]);
factory(\App\Models\ClientContact::class,10)->create([ factory(\App\Models\ClientContact::class,5)->create([
'user_id' => $user->id, 'user_id' => $user->id,
'client_id' => $c->id, 'client_id' => $c->id,
'company_id' => $company->id 'company_id' => $company->id
@ -111,10 +111,10 @@ class RandomDataSeeder extends Seeder
}); });
/** Product Factory */ /** Product Factory */
factory(\App\Models\Product::class,50)->create(['user_id' => $user->id, 'company_id' => $company->id]); factory(\App\Models\Product::class,20)->create(['user_id' => $user->id, 'company_id' => $company->id]);
/** Invoice Factory */ /** Invoice Factory */
factory(\App\Models\Invoice::class,50)->create(['user_id' => $user->id, 'company_id' => $company->id, 'client_id' => $client->id]); factory(\App\Models\Invoice::class,20)->create(['user_id' => $user->id, 'company_id' => $company->id, 'client_id' => $client->id]);
$invoices = Invoice::cursor(); $invoices = Invoice::cursor();
$invoice_repo = new InvoiceRepository(); $invoice_repo = new InvoiceRepository();
@ -162,7 +162,7 @@ class RandomDataSeeder extends Seeder
}); });
/** Recurring Invoice Factory */ /** Recurring Invoice Factory */
factory(\App\Models\RecurringInvoice::class,20)->create(['user_id' => $user->id, 'company_id' => $company->id, 'client_id' => $client->id]); factory(\App\Models\RecurringInvoice::class,10)->create(['user_id' => $user->id, 'company_id' => $company->id, 'client_id' => $client->id]);
// factory(\App\Models\Payment::class,20)->create(['user_id' => $user->id, 'company_id' => $company->id, 'client_id' => $client->id, 'settings' => ClientSettings::buildClientSettings($company->settings, $client->settings)]); // factory(\App\Models\Payment::class,20)->create(['user_id' => $user->id, 'company_id' => $company->id, 'client_id' => $client->id, 'settings' => ClientSettings::buildClientSettings($company->settings, $client->settings)]);

View File

@ -239,9 +239,7 @@ class PaymentTest extends TestCase
} }
catch(ValidationException $e) { catch(ValidationException $e) {
\Log::error('in the validator');
$message = json_decode($e->validator->getMessageBag(),1); $message = json_decode($e->validator->getMessageBag(),1);
\Log::error($message);
$this->assertNotNull($message); $this->assertNotNull($message);
} }

View File

@ -15,6 +15,7 @@ use App\DataMapper\ClientSettings;
use App\DataMapper\CompanySettings; use App\DataMapper\CompanySettings;
use App\DataMapper\DefaultSettings; use App\DataMapper\DefaultSettings;
use App\Factory\ClientFactory; use App\Factory\ClientFactory;
use App\Factory\CompanyUserFactory;
use App\Factory\InvoiceFactory; use App\Factory\InvoiceFactory;
use App\Factory\InvoiceItemFactory; use App\Factory\InvoiceItemFactory;
use App\Factory\InvoiceToRecurringInvoiceFactory; use App\Factory\InvoiceToRecurringInvoiceFactory;
@ -103,6 +104,11 @@ trait MockAccountData
]); ]);
} }
$cu = CompanyUserFactory::create($this->user->id, $this->company->id, $this->account->id);
$cu->is_owner = true;
$cu->is_admin = true;
$cu->save();
$this->token = \Illuminate\Support\Str::random(64); $this->token = \Illuminate\Support\Str::random(64);
$company_token = CompanyToken::create([ $company_token = CompanyToken::create([
@ -113,14 +119,14 @@ trait MockAccountData
'token' => $this->token, 'token' => $this->token,
]); ]);
$this->user->companies()->attach($this->company->id, [ // $this->user->companies()->attach($this->company->id, [
'account_id' => $this->account->id, // 'account_id' => $this->account->id,
'is_owner' => 1, // 'is_owner' => 1,
'is_admin' => 1, // 'is_admin' => 1,
'is_locked' => 0, // 'is_locked' => 0,
'permissions' => json_encode([]), // 'permissions' => '',
'settings' => json_encode(DefaultSettings::userSettings()), // 'settings' => json_encode(DefaultSettings::userSettings()),
]); // ]);
$this->client = ClientFactory::create($this->company->id, $this->user->id); $this->client = ClientFactory::create($this->company->id, $this->user->id);
$this->client->save(); $this->client->save();

View File

@ -66,7 +66,6 @@ class GeneratesCounterTest extends TestCase
public function testInvoiceNumberPattern() public function testInvoiceNumberPattern()
{ {
$settings = $this->client->company->settings; $settings = $this->client->company->settings;
$settings->invoice_number_prefix = '';
$settings->invoice_number_counter = 1; $settings->invoice_number_counter = 1;
$settings->invoice_number_pattern = '{$year}-{$counter}'; $settings->invoice_number_pattern = '{$year}-{$counter}';
@ -89,7 +88,6 @@ class GeneratesCounterTest extends TestCase
public function testInvoiceClientNumberPattern() public function testInvoiceClientNumberPattern()
{ {
$settings = $this->company->settings; $settings = $this->company->settings;
$settings->client_number_prefix = '';
$settings->client_number_pattern = '{$year}-{$clientCounter}'; $settings->client_number_pattern = '{$year}-{$clientCounter}';
$settings->client_number_counter = 10; $settings->client_number_counter = 10;
@ -155,7 +153,6 @@ class GeneratesCounterTest extends TestCase
public function testInvoicePrefix() public function testInvoicePrefix()
{ {
$settings = $this->company->settings; $settings = $this->company->settings;
$settings->invoice_number_prefix = 'X';
$this->company->settings = $settings; $this->company->settings = $settings;
$this->company->save(); $this->company->save();
@ -165,11 +162,11 @@ class GeneratesCounterTest extends TestCase
$invoice_number = $this->getNextInvoiceNumber($cliz); $invoice_number = $this->getNextInvoiceNumber($cliz);
$this->assertEquals($invoice_number, 'X0001'); $this->assertEquals($invoice_number, '0007');
$invoice_number = $this->getNextInvoiceNumber($cliz); $invoice_number = $this->getNextInvoiceNumber($cliz);
$this->assertEquals($invoice_number, 'X0002'); $this->assertEquals($invoice_number, '0008');
} }
@ -190,7 +187,6 @@ class GeneratesCounterTest extends TestCase
public function testClientNumberPrefix() public function testClientNumberPrefix()
{ {
$settings = $this->company->settings; $settings = $this->company->settings;
$settings->client_number_prefix = 'C';
$this->company->settings = $settings; $this->company->settings = $settings;
$this->company->save(); $this->company->save();
@ -200,11 +196,11 @@ class GeneratesCounterTest extends TestCase
$client_number = $this->getNextClientNumber($cliz); $client_number = $this->getNextClientNumber($cliz);
$this->assertEquals($client_number, 'C0001'); $this->assertEquals($client_number, '0001');
$client_number = $this->getNextClientNumber($cliz); $client_number = $this->getNextClientNumber($cliz);
$this->assertEquals($client_number, 'C0002'); $this->assertEquals($client_number, '0002');
} }
@ -212,7 +208,6 @@ class GeneratesCounterTest extends TestCase
public function testClientNumberPattern() public function testClientNumberPattern()
{ {
$settings = $this->company->settings; $settings = $this->company->settings;
$settings->client_number_prefix = '';
$settings->client_number_pattern = '{$year}-{$user_id}-{$counter}'; $settings->client_number_pattern = '{$year}-{$user_id}-{$counter}';
$this->company->settings = $settings; $this->company->settings = $settings;
$this->company->save(); $this->company->save();