mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-09 03:14:30 -04:00
Add support for multiple plans/terms
This commit is contained in:
parent
51cd82cba8
commit
9b1bfef698
@ -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'));
|
||||
|
@ -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'],
|
||||
|
@ -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)) {
|
||||
|
@ -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'];
|
||||
}
|
||||
}
|
||||
|
||||
|
21
app/Models/Company.php
Normal file
21
app/Models/Company.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php namespace App\Models;
|
||||
|
||||
use Eloquent;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Company extends Eloquent
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
public function accounts()
|
||||
{
|
||||
return $this->hasMany('App\Models\Account');
|
||||
}
|
||||
|
||||
public function payment()
|
||||
{
|
||||
return $this->belongsTo('App\Models\Payment');
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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;
|
||||
|
@ -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']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
111
database/migrations/2016_04_16_103943_enterprise_plan.php
Normal file
111
database/migrations/2016_04_16_103943_enterprise_plan.php
Normal file
@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use App\Models\Company;
|
||||
use App\Models\Account;
|
||||
|
||||
class EnterprisePlan extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('companies', function($table)
|
||||
{
|
||||
$table->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');
|
||||
}
|
||||
}
|
@ -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!<p/> <br/>
|
||||
<b>Next Steps</b><p/>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;
|
||||
|
@ -7,21 +7,113 @@
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<!--<div class="panel panel-default">
|
||||
@if (Utils::isNinjaProd())
|
||||
{!! Former::open('settings/change_plan')->addClass('change-plan') !!}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{!! trans('texts.plan_status') !!}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{{ trans('texts.plan') }}</label>
|
||||
<div class="col-sm-8">
|
||||
<p class="form-control-static">
|
||||
@if ($planDetails && $planDetails['active'])
|
||||
{{ trans('texts.plan_'.$planDetails['plan']) }}
|
||||
@if ($planDetails['trial'])
|
||||
({{ trans('texts.plan_trial') }})
|
||||
@elseif ($planDetails['expires'])
|
||||
({{ trans('texts.plan_term_'.$planDetails['term'].'ly') }})
|
||||
@endif
|
||||
@else
|
||||
{{ trans('texts.plan_free') }}
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@if ($planDetails && $planDetails['active'])
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 control-label">Plan</label>
|
||||
<div class="col-sm-10">
|
||||
<p class="form-control-static">{{ trans('texts.plan_'.$account->plan) }}</p>
|
||||
<label class="col-sm-4 control-label">
|
||||
@if($planDetails['active'])
|
||||
{{ trans('texts.expires') }}
|
||||
@else
|
||||
{{ trans('texts.expired') }}
|
||||
@endif
|
||||
</label>
|
||||
<div class="col-sm-8">
|
||||
<p class="form-control-static">
|
||||
@if ($planDetails['expires'] === false)
|
||||
{{ trans('texts.never') }}
|
||||
@else
|
||||
{{ Utils::dateToString($planDetails['expires']) }}
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@if ($account->company->pending_plan)
|
||||
<div class="form-group">
|
||||
<label class="col-sm-4 control-label">{{ trans('texts.pending_change_to') }}</label>
|
||||
<div class="col-sm-8">
|
||||
<p class="form-control-static">
|
||||
@if ($account->company->pending_plan == PLAN_FREE)
|
||||
{{ trans('texts.plan_free') }}
|
||||
@else
|
||||
{{ trans('texts.plan_'.$account->company->pending_plan) }}
|
||||
({{ trans('texts.plan_term_'.$account->company->pending_term.'ly') }})
|
||||
@endif
|
||||
<a href="#" onclick="cancelPendingChange()">{{ trans('texts.cancel') }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
{!! Former::actions( Button::info(trans('texts.plan_change'))->large()->withAttributes(['onclick' => 'showChangePlan()'])->appendIcon(Icon::create('edit'))) !!}
|
||||
@else
|
||||
{!! Former::actions( Button::success(trans('texts.plan_upgrade'))->large()->withAttributes(['onclick' => 'showChangePlan()'])->appendIcon(Icon::create('plus-sign'))) !!}
|
||||
@endif
|
||||
</div>
|
||||
</div>-->
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="changePlanModel" tabindex="-1" role="dialog" aria-labelledby="changePlanModelLabel" aria-hidden="true">
|
||||
<div class="modal-dialog" style="min-width:150px">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title" id="changePlanModelLabel">
|
||||
@if ($planDetails && $planDetails['active'])
|
||||
{!! trans('texts.plan_change') !!}
|
||||
@else
|
||||
{!! trans('texts.plan_upgrade') !!}
|
||||
@endif
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@if ($planDetails && $planDetails['active'])
|
||||
{!! Former::select('plan')
|
||||
->addOption(trans('texts.plan_enterprise'), PLAN_ENTERPRISE)
|
||||
->addOption(trans('texts.plan_pro'), PLAN_PRO)
|
||||
->addOption(trans('texts.plan_free'), PLAN_FREE)!!}
|
||||
@else
|
||||
{!! Former::select('plan')
|
||||
->addOption(trans('texts.plan_enterprise'), PLAN_ENTERPRISE)
|
||||
->addOption(trans('texts.plan_pro'), PLAN_PRO)!!}
|
||||
@endif
|
||||
{!! Former::select('plan_term')
|
||||
->addOption(trans('texts.plan_term_yearly'), PLAN_TERM_YEARLY)
|
||||
->addOption(trans('texts.plan_term_monthly'), PLAN_TERM_MONTHLY)!!}
|
||||
</div>
|
||||
<div class="modal-footer" style="margin-top: 0px">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">{{ trans('texts.go_back') }}</button>
|
||||
@if ($planDetails && $planDetails['active'])
|
||||
<button type="button" class="btn btn-primary" onclick="confirmChangePlan()">{{ trans('texts.plan_change') }}</button>
|
||||
@else
|
||||
<button type="button" class="btn btn-success" onclick="confirmChangePlan()">{{ trans('texts.plan_upgrade') }}</button>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!! Former::close() !!}
|
||||
@endif
|
||||
|
||||
{!! Former::open('settings/cancel_account')->addClass('cancel-account') !!}
|
||||
<div class="panel panel-default">
|
||||
@ -54,17 +146,51 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!! Former::close() !!}
|
||||
{!! Former::close() !!}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
function showConfirm() {
|
||||
$('#confirmCancelModal').modal('show');
|
||||
}
|
||||
function showChangePlan() {
|
||||
$('#changePlanModel').modal('show');
|
||||
}
|
||||
|
||||
function confirmCancel() {
|
||||
$('form.cancel-account').submit();
|
||||
}
|
||||
function confirmChangePlan() {
|
||||
$('form.change-plan').submit();
|
||||
}
|
||||
|
||||
function showConfirm() {
|
||||
$('#confirmCancelModal').modal('show');
|
||||
}
|
||||
|
||||
function confirmCancel() {
|
||||
$('form.cancel-account').submit();
|
||||
}
|
||||
|
||||
@if ($account->company->pending_plan)
|
||||
function cancelPendingChange(){
|
||||
$('#plan').val('{{ $planDetails['plan'] }}')
|
||||
$('#plan_term').val('{{ $planDetails['term'] }}')
|
||||
confirmChangePlan();
|
||||
return false;
|
||||
}
|
||||
@endif
|
||||
|
||||
jQuery(document).ready(function($){
|
||||
function updatePlanModal() {
|
||||
var plan = $('#plan').val();
|
||||
$('#plan_term').closest('.form-group').toggle(plan!='free');
|
||||
|
||||
if(plan=='{{PLAN_PRO}}'){
|
||||
$('#plan_term option[value=month]').text({!! json_encode(trans('texts.plan_price_monthly', ['price'=>PLAN_PRICE_PRO_MONTHLY])) !!});
|
||||
$('#plan_term option[value=year]').text({!! json_encode(trans('texts.plan_price_yearly', ['price'=>PLAN_PRICE_PRO_YEARLY])) !!});
|
||||
} else if(plan=='{{PLAN_ENTERPRISE}}') {
|
||||
$('#plan_term option[value=month]').text({!! json_encode(trans('texts.plan_price_monthly', ['price'=>PLAN_PRICE_ENTERPRISE_MONTHLY])) !!});
|
||||
$('#plan_term option[value=year]').text({!! json_encode(trans('texts.plan_price_yearly', ['price'=>PLAN_PRICE_ENTERPRISE_YEARLY])) !!});
|
||||
}
|
||||
}
|
||||
$('#plan_term, #plan').change(updatePlanModal);
|
||||
updatePlanModal();
|
||||
});
|
||||
</script>
|
||||
@stop
|
@ -726,8 +726,8 @@
|
||||
<center>
|
||||
<h2>{{ trans('texts.pro_plan_title') }}</h2>
|
||||
<img class="img-responsive price" alt="Only $50 Per Year" src="{{ asset('images/pro_plan/price.png') }}"/>
|
||||
@if (Auth::user()->isEligibleForTrial())
|
||||
<a class="button" href="{{ URL::to('start_trial') }}">{{ trans('texts.trial_call_to_action') }}</a>
|
||||
@if (Auth::user()->isEligibleForTrial(PLAN_PRO))
|
||||
<a class="button" href="{{ URL::to('start_trial/'.PLAN_PRO) }}">{{ trans('texts.trial_call_to_action') }}</a>
|
||||
@else
|
||||
<a class="button" href="#" onclick="submitProPlan()">{{ trans('texts.pro_plan_call_to_action') }}</a>
|
||||
@endif
|
||||
|
Loading…
x
Reference in New Issue
Block a user