1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-27 03:37:11 +02:00

Merge pull request #6548 from turbo124/v5-stable

v5.3.4
This commit is contained in:
David Bomba 2021-09-02 16:18:56 +10:00 committed by GitHub
commit 7c0787e7e1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 311 additions and 51 deletions

View File

@ -1 +1 @@
5.3.3
5.3.4

View File

@ -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

View File

@ -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 */

View File

@ -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)

View File

@ -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;
}

View File

@ -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'],
],

View File

@ -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());
}
}
}

View File

@ -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}");
}
}
});
}

View File

@ -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();
}
}

View File

@ -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');

View File

@ -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,
]);
}
}

View File

@ -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();
}
/**

View File

@ -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();

View File

@ -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);

View File

@ -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 */

View File

@ -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', ''),

View File

@ -52,6 +52,9 @@
<a class="text-xs text-gray-600 hover:text-gray-800 ease-in duration-100"
href="{{ route('client.password.request') }}">{{ trans('texts.forgot_password') }}</a>
</div>
@if($company)
<input type="hidden" name="db" value="{{$company->db}}">
@endif
<input type="password" name="password" id="password"
class="input"
autofocus>

View File

@ -0,0 +1,171 @@
@extends('portal.ninja2020.layout.app')
@section('meta_title', ctrans('texts.pro_plan_call_to_action'))
@section('body')
<style>
/* Toggle A */
input:checked ~ .dot {
transform: translateX(100%);
background-color: #48bb78;
}
/* Toggle B */
input:checked ~ .dot {
transform: translateX(100%);
background-color: #48bb78;
}
</style>
<div class="container flex flex-wrap pt-4 pb-10 m-auto mt-6 md:mt-15 lg:px-12 xl:px-16" x-data="{show: true}">
<div class="w-full px-0 lg:px-4">
<h2 class="px-12 text-base font-bold text-center md:text-2xl text-blue-700">
Choose your plan
</h2>
<p class="py-1 text-sm text-center text-blue-700 mb-10">
<!-- Toggle B -->
<div class="flex items-center justify-center w-full mb-12"">
<label for="toggleB" class="flex items-center cursor-pointer">
<!-- toggle -->
<div class="relative">
<!-- input -->
<input type="checkbox" id="toggleB" class="sr-only" @click="show = !show">
<!-- line -->
<div class="block bg-gray-600 w-14 h-8 rounded-full"></div>
<!-- dot -->
<div class="dot absolute left-1 top-1 bg-white w-6 h-6 rounded-full transition"></div>
</div>
<!-- label -->
<div class="ml-3 text-gray-700 font-medium">
Monthly vs Annual
</div>
</label>
</div>
</p>
<!-- monthly Plans -->
<div class="flex flex-wrap items-center justify-center py-4 pt-0" x-show=" show ">
<div class="w-full p-4 md:w-1/2 lg:w-1/2">
<label class="flex flex-col rounded-lg shadow-lg relative cursor-pointer hover:shadow-2xl">
<div class="w-full px-4 py-8 rounded-t-lg bg-blue-500">
<h3 class="mx-auto text-base font-semibold text-center underline text-white group-hover:text-white">
Pro Plan
</h3>
<p class="text-5xl font-bold text-center text-white">
$10
</p>
<p class="text-xs text-center uppercase text-white">
monthly
</p>
</div>
<div class="flex flex-col items-center justify-center w-full h-full py-6 rounded-b-lg bg-blue-700">
<p class="text-xl text-white">
Sign up!
</p>
<a type="button" class="w-5/6 py-2 mt-2 font-semibold text-center uppercase bg-white border border-transparent rounded text-blue-500" href="https://invoiceninja.invoicing.co/client/subscriptions/WJxbojagwO/purchase">
Purchase
</a>
</div>
</label>
</div>
<div class="w-full p-4 md:w-1/2 lg:w-1/2">
<label class="flex flex-col rounded-lg shadow-lg relative cursor-pointer hover:shadow-2xl">
<div class="w-full px-4 py-8 rounded-t-lg bg-blue-500">
<h3 class="mx-auto text-base font-semibold text-center underline text-white group-hover:text-white">
Enterprise (1-2 Users)
</h3>
<p class="text-5xl font-bold text-center text-white">
$14
</p>
<p class="text-xs text-center uppercase text-white">
monthly
</p>
</div>
<div class="flex flex-col items-center justify-center w-full h-full py-6 rounded-b-lg bg-blue-700">
<p class="text-xl text-white">
Sign up!
</p>
<a type="button" class="w-5/6 py-2 mt-2 font-semibold text-center uppercase bg-white border border-transparent rounded text-blue-500" href="https://invoiceninja.invoicing.co/client/subscriptions/7LDdwRb1YK/purchase">
Purchase
</a>
</div>
</label>
</div>
</div>
<!-- Annual Plans -->
<div class="flex flex-wrap items-center justify-center py-4 pt-0" x-show=" !show ">
<div class="w-full p-4 md:w-1/2 lg:w-1/2">
<label class="flex flex-col rounded-lg shadow-lg relative cursor-pointer hover:shadow-2xl">
<div class="w-full px-4 py-8 rounded-t-lg bg-blue-500">
<h3 class="mx-auto text-base font-semibold text-center underline text-white group-hover:text-white">
Pro Plan
</h3>
<p class="text-5xl font-bold text-center text-white">
$100
</p>
<p class="text-xs text-center uppercase text-white">
yearly
</p>
</div>
<div
class="flex flex-col items-center justify-center w-full h-full py-6 rounded-b-lg bg-blue-700"
>
<p class="text-xl text-white">
Buy 10 months get 2 free!
</p>
<a type="button" class="w-5/6 py-2 mt-2 font-semibold text-center uppercase bg-white border border-transparent rounded text-blue-500" href="https://invoiceninja.invoicing.co/client/subscriptions/q9wdL9wejP/purchase">
Purchase
</a>
</div>
</label>
</div>
<div class="w-full p-4 md:w-1/2 lg:w-1/2">
<label class="flex flex-col rounded-lg shadow-lg relative cursor-pointer hover:shadow-2xl">
<div class="w-full px-4 py-8 rounded-t-lg bg-blue-500">
<h3 class="mx-auto text-base font-semibold text-center underline text-white group-hover:text-white">
Enterprise (1-2 Users)
</h3>
<p class="text-5xl font-bold text-center text-white">
$140
</p>
<p class="text-xs text-center uppercase text-white">
yearly
</p>
</div>
<div
class="flex flex-col items-center justify-center w-full h-full py-6 rounded-b-lg bg-blue-700"
>
<p class="text-xl text-white">
Buy 10 months get 2 free!
</p>
<a type="button" class="w-5/6 py-2 mt-2 font-semibold text-center uppercase bg-white border border-transparent rounded text-blue-500" href="https://invoiceninja.invoicing.co/client/subscriptions/LYqaQWldnj/purchase">
Purchase
</a>
</div>
</label>
</div>
</div>
</div>
</div>
@endsection
@push('footer')
@endpush

View File

@ -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