From 9b1bfef6984874f4904a9449169c0337d56ceef8 Mon Sep 17 00:00:00 2001 From: Joshua Dwire Date: Sat, 16 Apr 2016 18:34:39 -0400 Subject: [PATCH] Add support for multiple plans/terms --- app/Http/Controllers/AccountController.php | 136 ++++++++++++- app/Http/routes.php | 15 +- app/Libraries/Utils.php | 39 +--- app/Models/Account.php | 184 +++++++++++++++--- app/Models/Company.php | 21 ++ app/Models/User.php | 4 +- app/Ninja/Repositories/AccountRepository.php | 56 ++++-- app/Ninja/Repositories/ReferralRepository.php | 8 +- app/Services/PaymentService.php | 45 +++-- .../2016_04_16_103943_enterprise_plan.php | 111 +++++++++++ resources/lang/en/texts.php | 32 ++- resources/views/accounts/management.blade.php | 154 +++++++++++++-- resources/views/header.blade.php | 4 +- 13 files changed, 687 insertions(+), 122 deletions(-) create mode 100644 app/Models/Company.php create mode 100644 database/migrations/2016_04_16_103943_enterprise_plan.php diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index b22de1887169..b8d7b84035a4 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -30,6 +30,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; @@ -39,8 +40,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(); @@ -48,6 +50,7 @@ class AccountController extends BaseController $this->userMailer = $userMailer; $this->contactMailer = $contactMailer; $this->referralRepository = $referralRepository; + $this->paymentService = $paymentService; } public function demo() @@ -110,10 +113,125 @@ 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); + + $credit = 0; + if ($planDetails['active']) { + 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)) { + $percent_used = $planDetails['paid']->diff(date_create())->days / $planDetails['paid']->diff($planDetails['expires'])->days; + $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) { @@ -235,9 +353,11 @@ class AccountController extends BaseController private function showAccountManagement() { + $account = Auth::user()->account; $data = [ - 'account' => Auth::user()->account, - 'title' => trans('texts.acount_management'), + 'account' => $account, + 'planDetails' => $account->getPlanDetails(), + 'title' => trans('texts.account_management'), ]; return View::make('accounts.management', $data); @@ -955,7 +1075,7 @@ class AccountController extends BaseController $user->registered = true; $user->save(); - $user->account->startTrial(); + $user->account->startTrial(PLAN_ENTERPRISE); if (Input::get('go_pro') == 'true') { Session::set(REQUESTED_PRO_PLAN, true); @@ -1027,12 +1147,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/routes.php b/app/Http/routes.php index 7c3f9c8739f9..7ebd29ecc759 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|enterprise']); 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'); @@ -583,6 +585,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); @@ -645,6 +651,13 @@ 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_TERM_MONTHLY', 'month'); + define('PLAN_TERM_YEARLY', 'year'); $creditCards = [ 1 => ['card' => 'images/credit_cards/Test-Visa-Icon.png', 'text' => 'Visa'], diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php index 439247f337c0..7213a2ef2b7d 100644 --- a/app/Libraries/Utils.php +++ b/app/Libraries/Utils.php @@ -440,7 +440,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 +966,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/Models/Account.php b/app/Models/Account.php index 92813865f2e1..1c871ae28fe6 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']; @@ -177,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; @@ -762,17 +778,18 @@ 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 isPro(&$plan_details = null) { if (!Utils::isNinjaProd()) { return true; @@ -782,14 +799,109 @@ class Account extends Eloquent return true; } - $datePaid = $this->pro_plan_paid; - $trialStart = $this->pro_plan_trial; + $plan_details = $this->getPlanDetails(); + + return $plan_details && $plan_details['active']; + } - 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['active'] && $plan_details['plan'] == PLAN_ENTERPRISE; + } + + public function getPlanDetails($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 && $this->company->plan_paid == NINJA_DATE) { + $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; + } + } + } + + // 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)) { + // Neither are active; use whichever expired most recently + $use_plan = $plan_expires >= $trial_expires; + } else { + // 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; + } + } + } + + 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() @@ -797,35 +909,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'] && $plan_details['active'];; } - 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(); + + if(!$planDetails || !$planDetails['trial']) { + return 0; + } + + $today = new DateTime('now'); + $interval = $today->diff($planDetails['expires']); 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'); + $planDetails = $this->getPlanDetails(); + + if ($planDetails && $planDetails['active']) { + $date = $planDetails['expires']; $date = max($date, date_create()); - } elseif ($this->isTrial()) { - $date = date_create(); - $date->modify('+'.$this->getCountTrialDaysLeft().' day'); } else { $date = date_create(); } @@ -840,13 +971,10 @@ class Account extends Eloquent } if (Utils::isNinjaProd()) { - return self::isPro() && $this->pro_plan_paid != NINJA_DATE; + return self::isPro($plan_details) && $plan_details['expires']; } else { - if ($this->pro_plan_paid == NINJA_DATE) { - return true; - } - - return Utils::withinPastYear($this->pro_plan_paid); + $plan_details = $this->getPlanDetails(); + return $plan_details && $plan_details['active']; } } diff --git a/app/Models/Company.php b/app/Models/Company.php new file mode 100644 index 000000000000..1345db9e4bfb --- /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/User.php b/app/Models/User.php index 1b2ae7815a36..c431dbb34484 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -122,9 +122,9 @@ 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() diff --git a/app/Ninja/Repositories/AccountRepository.php b/app/Ninja/Repositories/AccountRepository.php index cd07a37b5db2..0a4fe424b6c8 100644 --- a/app/Ninja/Repositories/AccountRepository.php +++ b/app/Ninja/Repositories/AccountRepository.php @@ -205,21 +205,23 @@ class AccountRepository return $data; } - public function enableProPlan() + public function enablePlan($plan = PLAN_PRO, $term = PLAN_TERM_MONTHLY, $credit = 0, $pending_monthly = false) { - if (Auth::user()->isPro() && ! Auth::user()->isTrial()) { - return false; - } - $account = Auth::user()->account; $client = $this->getNinjaClient($account); - $invitation = $this->createNinjaInvoice($client, $account); + $invitation = $this->createNinjaInvoice($client, $account, $plan, $term, $credit, $pending_monthly); return $invitation; } - public function createNinjaInvoice($client, $clientAccount) + public function createNinjaInvoice($client, $clientAccount, $plan = PLAN_PRO, $term = PLAN_TERM_MONTHLY, $credit = 0, $pending_monthly = false) { + if ($credit < 0) { + $credit = 0; + } + + $plan_cost = Account::$plan_prices[$plan][$term]; + $account = $this->getNinjaAccount(); $lastInvoice = Invoice::withTrashed()->whereAccountId($account->id)->orderBy('public_id', 'DESC')->first(); $publicId = $lastInvoice ? ($lastInvoice->public_id + 1) : 1; @@ -230,19 +232,39 @@ class AccountRepository $invoice->client_id = $client->id; $invoice->invoice_number = $account->getNextInvoiceNumber($invoice); $invoice->invoice_date = $clientAccount->getRenewalDate(); - $invoice->amount = PRO_PLAN_PRICE; - $invoice->balance = PRO_PLAN_PRICE; + $invoice->amount = $invoice->balance = $plan_cost - $credit; $invoice->save(); - $item = new InvoiceItem(); - $item->account_id = $account->id; - $item->user_id = $account->users()->first()->id; - $item->public_id = $publicId; + if ($credit) { + $credit_item = InvoiceItem::createNew($invoice); + $credit_item->qty = 1; + $credit_item->cost = -$credit; + $credit_item->notes = trans('texts.plan_credit_description'); + $credit_item->product_key = trans('texts.plan_credit_product'); + $invoice->invoice_items()->save($credit_item); + } + + $item = InvoiceItem::createNew($invoice); $item->qty = 1; - $item->cost = PRO_PLAN_PRICE; - $item->notes = trans('texts.pro_plan_description'); - $item->product_key = trans('texts.pro_plan_product'); + $item->cost = $plan_cost; + $item->notes = trans("texts.{$plan}_plan_{$term}_description"); + + // Don't change this without updating the regex in PaymentService->createPayment() + $item->product_key = 'Plan - '.ucfirst($plan).' ('.ucfirst($term).')'; $invoice->invoice_items()->save($item); + + if ($pending_monthly) { + $term_end = $term == PLAN_MONTHLY ? date_create('+1 month') : date_create('+1 year'); + $pending_monthly_item = InvoiceItem::createNew($invoice); + $item->qty = 1; + $pending_monthly_item->cost = 0; + $pending_monthly_item->notes = trans("texts.plan_pending_monthly", array('date', Utils::dateToString($term_end))); + + // Don't change this without updating the text in PaymentService->createPayment() + $pending_monthly_item->product_key = 'Pending Monthly'; + $invoice->invoice_items()->save($pending_monthly_item); + } + $invitation = new Invitation(); $invitation->account_id = $account->id; @@ -355,7 +377,7 @@ class AccountRepository $user->last_name = $lastName; $user->registered = true; - $user->account->startTrial(); + $user->account->startTrial(PLAN_ENTERPRISE); } $user->oauth_provider_id = $providerId; diff --git a/app/Ninja/Repositories/ReferralRepository.php b/app/Ninja/Repositories/ReferralRepository.php index c847f3386b39..0a215d40cc42 100644 --- a/app/Ninja/Repositories/ReferralRepository.php +++ b/app/Ninja/Repositories/ReferralRepository.php @@ -13,13 +13,17 @@ class ReferralRepository $counts = [ 'free' => 0, - 'pro' => 0 + 'pro' => 0, + 'enterprise' => 0 ]; foreach ($accounts as $account) { $counts['free']++; - if (Utils::withinPastYear($account->pro_plan_paid)) { + if ($account->isPro()) { $counts['pro']++; + if ($account->isEnterprise()) { + $counts['enterprise']++; + } } } diff --git a/app/Services/PaymentService.php b/app/Services/PaymentService.php index 1669d94d88d4..76e09256d2b4 100644 --- a/app/Services/PaymentService.php +++ b/app/Services/PaymentService.php @@ -216,17 +216,6 @@ class PaymentService extends BaseService { $invoice = $invitation->invoice; - // 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); - $account->pro_plan_paid = $account->getRenewalDate(); - $account->save(); - - // sync pro accounts - $user = $account->users()->first(); - $this->accountRepo->syncAccounts($user->id, $account->pro_plan_paid); - } - $payment = Payment::createNew($invitation); $payment->invitation_id = $invitation->id; $payment->account_gateway_id = $accountGateway->id; @@ -236,13 +225,45 @@ class PaymentService extends BaseService $payment->contact_id = $invitation->contact_id; $payment->transaction_reference = $ref; $payment->payment_date = date_create()->format('Y-m-d'); - + if ($payerId) { $payment->payer_id = $payerId; } $payment->save(); + // enable pro plan for hosted users + if ($invoice->account->account_key == NINJA_ACCOUNT_KEY) { + foreach ($invoice->invoice_items as $invoice_item) { + // Hacky, but invoices don't have meta fields to allow us to store this easily + if (1 == preg_match('/^Plan - (.+) \((.+)\)$/', $invoice_item->product_key, $matches)) { + $plan = strtolower($matches[1]); + $term = strtolower($matches[2]); + } elseif ($invoice_item->product_key == 'Pending Monthly') { + $pending_monthly = true; + } + } + + if (!empty($plan)) { + $account = Account::with('users')->find($invoice->client->public_id); + $account->company->payment_id = $payment->id; + $account->company->plan = $plan; + $account->company->plan_term = $term; + $account->company->plan_paid = $account->company->plan_started = date_create()->format('Y-m-d'); + $account->company->plan_expires = date_create($term == PLAN_TERM_MONTHLY ? '+1 month' : '+1 year')->format('Y-m-d'); + + if (!empty($pending_monthly)) { + $account->company->pending_plan = $plan; + $account->company->pending_term = PLAN_TERM_MONTHLY; + } else { + $account->company->pending_plan = null; + $account->company->pending_term = null; + } + + $account->company->save(); + } + } + return $payment; } diff --git a/database/migrations/2016_04_16_103943_enterprise_plan.php b/database/migrations/2016_04_16_103943_enterprise_plan.php new file mode 100644 index 000000000000..ab6bf8892258 --- /dev/null +++ b/database/migrations/2016_04_16_103943_enterprise_plan.php @@ -0,0 +1,111 @@ +increments('id'); + + $table->enum('plan', array('pro', 'enterprise'))->nullable(); + $table->enum('plan_term', array('month', 'year'))->nullable(); + $table->date('plan_started')->nullable(); + $table->date('plan_paid')->nullable(); + $table->date('plan_expires')->nullable(); + + $table->unsignedInteger('payment_id')->nullable(); + $table->foreign('payment_id')->references('id')->on('payments'); + + $table->date('trial_started')->nullable(); + $table->enum('trial_plan', array('pro', 'enterprise'))->nullable(); + + $table->enum('pending_plan', array('pro', 'enterprise', 'free'))->nullable(); + $table->enum('pending_term', array('month', 'year'))->nullable(); + + // Used when a user has started changing a plan but hasn't finished paying yet + $table->enum('temp_pending_plan', array('pro', 'enterprise'))->nullable(); + $table->enum('temp_pending_term', array('month', 'year'))->nullable(); + + $table->timestamps(); + $table->softDeletes(); + }); + + Schema::table('accounts', function($table) + { + $table->unsignedInteger('company_id')->nullable(); + $table->foreign('company_id')->references('id')->on('companies'); + }); + + foreach (Account::all() as $account) { + $company = Company::create(); + if ($account->pro_plan_paid && $account->pro_plan_paid != '0000-00-00') { + $company->plan = 'pro'; + $company->plan_term = 'year'; + $company->plan_started = $account->pro_plan_paid; + $company->plan_paid = $account->pro_plan_paid; + + if ($company->plan_paid != NINJA_DATE) { + $expires = DateTime::createFromFormat('Y-m-d', $account->pro_plan_paid); + $expires->modify('+1 year'); + $company->plan_expires = $expires->format('Y-m-d'); + } + } + + if ($account->pro_plan_trial && $account->pro_plan_trial != '0000-00-00') { + $company->trial_started = $account->pro_plan_trial; + $company->trial_plan = 'pro'; + } + + $company->save(); + + $account->company_id = $company->id; + $account->save(); + } + + /*Schema::table('accounts', function($table) + { + $table->dropColumn('pro_plan_paid'); + $table->dropColumn('pro_plan_trial'); + });*/ + } + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + /*Schema::table('accounts', function($table) + { + $table->date('pro_plan_paid')->nullable(); + $table->date('pro_plan_trial')->nullable(); + });*/ + + foreach (Company::all() as $company) { + foreach ($company->accounts as $account) { + $account->pro_plan_paid = $company->plan_paid; + $account->pro_plan_trial = $company->trial_started; + $account->save(); + } + } + + Schema::table('accounts', function($table) + { + $table->dropForeign('accounts_company_id_foreign'); + $table->dropColumn('company_id'); + }); + + Schema::drop('companies'); + } +} \ No newline at end of file diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 20c1ef7dae0a..f08be8ace67c 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -268,7 +268,6 @@ $LANG = array( 'erase_data' => 'This will permanently erase your data.', 'password' => 'Password', 'pro_plan_product' => 'Pro Plan', - 'pro_plan_description' => 'One year enrollment in the Invoice Ninja Pro Plan.', 'pro_plan_success' => 'Thanks for choosing Invoice Ninja\'s Pro plan!

 
Next Steps

A payable invoice has been sent to the email address associated with your account. To unlock all of the awesome @@ -1127,13 +1126,40 @@ $LANG = array( 'enable_client_portal_dashboard' => 'Dashboard', 'enable_client_portal_dashboard_help' => 'Show/hide the dashboard page in the client portal.', + + // Plans 'account_management' => 'Account Management', 'plan_status' => 'Plan Status', + 'plan_upgrade' => 'Upgrade', + 'plan_change' => 'Change Plan', + 'pending_change_to' => 'Pending Change To', + 'plan' => 'Plan', + 'expires' => 'Expires', + 'expired' => 'Expired', + 'never' => 'never', 'plan_free' => 'Free', 'plan_pro' => 'Pro', - 'plan_enterprise' => 'Enterprise' - + 'plan_enterprise' => 'Enterprise', + 'plan_trial' => 'Trial', + 'plan_term' => 'Term', + 'plan_term_monthly' => 'Monthly', + 'plan_term_yearly' => 'Yearly', + 'plan_term_month' => 'Month', + 'plan_term_year' => 'Year', + 'plan_price_monthly' => '$:price/Month', + 'plan_price_yearly' => '$:price/Year', + 'updated_plan' => 'Updated plan settings', + + 'pro_plan_year_description' => 'One year enrollment in the Invoice Ninja Pro Plan.', + 'pro_plan_month_description' => 'One month enrollment in the Invoice Ninja Pro Plan.', + 'enterprise_plan_product' => 'Enterprise Plan', + 'enterprise_plan_year_description' => 'One year enrollment in the Invoice Ninja Enterprise Plan.', + 'enterprise_plan_month_description' => 'One month enrollment in the Invoice Ninja Enterprise Plan.', + 'plan_credit_product' => 'Credit', + 'plan_credit_description' => 'Credit for unused time', + 'plan_pending_monthly' => 'Will switch to monthly on :date', + 'plan_refunded' => 'A refund has been issued.' ); return $LANG; diff --git a/resources/views/accounts/management.blade.php b/resources/views/accounts/management.blade.php index bc131db70028..395610b07f28 100644 --- a/resources/views/accounts/management.blade.php +++ b/resources/views/accounts/management.blade.php @@ -7,21 +7,113 @@

- +
+ + + {!! Former::close() !!} + @endif {!! Former::open('settings/cancel_account')->addClass('cancel-account') !!}
@@ -54,17 +146,51 @@
- {!! Former::close() !!} + {!! Former::close() !!} @stop \ No newline at end of file diff --git a/resources/views/header.blade.php b/resources/views/header.blade.php index 40b499fe8848..9a0185ea24ba 100644 --- a/resources/views/header.blade.php +++ b/resources/views/header.blade.php @@ -726,8 +726,8 @@

{{ trans('texts.pro_plan_title') }}

Only $50 Per Year - @if (Auth::user()->isEligibleForTrial()) - {{ trans('texts.trial_call_to_action') }} + @if (Auth::user()->isEligibleForTrial(PLAN_PRO)) + {{ trans('texts.trial_call_to_action') }} @else {{ trans('texts.pro_plan_call_to_action') }} @endif