diff --git a/VERSION.txt b/VERSION.txt index 3884b9bcd9..86f2a61e3d 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.3.3 \ No newline at end of file +5.3.4 \ No newline at end of file diff --git a/app/Http/Controllers/Auth/ContactLoginController.php b/app/Http/Controllers/Auth/ContactLoginController.php index 37530b5fb6..057adf6448 100644 --- a/app/Http/Controllers/Auth/ContactLoginController.php +++ b/app/Http/Controllers/Auth/ContactLoginController.php @@ -13,6 +13,7 @@ namespace App\Http\Controllers\Auth; use App\Events\Contact\ContactLoggedIn; use App\Http\Controllers\Controller; +use App\Libraries\MultiDB; use App\Models\Account; use App\Models\ClientContact; use App\Models\Company; @@ -40,8 +41,16 @@ class ContactLoginController extends Controller $company = null; }elseif (strpos($request->getHost(), 'invoicing.co') !== false) { $subdomain = explode('.', $request->getHost())[0]; + + MultiDB::findAndSetDbByDomain(['subdomain' => $subdomain]); + $company = Company::where('subdomain', $subdomain)->first(); - } elseif(Ninja::isHosted() && $company = Company::where('portal_domain', $request->getSchemeAndHttpHost())->first()){ + + } elseif(Ninja::isHosted()){ + + MultiDB::findAndSetDbByDomain(['portal_domain' => $request->getSchemeAndHttpHost()]); + + $company = Company::where('portal_domain', $request->getSchemeAndHttpHost())->first(); } elseif (Ninja::isSelfHost()) { @@ -61,6 +70,9 @@ class ContactLoginController extends Controller { Auth::shouldUse('contact'); + if(Ninja::isHosted() && $request->has('db')) + MultiDB::setDb($request->input('db')); + $this->validateLogin($request); // If the class is using the ThrottlesLogins trait, we can automatically throttle // the login attempts for this application. We'll key this by the username and diff --git a/app/Http/Controllers/ClientPortal/NinjaPlanController.php b/app/Http/Controllers/ClientPortal/NinjaPlanController.php index 81c0122cbe..2d8cd2240f 100644 --- a/app/Http/Controllers/ClientPortal/NinjaPlanController.php +++ b/app/Http/Controllers/ClientPortal/NinjaPlanController.php @@ -16,28 +16,28 @@ use App\Http\Controllers\Controller; use App\Http\Requests\ClientPortal\Uploads\StoreUploadRequest; use App\Libraries\MultiDB; use App\Models\ClientContact; +use App\Models\Company; use App\Utils\Ninja; +use Auth; use Illuminate\Contracts\Routing\ResponseFactory; use Illuminate\Http\Request; use Illuminate\Http\Response; -use Auth; class NinjaPlanController extends Controller { - public function index(string $contact_key) + public function index(string $contact_key, string $company_key) { + MultiDB::findAndSetDbByCompanyKey($company_key); + $company = Company::where('company_key', $company_key)->first(); + $account = $company->account; if (Ninja::isHosted() && MultiDB::findAndSetDbByContactKey(request()->segment(3)) && $client_contact = ClientContact::where('contact_key', request()->segment(3))->first()) - { - // auth()->guard('contact')->login($client_contact, true); - Auth::guard('contact')->login($client_contact); - - /* Harvest user account*/ - $account = $client_contact->company->account; + { + Auth::guard('contact')->login($client_contact,true); /* Current paid users get pushed straight to subscription overview page*/ - if($account->isPaid()) + if($account->isPaidHostedClient()) return redirect('/client/subscriptions'); /* Users that are not paid get pushed to a custom purchase page */ diff --git a/app/Http/Controllers/StripeController.php b/app/Http/Controllers/StripeController.php index 2e4227d81a..61d412a1c2 100644 --- a/app/Http/Controllers/StripeController.php +++ b/app/Http/Controllers/StripeController.php @@ -14,6 +14,7 @@ namespace App\Http\Controllers; use App\Jobs\Util\ImportStripeCustomers; use App\Jobs\Util\StripeUpdatePaymentMethods; +use App\Libraries\MultiDB; use App\Models\Client; use App\Models\CompanyGateway; @@ -60,6 +61,8 @@ class StripeController extends BaseController if(auth()->user()->isAdmin()) { + MultiDB::findAndSetDbByCompanyKey(auth()->user()->company()->company_key); + $company_gateway = CompanyGateway::where('company_id', auth()->user()->company()->id) ->where('is_deleted',0) ->whereIn('gateway_key', $this->stripe_keys) diff --git a/app/Http/ViewComposers/PortalComposer.php b/app/Http/ViewComposers/PortalComposer.php index 3c70aa695a..e5be85883e 100644 --- a/app/Http/ViewComposers/PortalComposer.php +++ b/app/Http/ViewComposers/PortalComposer.php @@ -79,7 +79,7 @@ class PortalComposer $data['currencies'] = TranslationHelper::getCurrencies(); $data['contact'] = auth('contact')->user(); - $data['multiple_contacts'] = session()->get('multiple_contacts'); + $data['multiple_contacts'] = session()->get('multiple_contacts') ?: collect(); return $data; } diff --git a/app/Jobs/Company/CompanyImport.php b/app/Jobs/Company/CompanyImport.php index 17017722a2..10a030bd04 100644 --- a/app/Jobs/Company/CompanyImport.php +++ b/app/Jobs/Company/CompanyImport.php @@ -875,7 +875,7 @@ class CompanyImport implements ShouldQueue { $this->genericImport(Design::class, - ['company_id', 'user_id'], + ['company_id', 'user_id', 'hashed_id'], [ ['users' => 'user_id'], ], diff --git a/app/Jobs/Cron/AutoBillCron.php b/app/Jobs/Cron/AutoBillCron.php index fcd6c3c876..ac74f307eb 100644 --- a/app/Jobs/Cron/AutoBillCron.php +++ b/app/Jobs/Cron/AutoBillCron.php @@ -51,8 +51,12 @@ class AutoBillCron ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) ->where('auto_bill_enabled', true) ->where('balance', '>', 0) - ->with('company') - ->cursor()->each(function ($invoice){ + ->where('is_deleted', false) + ->with('company'); + + nlog($auto_bill_partial_invoices->count(). " partial invoices to auto bill"); + + $auto_bill_partial_invoices->cursor()->each(function ($invoice){ $this->runAutoBiller($invoice); }); @@ -60,8 +64,12 @@ class AutoBillCron ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) ->where('auto_bill_enabled', true) ->where('balance', '>', 0) - ->with('company') - ->cursor()->each(function ($invoice){ + ->where('is_deleted', false) + ->with('company'); + + nlog($auto_bill_invoices->count(). " full invoices to auto bill"); + + $auto_bill_invoices->cursor()->each(function ($invoice){ $this->runAutoBiller($invoice); }); @@ -69,14 +77,19 @@ class AutoBillCron } else { //multiDB environment, need to foreach (MultiDB::$dbs as $db) { + MultiDB::setDB($db); - + $auto_bill_partial_invoices = Invoice::whereDate('partial_due_date', '<=', now()) ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) ->where('auto_bill_enabled', true) ->where('balance', '>', 0) - ->with('company') - ->cursor()->each(function ($invoice){ + ->where('is_deleted', false) + ->with('company'); + + nlog($auto_bill_partial_invoices->count(). " partial invoices to auto bill db = {$db}"); + + $auto_bill_partial_invoices->cursor()->each(function ($invoice){ $this->runAutoBiller($invoice); }); @@ -84,8 +97,12 @@ class AutoBillCron ->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) ->where('auto_bill_enabled', true) ->where('balance', '>', 0) - ->with('company') - ->cursor()->each(function ($invoice){ + ->where('is_deleted', false) + ->with('company'); + + nlog($auto_bill_invoices->count(). " full invoices to auto bill db = {$db}"); + + $auto_bill_invoices->cursor()->each(function ($invoice){ $this->runAutoBiller($invoice); }); @@ -96,6 +113,12 @@ class AutoBillCron private function runAutoBiller(Invoice $invoice) { info("Firing autobill for {$invoice->company_id} - {$invoice->number}"); - $invoice->service()->autoBill()->save(); + + try{ + $invoice->service()->autoBill()->save(); + } + catch(\Exception $e) { + nlog("Failed to capture payment for {$invoice->company_id} - {$invoice->number} ->" . $e->getMessage()); + } } } diff --git a/app/Jobs/Cron/RecurringInvoicesCron.php b/app/Jobs/Cron/RecurringInvoicesCron.php index ae993e180e..51b8bb874e 100644 --- a/app/Jobs/Cron/RecurringInvoicesCron.php +++ b/app/Jobs/Cron/RecurringInvoicesCron.php @@ -46,6 +46,7 @@ class RecurringInvoicesCron $recurring_invoices = RecurringInvoice::where('next_send_date', '<=', now()->toDateTimeString()) ->whereNotNull('next_send_date') ->whereNull('deleted_at') + ->where('is_deleted', false) ->where('status_id', RecurringInvoice::STATUS_ACTIVE) ->where('remaining_cycles', '!=', '0') ->whereHas('client', function ($query) { @@ -61,7 +62,13 @@ class RecurringInvoicesCron nlog("Current date = " . now()->format("Y-m-d") . " Recurring date = " .$recurring_invoice->next_send_date); if (!$recurring_invoice->company->is_disabled) { - SendRecurring::dispatchNow($recurring_invoice, $recurring_invoice->company->db); + + try{ + SendRecurring::dispatchNow($recurring_invoice, $recurring_invoice->company->db); + } + catch(\Exception $e){ + nlog("Unable to sending recurring invoice {$recurring_invoice->id}"); + } } }); } else { @@ -72,6 +79,7 @@ class RecurringInvoicesCron $recurring_invoices = RecurringInvoice::where('next_send_date', '<=', now()->toDateTimeString()) ->whereNotNull('next_send_date') ->whereNull('deleted_at') + ->where('is_deleted', false) ->where('status_id', RecurringInvoice::STATUS_ACTIVE) ->where('remaining_cycles', '!=', '0') ->whereHas('client', function ($query) { @@ -87,7 +95,13 @@ class RecurringInvoicesCron nlog("Current date = " . now()->format("Y-m-d") . " Recurring date = " .$recurring_invoice->next_send_date ." Recurring #id = ". $recurring_invoice->id); if (!$recurring_invoice->company->is_disabled) { - SendRecurring::dispatchNow($recurring_invoice, $recurring_invoice->company->db); + + try{ + SendRecurring::dispatchNow($recurring_invoice, $recurring_invoice->company->db); + } + catch(\Exception $e){ + nlog("Unable to sending recurring invoice {$recurring_invoice->id} on db {$db}"); + } } }); } diff --git a/app/Jobs/RecurringInvoice/SendRecurring.php b/app/Jobs/RecurringInvoice/SendRecurring.php index d19ebd3847..56f6bbbc3c 100644 --- a/app/Jobs/RecurringInvoice/SendRecurring.php +++ b/app/Jobs/RecurringInvoice/SendRecurring.php @@ -129,9 +129,21 @@ class SendRecurring implements ShouldQueue } }); - if ($invoice->client->getSetting('auto_bill_date') == 'on_send_date' && $this->recurring_invoice->auto_bill_enabled) { + if ($invoice->client->getSetting('auto_bill_date') == 'on_send_date' && $invoice->auto_bill_enabled) { + nlog("attempting to autobill {$invoice->number}"); $invoice->service()->autoBill()->save(); + + } + elseif($invoice->client->getSetting('auto_bill_date') == 'on_due_date' && $invoice->auto_bill_enabled) { + + if($invoice->due_date && Carbon\Carbon::parse($invoice->due_date)->startOfDay()->lte(now()->startOfDay())) { + + nlog("attempting to autobill {$invoice->number}"); + $invoice->service()->autoBill()->save(); + + } + } diff --git a/app/Mail/SupportMessageSent.php b/app/Mail/SupportMessageSent.php index 337495ab8e..e882513890 100644 --- a/app/Mail/SupportMessageSent.php +++ b/app/Mail/SupportMessageSent.php @@ -64,7 +64,7 @@ class SupportMessageSent extends Mailable $db = str_replace("db-ninja-", "", $company->db); if(Ninja::isHosted()) - $subject = "{$priority}Hosted-{$db} :: {$plan} :: ".date('M jS, g:ia'); + $subject = "{$priority}Hosted-{$db}-[{$company->is_large}] :: {$plan} :: ".date('M jS, g:ia'); else $subject = "{$priority}Self Hosted :: {$plan} :: ".date('M jS, g:ia'); diff --git a/app/Mail/TestMailServer.php b/app/Mail/TestMailServer.php index 7308c19ab7..a40d65843b 100644 --- a/app/Mail/TestMailServer.php +++ b/app/Mail/TestMailServer.php @@ -29,6 +29,7 @@ class TestMailServer extends Mailable $this->from_email = $from_email; } + /** * Test Server mail. * @@ -36,12 +37,18 @@ class TestMailServer extends Mailable */ public function build() { + + $settings = new \stdClass; + $settings->primary_color = "#4caf50"; + $settings->email_style = 'dark'; + return $this->from(config('mail.from.address'), config('mail.from.name')) ->subject(ctrans('texts.email')) ->markdown('email.support.message', [ 'support_message' => $this->support_messages, 'system_info' => '', 'laravel_log' => [], + 'settings' => $settings, ]); } } diff --git a/app/Models/ClientGatewayToken.php b/app/Models/ClientGatewayToken.php index cd3d273360..a3928ce4bf 100644 --- a/app/Models/ClientGatewayToken.php +++ b/app/Models/ClientGatewayToken.php @@ -45,7 +45,7 @@ class ClientGatewayToken extends BaseModel public function client() { - return $this->hasOne(Client::class)->withTrashed(); + return $this->belongsTo(Client::class)->withTrashed(); } public function gateway() @@ -60,12 +60,12 @@ class ClientGatewayToken extends BaseModel public function company() { - return $this->hasOne(Company::class); + return $this->belongsTo(Company::class); } public function user() { - return $this->hasOne(User::class)->withTrashed(); + return $this->belongsTo(User::class)->withTrashed(); } /** diff --git a/app/PaymentDrivers/Stripe/CreditCard.php b/app/PaymentDrivers/Stripe/CreditCard.php index cdb36d8d35..43fd5eb470 100644 --- a/app/PaymentDrivers/Stripe/CreditCard.php +++ b/app/PaymentDrivers/Stripe/CreditCard.php @@ -63,7 +63,7 @@ class CreditCard 'amount' => $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision, $this->stripe->client->currency()), 'currency' => $this->stripe->client->getCurrencyCode(), 'customer' => $this->stripe->findOrCreateCustomer(), - 'description' => ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number'), // TODO: More meaningful description. + 'description' => $this->decodeUnicodeString(ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number')), ]; $payment_intent_data['setup_future_usage'] = 'off_session'; @@ -74,6 +74,15 @@ class CreditCard return render('gateways.stripe.credit_card.pay', $data); } + private function decodeUnicodeString($string) + { + return iconv("UTF-8", "ISO-8859-1//TRANSLIT", $this->decode_encoded_utf8($string)); + } + + private function decode_encoded_utf8($string){ + return preg_replace_callback('#\\\\u([0-9a-f]{4})#ism', function($matches) { return mb_convert_encoding(pack("H*", $matches[1]), "UTF-8", "UCS-2BE"); }, $string); + } + public function paymentResponse(PaymentResponseRequest $request) { $this->stripe->init(); diff --git a/app/PaymentDrivers/Stripe/ImportCustomers.php b/app/PaymentDrivers/Stripe/ImportCustomers.php index 2baf26d5bb..4f561b98d3 100644 --- a/app/PaymentDrivers/Stripe/ImportCustomers.php +++ b/app/PaymentDrivers/Stripe/ImportCustomers.php @@ -23,6 +23,7 @@ use App\Models\Currency; use App\Models\GatewayType; use App\PaymentDrivers\StripePaymentDriver; use App\PaymentDrivers\Stripe\UpdatePaymentMethods; +use App\Utils\Ninja; use App\Utils\Traits\GeneratesCounter; use App\Utils\Traits\MakesHash; use Stripe\Customer; @@ -51,8 +52,8 @@ class ImportCustomers $this->update_payment_methods = new UpdatePaymentMethods($this->stripe); - if(strlen($this->stripe->company_gateway->getConfigField('account_id')) < 1) - throw new StripeConnectFailure('Stripe Connect has not been configured'); + if(Ninja::isHosted() && strlen($this->stripe->company_gateway->getConfigField('account_id')) < 1) + throw new StripeConnectFailure('Stripe Connect has not been configured'); $customers = Customer::all([], $this->stripe->stripe_connect_auth); @@ -61,9 +62,6 @@ class ImportCustomers $this->addCustomer($customer); } - /* Now call the update payment methods handler*/ - // $this->stripe->updateAllPaymentMethods(); - } private function addCustomer(Customer $customer) @@ -76,14 +74,21 @@ class ImportCustomers nlog("search Stripe for {$customer->id}"); - $existing_customer = $this->stripe + $existing_customer_token = $this->stripe ->company_gateway ->client_gateway_tokens() ->where('gateway_customer_reference', $customer->id) - ->exists(); + ->first(); - if($existing_customer){ - nlog("Skipping - Customer exists: {$customer->email}"); + if($existing_customer_token){ + nlog("Skipping - Customer exists: {$customer->email} just updating payment methods"); + $this->update_payment_methods->updateMethods($customer, $existing_customer_token->client); + return; + } + + if($customer->email && $contact = $this->stripe->company_gateway->company->client_contacts()->where('email', $customer->email)->first()){ + nlog("Customer exists: {$customer->email} just updating payment methods"); + $this->update_payment_methods->updateMethods($customer, $contact->client); return; } @@ -92,15 +97,15 @@ class ImportCustomers $client = ClientFactory::create($this->stripe->company_gateway->company_id, $this->stripe->company_gateway->user_id); - if(property_exists($customer, 'address')) + if($customer->address) { - $client->address1 = property_exists($customer->address, 'line1') ? $customer->address->line1 : ''; - $client->address2 = property_exists($customer->address, 'line2') ? $customer->address->line2 : ''; - $client->city = property_exists($customer->address, 'city') ? $customer->address->city : ''; - $client->state = property_exists($customer->address, 'state') ? $customer->address->state : ''; - $client->phone = property_exists($customer->address, 'phone') ? $customer->phone : ''; + $client->address1 = $customer->address->line1 ? $customer->address->line1 : ''; + $client->address2 = $customer->address->line2 ? $customer->address->line2 : ''; + $client->city = $customer->address->city ? $customer->address->city : ''; + $client->state = $customer->address->state ? $customer->address->state : ''; + $client->phone = $customer->address->phone ? $customer->phone : ''; - if(property_exists($customer->address, 'country')){ + if($customer->address->country){ $country = Country::where('iso_3166_2', $customer->address->country)->first(); @@ -124,7 +129,7 @@ class ImportCustomers } - $client->name = property_exists($customer, 'name') ? $customer->name : $customer->email; + $client->name = $customer->name ? $customer->name : $customer->email; if (!isset($client->number) || empty($client->number)) { $client->number = $this->getNextClientNumber($client); diff --git a/app/PaymentDrivers/Stripe/UpdatePaymentMethods.php b/app/PaymentDrivers/Stripe/UpdatePaymentMethods.php index 74d09d4abe..38949de575 100644 --- a/app/PaymentDrivers/Stripe/UpdatePaymentMethods.php +++ b/app/PaymentDrivers/Stripe/UpdatePaymentMethods.php @@ -130,6 +130,7 @@ class UpdatePaymentMethods $token_exists = ClientGatewayToken::where([ 'gateway_customer_reference' => $customer_reference, 'token' => $method->id, + 'company_id' => $client->company_id, ])->exists(); /* Already exists return */ diff --git a/config/ninja.php b/config/ninja.php index f469bdfe48..74ba6b62db 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -14,8 +14,8 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => '5.3.3', - 'app_tag' => '5.3.3', + 'app_version' => '5.3.4', + 'app_tag' => '5.3.4', 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', 'api_secret' => env('API_SECRET', ''), diff --git a/resources/views/portal/ninja2020/auth/login.blade.php b/resources/views/portal/ninja2020/auth/login.blade.php index ec2e188f8f..69b1ff0a71 100644 --- a/resources/views/portal/ninja2020/auth/login.blade.php +++ b/resources/views/portal/ninja2020/auth/login.blade.php @@ -52,6 +52,9 @@ {{ trans('texts.forgot_password') }} + @if($company) + + @endif diff --git a/resources/views/portal/ninja2020/subscriptions/ninja_plan.blade.php b/resources/views/portal/ninja2020/subscriptions/ninja_plan.blade.php new file mode 100644 index 0000000000..b1d4716d0d --- /dev/null +++ b/resources/views/portal/ninja2020/subscriptions/ninja_plan.blade.php @@ -0,0 +1,171 @@ +@extends('portal.ninja2020.layout.app') +@section('meta_title', ctrans('texts.pro_plan_call_to_action')) + +@section('body') + + +
+
+

+ Choose your plan +

+

+ + + +

+ + + +
+ +

+ + + +
+
+ +
+ + +
+ +
+ + + +
+ + + + + +
+
+ +
+ +
+ +
+ +
+ +
+
+@endsection + +@push('footer') + + +@endpush diff --git a/routes/client.php b/routes/client.php index b57bc2eedf..12e530ffa2 100644 --- a/routes/client.php +++ b/routes/client.php @@ -26,7 +26,7 @@ Route::get('client/magic_link/{magic_link}', 'ClientPortal\ContactHashLoginContr Route::get('documents/{document_hash}', 'ClientPortal\DocumentController@publicDownload')->name('documents.public_download')->middleware(['document_db']); Route::get('error', 'ClientPortal\ContactHashLoginController@errorPage')->name('client.error'); Route::get('client/payment/{contact_key}/{payment_id}', 'ClientPortal\InvitationController@paymentRouter')->middleware(['domain_db','contact_key_login']); -Route::get('client/ninja/{contact_key}', 'ClientPortal\NinjaPlanController@index')->name('client.ninja_contact_login')->middleware(['domain_db']); +Route::get('client/ninja/{contact_key}/{company_key}', 'ClientPortal\NinjaPlanController@index')->name('client.ninja_contact_login')->middleware(['domain_db']); Route::group(['middleware' => ['auth:contact', 'locale', 'check_client_existence','domain_db'], 'prefix' => 'client', 'as' => 'client.'], function () { Route::get('dashboard', 'ClientPortal\DashboardController@index')->name('dashboard'); // name = (dashboard. index / create / show / update / destroy / edit