1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-10 13:12:50 +01:00
invoiceninja/app/Models/Account.php

528 lines
16 KiB
PHP
Raw Normal View History

<?php
2019-05-11 05:32:07 +02:00
/**
* Invoice Ninja (https://invoiceninja.com).
2019-05-11 05:32:07 +02:00
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
2023-01-28 23:21:40 +01:00
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
2019-05-11 05:32:07 +02:00
*
2021-06-16 08:58:16 +02:00
* @license https://www.elastic.co/licensing/elastic-license
2019-05-11 05:32:07 +02:00
*/
namespace App\Models;
2021-08-07 12:56:42 +02:00
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
2021-08-07 13:10:01 +02:00
use App\Mail\Ninja\EmailQuotaExceeded;
2022-03-23 22:34:52 +01:00
use App\Mail\Ninja\GmailTokenInvalid;
2020-10-28 11:10:49 +01:00
use App\Models\Presenters\AccountPresenter;
2021-08-31 13:29:18 +02:00
use App\Notifications\Ninja\EmailQuotaNotification;
2022-03-23 22:34:52 +01:00
use App\Notifications\Ninja\GmailCredentialNotification;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
2021-04-10 00:27:02 +02:00
use Carbon\Carbon;
2020-10-28 11:10:49 +01:00
use DateTime;
use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException;
2021-08-31 13:29:18 +02:00
use Illuminate\Support\Facades\App;
2021-08-07 11:55:18 +02:00
use Illuminate\Support\Facades\Cache;
2021-08-07 12:56:42 +02:00
use Laracasts\Presenter\PresentableTrait;
class Account extends BaseModel
{
use PresentableTrait;
use MakesHash;
2022-08-19 11:19:17 +02:00
private $free_plan_email_quota = 20;
2021-08-08 13:50:13 +02:00
private $paid_plan_email_quota = 500;
/**
* @var string
*/
2020-10-28 11:10:49 +01:00
protected $presenter = AccountPresenter::class;
/**
* @var array
*/
protected $fillable = [
'plan',
'plan_term',
'plan_price',
'plan_paid',
'plan_started',
'plan_expires',
2018-10-24 05:50:15 +02:00
'utm_source',
'utm_medium',
'utm_campaign',
'utm_term',
'utm_content',
'user_agent',
2022-01-22 05:22:59 +01:00
'platform',
2022-06-16 05:04:05 +02:00
'set_react_as_default_ap',
2022-10-07 01:10:10 +02:00
'inapp_transaction_id',
'num_users',
];
/**
* @var array
*/
protected $dates = [
'deleted_at',
'promo_expires',
'discount_expires',
2021-07-14 09:07:02 +02:00
// 'trial_started',
2021-07-11 06:54:57 +02:00
// 'plan_expires'
];
2021-11-03 14:18:33 +01:00
protected $casts = [
2021-11-05 15:49:50 +01:00
'updated_at' => 'timestamp',
'created_at' => 'timestamp',
'deleted_at' => 'timestamp',
2022-06-16 05:04:05 +02:00
'onboarding' => 'object',
'set_react_as_default_ap' => 'bool'
2021-11-03 14:18:33 +01:00
];
const PLAN_FREE = 'free';
const PLAN_PRO = 'pro';
const PLAN_ENTERPRISE = 'enterprise';
const PLAN_WHITE_LABEL = 'white_label';
const PLAN_TERM_MONTHLY = 'month';
const PLAN_TERM_YEARLY = 'year';
const FEATURE_TASKS = 'tasks';
const FEATURE_EXPENSES = 'expenses';
const FEATURE_QUOTES = 'quotes';
2022-06-13 11:59:24 +02:00
const FEATURE_PURCHASE_ORDERS = 'purchase_orders';
const FEATURE_CUSTOMIZE_INVOICE_DESIGN = 'custom_designs';
const FEATURE_DIFFERENT_DESIGNS = 'different_designs';
const FEATURE_EMAIL_TEMPLATES_REMINDERS = 'template_reminders';
const FEATURE_INVOICE_SETTINGS = 'invoice_settings';
const FEATURE_CUSTOM_EMAILS = 'custom_emails';
const FEATURE_PDF_ATTACHMENT = 'pdf_attachments';
const FEATURE_MORE_INVOICE_DESIGNS = 'more_invoice_designs';
const FEATURE_REPORTS = 'reports';
const FEATURE_BUY_NOW_BUTTONS = 'buy_now_buttons';
const FEATURE_API = 'api';
const FEATURE_CLIENT_PORTAL_PASSWORD = 'client_portal_password';
const FEATURE_CUSTOM_URL = 'custom_url';
const FEATURE_MORE_CLIENTS = 'more_clients';
const FEATURE_WHITE_LABEL = 'white_label';
const FEATURE_REMOVE_CREATED_BY = 'remove_created_by';
const FEATURE_USERS = 'users'; // Grandfathered for old Pro users
const FEATURE_DOCUMENTS = 'documents';
const FEATURE_USER_PERMISSIONS = 'permissions';
const RESULT_FAILURE = 'failure';
const RESULT_SUCCESS = 'success';
public function getEntityType()
{
return self::class;
}
public function users()
{
return $this->hasMany(User::class)->withTrashed();
}
public function default_company()
{
2019-03-27 05:50:13 +01:00
return $this->hasOne(Company::class, 'id', 'default_company_id');
}
public function payment()
{
return $this->belongsTo(Payment::class)->withTrashed();
}
public function companies()
{
return $this->hasMany(Company::class);
}
2022-08-12 05:41:55 +02:00
public function bank_integrations()
{
return $this->hasMany(BankIntegration::class);
}
public function company_users()
{
return $this->hasMany(CompanyUser::class);
}
2019-09-18 14:43:37 +02:00
2021-07-11 06:54:57 +02:00
public function owner()
{
return $this->hasMany(CompanyUser::class)->where('is_owner', true)->first() ? $this->hasMany(CompanyUser::class)->where('is_owner', true)->first()->user : false;
}
2019-09-18 14:43:37 +02:00
public function getPlan()
{
2021-08-12 14:07:11 +02:00
if(Carbon::parse($this->plan_expires)->lt(now()))
2021-08-12 14:04:50 +02:00
return '';
2019-09-18 14:43:37 +02:00
return $this->plan ?: '';
}
public function hasFeature($feature)
{
$plan_details = $this->getPlanDetails();
$self_host = ! Ninja::isNinja();
switch ($feature) {
case self::FEATURE_TASKS:
case self::FEATURE_EXPENSES:
case self::FEATURE_QUOTES:
2022-06-13 11:59:24 +02:00
case self::FEATURE_PURCHASE_ORDERS:
return true;
case self::FEATURE_CUSTOMIZE_INVOICE_DESIGN:
case self::FEATURE_DIFFERENT_DESIGNS:
case self::FEATURE_EMAIL_TEMPLATES_REMINDERS:
case self::FEATURE_INVOICE_SETTINGS:
case self::FEATURE_CUSTOM_EMAILS:
case self::FEATURE_PDF_ATTACHMENT:
case self::FEATURE_MORE_INVOICE_DESIGNS:
case self::FEATURE_REPORTS:
case self::FEATURE_BUY_NOW_BUTTONS:
case self::FEATURE_API:
case self::FEATURE_CLIENT_PORTAL_PASSWORD:
case self::FEATURE_CUSTOM_URL:
return $self_host || ! empty($plan_details);
// Pro; No trial allowed, unless they're trialing enterprise with an active pro plan
case self::FEATURE_MORE_CLIENTS:
return $self_host || ! empty($plan_details) && (! $plan_details['trial'] || ! empty($this->getPlanDetails(false, false)));
// White Label
case self::FEATURE_WHITE_LABEL:
if (! $self_host && $plan_details && ! $plan_details['expires']) {
return false;
}
// Fallthrough
// no break
case self::FEATURE_REMOVE_CREATED_BY:
return ! empty($plan_details); // A plan is required even for self-hosted users
// Enterprise; No Trial allowed; grandfathered for old pro users
case self::FEATURE_USERS:// Grandfathered for old Pro users
2021-03-07 11:14:53 +01:00
if ($plan_details && $plan_details['trial']) {
// Do they have a non-trial plan?
2021-03-07 11:14:53 +01:00
$plan_details = $this->getPlanDetails(false, false);
}
2021-03-07 11:14:53 +01:00
return $self_host || ! empty($plan_details) && ($plan_details['plan'] == self::PLAN_ENTERPRISE);
// Enterprise; No Trial allowed
case self::FEATURE_DOCUMENTS:
case self::FEATURE_USER_PERMISSIONS:
2021-03-07 11:14:53 +01:00
return $self_host || ! empty($plan_details) && $plan_details['plan'] == self::PLAN_ENTERPRISE && ! $plan_details['trial'];
default:
return false;
}
}
public function isPaid()
{
return Ninja::isNinja() ? ($this->isPaidHostedClient() && ! $this->isTrial()) : $this->hasFeature(self::FEATURE_WHITE_LABEL);
}
public function isPaidHostedClient()
{
if (! Ninja::isNinja()) {
return false;
}
2022-07-22 03:00:29 +02:00
if($this->plan_expires && Carbon::parse($this->plan_expires)->lt(now()))
return false;
return $this->plan == 'pro' || $this->plan == 'enterprise';
}
public function isFreeHostedClient()
{
if (! Ninja::isNinja()) {
return false;
}
2022-07-22 03:00:29 +02:00
if($this->plan_expires && Carbon::parse($this->plan_expires)->lt(now()))
return true;
return $this->plan == 'free' || is_null($this->plan) || empty($this->plan);
}
public function isEnterpriseClient()
{
if (! Ninja::isNinja()) {
return false;
}
return $this->plan == 'enterprise';
}
public function isTrial()
{
if (! Ninja::isNinja()) {
return false;
}
$plan_details = $this->getPlanDetails();
return $plan_details && $plan_details['trial'];
}
2021-06-27 11:20:56 +02:00
public function startTrial($plan)
{
if (! Ninja::isNinja()) {
return;
}
if ($this->trial_started && $this->trial_started != '0000-00-00') {
return;
}
$this->trial_plan = $plan;
$this->trial_started = now();
$this->save();
}
public function getPlanDetails($include_inactive = false, $include_trial = true)
{
$plan = $this->plan;
$price = $this->plan_price;
$trial_plan = $this->trial_plan;
if ((! $plan || $plan == self::PLAN_FREE) && (! $trial_plan || ! $include_trial)) {
return null;
}
$trial_active = false;
2021-04-06 06:02:27 +02:00
2021-10-04 01:17:26 +02:00
//14 day trial
$duration = 60*60*24*14;
if ($trial_plan && $include_trial) {
2021-04-06 05:05:40 +02:00
$trial_started = $this->trial_started;
2021-10-04 01:17:26 +02:00
$trial_expires = Carbon::parse($this->trial_started)->addSeconds($duration);
2021-04-06 06:02:27 +02:00
if($trial_expires->greaterThan(now())){
$trial_active = true;
2021-04-06 05:05:40 +02:00
}
}
$plan_active = false;
if ($plan) {
if ($this->plan_expires == null) {
$plan_active = true;
$plan_expires = false;
} else {
2021-04-10 00:27:02 +02:00
$plan_expires = Carbon::parse($this->plan_expires);
if ($plan_expires->greaterThan(now())) {
$plan_active = true;
}
}
}
if (! $include_inactive && ! $plan_active && ! $trial_active) {
return null;
}
2021-04-06 06:02:27 +02:00
// Should we show plan details or trial details?
if (($plan && ! $trial_plan) || ! $include_trial) {
$use_plan = true;
} elseif (! $plan && $trial_plan) {
$use_plan = false;
} else {
// There is both a plan and a trial
if (! empty($plan_active) && empty($trial_active)) {
$use_plan = true;
} elseif (empty($plan_active) && ! empty($trial_active)) {
$use_plan = false;
} elseif (! empty($plan_active) && ! empty($trial_active)) {
// Both are active; use whichever is a better plan
if ($plan == self::PLAN_ENTERPRISE) {
$use_plan = true;
} elseif ($trial_plan == self::PLAN_ENTERPRISE) {
$use_plan = false;
} else {
// They're both the same; show the plan
$use_plan = true;
}
} else {
// Neither are active; use whichever expired most recently
$use_plan = $plan_expires >= $trial_expires;
}
}
if ($use_plan) {
return [
'account_id' => $this->id,
'num_users' => $this->num_users,
'plan_price' => $price,
'trial' => false,
'plan' => $plan,
2022-09-14 00:54:59 +02:00
'started' => $this->plan_started ? DateTime::createFromFormat('Y-m-d', $this->plan_started) : false,
'expires' => $plan_expires,
2022-09-14 00:54:59 +02:00
'paid' => $this->plan_paid ? DateTime::createFromFormat('Y-m-d', $this->plan_paid) : false,
'term' => $this->plan_term,
'active' => $plan_active,
];
} else {
return [
'account_id' => $this->id,
'num_users' => 1,
'plan_price' => 0,
'trial' => true,
'plan' => $trial_plan,
'started' => $trial_started,
'expires' => $trial_expires,
'active' => $trial_active,
];
}
}
2021-06-11 00:20:46 +02:00
2021-08-07 11:55:18 +02:00
public function getDailyEmailLimit()
{
if($this->is_flagged)
return 0;
2021-08-07 11:55:18 +02:00
2022-06-18 00:54:58 +02:00
if(Carbon::createFromTimestamp($this->created_at)->diffInWeeks() == 0)
2022-06-19 05:01:29 +02:00
return 20;
2022-06-18 00:54:58 +02:00
2022-06-29 03:37:40 +02:00
if(Carbon::createFromTimestamp($this->created_at)->diffInWeeks() <= 2 && !$this->payment_id)
return 20;
2021-08-08 13:50:13 +02:00
if($this->isPaid()){
$limit = $this->paid_plan_email_quota;
2022-08-19 11:19:17 +02:00
$limit += Carbon::createFromTimestamp($this->created_at)->diffInMonths() * 50;
2021-08-08 13:50:13 +02:00
}
else{
$limit = $this->free_plan_email_quota;
2022-08-19 11:19:17 +02:00
$limit += Carbon::createFromTimestamp($this->created_at)->diffInMonths() * 10;
2021-08-08 13:50:13 +02:00
}
2021-08-07 11:55:18 +02:00
return min($limit, 5000);
}
2021-08-10 03:40:58 +02:00
public function emailsSent()
{
if(is_null(Cache::get($this->key)))
return 0;
return Cache::get($this->key);
}
2021-08-07 11:55:18 +02:00
public function emailQuotaExceeded() :bool
{
if(is_null(Cache::get($this->key)))
return false;
2021-08-09 12:33:59 +02:00
try {
if(Cache::get($this->key) > $this->getDailyEmailLimit()) {
2021-08-07 12:56:42 +02:00
2021-08-09 12:33:59 +02:00
if(is_null(Cache::get("throttle_notified:{$this->key}"))) {
2021-08-07 12:56:42 +02:00
2021-08-31 13:29:18 +02:00
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations($this->companies()->first()->settings));
2021-08-09 12:33:59 +02:00
$nmo = new NinjaMailerObject;
$nmo->mailable = new EmailQuotaExceeded($this->companies()->first());
$nmo->company = $this->companies()->first();
$nmo->settings = $this->companies()->first()->settings;
$nmo->to_user = $this->companies()->first()->owner();
NinjaMailerJob::dispatch($nmo, true);
2021-08-07 12:56:42 +02:00
2021-08-09 12:33:59 +02:00
Cache::put("throttle_notified:{$this->key}", true, 60 * 24);
2021-08-31 13:29:18 +02:00
if(config('ninja.notification.slack'))
$this->companies()->first()->notification(new EmailQuotaNotification($this))->ninja();
2021-08-09 12:33:59 +02:00
}
2021-08-07 12:56:42 +02:00
2021-08-09 12:33:59 +02:00
return true;
}
}
catch(\Exception $e){
2021-10-01 00:55:35 +02:00
\Sentry\captureMessage("I encountered an error with email quotas for account {$this->key} - defaulting to SEND");
2021-08-07 12:56:42 +02:00
}
return false;
2021-08-07 11:55:18 +02:00
}
2022-03-23 22:34:52 +01:00
public function gmailCredentialNotification() :bool
{
2022-05-03 01:45:40 +02:00
nlog("checking if gmail credential notification has already been sent");
2022-03-23 22:34:52 +01:00
if(is_null(Cache::get($this->key)))
return false;
2022-05-03 01:45:40 +02:00
nlog("Sending notification");
2022-03-23 22:34:52 +01:00
try {
if(is_null(Cache::get("gmail_credentials_notified:{$this->key}"))) {
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations($this->companies()->first()->settings));
$nmo = new NinjaMailerObject;
$nmo->mailable = new GmailTokenInvalid($this->companies()->first());
$nmo->company = $this->companies()->first();
$nmo->settings = $this->companies()->first()->settings;
$nmo->to_user = $this->companies()->first()->owner();
NinjaMailerJob::dispatch($nmo, true);
2022-03-23 22:34:52 +01:00
Cache::put("gmail_credentials_notified:{$this->key}", true, 60 * 24);
if(config('ninja.notification.slack'))
$this->companies()->first()->notification(new GmailCredentialNotification($this))->ninja();
}
return true;
}
catch(\Exception $e){
\Sentry\captureMessage("I encountered an error with sending with gmail for account {$this->key}");
}
return false;
}
public function resolveRouteBinding($value, $field = null)
{
if (is_numeric($value)) {
throw new ModelNotFoundException("Record with value {$value} not found");
}
return $this
->where('id', $this->decodePrimaryKey($value))
->firstOrFail();
}
2022-07-11 02:42:05 +02:00
public function getTrialDays()
{
if($this->payment_id)
return 0;
$plan_expires = Carbon::parse($this->plan_expires);
2022-07-17 12:33:34 +02:00
if(!$this->payment_id && $plan_expires->gt(now())){
$diff = $plan_expires->diffInDays();
if($diff > 14);
return 0;
return $diff;
}
2022-07-11 02:42:05 +02:00
return 0;
}
}