diff --git a/app/Console/Commands/SendReminders.php b/app/Console/Commands/SendReminders.php index 3243ac016e..58c38a7d81 100644 --- a/app/Console/Commands/SendReminders.php +++ b/app/Console/Commands/SendReminders.php @@ -36,7 +36,7 @@ class SendReminders extends Command $this->info(count($accounts).' accounts found'); foreach ($accounts as $account) { - if (!$account->isPro()) { + if (!$account->hasFeature(FEATURE_EMAIL_TEMPLATES_REMINDERS)) { continue; } diff --git a/app/Console/Commands/SendRenewalInvoices.php b/app/Console/Commands/SendRenewalInvoices.php index 0faf1304a9..64db2fad4f 100644 --- a/app/Console/Commands/SendRenewalInvoices.php +++ b/app/Console/Commands/SendRenewalInvoices.php @@ -5,7 +5,7 @@ use DateTime; use Illuminate\Console\Command; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; -use App\Models\Account; +use App\Models\Company; use App\Ninja\Mailers\ContactMailer as Mailer; use App\Ninja\Repositories\AccountRepository; @@ -30,24 +30,32 @@ class SendRenewalInvoices extends Command $today = new DateTime(); $sentTo = []; - // get all accounts with pro plans expiring in 10 days - $accounts = Account::whereRaw('datediff(curdate(), pro_plan_paid) = 355') + // get all accounts with plans expiring in 10 days + $companies = Company::whereRaw('datediff(plan_expires, curdate()) = 10') ->orderBy('id') ->get(); - $this->info(count($accounts).' accounts found'); + $this->info(count($companies).' companies found'); - foreach ($accounts as $account) { - // don't send multiple invoices to multi-company users - if ($userAccountId = $this->accountRepo->getUserAccountId($account)) { - if (isset($sentTo[$userAccountId])) { - continue; - } else { - $sentTo[$userAccountId] = true; - } + foreach ($companies as $company) { + if (!count($company->accounts)) { + continue; } - + + $account = $company->accounts->sortBy('id')->first(); + $plan = $company->plan; + $term = $company->plan_term; + + if ($company->pending_plan) { + $plan = $company->pending_plan; + $term = $company->pending_term; + } + + if ($plan == PLAN_FREE || !$plan || !$term ){ + continue; + } + $client = $this->accountRepo->getNinjaClient($account); - $invitation = $this->accountRepo->createNinjaInvoice($client, $account); + $invitation = $this->accountRepo->createNinjaInvoice($client, $account, $plan, $term); // set the due date to 10 days from now $invoice = $invitation->invoice; diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 099f252ca3..0ccb33545f 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -31,6 +31,7 @@ use App\Ninja\Mailers\ContactMailer; use App\Events\UserSignedUp; use App\Events\UserSettingsChanged; use App\Services\AuthService; +use App\Services\PaymentService; use App\Http\Requests\UpdateAccountRequest; @@ -40,8 +41,9 @@ class AccountController extends BaseController protected $userMailer; protected $contactMailer; protected $referralRepository; + protected $paymentService; - public function __construct(AccountRepository $accountRepo, UserMailer $userMailer, ContactMailer $contactMailer, ReferralRepository $referralRepository) + public function __construct(AccountRepository $accountRepo, UserMailer $userMailer, ContactMailer $contactMailer, ReferralRepository $referralRepository, PaymentService $paymentService) { //parent::__construct(); @@ -49,6 +51,7 @@ class AccountController extends BaseController $this->userMailer = $userMailer; $this->contactMailer = $contactMailer; $this->referralRepository = $referralRepository; + $this->paymentService = $paymentService; } public function demo() @@ -111,10 +114,135 @@ class AccountController extends BaseController public function enableProPlan() { - $invitation = $this->accountRepo->enableProPlan(); + if (Auth::user()->isPro() && ! Auth::user()->isTrial()) { + return false; + } + + $invitation = $this->accountRepo->enablePlan(); return $invitation->invitation_key; } + + public function changePlan() { + $user = Auth::user(); + $account = $user->account; + + $plan = Input::get('plan'); + $term = Input::get('plan_term'); + + $planDetails = $account->getPlanDetails(false, false); + + $credit = 0; + if ($planDetails) { + if ($planDetails['plan'] == PLAN_PRO && $plan == PLAN_ENTERPRISE) { + // Upgrade from pro to enterprise + if($planDetails['term'] == PLAN_TERM_YEARLY && $term == PLAN_TERM_MONTHLY) { + // Upgrade to yearly for now; switch to monthly in a year + $pending_monthly = true; + $term = PLAN_TERM_YEARLY; + } + + $new_plan = array( + 'plan' => PLAN_ENTERPRISE, + 'term' => $term, + ); + } elseif ($planDetails['plan'] == $plan) { + // Term switch + if ($planDetails['term'] == PLAN_TERM_YEARLY && $term == PLAN_TERM_MONTHLY) { + $pending_change = array( + 'plan' => $plan, + 'term' => $term + ); + } elseif ($planDetails['term'] == PLAN_TERM_MONTHLY && $term == PLAN_TERM_YEARLY) { + $new_plan = array( + 'plan' => $plan, + 'term' => $term, + ); + } else { + // Cancel the pending change + $account->company->pending_plan = null; + $account->company->pending_term = null; + $account->company->save(); + Session::flash('message', trans('texts.updated_plan')); + } + } else { + // Downgrade + $refund_deadline = clone $planDetails['started']; + $refund_deadline->modify('+30 days'); + + if ($plan == PLAN_FREE && $refund_deadline >= date_create()) { + // Refund + $account->company->plan = null; + $account->company->plan_term = null; + $account->company->plan_started = null; + $account->company->plan_expires = null; + $account->company->plan_paid = null; + $account->company->pending_plan = null; + $account->company->pending_term = null; + + if ($account->company->payment) { + $payment = $account->company->payment; + + $gateway = $this->paymentService->createGateway($payment->account_gateway); + $refund = $gateway->refund(array( + 'transactionReference' => $payment->transaction_reference, + 'amount' => $payment->amount * 100 + )); + $refund->send(); + $payment->delete(); + Session::flash('message', trans('texts.plan_refunded')); + \Log::info("Refunded Plan Payment: {$account->name} - {$user->email}"); + } else { + Session::flash('message', trans('texts.updated_plan')); + } + + $account->company->save(); + + } else { + $pending_change = array( + 'plan' => $plan, + 'term' => $plan == PLAN_FREE ? null : $term, + ); + } + } + + if (!empty($new_plan)) { + $time_used = $planDetails['paid']->diff(date_create()); + $days_used = $time_used->days; + + if ($time_used->invert) { + // They paid in advance + $days_used *= -1; + } + + $days_total = $planDetails['paid']->diff($planDetails['expires'])->days; + + $percent_used = $days_used / $days_total; + $old_plan_price = Account::$plan_prices[$planDetails['plan']][$planDetails['term']]; + $credit = $old_plan_price * (1 - $percent_used); + } + } else { + $new_plan = array( + 'plan' => $plan, + 'term' => $term, + ); + } + + if (!empty($pending_change) && empty($new_plan)) { + $account->company->pending_plan = $pending_change['plan']; + $account->company->pending_term = $pending_change['term']; + $account->company->save(); + + Session::flash('message', trans('texts.updated_plan')); + } + + if (!empty($new_plan)) { + $invitation = $this->accountRepo->enablePlan($new_plan['plan'], $new_plan['term'], $credit, !empty($pending_monthly)); + return Redirect::to('payment/'.$invitation->invitation_key); + } + + return Redirect::to('/settings/'.ACCOUNT_MANAGEMENT, 301); + } public function setTrashVisible($entityType, $visible) { @@ -149,6 +277,8 @@ class AccountController extends BaseController return self::showInvoiceSettings(); } elseif ($section == ACCOUNT_IMPORT_EXPORT) { return View::make('accounts.import_export', ['title' => trans('texts.import_export')]); + } elseif ($section == ACCOUNT_MANAGEMENT) { + return self::showAccountManagement(); } elseif ($section == ACCOUNT_INVOICE_DESIGN || $section == ACCOUNT_CUSTOMIZE_DESIGN) { return self::showInvoiceDesign($section); } elseif ($section == ACCOUNT_CLIENT_PORTAL) { @@ -232,6 +362,18 @@ class AccountController extends BaseController return View::make('accounts.details', $data); } + private function showAccountManagement() + { + $account = Auth::user()->account; + $data = [ + 'account' => $account, + 'planDetails' => $account->getPlanDetails(true), + 'title' => trans('texts.account_management'), + ]; + + return View::make('accounts.management', $data); + } + public function showUserDetails() { $oauthLoginUrls = []; @@ -379,7 +521,7 @@ class AccountController extends BaseController $invoice->client = $client; $invoice->invoice_items = [$invoiceItem]; - //$invoice->documents = $account->isPro() ? [$document] : []; + //$invoice->documents = $account->hasFeature(FEATURE_DOCUMENTS) ? [$document] : []; $invoice->documents = []; $data['account'] = $account; @@ -553,7 +695,7 @@ class AccountController extends BaseController private function saveCustomizeDesign() { - if (Auth::user()->account->isPro()) { + if (Auth::user()->account->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN)) { $account = Auth::user()->account; $account->custom_design = Input::get('custom_design'); $account->invoice_design_id = CUSTOM_DESIGN; @@ -568,7 +710,7 @@ class AccountController extends BaseController private function saveClientPortal() { // Only allowed for pro Invoice Ninja users or white labeled self-hosted users - if ((Utils::isNinja() && Auth::user()->account->isPro()) || Auth::user()->account->isWhiteLabel()) { + if (Auth::user()->account->hasFeature(FEATURE_CLIENT_PORTAL_CSS)) { $input_css = Input::get('client_view_css'); if (Utils::isNinja()) { // Allow referencing the body element @@ -619,7 +761,7 @@ class AccountController extends BaseController private function saveEmailTemplates() { - if (Auth::user()->account->isPro()) { + if (Auth::user()->account->hasFeature(FEATURE_EMAIL_TEMPLATES_REMINDERS)) { $account = Auth::user()->account; foreach ([ENTITY_INVOICE, ENTITY_QUOTE, ENTITY_PAYMENT, REMINDER1, REMINDER2, REMINDER3] as $type) { @@ -681,7 +823,7 @@ class AccountController extends BaseController private function saveEmailSettings() { - if (Auth::user()->account->isPro()) { + if (Auth::user()->account->hasFeature(FEATURE_CUSTOM_EMAILS)) { $rules = []; $user = Auth::user(); $iframeURL = preg_replace('/[^a-zA-Z0-9_\-\:\/\.]/', '', substr(strtolower(Input::get('iframe_url')), 0, MAX_IFRAME_URL_LENGTH)); @@ -724,7 +866,7 @@ class AccountController extends BaseController private function saveInvoiceSettings() { - if (Auth::user()->account->isPro()) { + if (Auth::user()->account->hasFeature(FEATURE_INVOICE_SETTINGS)) { $rules = [ 'invoice_number_pattern' => 'has_counter', 'quote_number_pattern' => 'has_counter', @@ -805,7 +947,7 @@ class AccountController extends BaseController private function saveInvoiceDesign() { - if (Auth::user()->account->isPro()) { + if (Auth::user()->account->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN)) { $account = Auth::user()->account; $account->hide_quantity = Input::get('hide_quantity') ? true : false; $account->hide_paid_to_date = Input::get('hide_paid_to_date') ? true : false; @@ -1054,7 +1196,7 @@ class AccountController extends BaseController $user->registered = true; $user->save(); - $user->account->startTrial(); + $user->account->startTrial(PLAN_PRO); if (Input::get('go_pro') == 'true') { Session::set(REQUESTED_PRO_PLAN, true); @@ -1110,6 +1252,9 @@ class AccountController extends BaseController \Log::info("Canceled Account: {$account->name} - {$user->email}"); $this->accountRepo->unlinkAccount($account); + if ($account->company->accounts->count() == 1) { + $account->company->forceDelete(); + } $account->forceDelete(); Auth::logout(); @@ -1126,12 +1271,12 @@ class AccountController extends BaseController return Redirect::to('/settings/'.ACCOUNT_USER_DETAILS)->with('message', trans('texts.confirmation_resent')); } - public function startTrial() + public function startTrial($plan) { $user = Auth::user(); - if ($user->isEligibleForTrial()) { - $user->account->startTrial(); + if ($user->isEligibleForTrial($plan)) { + $user->account->startTrial($plan); } return Redirect::back()->with('message', trans('texts.trial_success')); diff --git a/app/Http/Controllers/Auth/AuthController.php b/app/Http/Controllers/Auth/AuthController.php index bd8813912d..e599890c6a 100644 --- a/app/Http/Controllers/Auth/AuthController.php +++ b/app/Http/Controllers/Auth/AuthController.php @@ -133,6 +133,9 @@ class AuthController extends Controller { if (Auth::check() && !Auth::user()->registered) { $account = Auth::user()->account; $this->accountRepo->unlinkAccount($account); + if ($account->company->accounts->count() == 1) { + $account->company->forceDelete(); + } $account->forceDelete(); } diff --git a/app/Http/Controllers/ClientAuth/AuthController.php b/app/Http/Controllers/ClientAuth/AuthController.php index c88c8a4b85..ed5f199ac2 100644 --- a/app/Http/Controllers/ClientAuth/AuthController.php +++ b/app/Http/Controllers/ClientAuth/AuthController.php @@ -33,7 +33,7 @@ class AuthController extends Controller { $client = $invoice->client; $account = $client->account; - $data['hideLogo'] = $account->isWhiteLabel(); + $data['hideLogo'] = $account->hasFeature(FEATURE_WHITE_LABEL); $data['clientViewCSS'] = $account->clientViewCSS(); $data['clientFontUrl'] = $account->getFontsUrl(); } diff --git a/app/Http/Controllers/ClientAuth/PasswordController.php b/app/Http/Controllers/ClientAuth/PasswordController.php index beefb01612..35576b47ae 100644 --- a/app/Http/Controllers/ClientAuth/PasswordController.php +++ b/app/Http/Controllers/ClientAuth/PasswordController.php @@ -50,7 +50,7 @@ class PasswordController extends Controller { $client = $invoice->client; $account = $client->account; - $data['hideLogo'] = $account->isWhiteLabel(); + $data['hideLogo'] = $account->hasFeature(FEATURE_WHITE_LABEL); $data['clientViewCSS'] = $account->clientViewCSS(); $data['clientFontUrl'] = $account->getFontsUrl(); } @@ -117,7 +117,7 @@ class PasswordController extends Controller { $client = $invoice->client; $account = $client->account; - $data['hideLogo'] = $account->isWhiteLabel(); + $data['hideLogo'] = $account->hasFeature(FEATURE_WHITE_LABEL); $data['clientViewCSS'] = $account->clientViewCSS(); $data['clientFontUrl'] = $account->getFontsUrl(); } diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index 7f2fac3eed..2992eb9a95 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -114,7 +114,7 @@ class ClientController extends BaseController if(Task::canCreate()){ $actionLinks[] = ['label' => trans('texts.new_task'), 'url' => URL::to('/tasks/create/'.$client->public_id)]; } - if (Utils::isPro() && Invoice::canCreate()) { + if (Utils::hasFeature(FEATURE_QUOTES) && Invoice::canCreate()) { $actionLinks[] = ['label' => trans('texts.new_quote'), 'url' => URL::to('/quotes/create/'.$client->public_id)]; } @@ -201,7 +201,7 @@ class ClientController extends BaseController if (Auth::user()->account->isNinjaAccount()) { if ($account = Account::whereId($client->public_id)->first()) { - $data['proPlanPaid'] = $account['pro_plan_paid']; + $data['planDetails'] = $account->getPlanDetails(false, false); } } diff --git a/app/Http/Controllers/DocumentController.php b/app/Http/Controllers/DocumentController.php index 8af0d560d6..15f3ced57f 100644 --- a/app/Http/Controllers/DocumentController.php +++ b/app/Http/Controllers/DocumentController.php @@ -114,7 +114,7 @@ class DocumentController extends BaseController public function postUpload() { - if (!Utils::isPro()) { + if (!Utils::hasFeature(FEATURE_DOCUMENTS)) { return; } diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index 794f09404a..774ed7a7a7 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -132,7 +132,11 @@ class InvoiceController extends BaseController $invoice->start_date = Utils::fromSqlDate($invoice->start_date); $invoice->end_date = Utils::fromSqlDate($invoice->end_date); $invoice->last_sent_date = Utils::fromSqlDate($invoice->last_sent_date); - $invoice->is_pro = Auth::user()->isPro(); + $invoice->features = [ + 'customize_invoice_design' => Auth::user()->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN), + 'remove_created_by' => Auth::user()->hasFeature(FEATURE_REMOVE_CREATED_BY), + 'invoice_settings' => Auth::user()->hasFeature(FEATURE_INVOICE_SETTINGS), + ]; $actions = [ ['url' => 'javascript:onCloneClick()', 'label' => trans("texts.clone_{$entityType}")], @@ -573,7 +577,11 @@ class InvoiceController extends BaseController $invoice->load('user', 'invoice_items', 'documents', 'expenses', 'expenses.documents', 'account.country', 'client.contacts', 'client.country'); $invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date); $invoice->due_date = Utils::fromSqlDate($invoice->due_date); - $invoice->is_pro = Auth::user()->isPro(); + $invoice->features = [ + 'customize_invoice_design' => Auth::user()->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN), + 'remove_created_by' => Auth::user()->hasFeature(FEATURE_REMOVE_CREATED_BY), + 'invoice_settings' => Auth::user()->hasFeature(FEATURE_INVOICE_SETTINGS), + ]; $invoice->is_quote = intval($invoice->is_quote); $activityTypeId = $invoice->is_quote ? ACTIVITY_TYPE_UPDATE_QUOTE : ACTIVITY_TYPE_UPDATE_INVOICE; @@ -591,7 +599,11 @@ class InvoiceController extends BaseController $backup = json_decode($activity->json_backup); $backup->invoice_date = Utils::fromSqlDate($backup->invoice_date); $backup->due_date = Utils::fromSqlDate($backup->due_date); - $backup->is_pro = Auth::user()->isPro(); + $invoice->features = [ + 'customize_invoice_design' => Auth::user()->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN), + 'remove_created_by' => Auth::user()->hasFeature(FEATURE_REMOVE_CREATED_BY), + 'invoice_settings' => Auth::user()->hasFeature(FEATURE_INVOICE_SETTINGS), + ]; $backup->is_quote = isset($backup->is_quote) && intval($backup->is_quote); $backup->account = $invoice->account->toArray(); diff --git a/app/Http/Controllers/PaymentController.php b/app/Http/Controllers/PaymentController.php index 2f2cdd92cc..1f53b4a0bc 100644 --- a/app/Http/Controllers/PaymentController.php +++ b/app/Http/Controllers/PaymentController.php @@ -191,7 +191,7 @@ class PaymentController extends BaseController 'currencyId' => $client->getCurrencyId(), 'currencyCode' => $client->currency ? $client->currency->code : ($account->currency ? $account->currency->code : 'USD'), 'account' => $client->account, - 'hideLogo' => $account->isWhiteLabel(), + 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), 'hideHeader' => $account->isNinjaAccount(), 'clientViewCSS' => $account->clientViewCSS(), 'clientFontUrl' => $account->getFontsUrl(), diff --git a/app/Http/Controllers/PublicClientController.php b/app/Http/Controllers/PublicClientController.php index b7649471f0..32d6d53c38 100644 --- a/app/Http/Controllers/PublicClientController.php +++ b/app/Http/Controllers/PublicClientController.php @@ -72,7 +72,11 @@ class PublicClientController extends BaseController $invoice->invoice_date = Utils::fromSqlDate($invoice->invoice_date); $invoice->due_date = Utils::fromSqlDate($invoice->due_date); - $invoice->is_pro = $account->isPro(); + $invoice->features = [ + 'customize_invoice_design' => $account->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN), + 'remove_created_by' => $account->hasFeature(FEATURE_REMOVE_CREATED_BY), + 'invoice_settings' => $account->hasFeature(FEATURE_INVOICE_SETTINGS), + ]; $invoice->invoice_fonts = $account->getFontsData(); if ($invoice->invoice_design_id == CUSTOM_DESIGN) { @@ -122,10 +126,10 @@ class PublicClientController extends BaseController 'account' => $account, 'showApprove' => $showApprove, 'showBreadcrumbs' => false, - 'hideLogo' => $account->isWhiteLabel(), + 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), 'hideHeader' => $account->isNinjaAccount() || !$account->enable_client_portal, 'hideDashboard' => !$account->enable_client_portal_dashboard, - 'showDocuments' => $account->isPro(), + 'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS), 'clientViewCSS' => $account->clientViewCSS(), 'clientFontUrl' => $account->getFontsUrl(), 'invoice' => $invoice->hidePrivateFields(), @@ -140,7 +144,7 @@ class PublicClientController extends BaseController 'phantomjs' => Input::has('phantomjs'), ); - if($account->isPro() && $this->canCreateZip()){ + if($account->hasFeature(FEATURE_DOCUMENTS) && $this->canCreateZip()){ $zipDocs = $this->getInvoiceZipDocuments($invoice, $size); if(count($zipDocs) > 1){ @@ -220,8 +224,8 @@ class PublicClientController extends BaseController 'color' => $color, 'account' => $account, 'client' => $client, - 'hideLogo' => $account->isWhiteLabel(), - 'showDocuments' => $account->isPro(), + 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), + 'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS), 'clientViewCSS' => $account->clientViewCSS(), 'clientFontUrl' => $account->getFontsUrl(), ]; @@ -273,9 +277,9 @@ class PublicClientController extends BaseController $data = [ 'color' => $color, - 'hideLogo' => $account->isWhiteLabel(), + 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), 'hideDashboard' => !$account->enable_client_portal_dashboard, - 'showDocuments' => $account->isPro(), + 'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS), 'clientViewCSS' => $account->clientViewCSS(), 'clientFontUrl' => $account->getFontsUrl(), 'title' => trans('texts.invoices'), @@ -310,9 +314,9 @@ class PublicClientController extends BaseController $color = $account->primary_color ? $account->primary_color : '#0b4d78'; $data = [ 'color' => $color, - 'hideLogo' => $account->isWhiteLabel(), + 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), 'hideDashboard' => !$account->enable_client_portal_dashboard, - 'showDocuments' => $account->isPro(), + 'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS), 'clientViewCSS' => $account->clientViewCSS(), 'clientFontUrl' => $account->getFontsUrl(), 'entityType' => ENTITY_PAYMENT, @@ -354,9 +358,9 @@ class PublicClientController extends BaseController $color = $account->primary_color ? $account->primary_color : '#0b4d78'; $data = [ 'color' => $color, - 'hideLogo' => $account->isWhiteLabel(), + 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), 'hideDashboard' => !$account->enable_client_portal_dashboard, - 'showDocuments' => $account->isPro(), + 'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS), 'clientViewCSS' => $account->clientViewCSS(), 'clientFontUrl' => $account->getFontsUrl(), 'title' => trans('texts.quotes'), @@ -392,9 +396,9 @@ class PublicClientController extends BaseController $color = $account->primary_color ? $account->primary_color : '#0b4d78'; $data = [ 'color' => $color, - 'hideLogo' => $account->isWhiteLabel(), + 'hideLogo' => $account->hasFeature(FEATURE_WHITE_LABEL), 'hideDashboard' => !$account->enable_client_portal_dashboard, - 'showDocuments' => $account->isPro(), + 'showDocuments' => $account->hasFeature(FEATURE_DOCUMENTS), 'clientViewCSS' => $account->clientViewCSS(), 'clientFontUrl' => $account->getFontsUrl(), 'title' => trans('texts.documents'), diff --git a/app/Http/Controllers/QuoteController.php b/app/Http/Controllers/QuoteController.php index 20fdd484ba..4aaa504bfd 100644 --- a/app/Http/Controllers/QuoteController.php +++ b/app/Http/Controllers/QuoteController.php @@ -47,7 +47,7 @@ class QuoteController extends BaseController public function index() { - if (!Utils::isPro()) { + if (!Utils::hasFeature(FEATURE_QUOTES)) { return Redirect::to('/invoices/create'); } @@ -84,7 +84,7 @@ class QuoteController extends BaseController return $response; } - if (!Utils::isPro()) { + if (!Utils::hasFeature(FEATURE_QUOTES)) { return Redirect::to('/invoices/create'); } diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index 2d21e0b593..a63b16d9fc 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -21,7 +21,7 @@ class ReportController extends BaseController $message = ''; $fileName = storage_path().'/dataviz_sample.txt'; - if (Auth::user()->account->isPro()) { + if (Auth::user()->account->hasFeature(FEATURE_REPORTS)) { $account = Account::where('id', '=', Auth::user()->account->id) ->with(['clients.invoices.invoice_items', 'clients.contacts']) ->first(); @@ -99,7 +99,7 @@ class ReportController extends BaseController 'title' => trans('texts.charts_and_reports'), ]; - if (Auth::user()->account->isPro()) { + if (Auth::user()->account->hasFeature(FEATURE_REPORTS)) { if ($enableReport) { $isExport = $action == 'export'; $params = array_merge($params, self::generateReport($reportType, $startDate, $endDate, $dateField, $isExport)); diff --git a/app/Http/Controllers/TokenController.php b/app/Http/Controllers/TokenController.php index 8e255d6057..aa5434a269 100644 --- a/app/Http/Controllers/TokenController.php +++ b/app/Http/Controllers/TokenController.php @@ -93,7 +93,7 @@ class TokenController extends BaseController */ public function save($tokenPublicId = false) { - if (Auth::user()->account->isPro()) { + if (Auth::user()->account->hasFeature(FEATURE_API)) { $rules = [ 'name' => 'required', ]; diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 2e3f675aa5..6b3ab9ca5b 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -164,7 +164,7 @@ class UserController extends BaseController */ public function save($userPublicId = false) { - if (Auth::user()->isPro() && ! Auth::user()->isTrial()) { + if (Auth::user()->hasFeature(FEATURE_USERS)) { $rules = [ 'first_name' => 'required', 'last_name' => 'required', @@ -190,8 +190,10 @@ class UserController extends BaseController $user->last_name = trim(Input::get('last_name')); $user->username = trim(Input::get('email')); $user->email = trim(Input::get('email')); - $user->is_admin = boolval(Input::get('is_admin')); - $user->permissions = Input::get('permissions'); + if (Auth::user()->hasFeature(FEATURE_USER_PERMISSIONS)) { + $user->is_admin = boolval(Input::get('is_admin')); + $user->permissions = Input::get('permissions'); + } } else { $lastUser = User::withTrashed()->where('account_id', '=', Auth::user()->account_id) ->orderBy('public_id', 'DESC')->first(); @@ -202,12 +204,14 @@ class UserController extends BaseController $user->last_name = trim(Input::get('last_name')); $user->username = trim(Input::get('email')); $user->email = trim(Input::get('email')); - $user->is_admin = boolval(Input::get('is_admin')); $user->registered = true; $user->password = str_random(RANDOM_KEY_LENGTH); $user->confirmation_code = str_random(RANDOM_KEY_LENGTH); $user->public_id = $lastUser->public_id + 1; - $user->permissions = Input::get('permissions'); + if (Auth::user()->hasFeature(FEATURE_USER_PERMISSIONS)) { + $user->is_admin = boolval(Input::get('is_admin')); + $user->permissions = Input::get('permissions'); + } } $user->save(); @@ -286,6 +290,9 @@ class UserController extends BaseController if (!Auth::user()->registered) { $account = Auth::user()->account; $this->accountRepo->unlinkAccount($account); + if ($account->company->accounts->count() == 1) { + $account->company->forceDelete(); + } $account->forceDelete(); } } diff --git a/app/Http/Controllers/VendorController.php b/app/Http/Controllers/VendorController.php index 032f8d423d..b337c1df29 100644 --- a/app/Http/Controllers/VendorController.php +++ b/app/Http/Controllers/VendorController.php @@ -175,8 +175,8 @@ class VendorController extends BaseController $data = array_merge($data, self::getViewModel()); if (Auth::user()->account->isNinjaAccount()) { - if ($account = Account::whereId($vendor->public_id)->first()) { - $data['proPlanPaid'] = $account['pro_plan_paid']; + if ($account = Account::whereId($client->public_id)->first()) { + $data['planDetails'] = $account->getPlanDetails(false, false); } } diff --git a/app/Http/Middleware/ApiCheck.php b/app/Http/Middleware/ApiCheck.php index 63e370cf41..8b38c60fe2 100644 --- a/app/Http/Middleware/ApiCheck.php +++ b/app/Http/Middleware/ApiCheck.php @@ -47,7 +47,7 @@ class ApiCheck { return $next($request); } - if (!Utils::isPro() && !$loggingIn) { + if (!Utils::hasFeature(FEATURE_API) && !$loggingIn) { return Response::json('API requires pro plan', 403, $headers); } else { $key = Auth::check() ? Auth::user()->account->id : $request->getClientIp(); diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php index 3a49c762a9..81fde62439 100644 --- a/app/Http/Middleware/Authenticate.php +++ b/app/Http/Middleware/Authenticate.php @@ -42,7 +42,7 @@ class Authenticate { // Does this account require portal passwords? $account = Account::whereId($account_id)->first(); - if($account && (!$account->enable_portal_password || !$account->isPro())){ + if($account && (!$account->enable_portal_password || !$account->hasFeature(FEATURE_CLIENT_PORTAL_PASSWORD))){ $authenticated = true; } diff --git a/app/Http/Middleware/StartupCheck.php b/app/Http/Middleware/StartupCheck.php index 14344e10e9..be96ade97e 100644 --- a/app/Http/Middleware/StartupCheck.php +++ b/app/Http/Middleware/StartupCheck.php @@ -141,9 +141,10 @@ class StartupCheck } } elseif ($productId == PRODUCT_WHITE_LABEL) { if ($data == 'valid') { - $account = Auth::user()->account; - $account->pro_plan_paid = date_create()->format('Y-m-d'); - $account->save(); + $company = Auth::user()->account->company; + $company->plan_paid = date_create()->format('Y-m-d'); + $company->plan = PLAN_WHITE_LABEL; + $company->save(); Session::flash('message', trans('texts.bought_white_label')); } diff --git a/app/Http/routes.php b/app/Http/routes.php index 8066690990..48f06e5e7b 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -188,7 +188,8 @@ Route::group([ Route::resource('users', 'UserController'); Route::post('users/bulk', 'UserController@bulk'); Route::get('send_confirmation/{user_id}', 'UserController@sendConfirmation'); - Route::get('start_trial', 'AccountController@startTrial'); + Route::get('start_trial/{plan}', 'AccountController@startTrial') + ->where(['plan'=>'pro']); Route::get('restore_user/{user_id}', 'UserController@restoreUser'); Route::post('users/change_password', 'UserController@changePassword'); Route::get('/switch_account/{user_id}', 'UserController@switchAccount'); @@ -212,6 +213,7 @@ Route::group([ Route::get('settings/charts_and_reports', 'ReportController@showReports'); Route::post('settings/charts_and_reports', 'ReportController@showReports'); + Route::post('settings/change_plan', 'AccountController@changePlan'); Route::post('settings/cancel_account', 'AccountController@cancelAccount'); Route::post('settings/company_details', 'AccountController@updateDetails'); Route::get('settings/{section?}', 'AccountController@showSection'); @@ -354,6 +356,7 @@ if (!defined('CONTACT_EMAIL')) { define('ACCOUNT_LOCALIZATION', 'localization'); define('ACCOUNT_NOTIFICATIONS', 'notifications'); define('ACCOUNT_IMPORT_EXPORT', 'import_export'); + define('ACCOUNT_MANAGEMENT', 'account_management'); define('ACCOUNT_PAYMENTS', 'online_payments'); define('ACCOUNT_BANKS', 'bank_accounts'); define('ACCOUNT_IMPORT_EXPENSES', 'import_expenses'); @@ -552,7 +555,6 @@ if (!defined('CONTACT_EMAIL')) { define('NINJA_WEB_URL', 'https://www.invoiceninja.com'); define('NINJA_APP_URL', 'https://app.invoiceninja.com'); define('NINJA_VERSION', '2.5.1.3'); - define('NINJA_DATE', '2000-01-01'); define('SOCIAL_LINK_FACEBOOK', 'https://www.facebook.com/invoiceninja'); define('SOCIAL_LINK_TWITTER', 'https://twitter.com/invoiceninja'); @@ -582,6 +584,10 @@ if (!defined('CONTACT_EMAIL')) { define('SELF_HOST_AFFILIATE_KEY', '8S69AD'); define('PRO_PLAN_PRICE', 50); + define('PLAN_PRICE_PRO_MONTHLY', 5); + define('PLAN_PRICE_PRO_YEARLY', 50); + define('PLAN_PRICE_ENTERPRISE_MONTHLY', 10); + define('PLAN_PRICE_ENTERPRISE_YEARLY', 100); define('WHITE_LABEL_PRICE', 20); define('INVOICE_DESIGNS_PRICE', 10); @@ -644,7 +650,46 @@ if (!defined('CONTACT_EMAIL')) { define('RESELLER_REVENUE_SHARE', 'A'); define('RESELLER_LIMITED_USERS', 'B'); + + // These must be lowercase + define('PLAN_FREE', 'free'); + define('PLAN_PRO', 'pro'); + define('PLAN_ENTERPRISE', 'enterprise'); + define('PLAN_WHITE_LABEL', 'white_label'); + define('PLAN_TERM_MONTHLY', 'month'); + define('PLAN_TERM_YEARLY', 'year'); + + // Pro + define('FEATURE_CUSTOMIZE_INVOICE_DESIGN', 'customize_invoice_design'); + define('FEATURE_REMOVE_CREATED_BY', 'remove_created_by'); + define('FEATURE_DIFFERENT_DESIGNS', 'different_designs'); + define('FEATURE_EMAIL_TEMPLATES_REMINDERS', 'email_templates_reminders'); + define('FEATURE_INVOICE_SETTINGS', 'invoice_settings'); + define('FEATURE_CUSTOM_EMAILS', 'custom_emails'); + define('FEATURE_PDF_ATTACHMENT', 'pdf_attachment'); + define('FEATURE_MORE_INVOICE_DESIGNS', 'more_invoice_designs'); + define('FEATURE_QUOTES', 'quotes'); + define('FEATURE_REPORTS', 'reports'); + define('FEATURE_API', 'api'); + define('FEATURE_CLIENT_PORTAL_PASSWORD', 'client_portal_password'); + define('FEATURE_CUSTOM_URL', 'custom_url'); + + define('FEATURE_MORE_CLIENTS', 'more_clients'); // No trial allowed + + // Whitelabel + define('FEATURE_CLIENT_PORTAL_CSS', 'client_portal_css'); + define('FEATURE_WHITE_LABEL', 'feature_white_label'); + // Enterprise + define('FEATURE_DOCUMENTS', 'documents'); + + // No Trial allowed + define('FEATURE_USERS', 'users');// Grandfathered for old Pro users + define('FEATURE_USER_PERMISSIONS', 'user_permissions'); + + // Pro users who started paying on or before this date will be able to manage users + define('PRO_USERS_GRANDFATHER_DEADLINE', '2016-05-15'); + $creditCards = [ 1 => ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'], 2 => ['card' => 'images/credit_cards/Test-MasterCard-Icon.png', 'text' => 'Master Card'], diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php index 439247f337..e4ad99c6ce 100644 --- a/app/Libraries/Utils.php +++ b/app/Libraries/Utils.php @@ -118,6 +118,11 @@ class Utils return Auth::check() && Auth::user()->isPro(); } + public static function hasFeature($feature) + { + return Auth::check() && Auth::user()->hasFeature($feature); + } + public static function isAdmin() { return Auth::check() && Auth::user()->is_admin; @@ -440,7 +445,12 @@ class Utils return false; } - $dateTime = new DateTime($date); + if ($date instanceof DateTime) { + $dateTime = $date; + } else { + $dateTime = new DateTime($date); + } + $timestamp = $dateTime->getTimestamp(); $format = Session::get(SESSION_DATE_FORMAT, DEFAULT_DATE_FORMAT); @@ -961,38 +971,6 @@ class Utils return $entity1; } - public static function withinPastYear($date) - { - if (!$date || $date == '0000-00-00') { - return false; - } - - $today = new DateTime('now'); - $datePaid = DateTime::createFromFormat('Y-m-d', $date); - $interval = $today->diff($datePaid); - - return $interval->y == 0; - } - - public static function getInterval($date) - { - if (!$date || $date == '0000-00-00') { - return false; - } - - $today = new DateTime('now'); - $datePaid = DateTime::createFromFormat('Y-m-d', $date); - - return $today->diff($datePaid); - } - - public static function withinPastTwoWeeks($date) - { - $interval = Utils::getInterval($date); - - return $interval && $interval->d <= 14; - } - public static function addHttp($url) { if (!preg_match("~^(?:f|ht)tps?://~i", $url)) { diff --git a/app/Listeners/InvoiceListener.php b/app/Listeners/InvoiceListener.php index 9e40bbbe6d..91f3de8cc2 100644 --- a/app/Listeners/InvoiceListener.php +++ b/app/Listeners/InvoiceListener.php @@ -14,10 +14,11 @@ class InvoiceListener { public function createdInvoice(InvoiceWasCreated $event) { - if (Utils::isPro()) { + if (Utils::hasFeature(FEATURE_DIFFERENT_DESIGNS)) { return; } + // Make sure the account has the same design set as the invoice does if (Auth::check()) { $invoice = $event->invoice; $account = Auth::user()->account; diff --git a/app/Models/Account.php b/app/Models/Account.php index 8d1261d928..3508452745 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -18,6 +18,17 @@ class Account extends Eloquent { use PresentableTrait; use SoftDeletes; + + public static $plan_prices = array( + PLAN_PRO => array( + PLAN_TERM_MONTHLY => PLAN_PRICE_PRO_MONTHLY, + PLAN_TERM_YEARLY => PLAN_PRICE_PRO_YEARLY, + ), + PLAN_ENTERPRISE => array( + PLAN_TERM_MONTHLY => PLAN_PRICE_ENTERPRISE_MONTHLY, + PLAN_TERM_YEARLY => PLAN_PRICE_ENTERPRISE_YEARLY, + ), + ); protected $presenter = 'App\Ninja\Presenters\AccountPresenter'; protected $dates = ['deleted_at']; @@ -57,6 +68,7 @@ class Account extends Eloquent ACCOUNT_PRODUCTS, ACCOUNT_NOTIFICATIONS, ACCOUNT_IMPORT_EXPORT, + ACCOUNT_MANAGEMENT, ]; public static $advancedSettings = [ @@ -176,6 +188,11 @@ class Account extends Eloquent return $this->hasMany('App\Models\Payment','account_id','id')->withTrashed(); } + public function company() + { + return $this->belongsTo('App\Models\Company'); + } + public function setIndustryIdAttribute($value) { $this->attributes['industry_id'] = $value ?: null; @@ -516,7 +533,7 @@ class Account extends Eloquent public function getNumberPrefix($isQuote) { - if ( ! $this->isPro()) { + if ( ! $this->hasFeature(FEATURE_INVOICE_SETTINGS)) { return ''; } @@ -525,7 +542,7 @@ class Account extends Eloquent public function hasNumberPattern($isQuote) { - if ( ! $this->isPro()) { + if ( ! $this->hasFeature(FEATURE_INVOICE_SETTINGS)) { return false; } @@ -645,7 +662,7 @@ class Account extends Eloquent $default = $this->invoice_number_counter; $actual = Utils::parseInt($invoice->invoice_number); - if ( ! $this->isPro() && $default != $actual) { + if ( ! $this->hasFeature(FEATURE_INVOICE_SETTINGS) && $default != $actual) { $this->invoice_number_counter = $actual + 1; } else { $this->invoice_number_counter += 1; @@ -766,17 +783,80 @@ class Account extends Eloquent return $this->account_key === NINJA_ACCOUNT_KEY; } - public function startTrial() + public function startTrial($plan) { if ( ! Utils::isNinja()) { return; } - $this->pro_plan_trial = date_create()->format('Y-m-d'); - $this->save(); + $this->company->trial_plan = $plan; + $this->company->trial_started = date_create()->format('Y-m-d'); + $this->company->save(); } - public function isPro() + public function hasFeature($feature) + { + $planDetails = $this->getPlanDetails(); + $selfHost = !Utils::isNinjaProd(); + + if (!$selfHost && function_exists('ninja_account_features')) { + $result = ninja_account_features($this, $feature); + + if ($result != null) { + return $result; + } + } + + switch ($feature) { + // Pro + case FEATURE_CUSTOMIZE_INVOICE_DESIGN: + case FEATURE_REMOVE_CREATED_BY: + case FEATURE_DIFFERENT_DESIGNS: + case FEATURE_EMAIL_TEMPLATES_REMINDERS: + case FEATURE_INVOICE_SETTINGS: + case FEATURE_CUSTOM_EMAILS: + case FEATURE_PDF_ATTACHMENT: + case FEATURE_MORE_INVOICE_DESIGNS: + case FEATURE_QUOTES: + case FEATURE_REPORTS: + case FEATURE_API: + case FEATURE_CLIENT_PORTAL_PASSWORD: + case FEATURE_CUSTOM_URL: + return $selfHost || !empty($planDetails); + + // Pro; No trial allowed, unless they're trialing enterprise with an active pro plan + case FEATURE_MORE_CLIENTS: + return $selfHost || !empty($planDetails) && (!$planDetails['trial'] || !empty($this->getPlanDetails(false, false))); + + // White Label + case FEATURE_WHITE_LABEL: + if ($this->isNinjaAccount() || (!$selfHost && $planDetails && !$plan_details['expires'])) { + return false; + } + // Fallthrough + case FEATURE_CLIENT_PORTAL_CSS: + return !empty($planDetails);// A plan is required even for self-hosted users + + // Enterprise; No Trial allowed; grandfathered for old pro users + case FEATURE_USERS:// Grandfathered for old Pro users + if($planDetails && $planDetails['trial']) { + // Do they have a non-trial plan? + $planDetails = $this->getPlanDetails(false, false); + } + + return $selfHost || !empty($planDetails) && ($planDetails['plan'] == PLAN_ENTERPRISE || $planDetails['started'] <= date_create(PRO_USERS_GRANDFATHER_DEADLINE)); + + // Enterprise; No Trial allowed + case FEATURE_DOCUMENTS: + case FEATURE_USER_PERMISSIONS: + return $selfHost || !empty($planDetails) && $planDetails['plan'] == PLAN_ENTERPRISE && !$planDetails['trial']; + + default: + return false; + } + } + + public function isPro(&$plan_details = null) { if (!Utils::isNinjaProd()) { return true; @@ -786,14 +866,113 @@ class Account extends Eloquent return true; } - $datePaid = $this->pro_plan_paid; - $trialStart = $this->pro_plan_trial; + $plan_details = $this->getPlanDetails(); + + return !empty($plan_details); + } - if ($datePaid == NINJA_DATE) { + public function isEnterprise(&$plan_details = null) + { + if (!Utils::isNinjaProd()) { return true; } - return Utils::withinPastTwoWeeks($trialStart) || Utils::withinPastYear($datePaid); + if ($this->isNinjaAccount()) { + return true; + } + + $plan_details = $this->getPlanDetails(); + + return $plan_details && $plan_details['plan'] == PLAN_ENTERPRISE; + } + + public function getPlanDetails($include_inactive = false, $include_trial = true) + { + if (!$this->company) { + return null; + } + + $plan = $this->company->plan; + $trial_plan = $this->company->trial_plan; + + if(!$plan && (!$trial_plan || !$include_trial)) { + return null; + } + + $trial_active = false; + if ($trial_plan && $include_trial) { + $trial_started = DateTime::createFromFormat('Y-m-d', $this->company->trial_started); + $trial_expires = clone $trial_started; + $trial_expires->modify('+2 weeks'); + + if ($trial_expires >= date_create()) { + $trial_active = true; + } + } + + $plan_active = false; + if ($plan) { + if ($this->company->plan_expires == null) { + $plan_active = true; + $plan_expires = false; + } else { + $plan_expires = DateTime::createFromFormat('Y-m-d', $this->company->plan_expires); + if ($plan_expires >= date_create()) { + $plan_active = true; + } + } + } + + if (!$include_inactive && !$plan_active && !$trial_active) { + return null; + } + + // 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 == PLAN_ENTERPRISE) { + $use_plan = true; + } elseif ($trial_plan == 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 array( + 'trial' => false, + 'plan' => $plan, + 'started' => DateTime::createFromFormat('Y-m-d', $this->company->plan_started), + 'expires' => $plan_expires, + 'paid' => DateTime::createFromFormat('Y-m-d', $this->company->plan_paid), + 'term' => $this->company->plan_term, + 'active' => $plan_active, + ); + } else { + return array( + 'trial' => true, + 'plan' => $trial_plan, + 'started' => $trial_started, + 'expires' => $trial_expires, + 'active' => $trial_active, + ); + } } public function isTrial() @@ -801,35 +980,54 @@ class Account extends Eloquent if (!Utils::isNinjaProd()) { return false; } + + $plan_details = $this->getPlanDetails(); - if ($this->pro_plan_paid && $this->pro_plan_paid != '0000-00-00') { - return false; - } - - return Utils::withinPastTwoWeeks($this->pro_plan_trial); + return $plan_details && $plan_details['trial']; } - public function isEligibleForTrial() + public function isEligibleForTrial($plan = null) { - return ! $this->pro_plan_trial || $this->pro_plan_trial == '0000-00-00'; + if (!$this->company->trial_plan) { + if ($plan) { + return $plan == PLAN_PRO || $plan == PLAN_ENTERPRISE; + } else { + return array(PLAN_PRO, PLAN_ENTERPRISE); + } + } + + if ($this->company->trial_plan == PLAN_PRO) { + if ($plan) { + return $plan != PLAN_PRO; + } else { + return array(PLAN_ENTERPRISE); + } + } + + return false; } public function getCountTrialDaysLeft() { - $interval = Utils::getInterval($this->pro_plan_trial); + $planDetails = $this->getPlanDetails(true); - return $interval ? 14 - $interval->d : 0; + if(!$planDetails || !$planDetails['trial']) { + return 0; + } + + $today = new DateTime('now'); + $interval = $today->diff($planDetails['expires']); + + return $interval ? $interval->d : 0; } public function getRenewalDate() { - if ($this->pro_plan_paid && $this->pro_plan_paid != '0000-00-00') { - $date = DateTime::createFromFormat('Y-m-d', $this->pro_plan_paid); - $date->modify('+1 year'); + $planDetails = $this->getPlanDetails(); + + if ($planDetails) { + $date = $planDetails['expires']; $date = max($date, date_create()); - } elseif ($this->isTrial()) { - $date = date_create(); - $date->modify('+'.$this->getCountTrialDaysLeft().' day'); } else { $date = date_create(); } @@ -837,23 +1035,6 @@ class Account extends Eloquent return $date->format('Y-m-d'); } - public function isWhiteLabel() - { - if ($this->isNinjaAccount()) { - return false; - } - - if (Utils::isNinjaProd()) { - return self::isPro() && $this->pro_plan_paid != NINJA_DATE; - } else { - if ($this->pro_plan_paid == NINJA_DATE) { - return true; - } - - return Utils::withinPastYear($this->pro_plan_paid); - } - } - public function getLogoSize() { if(!$this->hasLogo()){ @@ -930,7 +1111,7 @@ class Account extends Eloquent public function getEmailSubject($entityType) { - if ($this->isPro()) { + if ($this->hasFeature(FEATURE_CUSTOM_EMAILS)) { $field = "email_subject_{$entityType}"; $value = $this->$field; @@ -950,7 +1131,7 @@ class Account extends Eloquent $template = "
\$client,

"; - if ($this->isPro() && $this->email_design_id != EMAIL_DESIGN_PLAIN) { + if ($this->hasFeature(FEATURE_CUSTOM_EMAILS) && $this->email_design_id != EMAIL_DESIGN_PLAIN) { $template .= "
" . trans("texts.{$entityType}_message_button", ['amount' => '$amount']) . "

" . "
\$viewButton

"; } else { @@ -969,7 +1150,7 @@ class Account extends Eloquent { $template = false; - if ($this->isPro()) { + if ($this->hasFeature(FEATURE_CUSTOM_EMAILS)) { $field = "email_template_{$entityType}"; $template = $this->$field; } @@ -1065,7 +1246,7 @@ class Account extends Eloquent public function showCustomField($field, $entity = false) { - if ($this->isPro()) { + if ($this->hasFeature(FEATURE_INVOICE_SETTINGS)) { return $this->$field ? true : false; } @@ -1081,18 +1262,18 @@ class Account extends Eloquent public function attatchPDF() { - return $this->isPro() && $this->pdf_email_attachment; + return $this->hasFeaure(FEATURE_PDF_ATTACHMENT) && $this->pdf_email_attachment; } public function getEmailDesignId() { - return $this->isPro() ? $this->email_design_id : EMAIL_DESIGN_PLAIN; + return $this->hasFeature(FEATURE_CUSTOM_EMAILS) ? $this->email_design_id : EMAIL_DESIGN_PLAIN; } public function clientViewCSS(){ - $css = null; + $css = ''; - if ($this->isPro()) { + if ($this->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN)) { $bodyFont = $this->getBodyFontCss(); $headerFont = $this->getHeaderFontCss(); @@ -1100,11 +1281,10 @@ class Account extends Eloquent if ($headerFont != $bodyFont) { $css .= 'h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{'.$headerFont.'}'; } - - if ((Utils::isNinja() && $this->isPro()) || $this->isWhiteLabel()) { - // For self-hosted users, a white-label license is required for custom CSS - $css .= $this->client_view_css; - } + } + if ($this->hasFeature(FEATURE_CLIENT_PORTAL_CSS)) { + // For self-hosted users, a white-label license is required for custom CSS + $css .= $this->client_view_css; } return $css; @@ -1126,11 +1306,11 @@ class Account extends Eloquent } public function getHeaderFontId() { - return ($this->isPro() && $this->header_font_id) ? $this->header_font_id : DEFAULT_HEADER_FONT; + return ($this->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN) && $this->header_font_id) ? $this->header_font_id : DEFAULT_HEADER_FONT; } public function getBodyFontId() { - return ($this->isPro() && $this->body_font_id) ? $this->body_font_id : DEFAULT_BODY_FONT; + return ($this->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN) && $this->body_font_id) ? $this->body_font_id : DEFAULT_BODY_FONT; } public function getHeaderFontName(){ diff --git a/app/Models/Client.php b/app/Models/Client.php index 71970b7c9d..85cb543a6b 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -155,7 +155,7 @@ class Client extends EntityModel $contact->send_invoice = true; } - if (!Utils::isPro() || $this->account->enable_portal_password){ + if (Utils::hasFeature(FEATURE_CLIENT_PORTAL_PASSWORD) && $this->account->enable_portal_password){ if(!empty($data['password']) && $data['password']!='-%unchanged%-'){ $contact->password = bcrypt($data['password']); } else if(empty($data['password'])){ diff --git a/app/Models/Company.php b/app/Models/Company.php new file mode 100644 index 0000000000..1345db9e4b --- /dev/null +++ b/app/Models/Company.php @@ -0,0 +1,21 @@ +hasMany('App\Models\Account'); + } + + public function payment() + { + return $this->belongsTo('App\Models\Payment'); + } +} diff --git a/app/Models/Invitation.php b/app/Models/Invitation.php index 687e987345..becd0197c9 100644 --- a/app/Models/Invitation.php +++ b/app/Models/Invitation.php @@ -40,7 +40,7 @@ class Invitation extends EntityModel $url = SITE_URL; $iframe_url = $this->account->iframe_url; - if ($this->account->isPro()) { + if ($this->account->hasFeature(FEATURE_CUSTOM_URL)) { if ($iframe_url && !$forceOnsite) { return "{$iframe_url}?{$this->invitation_key}"; } elseif ($this->account->subdomain) { diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index c40f22d34a..c0da6b8faf 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -409,7 +409,7 @@ class Invoice extends EntityModel implements BalanceAffecting 'invoice_design', 'invoice_design_id', 'invoice_fonts', - 'is_pro', + 'features', 'is_quote', 'custom_value1', 'custom_value2', diff --git a/app/Models/User.php b/app/Models/User.php index 1b2ae7815a..32a4960a84 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -112,9 +112,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon return $this->account->isPro(); } + public function hasFeature($feature) + { + return $this->account->hasFeature($feature); + } + public function isPaidPro() { - return $this->isPro() && ! $this->isTrial(); + return $this->isPro($accountDetails) && !$accountDetails['trial']; } public function isTrial() @@ -122,14 +127,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon return $this->account->isTrial(); } - public function isEligibleForTrial() + public function isEligibleForTrial($plan = null) { - return $this->account->isEligibleForTrial(); + return $this->account->isEligibleForTrial($plan); } public function maxInvoiceDesignId() { - return $this->isPro() ? 11 : (Utils::isNinja() ? COUNT_FREE_DESIGNS : COUNT_FREE_DESIGNS_SELF_HOST); + return $this->hasFeature(FEATURE_MORE_INVOICE_DESIGNS) ? 11 : (Utils::isNinja() ? COUNT_FREE_DESIGNS : COUNT_FREE_DESIGNS_SELF_HOST); } public function getDisplayName() @@ -173,7 +178,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon public function getMaxNumClients() { - if ($this->isPro() && ! $this->isTrial()) { + if ($this->hasFeature(FEATURE_MORE_CLIENTS)) { return MAX_NUM_CLIENTS_PRO; } @@ -186,7 +191,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon public function getMaxNumVendors() { - if ($this->isPro() && ! $this->isTrial()) { + if ($this->hasFeature(FEATURE_MORE_CLIENTS)) { return MAX_NUM_VENDORS_PRO; } diff --git a/app/Ninja/Mailers/ContactMailer.php b/app/Ninja/Mailers/ContactMailer.php index 229895067e..4b176628eb 100644 --- a/app/Ninja/Mailers/ContactMailer.php +++ b/app/Ninja/Mailers/ContactMailer.php @@ -136,7 +136,7 @@ class ContactMailer extends Mailer 'amount' => $invoice->getRequestedAmount() ]; - if (empty($invitation->contact->password) && $account->isPro() && $account->enable_portal_password && $account->send_portal_password) { + if (empty($invitation->contact->password) && $account->hasFeature(FEATURE_CLIENT_PORTAL_PASSWORD) && $account->enable_portal_password && $account->send_portal_password) { // The contact needs a password $variables['password'] = $password = $this->generatePassword(); $invitation->contact->password = bcrypt($password); @@ -291,7 +291,7 @@ class ContactMailer extends Mailer $passwordHTML = isset($data['password'])?'

'.trans('texts.password').': '.$data['password'].'

':false; $documentsHTML = ''; - if($account->isPro() && $invoice->hasDocuments()){ + if($account->hasFeature(FEATURE_DOCUMENTS) && $invoice->hasDocuments()){ $documentsHTML .= trans('texts.email_documents_header').'

@@ -330,7 +330,7 @@ ') !!} - @if ($account->isPro()) + @if ($account->hasFeature(FEATURE_DOCUMENTS))
@@ -493,7 +493,7 @@ {!! Former::text('pdfupload') !!}
- @if (!Utils::isPro() || \App\Models\InvoiceDesign::count() == COUNT_FREE_DESIGNS_SELF_HOST) + @if (!Utils::hasFeature(FEATURE_MORE_INVOICE_DESIGNS) || \App\Models\InvoiceDesign::count() == COUNT_FREE_DESIGNS_SELF_HOST) {!! Former::select('invoice_design_id')->style('display:inline;width:150px;background-color:white !important')->raw()->fromQuery($invoiceDesigns, 'name', 'id')->data_bind("value: invoice_design_id")->addOption(trans('texts.more_designs') . '...', '-1') !!} @else {!! Former::select('invoice_design_id')->style('display:inline;width:150px;background-color:white !important')->raw()->fromQuery($invoiceDesigns, 'name', 'id')->data_bind("value: invoice_design_id") !!} @@ -565,7 +565,7 @@ - @if (Auth::user()->isPro()) + @if (Auth::user()->hasFeature(FEATURE_INVOICE_SETTINGS)) @if ($account->custom_client_label1) {!! Former::text('client[custom_value1]') ->label($account->custom_client_label1) @@ -620,7 +620,7 @@ ->addClass('client-email') !!} {!! Former::text('phone')->data_bind("value: phone, valueUpdate: 'afterkeydown', attr: {name: 'client[contacts][' + \$index() + '][phone]'}") !!} - @if ($account->isPro() && $account->enable_portal_password) + @if ($account->hasFeature(FEATURE_CLIENT_PORTAL_PASSWORD) && $account->enable_portal_password) {!! Former::password('password')->data_bind("value: (typeof password=='function'?password():null)?'-%unchanged%-':'', valueUpdate: 'afterkeydown', attr: {name: 'client[contacts][' + \$index() + '][password]'}") !!} @endif @@ -960,7 +960,7 @@ applyComboboxListeners(); - @if (Auth::user()->account->isPro()) + @if (Auth::user()->account->hasFeature(FEATURE_DOCUMENTS)) $('.main-form').submit(function(){ if($('#document-upload .dropzone .fallback input').val())$(this).attr('enctype', 'multipart/form-data') else $(this).removeAttr('enctype') @@ -1056,7 +1056,11 @@ var model = ko.toJS(window.model); if(!model)return; var invoice = model.invoice; - invoice.is_pro = {{ Auth::user()->isPro() ? 'true' : 'false' }}; + invoice.features = { + customize_invoice_design:{{ Auth::user()->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN) ? 'true' : 'false' }}, + remove_created_by:{{ Auth::user()->hasFeature(FEATURE_REMOVE_CREATED_BY) ? 'true' : 'false' }}, + invoice_settings:{{ Auth::user()->hasFeature(FEATURE_INVOICE_SETTINGS) ? 'true' : 'false' }} + }; invoice.is_quote = {{ $entityType == ENTITY_QUOTE ? 'true' : 'false' }}; invoice.contact = _.findWhere(invoice.client.contacts, {send_invoice: true}); @@ -1369,7 +1373,7 @@ model.invoice().invoice_number(number); } - @if ($account->isPro()) + @if ($account->hasFeature(FEATURE_DOCUMENTS)) function handleDocumentAdded(file){ if(file.mock)return; file.index = model.invoice().documents().length; @@ -1393,7 +1397,7 @@ @endif - @if ($account->isPro() && $account->invoice_embed_documents) + @if ($account->hasFeature(FEATURE_DOCUMENTS) && $account->invoice_embed_documents) @foreach ($invoice->documents as $document) @if($document->isPDFEmbeddable()) diff --git a/resources/views/invoices/history.blade.php b/resources/views/invoices/history.blade.php index baa5c925aa..18fedecc6b 100644 --- a/resources/views/invoices/history.blade.php +++ b/resources/views/invoices/history.blade.php @@ -57,7 +57,7 @@ @include('invoices.pdf', ['account' => Auth::user()->account, 'pdfHeight' => 800]) - @if (Utils::isPro() && $invoice->account->invoice_embed_documents) + @if (Utils::hasFeature(FEATURE_DOCUMENTS) && $invoice->account->invoice_embed_documents) @foreach ($invoice->documents as $document) @if($document->isPDFEmbeddable()) diff --git a/resources/views/invoices/pdf.blade.php b/resources/views/invoices/pdf.blade.php index 1c8389affe..81349390ad 100644 --- a/resources/views/invoices/pdf.blade.php +++ b/resources/views/invoices/pdf.blade.php @@ -84,7 +84,7 @@ @endif var NINJA = NINJA || {}; - @if ($account->isPro()) + @if ($account->hasFeature(FEATURE_CUSTOMIZE_INVOICE_DESIGN)) NINJA.primaryColor = "{{ $account->primary_color }}"; NINJA.secondaryColor = "{{ $account->secondary_color }}"; NINJA.fontSize = {{ $account->font_size }}; diff --git a/resources/views/invoices/view.blade.php b/resources/views/invoices/view.blade.php index 08f5cd9827..730094e659 100644 --- a/resources/views/invoices/view.blade.php +++ b/resources/views/invoices/view.blade.php @@ -65,7 +65,7 @@
@endif - @if ($account->isPro() && $account->invoice_embed_documents) + @if ($account->hasFeature(FEATURE_DOCUMENTS) && $account->invoice_embed_documents) @foreach ($invoice->documents as $document) @if($document->isPDFEmbeddable()) @@ -82,7 +82,11 @@ @endif