diff --git a/VERSION.txt b/VERSION.txt index f7b8d4c4c0..339c435943 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.5.84 \ No newline at end of file +5.5.85 \ No newline at end of file diff --git a/app/Http/Controllers/ClientPortal/InvitationController.php b/app/Http/Controllers/ClientPortal/InvitationController.php index 52ca200c8c..eece9465c5 100644 --- a/app/Http/Controllers/ClientPortal/InvitationController.php +++ b/app/Http/Controllers/ClientPortal/InvitationController.php @@ -85,7 +85,8 @@ class InvitationController extends Controller ->with('contact.client') ->firstOrFail(); - if ($invitation->{$entity}->is_deleted) { + //09-03-2023 do not show entity if the invitation has been trashed. + if ($invitation->trashed() || $invitation->{$entity}->is_deleted) { return $this->render('generic.not_available', ['account' => $invitation->company->account, 'company' => $invitation->company]); } diff --git a/app/Http/Requests/Company/UpdateCompanyRequest.php b/app/Http/Requests/Company/UpdateCompanyRequest.php index c2228b1c50..f1c66b0a3a 100644 --- a/app/Http/Requests/Company/UpdateCompanyRequest.php +++ b/app/Http/Requests/Company/UpdateCompanyRequest.php @@ -93,7 +93,7 @@ class UpdateCompanyRequest extends Request * are saveable * * @param object $settings - * @return stdClass $settings + * @return \stdClass $settings */ private function filterSaveableSettings($settings) { diff --git a/app/Http/Requests/User/UpdateUserRequest.php b/app/Http/Requests/User/UpdateUserRequest.php index 2bbc24e21e..d4394e028c 100644 --- a/app/Http/Requests/User/UpdateUserRequest.php +++ b/app/Http/Requests/User/UpdateUserRequest.php @@ -73,6 +73,11 @@ class UpdateUserRequest extends Request $input['oauth_user_id'] = ''; } + if (array_key_exists('oauth_user_token', $input) && $input['oauth_user_token'] == '***') { + unset($input['oauth_user_token']); + } + + $this->replace($input); } } diff --git a/app/Http/Requests/Vendor/StoreVendorRequest.php b/app/Http/Requests/Vendor/StoreVendorRequest.php index e2ee08adf7..3ede512b7d 100644 --- a/app/Http/Requests/Vendor/StoreVendorRequest.php +++ b/app/Http/Requests/Vendor/StoreVendorRequest.php @@ -23,26 +23,20 @@ class StoreVendorRequest extends Request /** * Determine if the user is authorized to make this request. * - * @return bool - * @method static \Illuminate\Contracts\Auth\Authenticatable|null user() */ public function authorize() : bool { - /** @var \App\User|null $user */ - $user = auth()->user(); - - return $user->can('create', Vendor::class); + return auth()->user()->can('create', Vendor::class); } public function rules() { - /** @var \App\User|null $user */ - $user = auth()->user(); - + $rules = []; + $rules['contacts.*.email'] = 'bail|nullable|distinct|sometimes|email'; if (isset($this->number)) { - $rules['number'] = Rule::unique('vendors')->where('company_id', $user->company()->id); + $rules['number'] = Rule::unique('vendors')->where('company_id', auth()->user()->company()->id); } $rules['currency_id'] = 'bail|required|exists:currencies,id'; @@ -63,13 +57,11 @@ class StoreVendorRequest extends Request public function prepareForValidation() { - /** @var \App\User|null $user */ - $user = auth()->user(); $input = $this->all(); if (!array_key_exists('currency_id', $input) || empty($input['currency_id'])) { - $input['currency_id'] = $user->company()->settings->currency_id; + $input['currency_id'] = auth()->user()->company()->settings->currency_id; } $input = $this->decodePrimaryKeys($input); diff --git a/app/Jobs/Invoice/UpdateReminders.php b/app/Jobs/Invoice/UpdateReminders.php index dee22412f3..026035cecc 100644 --- a/app/Jobs/Invoice/UpdateReminders.php +++ b/app/Jobs/Invoice/UpdateReminders.php @@ -24,11 +24,8 @@ class UpdateReminders implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public Company $company; - - public function __construct(Company $company) + public function __construct(public Company $company) { - $this->company = $company; } /** diff --git a/app/Jobs/Ninja/AdjustEmailQuota.php b/app/Jobs/Ninja/AdjustEmailQuota.php index 7e0a1cddab..1cec63ab0d 100644 --- a/app/Jobs/Ninja/AdjustEmailQuota.php +++ b/app/Jobs/Ninja/AdjustEmailQuota.php @@ -22,7 +22,7 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Cache; use Turbo124\Beacon\Facades\LightLogs; - +use Illuminate\Support\Facades\Redis; class AdjustEmailQuota implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -61,18 +61,47 @@ class AdjustEmailQuota implements ShouldQueue Account::query()->cursor()->each(function ($account) { nlog("resetting email quota for {$account->key}"); - $email_count = Cache::get($account->key); + $email_count = Cache::get("email_quota".$account->key); if ($email_count > 0) { try { - LightLogs::create(new EmailCount($email_count, $account->key))->send(); + LightLogs::create(new EmailCount($email_count, $account->key))->send(); // this runs syncronously } catch(\Exception $e) { nlog($e->getMessage()); } } - - Cache::forget($account->key); - Cache::forget("throttle_notified:{$account->key}"); }); + + /** Use redis pipelines to execute bulk deletes efficiently */ + $redis = Redis::connection('sentinel-cache'); + $prefix = config('cache.prefix'). ":email_quota*"; + + $keys = $redis->keys($prefix); + + if(is_array($keys)) + { + $redis->pipeline(function ($pipe) use ($keys) { + + foreach ($keys as $key) { + $pipe->del($key); + } + + }); + } + $keys = null; + + $prefix = config('cache.prefix'). ":throttle_notified*"; + + $keys = $redis->keys($prefix); + + if (is_array($keys)) { + $redis->pipeline(function ($pipe) use ($keys) { + foreach ($keys as $key) { + $pipe->del($key); + } + }); + } + + } } diff --git a/app/Jobs/Subscription/CleanStaleInvoiceOrder.php b/app/Jobs/Subscription/CleanStaleInvoiceOrder.php index f58084239c..6d042ccd82 100644 --- a/app/Jobs/Subscription/CleanStaleInvoiceOrder.php +++ b/app/Jobs/Subscription/CleanStaleInvoiceOrder.php @@ -26,8 +26,6 @@ class CleanStaleInvoiceOrder implements ShouldQueue /** * Create a new job instance. * - * @param int invoice_id - * @param string $db */ public function __construct() { diff --git a/app/Models/Account.php b/app/Models/Account.php index d57a3225c6..7eb9bd228c 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -339,7 +339,8 @@ class Account extends BaseModel return false; } - if ($this->plan_expires && Carbon::parse($this->plan_expires)->lt(now())) { + // 09-03-2023 - winds forward expiry checks to ensure we don't cut off users prior to billing cycle being commenced + if ($this->plan_expires && Carbon::parse($this->plan_expires)->lt(now()->subHours(12))) { return false; } @@ -352,7 +353,7 @@ class Account extends BaseModel return false; } - if ($this->plan_expires && Carbon::parse($this->plan_expires)->lt(now())) { + if ($this->plan_expires && Carbon::parse($this->plan_expires)->lt(now()->subHours(12))) { return true; } @@ -526,12 +527,12 @@ class Account extends BaseModel public function emailQuotaExceeded() :bool { - if (is_null(Cache::get($this->key))) { + if (is_null(Cache::get("email_quota".$this->key))) { return false; } try { - if (Cache::get($this->key) > $this->getDailyEmailLimit()) { + if (Cache::get("email_quota".$this->key) > $this->getDailyEmailLimit()) { if (is_null(Cache::get("throttle_notified:{$this->key}"))) { App::forgetInstance('translator'); $t = app('translator'); diff --git a/app/Models/License.php b/app/Models/License.php new file mode 100644 index 0000000000..bc4f20b70d --- /dev/null +++ b/app/Models/License.php @@ -0,0 +1,19 @@ +gateway_response); $bank_account_response = json_decode($request->bank_account_response); + if($response->status == 'requires_source_action' && $response->next_action->type == 'verify_with_microdeposits') + { + $method = $bank_account_response->payment_method->us_bank_account; + $method = $bank_account_response->payment_method->us_bank_account; + $method->id = $response->payment_method; + $method->state = 'unauthorized'; + $method->next_action = $response->next_action->verify_with_microdeposits->hosted_verification_url; + + $customer = $this->stripe->getCustomer($request->customer); + $cgt = $this->storePaymentMethod($method, GatewayType::BANK_TRANSFER, $customer); + + return redirect()->route('client.payment_methods.show', ['payment_method' => $cgt->hashed_id]); + } + $method = $bank_account_response->payment_method->us_bank_account; $method->id = $response->payment_method; $method->state = 'authorized'; @@ -547,6 +561,10 @@ class ACH $payment_meta->type = GatewayType::BANK_TRANSFER; $payment_meta->state = $state; + if(property_exists($method, 'next_action')) { + $payment_meta->next_action = $method->next_action; + } + $data = [ 'payment_meta' => $payment_meta, 'token' => $method->id, diff --git a/app/PaymentDrivers/Stripe/Jobs/PaymentIntentProcessingWebhook.php b/app/PaymentDrivers/Stripe/Jobs/PaymentIntentProcessingWebhook.php index 06ce90182e..5725e56dc9 100644 --- a/app/PaymentDrivers/Stripe/Jobs/PaymentIntentProcessingWebhook.php +++ b/app/PaymentDrivers/Stripe/Jobs/PaymentIntentProcessingWebhook.php @@ -78,8 +78,19 @@ class PaymentIntentProcessingWebhook implements ShouldQueue $this->payment_completed = true; } - } + + if(isset($transaction['payment_method'])) + { + $cgt = ClientGatewayToken::where('token', $transaction['payment_method'])->first(); + if($cgt && $cgt->meta?->state == 'unauthorized'){ + $meta = $cgt->meta; + $meta->state = 'authorized'; + $cgt->meta = $meta; + $cgt->save(); + } + } + } if ($this->payment_completed) { return; diff --git a/app/PaymentDrivers/Stripe/UpdatePaymentMethods.php b/app/PaymentDrivers/Stripe/UpdatePaymentMethods.php index 49e994c2bd..6034f62ba4 100644 --- a/app/PaymentDrivers/Stripe/UpdatePaymentMethods.php +++ b/app/PaymentDrivers/Stripe/UpdatePaymentMethods.php @@ -90,15 +90,21 @@ class UpdatePaymentMethods ); foreach ($bank_methods->data as $method) { - $token_exists = ClientGatewayToken::where([ + $token = ClientGatewayToken::where([ 'gateway_customer_reference' => $customer->id, 'token' => $method->id, 'client_id' => $client->id, 'company_id' => $client->company_id, - ])->exists(); + ])->first(); /* Already exists return */ - if ($token_exists) { + if ($token) { + + $meta = $token->meta; + $meta->state = 'authorized'; + $token->meta = $meta; + $token->save(); + continue; } diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index e476a7943e..15d441ec2e 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -11,16 +11,20 @@ namespace App\Providers; +use App\Utils\Ninja; use App\Models\Scheduler; use App\Utils\Traits\MakesHash; +use Illuminate\Support\Facades\Route; +use Illuminate\Cache\RateLimiting\Limit; +use Illuminate\Support\Facades\RateLimiter; use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; -use Illuminate\Support\Facades\Route; class RouteServiceProvider extends ServiceProvider { use MakesHash; + private int $default_rate_limit = 5000; /** * Define your route model bindings, pattern filters, etc. * @@ -40,6 +44,37 @@ class RouteServiceProvider extends ServiceProvider ->company() ->where('id', $this->decodePrimaryKey($value))->firstOrFail(); }); + + RateLimiter::for('login', function () { + + if (Ninja::isSelfHost()) { + return Limit::none(); + }else { + return Limit::perMinute(50); + } + + }); + + RateLimiter::for('api', function () { + + if (Ninja::isSelfHost()) { + return Limit::none(); + }else { + return Limit::perMinute(300); + } + + }); + + RateLimiter::for('refresh', function () { + + if (Ninja::isSelfHost()) { + return Limit::none(); + }else { + return Limit::perMinute(200); + } + + }); + } /** diff --git a/app/Services/Email/EmailDefaults.php b/app/Services/Email/EmailDefaults.php index d90ede9e61..e37de7e6b6 100644 --- a/app/Services/Email/EmailDefaults.php +++ b/app/Services/Email/EmailDefaults.php @@ -134,7 +134,7 @@ class EmailDefaults return $this; } - $this->email->email_object->from = new Address($this->email->company->owner()->email, $this->email->company->owner()->name()); + $this->email->email_object->from = new Address(config('mail.from.address'), config('mail.from.name')); return $this; } diff --git a/app/Services/Subscription/SubscriptionService.php b/app/Services/Subscription/SubscriptionService.php index a86d8cd90a..c0a6292b9b 100644 --- a/app/Services/Subscription/SubscriptionService.php +++ b/app/Services/Subscription/SubscriptionService.php @@ -11,38 +11,42 @@ namespace App\Services\Subscription; -use App\DataMapper\InvoiceItem; -use App\Factory\CreditFactory; -use App\Factory\InvoiceFactory; -use App\Factory\PaymentFactory; -use App\Factory\RecurringInvoiceFactory; -use App\Jobs\Mail\NinjaMailer; -use App\Jobs\Mail\NinjaMailerJob; -use App\Jobs\Mail\NinjaMailerObject; -use App\Jobs\Util\SystemLogger; -use App\Libraries\MultiDB; -use App\Mail\RecurringInvoice\ClientContactRequestCancellationObject; +use Carbon\Carbon; use App\Models\Client; -use App\Models\ClientContact; use App\Models\Credit; use App\Models\Invoice; +use App\Models\License; +use App\Models\Product; +use App\Models\SystemLog; +use App\Libraries\MultiDB; use App\Models\PaymentHash; use App\Models\PaymentType; -use App\Models\Product; -use App\Models\RecurringInvoice; +use Illuminate\Support\Str; use App\Models\Subscription; -use App\Models\SystemLog; +use App\Models\ClientContact; +use App\Services\Email\Email; +use App\Factory\CreditFactory; +use App\Jobs\Mail\NinjaMailer; +use App\DataMapper\InvoiceItem; +use App\Factory\InvoiceFactory; +use App\Factory\PaymentFactory; +use App\Jobs\Util\SystemLogger; +use App\Utils\Traits\MakesHash; +use App\Models\RecurringInvoice; +use App\Jobs\Mail\NinjaMailerJob; +use App\Services\Email\EmailObject; +use App\Jobs\Mail\NinjaMailerObject; +use App\Utils\Traits\CleanLineItems; use App\Repositories\CreditRepository; use App\Repositories\InvoiceRepository; use App\Repositories\PaymentRepository; -use App\Repositories\RecurringInvoiceRepository; -use App\Repositories\SubscriptionRepository; -use App\Utils\Traits\CleanLineItems; -use App\Utils\Traits\MakesHash; -use App\Utils\Traits\Notifications\UserNotifies; +use App\Factory\RecurringInvoiceFactory; use App\Utils\Traits\SubscriptionHooker; -use Carbon\Carbon; +use App\Repositories\SubscriptionRepository; +use App\Repositories\RecurringInvoiceRepository; +use App\Utils\Traits\Notifications\UserNotifies; use Illuminate\Contracts\Container\BindingResolutionException; +use App\Mail\RecurringInvoice\ClientContactRequestCancellationObject; class SubscriptionService { @@ -54,6 +58,8 @@ class SubscriptionService /** @var subscription */ private $subscription; + private const WHITE_LABEL = 4316; + private float $credit_payments = 0; public function __construct(Subscription $subscription) @@ -75,6 +81,11 @@ class SubscriptionService return $this->handlePlanChange($payment_hash); } + if ($payment_hash->data->billing_context->context == 'whitelabel') { + return $this->handleWhiteLabelPurchase($payment_hash); + } + + // if we have a recurring product - then generate a recurring invoice if (strlen($this->subscription->recurring_product_ids) >=1) { if (isset($payment_hash->data->billing_context->bundle)) { @@ -153,6 +164,45 @@ class SubscriptionService return $response; } + private function handleWhiteLabelPurchase(PaymentHash $payment_hash): bool + { + //send license to the user. + $invoice = $payment_hash->fee_invoice; + $license_key = Str::uuid()->toString(); + $invoice->public_notes = $license_key; + $invoice->save(); + $invoice->service()->touchPdf(); + + $contact = $invoice->client->contacts()->whereNotNull('email')->first(); + + $license = new License; + $license->license_key = $license_key; + $license->email = $contact ? $contact->email : ' '; + $license->first_name = $contact ? $contact->first_name : ' '; + $license->last_name = $contact ? $contact->last_name : ' '; + $license->is_claimed = 1; + $license->transaction_reference = $payment_hash?->payment?->transaction_reference ?: ' '; + $license->product_id = self::WHITE_LABEL; + + $license->save(); + + $email_object = new EmailObject; + $email_object->to = $contact->email; + $email_object->subject = ctrans('texts.white_label_link') . " " .ctrans('texts.payment_subject'); + $email_object->body = ctrans('texts.white_label_body',['license_key' => $license_key]); + $email_object->client_id = $invoice->client_id; + $email_object->client_contact_id = $contact->id; + $email_object->invitation_key = $invoice->invitations()->first()->invitation_key; + $email_object->entity_id = $invoice->id; + $email_object->entity_class = Invoice::class; + $email_object->user_id = $invoice->user_id; + + Email::dispatch($email_object, $invoice->company); + + return true; + + } + /* Starts the process to create a trial - we create a recurring invoice, which is has its next_send_date as now() + trial_duration - we then hit the client API end point to advise the trial payload diff --git a/config/ninja.php b/config/ninja.php index 59b1366a1a..bf48b77ba5 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -14,8 +14,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => '5.5.84', - 'app_tag' => '5.5.84', + 'app_version' => '5.5.85', + 'app_tag' => '5.5.85', 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', ''), diff --git a/lang/en/texts.php b/lang/en/texts.php index a72d217ab0..f9e2a1d082 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -5014,6 +5014,8 @@ $LANG = array( 'no_assigned_tasks' => 'No billable tasks for this project', 'authorization_failure' => 'Insufficient permissions to perform this action', 'authorization_sms_failure' => 'Please verify your account to send emails.', + 'white_label_body' => 'Thank you for purchasing a white label license. Your license key is :license_key.', + ); diff --git a/phpstan.neon b/phpstan.neon index 75a327b3ec..772e2bd19f 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,4 +2,4 @@ parameters: level: 2 paths: - app - - tests +# - tests diff --git a/resources/views/email/template/admin.blade.php b/resources/views/email/template/admin.blade.php index 98c8b21284..96547c9d58 100644 --- a/resources/views/email/template/admin.blade.php +++ b/resources/views/email/template/admin.blade.php @@ -1,6 +1,6 @@ @php $primary_color = isset($settings) ? $settings->primary_color : '#4caf50'; - $email_alignment = isset($settings) ? $settings->email_alignment : 'center'; + $email_alignment = isset($settings) && $settings?->email_alignment ? $settings->email_alignment : 'center'; @endphp