diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 57df9ebf9099..56a7d1a82b43 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -916,6 +916,8 @@ class AccountController extends BaseController $user->registered = true; $user->save(); + $user->account->startTrial(); + if (Input::get('go_pro') == 'true') { Session::set(REQUESTED_PRO_PLAN, true); } @@ -980,6 +982,17 @@ class AccountController extends BaseController return Redirect::to('/settings/'.ACCOUNT_USER_DETAILS)->with('message', trans('texts.confirmation_resent')); } + public function startTrial() + { + $user = Auth::user(); + + if ($user->isEligibleForTrial()) { + $user->account->startTrial(); + } + + return Redirect::back()->with('message', trans('texts.trial_success')); + } + public function redirectLegacy($section, $subSection = false) { if ($section === 'details') { diff --git a/app/Http/Controllers/AccountGatewayController.php b/app/Http/Controllers/AccountGatewayController.php index 691e01c46990..dfbd2b386162 100644 --- a/app/Http/Controllers/AccountGatewayController.php +++ b/app/Http/Controllers/AccountGatewayController.php @@ -200,7 +200,11 @@ class AccountGatewayController extends BaseController if ($gatewayId == GATEWAY_DWOLLA) { $optional = array_merge($optional, ['key', 'secret']); } elseif ($gatewayId == GATEWAY_STRIPE) { - $rules['publishable_key'] = 'required'; + if (Utils::isNinjaDev() && Input::get('23_apiKey') == env('TEST_API_KEY')) { + // do nothing - we're unable to acceptance test with StripeJS + } else { + $rules['publishable_key'] = 'required'; + } } foreach ($fields as $field => $details) { diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index afad67b0d3ec..45549a1030dc 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -166,7 +166,7 @@ class UserController extends BaseController */ public function save($userPublicId = false) { - if (Auth::user()->account->isPro()) { + if (Auth::user()->isPro() && ! Auth::user()->isTrial()) { $rules = [ 'first_name' => 'required', 'last_name' => 'required', diff --git a/app/Http/routes.php b/app/Http/routes.php index d0e623e0bd7d..84cabb53baf1 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -97,6 +97,7 @@ Route::group(['middleware' => 'auth'], function() { 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('restore_user/{user_id}', 'UserController@restoreUser'); Route::post('users/change_password', 'UserController@changePassword'); Route::get('/switch_account/{user_id}', 'UserController@switchAccount'); @@ -425,7 +426,6 @@ if (!defined('CONTACT_EMAIL')) { define('MAX_NUM_VENDORS', 100); define('MAX_NUM_VENDORS_PRO', 20000); - define('MAX_NUM_VENDORS_LEGACY', 500); define('INVOICE_STATUS_DRAFT', 1); define('INVOICE_STATUS_SENT', 2); @@ -669,9 +669,8 @@ if (Utils::isNinjaDev()) { */ /* -if (Auth::check() && Auth::user()->id === 1) +if (Utils::isNinjaDev() && Auth::check() && Auth::user()->id === 1) { Auth::loginUsingId(1); } -*/ - +*/ \ No newline at end of file diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php index 7ff8a67d83a0..3515dc1c83c6 100644 --- a/app/Libraries/Utils.php +++ b/app/Libraries/Utils.php @@ -72,6 +72,10 @@ class Utils public static function requireHTTPS() { + if (Request::root() === 'http://ninja.dev') { + return false; + } + return Utils::isNinjaProd() || (isset($_ENV['REQUIRE_HTTPS']) && $_ENV['REQUIRE_HTTPS'] == 'true'); } @@ -114,6 +118,11 @@ class Utils return Auth::check() && Auth::user()->isPro(); } + public static function isTrial() + { + return Auth::check() && Auth::user()->isTrial(); + } + public static function isEnglish() { return App::getLocale() == 'en'; @@ -961,6 +970,25 @@ class Utils 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/Models/Account.php b/app/Models/Account.php index baca7d0c78c0..746f0d361b52 100644 --- a/app/Models/Account.php +++ b/app/Models/Account.php @@ -464,8 +464,21 @@ class Account extends Eloquent return $invoice; } + public function getNumberPrefix($isQuote) + { + if ( ! $this->isPro()) { + return ''; + } + + return ($isQuote ? $this->quote_number_prefix : $this->invoice_number_prefix) ?: ''; + } + public function hasNumberPattern($isQuote) { + if ( ! $this->isPro()) { + return false; + } + return $isQuote ? ($this->quote_number_pattern ? true : false) : ($this->invoice_number_pattern ? true : false); } @@ -549,7 +562,7 @@ class Account extends Eloquent } $counter = $this->getCounter($invoice->is_quote); - $prefix = $invoice->is_quote ? $this->quote_number_prefix : $this->invoice_number_prefix; + $prefix = $this->getNumberPrefix($invoice->is_quote); $counterOffset = 0; // confirm the invoice number isn't already taken @@ -681,6 +694,16 @@ class Account extends Eloquent return $this->account_key === NINJA_ACCOUNT_KEY; } + public function startTrial() + { + if ( ! Utils::isNinja()) { + return; + } + + $this->pro_plan_trial = date_create()->format('Y-m-d'); + $this->save(); + } + public function isPro() { if (!Utils::isNinjaProd()) { @@ -692,12 +715,50 @@ class Account extends Eloquent } $datePaid = $this->pro_plan_paid; + $trialStart = $this->pro_plan_trial; if ($datePaid == NINJA_DATE) { return true; } - return Utils::withinPastYear($datePaid); + return Utils::withinPastTwoWeeks($trialStart) || Utils::withinPastYear($datePaid); + } + + public function isTrial() + { + if ($this->pro_plan_paid && $this->pro_plan_paid != '0000-00-00') { + return false; + } + + return Utils::withinPastTwoWeeks($this->pro_plan_trial); + } + + public function isEligibleForTrial() + { + return ! $this->pro_plan_trial || $this->pro_plan_trial == '0000-00-00'; + } + + public function getCountTrialDaysLeft() + { + $interval = Utils::getInterval($this->pro_plan_trial); + + return $interval ? 14 - $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'); + $date = max($date, date_create()); + } elseif ($this->isTrial()) { + $date = date_create(); + $date->modify('+'.$this->getCountTrialDaysLeft().' day'); + } else { + $date = date_create(); + } + + return $date->format('Y-m-d'); } public function isWhiteLabel() @@ -944,6 +1005,11 @@ class Account extends Eloquent return $this->isPro() && $this->pdf_email_attachment; } + public function getEmailDesignId() + { + return $this->isPro() ? $this->email_design_id : EMAIL_DESIGN_PLAIN; + } + public function clientViewCSS(){ $css = null; diff --git a/app/Models/User.php b/app/Models/User.php index e74d76b5f9bf..f62e4a5846c6 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -107,6 +107,16 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon return $this->account->isPro(); } + public function isTrial() + { + return $this->account->isTrial(); + } + + public function isEligibleForTrial() + { + return $this->account->isEligibleForTrial(); + } + public function maxInvoiceDesignId() { return $this->isPro() ? 11 : (Utils::isNinja() ? COUNT_FREE_DESIGNS : COUNT_FREE_DESIGNS_SELF_HOST); @@ -153,7 +163,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon public function getMaxNumClients() { - if ($this->isPro()) { + if ($this->isPro() && ! $this->isTrial()) { return MAX_NUM_CLIENTS_PRO; } @@ -166,14 +176,10 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon public function getMaxNumVendors() { - if ($this->isPro()) { + if ($this->isPro() && ! $this->isTrial()) { return MAX_NUM_VENDORS_PRO; } - if ($this->id < LEGACY_CUTOFF) { - return MAX_NUM_VENDORS_LEGACY; - } - return MAX_NUM_VENDORS; } diff --git a/app/Ninja/Mailers/ContactMailer.php b/app/Ninja/Mailers/ContactMailer.php index 3c715c6f2d61..8959602ce6a2 100644 --- a/app/Ninja/Mailers/ContactMailer.php +++ b/app/Ninja/Mailers/ContactMailer.php @@ -127,10 +127,10 @@ class ContactMailer extends Mailer $subject = $this->processVariables($subject, $variables); $fromEmail = $user->email; - if ($account->email_design_id == EMAIL_DESIGN_PLAIN) { + if ($account->getEmailDesignId() == EMAIL_DESIGN_PLAIN) { $view = ENTITY_INVOICE; } else { - $view = 'design' . ($account->email_design_id - 1); + $view = 'design' . ($account->getEmailDesignId() - 1); } $response = $this->sendTo($invitation->contact->email, $fromEmail, $account->getDisplayName(), $subject, $view, $data); @@ -189,10 +189,10 @@ class ContactMailer extends Mailer $subject = $this->processVariables($emailSubject, $variables); $data['invoice_id'] = $payment->invoice->id; - if ($account->email_design_id == EMAIL_DESIGN_PLAIN) { + if ($account->getEmailDesignId() == EMAIL_DESIGN_PLAIN) { $view = 'payment_confirmation'; } else { - $view = 'design' . ($account->email_design_id - 1); + $view = 'design' . ($account->getEmailDesignId() - 1); } if ($user->email && $contact->email) { diff --git a/app/Ninja/Repositories/AccountRepository.php b/app/Ninja/Repositories/AccountRepository.php index af708fcb320f..0123b4c974f3 100644 --- a/app/Ninja/Repositories/AccountRepository.php +++ b/app/Ninja/Repositories/AccountRepository.php @@ -119,7 +119,7 @@ class AccountRepository public function enableProPlan() { - if (Auth::user()->isPro()) { + if (Auth::user()->isPro() && ! Auth::user()->isTrial()) { return false; } @@ -141,7 +141,7 @@ class AccountRepository $invoice->public_id = $publicId; $invoice->client_id = $client->id; $invoice->invoice_number = $account->getNextInvoiceNumber($invoice); - $invoice->invoice_date = date_create()->format('Y-m-d'); + $invoice->invoice_date = Auth::user()->account->getRenewalDate(); $invoice->amount = PRO_PLAN_PRICE; $invoice->balance = PRO_PLAN_PRICE; $invoice->save(); @@ -266,6 +266,8 @@ class AccountRepository $user->first_name = $firstName; $user->last_name = $lastName; $user->registered = true; + + $user->account->startTrial(); } $user->oauth_provider_id = $providerId; diff --git a/app/Services/InvoiceService.php b/app/Services/InvoiceService.php index 8648f315f6cf..203edb8a1b05 100644 --- a/app/Services/InvoiceService.php +++ b/app/Services/InvoiceService.php @@ -84,7 +84,7 @@ class InvoiceService extends BaseService return null; } - if ($account->auto_convert_quote) { + if ($account->auto_convert_quote || ! $account->isPro()) { $invoice = $this->convertQuote($quote, $invitation); event(new QuoteInvitationWasApproved($quote, $invoice, $invitation)); diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index dab7bd2924fe..78e6a031c1f7 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -212,20 +212,13 @@ class PaymentService extends BaseService { $invoice = $invitation->invoice; - // sync pro accounts - if ($invoice->account->account_key == NINJA_ACCOUNT_KEY - && $invoice->amount == PRO_PLAN_PRICE) { + // enable pro plan for hosted users + if ($invoice->account->account_key == NINJA_ACCOUNT_KEY && $invoice->amount == PRO_PLAN_PRICE) { $account = Account::with('users')->find($invoice->client->public_id); - if ($account->pro_plan_paid && $account->pro_plan_paid != '0000-00-00') { - $date = DateTime::createFromFormat('Y-m-d', $account->pro_plan_paid); - $date->modify('+1 year'); - $date = max($date, date_create()); - $account->pro_plan_paid = $date->format('Y-m-d'); - } else { - $account->pro_plan_paid = date_create()->format('Y-m-d'); - } + $account->pro_plan_paid = $account->getRenewalDate(); $account->save(); + // sync pro accounts $user = $account->users()->first(); $this->accountRepo->syncAccounts($user->id, $account->pro_plan_paid); } diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index bafe82800826..2b9c80f9740a 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -1103,7 +1103,7 @@ return array( 'quote_message_button' => 'To view your quote for :amount, click the button below.', 'payment_message_button' => 'Thank you for your payment of :amount.', 'payment_type_direct_debit' => 'Direct Debit', - 'bank_accounts' => 'Bank Accounts', + 'bank_accounts' => 'Credit Cards & Banks', 'add_bank_account' => 'Add Bank Account', 'setup_account' => 'Setup Account', 'import_expenses' => 'Import Expenses', @@ -1144,4 +1144,9 @@ return array( 'enable_https' => 'We strongly recommend using HTTPS to accept credit card details online.', 'quote_issued_to' => 'Quote issued to', 'show_currency_code' => 'Currency Code', + 'trial_message' => 'Your account will receive a free two week trial of our pro plan.', + 'trial_footer' => 'Your free trial lasts :count more days, :link to upgrade now.', + 'trial_footer_last_day' => 'This is the last day of your free trial, :link to upgrade now.', + 'trial_call_to_action' => 'Start Free Trial', + 'trial_success' => 'Successfully enabled two week free pro plan trial', ); diff --git a/resources/views/accounts/user_management.blade.php b/resources/views/accounts/user_management.blade.php index 969154faec0b..0fc17a7a3fa4 100644 --- a/resources/views/accounts/user_management.blade.php +++ b/resources/views/accounts/user_management.blade.php @@ -6,7 +6,7 @@
- @if (Utils::isPro()) + @if (Utils::isPro() && ! Utils::isTrial()) {!! Button::primary(trans('texts.add_user'))->asLinkTo(URL::to('/users/create'))->appendIcon(Icon::create('plus-sign')) !!} @endif
diff --git a/resources/views/header.blade.php b/resources/views/header.blade.php index 8e0945e3b7cd..5b7ff15ba803 100644 --- a/resources/views/header.blade.php +++ b/resources/views/header.blade.php @@ -185,7 +185,7 @@ window.open('{{ Utils::isNinjaDev() ? '' : NINJA_APP_URL }}/license?affiliate_key=' + affiliateKey + '&product_id=' + productId + '&return_url=' + window.location); } - @if (Auth::check() && !Auth::user()->isPro()) + @if (Auth::check() && (!Auth::user()->isPro() || Auth::user()->isTrial())) function submitProPlan() { fbq('track', 'AddPaymentInfo'); trackEvent('/account', '/submit_pro_plan/' + NINJA.proPlanFeature); @@ -382,7 +382,7 @@ @if (Auth::check()) @if (!Auth::user()->registered) {!! Button::success(trans('texts.sign_up'))->withAttributes(array('id' => 'signUpButton', 'data-toggle'=>'modal', 'data-target'=>'#signUpModal'))->small() !!}   - @elseif (!Auth::user()->isPro()) + @elseif (Utils::isNinjaProd() && (!Auth::user()->isPro() || Auth::user()->isTrial())) {!! Button::success(trans('texts.go_pro'))->withAttributes(array('id' => 'proPlanButton', 'onclick' => 'showProPlan("")'))->small() !!}   @endif @endif @@ -599,10 +599,16 @@ {{ Former::setOption('TwitterBootstrap3.labelWidths.large', 4) }} {{ Former::setOption('TwitterBootstrap3.labelWidths.small', 4) }} + +
+
{{ trans('texts.trial_message') }}
+
{!! Former::close() !!} - + + +

@@ -655,11 +661,10 @@ @endif -@if (Auth::check() && !Auth::user()->isPro()) +@if (Auth::check() && (!Auth::user()->isPro() || Auth::user()->isTrial()))