Merge pull request #7374 from turbo124/v5-develop

v5.3.80
This commit is contained in:
David Bomba 2022-04-19 16:06:08 +09:30 committed by GitHub
commit abeac50aba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 659 additions and 79 deletions

View File

@ -35,7 +35,6 @@ jobs:
php artisan key:generate php artisan key:generate
php artisan optimize php artisan optimize
php artisan storage:link php artisan storage:link
php artisan livewire:publish
sudo php artisan cache:clear sudo php artisan cache:clear
sudo find ./vendor/bin/ -type f -exec chmod +x {} \; sudo find ./vendor/bin/ -type f -exec chmod +x {} \;
sudo find ./ -type d -exec chmod 755 {} \; sudo find ./ -type d -exec chmod 755 {} \;

View File

@ -1 +1 @@
5.3.79 5.3.80

View File

@ -37,6 +37,15 @@ class ClientSettings extends BaseSettings
'size_id' => 'string', 'size_id' => 'string',
]; ];
public static $property_casts = [
'language_id' => 'string',
'currency_id' => 'string',
'payment_terms' => 'string',
'valid_until' => 'string',
'default_task_rate' => 'float',
'send_reminders' => 'bool',
];
/** /**
* Cast object values and return entire class * Cast object values and return entire class
* prevents missing properties from not being returned * prevents missing properties from not being returned

View File

@ -503,6 +503,7 @@ class CompanySettings extends BaseSettings
'language_id' => 'string', 'language_id' => 'string',
'show_currency_code' => 'bool', 'show_currency_code' => 'bool',
'website' => 'string', 'website' => 'string',
'default_task_rate' => 'float',
]; ];
/** /**

View File

@ -28,7 +28,11 @@ class CloneQuoteToInvoiceFactory
unset($quote_array['invoice_id']); unset($quote_array['invoice_id']);
unset($quote_array['id']); unset($quote_array['id']);
unset($quote_array['invitations']); unset($quote_array['invitations']);
//preserve terms if they exist on Quotes
if(array_key_exists('terms', $quote_array) && strlen($quote_array['terms']) < 2)
unset($quote_array['terms']); unset($quote_array['terms']);
// unset($quote_array['public_notes']); // unset($quote_array['public_notes']);
unset($quote_array['footer']); unset($quote_array['footer']);
unset($quote_array['design_id']); unset($quote_array['design_id']);

View File

@ -24,7 +24,6 @@ class CompanyGatewayFactory
$company_gateway->require_billing_address = false; $company_gateway->require_billing_address = false;
$company_gateway->require_shipping_address = false; $company_gateway->require_shipping_address = false;
$company_gateway->config = encrypt(json_encode(new \stdClass)); $company_gateway->config = encrypt(json_encode(new \stdClass));
// $company_gateway->fees_and_limits = new FeesAndLimits;
return $company_gateway; return $company_gateway;
} }

View File

@ -48,6 +48,16 @@ class RecurringInvoiceToInvoiceFactory
$invoice->custom_value4 = $recurring_invoice->custom_value4; $invoice->custom_value4 = $recurring_invoice->custom_value4;
$invoice->amount = $recurring_invoice->amount; $invoice->amount = $recurring_invoice->amount;
$invoice->uses_inclusive_taxes = $recurring_invoice->uses_inclusive_taxes; $invoice->uses_inclusive_taxes = $recurring_invoice->uses_inclusive_taxes;
$invoice->custom_surcharge1 = $recurring_invoice->custom_surcharge1;
$invoice->custom_surcharge2 = $recurring_invoice->custom_surcharge2;
$invoice->custom_surcharge3 = $recurring_invoice->custom_surcharge3;
$invoice->custom_surcharge4 = $recurring_invoice->custom_surcharge4;
$invoice->custom_surcharge_tax1 = $recurring_invoice->custom_surcharge_tax1;
$invoice->custom_surcharge_tax2 = $recurring_invoice->custom_surcharge_tax2;
$invoice->custom_surcharge_tax3 = $recurring_invoice->custom_surcharge_tax3;
$invoice->custom_surcharge_tax4 = $recurring_invoice->custom_surcharge_tax4;
// $invoice->balance = $recurring_invoice->balance; // $invoice->balance = $recurring_invoice->balance;
$invoice->user_id = $recurring_invoice->user_id; $invoice->user_id = $recurring_invoice->user_id;
$invoice->assigned_user_id = $recurring_invoice->assigned_user_id; $invoice->assigned_user_id = $recurring_invoice->assigned_user_id;

View File

@ -168,7 +168,7 @@ abstract class QueryFilters
public function created_at($value) public function created_at($value)
{ {
$created_at = $value ? $value : 0; $created_at = $value ? (int)$value : 0;
$created_at = date('Y-m-d H:i:s', $value); $created_at = date('Y-m-d H:i:s', $value);

View File

@ -218,7 +218,7 @@ class BaseController extends Controller
$query->with( $query->with(
[ [
'company' => function ($query) use ($updated_at, $user) { 'company' => function ($query) use ($updated_at, $user) {
$query->whereNotNull('updated_at')->with('documents')->with('users'); $query->whereNotNull('updated_at')->with('documents','users');
}, },
'company.clients' => function ($query) use ($updated_at, $user) { 'company.clients' => function ($query) use ($updated_at, $user) {
$query->where('clients.updated_at', '>=', $updated_at)->with('contacts.company', 'gateway_tokens', 'documents'); $query->where('clients.updated_at', '>=', $updated_at)->with('contacts.company', 'gateway_tokens', 'documents');
@ -392,7 +392,7 @@ class BaseController extends Controller
$query->with( $query->with(
[ [
'company' => function ($query) use ($created_at, $user) { 'company' => function ($query) use ($created_at, $user) {
$query->whereNotNull('created_at')->with('documents'); $query->whereNotNull('created_at')->with('documents','users');
}, },
'company.designs'=> function ($query) use ($created_at, $user) { 'company.designs'=> function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at)->with('company'); $query->where('created_at', '>=', $created_at)->with('company');
@ -466,7 +466,7 @@ class BaseController extends Controller
$query->with( $query->with(
[ [
'company' => function ($query) use ($created_at, $user) { 'company' => function ($query) use ($created_at, $user) {
$query->whereNotNull('created_at')->with('documents'); $query->whereNotNull('created_at')->with('documents','users');
}, },
'company.clients' => function ($query) use ($created_at, $user) { 'company.clients' => function ($query) use ($created_at, $user) {
$query->where('clients.created_at', '>=', $created_at)->with('contacts.company', 'gateway_tokens', 'documents'); $query->where('clients.created_at', '>=', $created_at)->with('contacts.company', 'gateway_tokens', 'documents');
@ -500,9 +500,6 @@ class BaseController extends Controller
}, },
'company.groups' => function ($query) use ($created_at, $user) { 'company.groups' => function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at)->with('documents'); $query->where('created_at', '>=', $created_at)->with('documents');
// if(!$user->isAdmin())
// $query->where('group_settings.user_id', $user->id);
}, },
'company.invoices'=> function ($query) use ($created_at, $user) { 'company.invoices'=> function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at)->with('invitations', 'documents'); $query->where('created_at', '>=', $created_at)->with('invitations', 'documents');
@ -583,13 +580,30 @@ class BaseController extends Controller
$query->where('activities.user_id', $user->id); $query->where('activities.user_id', $user->id);
}, },
'company.webhooks'=> function ($query) use($user) {
if(!$user->isAdmin())
$query->where('webhooks.user_id', $user->id);
},
'company.tokens'=> function ($query) use($user) {
$query->where('company_tokens.user_id', $user->id);
},
'company.system_logs',
'company.subscriptions'=> function ($query) use($created_at, $user) { 'company.subscriptions'=> function ($query) use($created_at, $user) {
$query->where('created_at', '>=', $created_at); $query->where('created_at', '>=', $created_at);
if(!$user->isAdmin()) if(!$user->isAdmin())
$query->where('subscriptions.user_id', $user->id); $query->where('subscriptions.user_id', $user->id);
} },
'company.recurring_expenses'=> function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at)->with('documents');
if(!$user->hasPermission('view_recurring_expense'))
$query->where('recurring_expenses.user_id', $user->id)->orWhere('recurring_expenses.assigned_user_id', $user->id);
},
] ]
); );

View File

@ -21,6 +21,15 @@ class SelfUpdateController extends BaseController
{ {
use DispatchesJobs; use DispatchesJobs;
private array $purge_file_list = [
'bootstrap/cache/compiled.php',
'bootstrap/cache/config.php',
'bootstrap/cache/packages.php',
'bootstrap/cache/services.php',
'bootstrap/cache/routes-v7.php',
'bootstrap/cache/livewire-components.php',
];
public function __construct() public function __construct()
{ {
} }
@ -117,10 +126,12 @@ class SelfUpdateController extends BaseController
unlink($file); unlink($file);
$cacheCompiled = base_path('bootstrap/cache/compiled.php'); foreach($this->purge_file_list as $purge_file_path)
if (file_exists($cacheCompiled)) { unlink ($cacheCompiled); } {
$cacheServices = base_path('bootstrap/cache/services.php'); $purge_file = base_path($purge_file_path);
if (file_exists($cacheServices)) { unlink ($cacheServices); } if (file_exists($purge_file)) { unlink ($purge_file); }
}
Artisan::call('clear-compiled'); Artisan::call('clear-compiled');
Artisan::call('route:clear'); Artisan::call('route:clear');

View File

@ -36,7 +36,7 @@ class StoreClientRequest extends Request
} }
public function rules() public function rules()
{ {nlog($this->input);
if ($this->input('documents') && is_array($this->input('documents'))) { if ($this->input('documents') && is_array($this->input('documents'))) {
$documents = count($this->input('documents')); $documents = count($this->input('documents'));
@ -95,6 +95,10 @@ class StoreClientRequest extends Request
if (array_key_exists('settings', $input) && ! empty($input['settings'])) { if (array_key_exists('settings', $input) && ! empty($input['settings'])) {
foreach ($input['settings'] as $key => $value) { foreach ($input['settings'] as $key => $value) {
if($key == 'default_task_rate')
$value = floatval($value);
$settings->{$key} = $value; $settings->{$key} = $value;
} }
} }

View File

@ -157,6 +157,10 @@ class UpdateClientRequest extends Request
if (! array_key_exists($key, $saveable_casts)) { if (! array_key_exists($key, $saveable_casts)) {
unset($settings->{$key}); unset($settings->{$key});
} }
if($key == 'default_task_rate'){
$settings->default_task_rate = floatval($value);
}
} }
return $settings; return $settings;

View File

@ -140,7 +140,7 @@ class CompanyImport implements ShouldQueue
'expenses', 'expenses',
'tasks', 'tasks',
'payments', 'payments',
'activities', // 'activities',
// 'backups', // 'backups',
'company_ledger', 'company_ledger',
'designs', 'designs',

View File

@ -84,7 +84,7 @@ class ReminderJob implements ShouldQueue
//check if this reminder needs to be emailed //check if this reminder needs to be emailed
//15-01-2022 - insert addition if block if send_reminders is definitely set //15-01-2022 - insert addition if block if send_reminders is definitely set
if(in_array($reminder_template, ['reminder1','reminder2','reminder3','reminder_endless']) && $invoice->client->getSetting("enable_".$reminder_template) && $invoice->client->getSetting("send_reminders")) if(in_array($reminder_template, ['reminder1','reminder2','reminder3','reminder_endless']) && $invoice->client->getSetting("enable_".$reminder_template) && $invoice->client->getSetting("send_reminders") && $invoice->company->account->isPaidHostedClient())
{ {
$invoice->invitations->each(function ($invitation) use ($invoice, $reminder_template) { $invoice->invitations->each(function ($invitation) use ($invoice, $reminder_template) {
EmailEntity::dispatch($invitation, $invitation->company, $reminder_template); EmailEntity::dispatch($invitation, $invitation->company, $reminder_template);

View File

@ -190,7 +190,7 @@ class BaseModel extends Model
public function numberFormatter() public function numberFormatter()
{ {
$number = strlen($this->number) >= 1 ? $this->number : class_basename($this) . "_" . Str::random(5); ; $number = strlen($this->number) >= 1 ? $this->number : class_basename($this) . "_" . Str::random(5);
$formatted_number = mb_ereg_replace("([^\w\s\d\-_~,;\[\]\(\).])", '', $number); $formatted_number = mb_ereg_replace("([^\w\s\d\-_~,;\[\]\(\).])", '', $number);
// Remove any runs of periods (thanks falstro!) // Remove any runs of periods (thanks falstro!)

View File

@ -52,6 +52,8 @@ class CompanyUser extends Pivot
protected $touches = ['user']; protected $touches = ['user'];
protected $with = ['user','account'];
public function getEntityType() public function getEntityType()
{ {
return self::class; return self::class;

View File

@ -42,6 +42,9 @@ class Webhook extends BaseModel
const EVENT_LATE_INVOICE = 22; const EVENT_LATE_INVOICE = 22;
const EVENT_EXPIRED_QUOTE = 23; const EVENT_EXPIRED_QUOTE = 23;
const EVENT_REMIND_INVOICE = 24; const EVENT_REMIND_INVOICE = 24;
const EVENT_PROJECT_CREATE = 25;
const EVENT_PROJECT_UPDATE = 26;
public static $valid_events = [ public static $valid_events = [
self::EVENT_CREATE_CLIENT, self::EVENT_CREATE_CLIENT,
@ -68,6 +71,8 @@ class Webhook extends BaseModel
self::EVENT_LATE_INVOICE, self::EVENT_LATE_INVOICE,
self::EVENT_EXPIRED_QUOTE, self::EVENT_EXPIRED_QUOTE,
self::EVENT_REMIND_INVOICE, self::EVENT_REMIND_INVOICE,
self::EVENT_PROJECT_CREATE,
self::EVENT_PROJECT_UPDATE
]; ];
protected $fillable = [ protected $fillable = [

View File

@ -0,0 +1,89 @@
<?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 App\Observers;
use App\Jobs\Util\WebhookHandler;
use App\Models\Project;
use App\Models\Webhook;
class ProjectObserver
{
public $afterCommit = true;
/**
* Handle the product "created" event.
*
* @param Project $project
* @return void
*/
public function created(Project $project)
{
$subscriptions = Webhook::where('company_id', $project->company_id)
->where('event_id', Webhook::EVENT_PROJECT_CREATE)
->exists();
if ($subscriptions) {
WebhookHandler::dispatch(Webhook::EVENT_PROJECT_CREATE, $project, $project->company, 'client');
}
}
/**
* Handle the product "updated" event.
*
* @param Project $project
* @return void
*/
public function updated(Project $project)
{
$subscriptions = Webhook::where('company_id', $project->company_id)
->where('event_id', Webhook::EVENT_PROJECT_UPDATE)
->exists();
if ($subscriptions) {
WebhookHandler::dispatch(Webhook::EVENT_PROJECT_UPDATE, $project, $project->company, 'client');
}
}
/**
* Handle the product "deleted" event.
*
* @param Project $project
* @return void
*/
public function deleted(Project $project)
{
//
}
/**
* Handle the product "restored" event.
*
* @param Project $project
* @return void
*/
public function restored(Project $project)
{
//
}
/**
* Handle the product "force deleted" event.
*
* @param Project $project
* @return void
*/
public function forceDeleted(Project $project)
{
//
}
}

View File

@ -164,13 +164,13 @@ class AuthorizePaymentMethod
if ($contact) { if ($contact) {
// Create the Bill To info for new payment type // Create the Bill To info for new payment type
$billto = new CustomerAddressType(); $billto = new CustomerAddressType();
$billto->setFirstName($contact->present()->first_name()); $billto->setFirstName(substr(0,50,$contact->present()->first_name()));
$billto->setLastName($contact->present()->last_name()); $billto->setLastName(substr(0,50,$contact->present()->last_name()));
$billto->setCompany($this->authorize->client->present()->name()); $billto->setCompany(substr(0,50,$this->authorize->client->present()->name()));
$billto->setAddress($this->authorize->client->address1); $billto->setAddress(substr(0,60,$this->authorize->client->address1));
$billto->setCity($this->authorize->client->city); $billto->setCity(substr(0,40,$this->authorize->client->city));
$billto->setState($this->authorize->client->state); $billto->setState(substr(0,40,$this->authorize->client->state));
$billto->setZip($this->authorize->client->postal_code); $billto->setZip(substr(0,20,$this->authorize->client->postal_code));
if ($this->authorize->client->country_id) { if ($this->authorize->client->country_id) {
$billto->setCountry($this->authorize->client->country->name); $billto->setCountry($this->authorize->client->country->name);

View File

@ -170,16 +170,52 @@ class CreditCard
$this->logResponse($response); $this->logResponse($response);
$response_status = ErrorCode::getStatus($response->ResponseMessage); // if(!$response || !property_exists($response, 'ResponseMessage'))
// throw new PaymentFailed('The gateway did not return a valid response. Please check your gateway credentials.', 400);
if(!$response_status['success']){ // $response_status = ErrorCode::getStatus($response->ResponseMessage);
$this->eway_driver->sendFailureMail($response_status['message']); // if(!$response_status['success']){
throw new PaymentFailed($response_status['message'], 400); // if($response->getErrors())
// {
// $message = false;
// foreach ($response->getErrors() as $error) {
// $message = \Eway\Rapid::getMessage($error);
// }
// $return_message = $message ?: $response_status['message'];
// }
// $this->eway_driver->sendFailureMail($response_status['message']);
// throw new PaymentFailed($response_status['message'], 400);
// }
if($response->TransactionStatus)
$payment = $this->storePayment($response);
else {
$message = 'Error processing payment.';
if(isset($response->ResponseMessage))
$message .= " Gateway Error Code = {$response->ResponseMessage}";
if($response->getErrors())
{
foreach ($response->getErrors() as $error) {
$message = \Eway\Rapid::getMessage($error);
}
}
$this->eway_driver->sendFailureMail($message);
throw new PaymentFailed($message, 400);
} }
$payment = $this->storePayment($response);
return redirect()->route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)]); return redirect()->route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)]);
@ -257,21 +293,33 @@ class CreditCard
$response = $this->eway_driver->init()->eway->createTransaction(\Eway\Rapid\Enum\ApiMethod::DIRECT, $transaction); $response = $this->eway_driver->init()->eway->createTransaction(\Eway\Rapid\Enum\ApiMethod::DIRECT, $transaction);
$response_status = ErrorCode::getStatus($response->ResponseMessage); if($response->TransactionStatus){
$this->logResponse($response, true);
$payment = $this->storePayment($response);
}
else {
if(!$response_status['success']){ $message = 'Error processing payment.';
if(isset($response->ResponseMessage))
$message .= " Gateway Error Code = {$response->ResponseMessage}";
if($response->getErrors())
{
foreach ($response->getErrors() as $error) {
$message = \Eway\Rapid::getMessage($error);
}
}
$this->logResponse($response, false); $this->logResponse($response, false);
$this->eway_driver->sendFailureMail($response_status['message']); $this->eway_driver->sendFailureMail($message);
throw new PaymentFailed($response_status['message'], 400); throw new PaymentFailed($message, 400);
} }
$this->logResponse($response, true);
$payment = $this->storePayment($response);
return $payment; return $payment;
} }
} }

View File

@ -32,6 +32,8 @@ class PayPalExpressPaymentDriver extends BaseDriver
private $omnipay_gateway; private $omnipay_gateway;
private float $fee = 0;
const SYSTEM_LOG_TYPE = SystemLog::TYPE_PAYPAL; const SYSTEM_LOG_TYPE = SystemLog::TYPE_PAYPAL;
public function gatewayTypes() public function gatewayTypes()
@ -173,11 +175,17 @@ class PayPalExpressPaymentDriver extends BaseDriver
public function generatePaymentDetails(array $data) public function generatePaymentDetails(array $data)
{ {
$_invoice = collect($this->payment_hash->data->invoices)->first();
$invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($_invoice->invoice_id));
$this->fee = $this->feeCalc($invoice, $data['total']['amount_with_fee']);
return [ return [
'currency' => $this->client->getCurrencyCode(), 'currency' => $this->client->getCurrencyCode(),
'transactionType' => 'Purchase', 'transactionType' => 'Purchase',
'clientIp' => request()->getClientIp(), 'clientIp' => request()->getClientIp(),
'amount' => $data['total']['amount_with_fee'], 'amount' => $data['total']['amount_with_fee'] + $this->fee,
'returnUrl' => route('client.payments.response', [ 'returnUrl' => route('client.payments.response', [
'company_gateway_id' => $this->company_gateway->id, 'company_gateway_id' => $this->company_gateway->id,
'payment_hash' => $this->payment_hash->hash, 'payment_hash' => $this->payment_hash->hash,
@ -200,8 +208,6 @@ class PayPalExpressPaymentDriver extends BaseDriver
$_invoice = collect($this->payment_hash->data->invoices)->first(); $_invoice = collect($this->payment_hash->data->invoices)->first();
$invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($_invoice->invoice_id)); $invoice = Invoice::withTrashed()->find($this->decodePrimaryKey($_invoice->invoice_id));
$line_item = collect($invoice->line_items)->first();
$items = []; $items = [];
$items[] = new Item([ $items[] = new Item([
@ -211,8 +217,44 @@ class PayPalExpressPaymentDriver extends BaseDriver
'quantity' => 1, 'quantity' => 1,
]); ]);
if($this->fee > 0.1){
$items[] = new Item([
'name' => " ",
'description' => ctrans('texts.gateway_fee_description'),
'price' => $this->fee,
'quantity' => 1,
]);
}
return $items; return $items;
} }
private function feeCalc($invoice, $invoice_total)
{
$invoice->service()->removeUnpaidGatewayFees();
$invoice = $invoice->fresh();
$balance = floatval($invoice->balance);
$_updated_invoice = $invoice->service()->addGatewayFee($this->company_gateway, GatewayType::PAYPAL, $invoice_total)->save();
if(floatval($_updated_invoice->balance) > $balance){
$fee = floatval($_updated_invoice->balance) - $balance;
$this->payment_hash->fee_total = $fee;
$this->payment_hash->save();
return $fee;
}
return 0;
}
} }

View File

@ -25,6 +25,7 @@ use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Illuminate\Database\Eloquent\Model;
use Livewire\Livewire; use Livewire\Livewire;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
@ -69,6 +70,11 @@ class AppServiceProvider extends ServiceProvider
app()->instance(TruthSource::class, new TruthSource()); app()->instance(TruthSource::class, new TruthSource());
// Model::preventLazyLoading(
// !$this->app->isProduction()
// );
} }
/** /**

View File

@ -213,6 +213,7 @@ use App\Models\Expense;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Models\Product; use App\Models\Product;
use App\Models\Project;
use App\Models\Proposal; use App\Models\Proposal;
use App\Models\Quote; use App\Models\Quote;
use App\Models\Subscription; use App\Models\Subscription;
@ -228,6 +229,7 @@ use App\Observers\ExpenseObserver;
use App\Observers\InvoiceObserver; use App\Observers\InvoiceObserver;
use App\Observers\PaymentObserver; use App\Observers\PaymentObserver;
use App\Observers\ProductObserver; use App\Observers\ProductObserver;
use App\Observers\ProjectObserver;
use App\Observers\ProposalObserver; use App\Observers\ProposalObserver;
use App\Observers\QuoteObserver; use App\Observers\QuoteObserver;
use App\Observers\SubscriptionObserver; use App\Observers\SubscriptionObserver;
@ -586,6 +588,7 @@ class EventServiceProvider extends ServiceProvider
Invoice::observe(InvoiceObserver::class); Invoice::observe(InvoiceObserver::class);
Payment::observe(PaymentObserver::class); Payment::observe(PaymentObserver::class);
Product::observe(ProductObserver::class); Product::observe(ProductObserver::class);
Project::observe(ProjectObserver::class);
Proposal::observe(ProposalObserver::class); Proposal::observe(ProposalObserver::class);
Quote::observe(QuoteObserver::class); Quote::observe(QuoteObserver::class);
Task::observe(TaskObserver::class); Task::observe(TaskObserver::class);

View File

@ -19,20 +19,14 @@ use App\Models\Client;
use App\Models\Design; use App\Models\Design;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Models\Product;
use App\Services\PdfMaker\Design as PdfMakerDesign; use App\Services\PdfMaker\Design as PdfMakerDesign;
use App\Services\PdfMaker\PdfMaker; use App\Services\PdfMaker\PdfMaker;
use App\Utils\HostedPDF\NinjaPdf; use App\Utils\HostedPDF\NinjaPdf;
use App\Utils\HtmlEngine; use App\Utils\HtmlEngine;
use App\Utils\Ninja;
use App\Utils\Number; use App\Utils\Number;
use App\Utils\PhantomJS\Phantom; use App\Utils\PhantomJS\Phantom;
use App\Utils\Traits\Pdf\PdfMaker as PdfMakerTrait; use App\Utils\Traits\Pdf\PdfMaker as PdfMakerTrait;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\LazyCollection;
class Statement class Statement
{ {
@ -231,7 +225,7 @@ class Statement
->where('client_id', $this->client->id) ->where('client_id', $this->client->id)
->whereIn('status_id', $this->invoiceStatuses()) ->whereIn('status_id', $this->invoiceStatuses())
->whereBetween('date', [Carbon::parse($this->options['start_date']), Carbon::parse($this->options['end_date'])]) ->whereBetween('date', [Carbon::parse($this->options['start_date']), Carbon::parse($this->options['end_date'])])
->orderBy('date', 'ASC') ->orderBy('due_date', 'ASC')
->cursor(); ->cursor();
} }

View File

@ -79,7 +79,7 @@ class CompanyUserTransformer extends EntityTransformer
public function includeToken(CompanyUser $company_user) public function includeToken(CompanyUser $company_user)
{ {
$token = $company_user->tokens->where('company_id', $company_user->company_id)->where('user_id', $company_user->user_id)->first(); $token = $company_user->tokens()->where('company_id', $company_user->company_id)->where('user_id', $company_user->user_id)->first();
$transformer = new CompanyTokenTransformer($this->serializer); $transformer = new CompanyTokenTransformer($this->serializer);

View File

@ -122,6 +122,9 @@ class HtmlEngine
$data['$invoice.date'] = &$data['$date']; $data['$invoice.date'] = &$data['$date'];
$data['$invoiceDate'] = &$data['$date']; $data['$invoiceDate'] = &$data['$date'];
$data['$due_date'] = ['value' => $this->translateDate($this->entity->due_date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')]; $data['$due_date'] = ['value' => $this->translateDate($this->entity->due_date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')];
$data['$partial_due_date'] = ['value' => $this->translateDate($this->entity->partial_due_date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.'.$this->entity_string.'_due_date')];
$data['$dueDate'] = &$data['$due_date']; $data['$dueDate'] = &$data['$due_date'];
$data['$payment_due'] = ['value' => $this->translateDate($this->entity->due_date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.payment_due')]; $data['$payment_due'] = ['value' => $this->translateDate($this->entity->due_date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.payment_due')];

View File

@ -11,6 +11,7 @@
namespace App\Utils\Traits; namespace App\Utils\Traits;
use App\DataMapper\ClientSettings;
use App\DataMapper\CompanySettings; use App\DataMapper\CompanySettings;
use stdClass; use stdClass;
@ -63,15 +64,6 @@ trait ClientGroupSettingsSaver
$entity_settings->{$key} = $value; $entity_settings->{$key} = $value;
} }
//this pass will handle any null values that are in the translations
// foreach ($settings->translations as $key => $value) {
// if (is_null($settings->translations[$key])) {
// $settings->translations[$key] = '';
// }
// }
// $entity_settings->translations = $settings->translations;
$entity->settings = $entity_settings; $entity->settings = $entity_settings;
$entity->save(); $entity->save();
@ -121,7 +113,11 @@ trait ClientGroupSettingsSaver
continue; continue;
} }
/*Separate loop if it is a _id field which is an integer cast as a string*/ /*Separate loop if it is a _id field which is an integer cast as a string*/
elseif (substr($key, -3) == '_id' || substr($key, -14) == 'number_counter') { elseif (substr($key, -3) == '_id' ||
substr($key, -14) == 'number_counter' ||
($key == 'payment_terms' && property_exists($settings, 'payment_terms') && strlen($settings->{$key}) >= 1) ||
($key == 'valid_until' && property_exists($settings, 'valid_until') && strlen($settings->{$key}) >= 1)) {
$value = 'integer'; $value = 'integer';
if (! property_exists($settings, $key)) { if (! property_exists($settings, $key)) {
@ -170,7 +166,11 @@ trait ClientGroupSettingsSaver
} }
/*Separate loop if it is a _id field which is an integer cast as a string*/ /*Separate loop if it is a _id field which is an integer cast as a string*/
if (substr($key, -3) == '_id' || substr($key, -14) == 'number_counter') { if (substr($key, -3) == '_id' ||
substr($key, -14) == 'number_counter' ||
($key == 'payment_terms' && property_exists($settings, 'payment_terms') && strlen($settings->{$key}) >= 1) ||
($key == 'valid_until' && property_exists($settings, 'valid_until') && strlen($settings->{$key}) >= 1)) {
$value = 'integer'; $value = 'integer';
if (! property_exists($settings, $key)) { if (! property_exists($settings, $key)) {
@ -219,8 +219,7 @@ trait ClientGroupSettingsSaver
switch ($key) { switch ($key) {
case 'int': case 'int':
case 'integer': case 'integer':
// return ctype_digit(strval(abs($value))); return is_numeric($value) && ctype_digit(strval(abs($value)));
return ctype_digit(strval($value));
case 'real': case 'real':
case 'float': case 'float':
case 'double': case 'double':

View File

@ -32,7 +32,7 @@ trait UserNotifies
$notifiable_methods = []; $notifiable_methods = [];
$notifications = $company_user->notifications; $notifications = $company_user->notifications;
if ($company_user->company->is_disabled && is_array($notifications->email) || $company_user->trashed() || $company_user->user->trashed()) { if ($invitation->company->is_disabled && is_array($notifications->email) || $company_user->trashed() || $company_user->user->trashed()) {
return []; return [];
} }
@ -56,7 +56,7 @@ trait UserNotifies
$notifiable_methods = []; $notifiable_methods = [];
$notifications = $company_user->notifications; $notifications = $company_user->notifications;
if ($company_user->company->is_disabled || ! $notifications || $company_user->trashed() || $company_user->user->trashed()) { if ($entity->company->is_disabled || ! $notifications || $company_user->trashed() || $company_user->user->trashed()) {
return []; return [];
} }

View File

@ -52,7 +52,7 @@ trait SettingsSaver
continue; continue;
} }
/*Separate loop if it is a _id field which is an integer cast as a string*/ /*Separate loop if it is a _id field which is an integer cast as a string*/
elseif (substr($key, -3) == '_id' || substr($key, -14) == 'number_counter') { elseif (substr($key, -3) == '_id' || substr($key, -14) == 'number_counter' || ($key == 'payment_terms' && strlen($settings->{$key}) >= 1) || ($key == 'valid_until' && strlen($settings->{$key}) >= 1)) {
$value = 'integer'; $value = 'integer';
if($key == 'gmail_sending_user_id') if($key == 'gmail_sending_user_id')
@ -94,12 +94,11 @@ trait SettingsSaver
switch ($key) { switch ($key) {
case 'int': case 'int':
case 'integer': case 'integer':
return ctype_digit(strval(abs($value))); return is_numeric($value) && ctype_digit(strval(abs($value)));
case 'real': case 'real':
case 'float': case 'float':
case 'double': case 'double':
return !is_string($value) && (is_float($value) || is_numeric(strval($value))); return !is_string($value) && (is_float($value) || is_numeric(strval($value)));
// return is_float($value) || is_numeric(strval($value));
case 'string': case 'string':
return !is_int($value) || ( is_string( $value ) && method_exists($value, '__toString') ) || is_null($value) || is_string($value); return !is_int($value) || ( is_string( $value ) && method_exists($value, '__toString') ) || is_null($value) || is_string($value);
case 'bool': case 'bool':

View File

@ -54,7 +54,7 @@ return [
| |
*/ */
'asset_url' => null, 'asset_url' => env('ASSET_URL', null),
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@ -14,8 +14,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true), 'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => '5.3.79', 'app_version' => '5.3.80',
'app_tag' => '5.3.79', 'app_tag' => '5.3.80',
'minimum_client_version' => '5.0.16', 'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1', 'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''), 'api_secret' => env('API_SECRET', ''),

View File

@ -50,8 +50,6 @@ class CompanySettingsTest extends TestCase
$this->company->saveSettings($settings, $this->company); $this->company->saveSettings($settings, $this->company);
//$this->withoutExceptionHandling();
$response = false; $response = false;
try { try {

View File

@ -36,7 +36,7 @@ class UpdateCompanyUserTest extends TestCase
public function testUpdatingCompanyUserAsAdmin() public function testUpdatingCompanyUserAsAdmin()
{ {
User::unguard(); // User::unguard();
$settings = new \stdClass; $settings = new \stdClass;
$settings->invoice = 'ninja'; $settings->invoice = 'ninja';

View File

@ -0,0 +1,337 @@
<?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://opensource.org/licenses/AAL
*/
namespace Tests\Unit;
use App\DataMapper\ClientSettings;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Validation\ValidationException;
use Tests\MockAccountData;
use Tests\TestCase;
/**
* @test
*/
class ClientSettingsTest extends TestCase
{
use MockAccountData;
use DatabaseTransactions;
public function setUp() :void
{
parent::setUp();
$this->makeTestData();
$this->faker = \Faker\Factory::create();
}
public function testClientBaseline()
{
$data = [
'name' => $this->faker->firstName,
'id_number' => 'Coolio',
];
$response = false;
try{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/clients/', $data);
} catch (ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(), 1);
nlog($message);
}
$response->assertStatus(200);
$arr = $response->json();
$this->assertEquals("1", $arr['data']['settings']['currency_id']);
}
public function testClientValidSettings()
{
$data = [
'name' => $this->faker->firstName,
'id_number' => 'Coolio',
'settings' => [
'currency_id' => '1',
'language_id' => '1',
'payment_terms' => '1',
'valid_until' => '1',
'default_task_rate' => 10,
'send_reminders' => true
]
];
$response = false;
try{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/clients/', $data);
} catch (ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(), 1);
nlog($message);
}
$response->assertStatus(200);
$arr = $response->json();
$this->assertEquals("1", $arr['data']['settings']['currency_id']);
$this->assertEquals("1", $arr['data']['settings']['language_id']);
$this->assertEquals("1", $arr['data']['settings']['payment_terms']);
$this->assertEquals(10, $arr['data']['settings']['default_task_rate']);
$this->assertEquals(true, $arr['data']['settings']['send_reminders']);
$this->assertEquals("1", $arr['data']['settings']['valid_until']);
}
public function testClientIllegalCurrency()
{
$data = [
'name' => $this->faker->firstName,
'id_number' => 'Coolio',
'settings' => [
'currency_id' => 'a',
'language_id' => '1',
'payment_terms' => '1',
'valid_until' => '1',
'default_task_rate' => 10,
'send_reminders' => true
]
];
$response = false;
try{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/clients/', $data);
} catch (ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(), 1);
nlog($message);
}
$response->assertStatus(302);
}
public function testClientIllegalLanguage()
{
$data = [
'name' => $this->faker->firstName,
'id_number' => 'Coolio',
'settings' => [
'currency_id' => '1',
'language_id' => 'a',
'payment_terms' => '1',
'valid_until' => '1',
'default_task_rate' => 10,
'send_reminders' => true
]
];
$response = false;
try{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/clients/', $data);
} catch (ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(), 1);
nlog($message);
}
$response->assertStatus(302);
}
public function testClientIllegalPaymenTerms()
{
$data = [
'name' => $this->faker->firstName,
'id_number' => 'Coolio',
'settings' => [
'currency_id' => '1',
'language_id' => '1',
'payment_terms' => 'a',
'valid_until' => '1',
'default_task_rate' => 10,
'send_reminders' => true
]
];
$response = false;
try{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/clients/', $data);
} catch (ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(), 1);
nlog($message);
}
$response->assertStatus(302);
}
public function testClientIllegalValidUntil()
{
$data = [
'name' => $this->faker->firstName,
'id_number' => 'Coolio',
'settings' => [
'currency_id' => '1',
'language_id' => '1',
'payment_terms' => '1',
'valid_until' => 'a',
'default_task_rate' => 10,
'send_reminders' => true
]
];
$response = false;
try{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/clients/', $data);
} catch (ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(), 1);
nlog($message);
}
$response->assertStatus(302);
}
public function testClientIllegalDefaultTaskRate()
{
$data = [
'name' => $this->faker->firstName,
'id_number' => 'Coolio',
'settings' => [
'currency_id' => '1',
'language_id' => '1',
'payment_terms' => '1',
'valid_until' => '1',
'default_task_rate' => "a",
'send_reminders' => true
]
];
$response = false;
try{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/clients/', $data);
} catch (ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(), 1);
nlog($message);
}
$response->assertStatus(200);
$arr = $response->json();
$this->assertFalse(array_key_exists('default_task_rate', $arr));
}
public function testClientIllegalSendReminderBool()
{
$data = [
'name' => $this->faker->firstName,
'id_number' => 'Coolio',
'settings' => [
'currency_id' => '1',
'language_id' => '1',
'payment_terms' => '1',
'valid_until' => '1',
'default_task_rate' => "a",
'send_reminders' => "faaalse"
]
];
$response = false;
try{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/clients/', $data);
} catch (ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(), 1);
nlog($message);
}
$response->assertStatus(302);
}
public function testClientSettingBools()
{
$data = [
'name' => $this->faker->firstName,
'id_number' => 'Coolio',
'settings' => [
'currency_id' => '1',
'language_id' => '1',
'payment_terms' => '1',
'valid_until' => '1',
'default_task_rate' => "a",
'send_reminders' => "true"
]
];
$response = false;
try{
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/clients/', $data);
} catch (ValidationException $e) {
$message = json_decode($e->validator->getMessageBag(), 1);
nlog($message);
}
$response->assertStatus(200);
}
}