1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-28 20:27:13 +02:00

Merge pull request #5862 from turbo124/v5-stable

v5.1.67
This commit is contained in:
David Bomba 2021-05-31 17:19:33 +10:00 committed by GitHub
commit b244555fec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
160 changed files with 265583 additions and 226064 deletions

View File

@ -1 +1 @@
5.1.65
5.1.67

View File

@ -65,7 +65,7 @@ class CheckData extends Command
/**
* @var string
*/
protected $name = 'ninja:check-data';
protected $signature = 'ninja:check-data {--database=} {--fix=} {--client_id=}';
/**
* @var string

View File

@ -34,6 +34,7 @@ use App\Models\Project;
use App\Models\Quote;
use App\Models\RecurringInvoice;
use App\Models\Task;
use App\Models\TaxRate;
use App\Models\User;
use App\Models\Vendor;
use App\Models\VendorContact;
@ -109,6 +110,14 @@ class CreateSingleAccount extends Command
'portal_domain' => 'http://ninja.test:8000',
]);
$settings = $company->settings;
$settings->invoice_terms = 'Default company invoice terms';
$settings->quote_terms = 'Default company quote terms';
$settings->invoice_footer = 'Default invoice footer';
$company->settings = $settings;
$company->save();
$account->default_company_id = $company->id;
$account->save();
@ -146,6 +155,29 @@ class CreateSingleAccount extends Command
'company_id' => $company->id,
]);
TaxRate::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'name' => 'GST',
'rate' => 10
]);
TaxRate::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'name' => 'VAT',
'rate' => 17.5
]);
TaxRate::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'name' => 'CA Sales Tax',
'rate' => 5
]);
$this->info('Creating '.$this->count.' clients');
for ($x = 0; $x < $this->count; $x++) {

View File

@ -12,12 +12,12 @@
namespace App\Console\Commands;
use App\Jobs\Ninja\SendReminders;
use App\Jobs\Util\WebHookHandler;
use App\Libraries\MultiDB;
use App\Models\Invoice;
use App\Models\Quote;
use App\Models\Webhook;
use Illuminate\Console\Command;
use App\Jobs\Util\WebhookHandler;
class SendRemindersCron extends Command
{
@ -54,8 +54,8 @@ class SendRemindersCron extends Command
{
SendReminders::dispatchNow();
$this->webHookOverdueInvoices();
$this->webHookExpiredQuotes();
$this->webHookOverdueInvoices();
$this->webHookExpiredQuotes();
}
private function webHookOverdueInvoices()
@ -90,6 +90,7 @@ class SendRemindersCron extends Command
$invoices->each(function ($invoice) {
WebHookHandler::dispatch(Webhook::EVENT_LATE_INVOICE, $invoice, $invoice->company);
});
$quotes = Quote::where('is_deleted', 0)

View File

@ -11,8 +11,9 @@
namespace App\Console;
use App\Jobs\Cron\SubscriptionCron;
use App\Jobs\Cron\AutoBillCron;
use App\Jobs\Cron\RecurringInvoicesCron;
use App\Jobs\Cron\SubscriptionCron;
use App\Jobs\Ninja\AdjustEmailQuota;
use App\Jobs\Ninja\CompanySizeCheck;
use App\Jobs\Util\ReminderJob;
@ -46,7 +47,7 @@ class Kernel extends ConsoleKernel
$schedule->job(new VersionCheck)->daily();
$schedule->command('ninja:check-data')->daily()->withoutOverlapping();
$schedule->command('ninja:check-data --database=db-ninja-01')->daily()->withoutOverlapping();
$schedule->job(new ReminderJob)->daily()->withoutOverlapping();
@ -58,6 +59,8 @@ class Kernel extends ConsoleKernel
$schedule->job(new RecurringInvoicesCron)->hourly()->withoutOverlapping();
$schedule->job(new AutoBillCron)->dailyAt('00:30')->withoutOverlapping();
$schedule->job(new SchedulerCheck)->everyFiveMinutes();
/* Run hosted specific jobs */
@ -65,6 +68,7 @@ class Kernel extends ConsoleKernel
$schedule->job(new AdjustEmailQuota)->daily()->withoutOverlapping();
$schedule->job(new SendFailedEmails)->daily()->withoutOverlapping();
$schedule->command('ninja:check-data --database=db-ninja-02')->daily()->withoutOverlapping();
}

View File

@ -202,7 +202,7 @@ class CompanySettings extends BaseSettings
public $schedule_reminder2 = ''; // (enum: after_invoice_date, before_due_date, after_due_date) implmemented
public $schedule_reminder3 = ''; // (enum: after_invoice_date, before_due_date, after_due_date) implmemented
public $reminder_send_time = 32400; //number of seconds from UTC +0 to send reminders @TODO
public $reminder_send_time = 0; //number of seconds from UTC +0 to send reminders @TODO
public $late_fee_amount1 = 0; //@implemented
public $late_fee_amount2 = 0; //@implemented
@ -245,8 +245,8 @@ class CompanySettings extends BaseSettings
public $hide_paid_to_date = false; //@TODO where?
public $embed_documents = false; //@TODO where?
public $all_pages_header = false; //@implemented
public $all_pages_footer = false; //@implemented
public $all_pages_header = false; //@deprecated 31-05-2021
public $all_pages_footer = false; //@deprecated 31-05-2021
public $pdf_variables = ''; //@implemented
public $portal_custom_head = ''; //@TODO @BEN
@ -667,8 +667,9 @@ class CompanySettings extends BaseSettings
'$custom_surcharge4',
'$total_taxes',
'$line_taxes',
'$paid_to_date',
'$total',
'$paid_to_date',
'$outstanding',
],
];

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class ImportCompanyFailed extends Exception
{
// ..
}

View File

@ -12,7 +12,9 @@
namespace App\Factory;
use App\DataMapper\CompanySettings;
use App\Libraries\MultiDB;
use App\Models\Company;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
class CompanyFactory
@ -33,7 +35,12 @@ class CompanyFactory
$company->db = config('database.default');
//$company->custom_fields = (object) ['invoice1' => '1', 'invoice2' => '2', 'client1'=>'3'];
$company->custom_fields = (object) [];
$company->subdomain = '';
if(Ninja::isHosted())
$company->subdomain = MultiDB::randomSubdomainGenerator();
else
$company->subdomain = '';
$company->enabled_modules = config('ninja.enabled_modules'); //32767;//8191; //4095
$company->default_password_timeout = 1800000;

View File

@ -142,7 +142,7 @@ class AccountController extends BaseController
*/
public function store(CreateAccountRequest $request)
{
$account = CreateAccount::dispatchNow($request->all());
$account = CreateAccount::dispatchNow($request->all(), $request->getClientIp());
if (! ($account instanceof Account)) {
return $account;

View File

@ -13,6 +13,7 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Libraries\MultiDB;
use App\Models\Account;
use Illuminate\Contracts\View\Factory;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Http\Request;
@ -50,11 +51,15 @@ class ContactForgotPasswordController extends Controller
*
* @return Factory|View
*/
public function showLinkRequestForm()
public function showLinkRequestForm(Request $request)
{
$account_id = $request->get('account_id');
$account = Account::find($account_id);
return $this->render('auth.passwords.request', [
'title' => 'Client Password Reset',
'passwordEmailRoute' => 'client.password.email',
'account' => $account
]);
}

View File

@ -13,6 +13,7 @@ namespace App\Http\Controllers\Auth;
use App\Events\Contact\ContactLoggedIn;
use App\Http\Controllers\Controller;
use App\Models\Account;
use App\Models\ClientContact;
use App\Utils\Ninja;
use Auth;
@ -31,9 +32,13 @@ class ContactLoginController extends Controller
$this->middleware('guest:contact', ['except' => ['logout']]);
}
public function showLoginForm()
public function showLoginForm(Request $request)
{
return $this->render('auth.login');
$account_id = $request->get('account_id');
$account = Account::find($account_id);
return $this->render('auth.login', ['account' => $account]);
}
public function login(Request $request)

View File

@ -24,7 +24,7 @@ class ContactRegisterController extends Controller
$company = Company::where('company_key', $key)->firstOrFail();
return render('auth.register', ['company' => $company]);
return render('auth.register', ['company' => $company, 'account' => $company->account]);
}
public function register(RegisterRequest $request)

View File

@ -12,6 +12,7 @@
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\Account;
use Illuminate\Contracts\View\Factory;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\Request;
@ -62,8 +63,11 @@ class ContactResetPasswordController extends Controller
*/
public function showResetForm(Request $request, $token = null)
{
$account_id = $request->get('account_id');
$account = Account::find($account_id);
return $this->render('auth.passwords.reset')->with(
['token' => $token, 'email' => $request->email]
['token' => $token, 'email' => $request->email, 'account' => $account]
);
}

View File

@ -13,6 +13,7 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Libraries\MultiDB;
use App\Models\Account;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
@ -104,9 +105,8 @@ class ForgotPasswordController extends Controller
*/
public function sendResetLinkEmail(Request $request)
{
// MultiDB::userFindAndSetDb($request->input('email'));
// $user = MultiDB::hasUser(['email' => $request->input('email')]);
MultiDB::userFindAndSetDb($request->input('email'));
$user = MultiDB::hasUser(['email' => $request->input('email')]);
$this->validateEmail($request);

View File

@ -31,6 +31,7 @@ use App\Models\User;
use App\Transformers\CompanyUserTransformer;
use App\Utils\Ninja;
use App\Utils\Traits\UserSessionAttributes;
use App\Utils\Traits\User\LoginCache;
use Google_Client;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
@ -55,6 +56,7 @@ class LoginController extends BaseController
use AuthenticatesUsers;
use UserSessionAttributes;
use LoginCache;
protected $entity_type = CompanyUser::class;
@ -178,8 +180,7 @@ class LoginController extends BaseController
event(new UserLoggedIn($user, $user->account->default_company, Ninja::eventVars($user->id)));
//if user has 2fa enabled - lets check this now:
//2FA
if($user->google_2fa_secret && $request->has('one_time_password'))
{
$google2fa = new Google2FA();
@ -203,14 +204,7 @@ class LoginController extends BaseController
$user->setCompany($user->account->default_company);
$timeout = $user->company()->default_password_timeout;
if($timeout == 0)
$timeout = 30*60*1000*1000;
else
$timeout = $timeout/1000;
Cache::put($user->hashed_id.'_'.$user->account_id.'_logged_in', Str::random(64), $timeout);
$this->setLoginCache($user);
$cu = CompanyUser::query()
->where('user_id', auth()->user()->id);
@ -228,7 +222,7 @@ class LoginController extends BaseController
});
return $this->timeConstrainedResponse($cu);
// return $this->listResponse($cu);
} else {
@ -351,6 +345,7 @@ class LoginController extends BaseController
if (is_array($user)) {
//
$query = [
'oauth_user_id' => $google->harvestSubField($user),
'oauth_provider_id'=> 'google',
@ -361,14 +356,7 @@ class LoginController extends BaseController
Auth::login($existing_user, true);
$existing_user->setCompany($existing_user->account->default_company);
$timeout = $existing_user->company()->default_password_timeout;
if($timeout == 0)
$timeout = 30*60*1000*1000;
else
$timeout = $timeout/1000;
Cache::put($existing_user->hashed_id.'_'.$existing_user->account_id.'_logged_in', Str::random(64), $timeout);
$this->setLoginCache($existing_user);
$cu = CompanyUser::query()
->where('user_id', auth()->user()->id);
@ -384,10 +372,68 @@ class LoginController extends BaseController
return $this->timeConstrainedResponse($cu);
}
//If this is a result user/email combo - lets add their OAuth details details
if($existing_login_user = MultiDB::hasUser(['email' => $google->harvestEmail($user)]))
{
Auth::login($existing_login_user, true);
$existing_login_user->setCompany($existing_login_user->account->default_company);
$this->setLoginCache($existing_login_user);
auth()->user()->update([
'oauth_user_id' => $google->harvestSubField($user),
'oauth_provider_id'=> 'google',
]);
$cu = CompanyUser::query()
->where('user_id', auth()->user()->id);
$cu->first()->account->companies->each(function ($company) use($cu){
if($company->tokens()->where('is_system', true)->count() == 0)
{
CreateCompanyToken::dispatchNow($company, $cu->first()->user, request()->server('HTTP_USER_AGENT'));
}
});
return $this->timeConstrainedResponse($cu);
}
}
if ($user) {
//check the user doesn't already exist in some form
if($existing_login_user = MultiDB::hasUser(['email' => $google->harvestEmail($user)]))
{
Auth::login($existing_login_user, true);
$existing_login_user->setCompany($existing_login_user->account->default_company);
$this->setLoginCache($existing_login_user);
auth()->user()->update([
'oauth_user_id' => $google->harvestSubField($user),
'oauth_provider_id'=> 'google',
]);
$cu = CompanyUser::query()
->where('user_id', auth()->user()->id);
$cu->first()->account->companies->each(function ($company) use($cu){
if($company->tokens()->where('is_system', true)->count() == 0)
{
CreateCompanyToken::dispatchNow($company, $cu->first()->user, request()->server('HTTP_USER_AGENT'));
}
});
return $this->timeConstrainedResponse($cu);
}
//user not found anywhere - lets sign them up.
$name = OAuth::splitName($google->harvestName($user));
$new_account = [
@ -403,21 +449,14 @@ class LoginController extends BaseController
MultiDB::setDefaultDatabase();
$account = CreateAccount::dispatchNow($new_account);
$account = CreateAccount::dispatchNow($new_account, request()->getClientIp());
Auth::login($account->default_company->owner(), true);
auth()->user()->email_verified_at = now();
auth()->user()->save();
$timeout = auth()->user()->company()->default_password_timeout;
if($timeout == 0)
$timeout = 30*60*1000*1000;
else
$timeout = $timeout/1000;
Cache::put(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in', Str::random(64), $timeout);
$this->setLoginCache(auth()->user());
$cu = CompanyUser::whereUserId(auth()->user()->id);
@ -437,4 +476,62 @@ class LoginController extends BaseController
->header('X-App-Version', config('ninja.app_version'))
->header('X-Api-Version', config('ninja.minimum_client_version'));
}
public function redirectToProvider(string $provider)
{
//'https://www.googleapis.com/auth/gmail.send','email','profile','openid'
$scopes = [];
if($provider == 'google'){
$scopes = ['https://www.googleapis.com/auth/gmail.send','email','profile','openid'];
}
if (request()->has('code')) {
return $this->handleProviderCallback($provider);
} else {
return Socialite::driver($provider)->scopes($scopes)->redirect();
}
}
public function handleProviderCallback(string $provider)
{
$socialite_user = Socialite::driver($provider)
->stateless()
->user();
// if($user = OAuth::handleAuth($socialite_user, $provider))
// {
// Auth::login($user, true);
// return redirect($this->redirectTo);
// }
// else if(MultiDB::checkUserEmailExists($socialite_user->getEmail()))
// {
// Session::flash('error', 'User exists in system, but not with this authentication method'); //todo add translations
// return view('auth.login');
// }
// else {
// //todo
// $name = OAuth::splitName($socialite_user->getName());
// $new_account = [
// 'first_name' => $name[0],
// 'last_name' => $name[1],
// 'password' => '',
// 'email' => $socialite_user->getEmail(),
// 'oauth_user_id' => $socialite_user->getId(),
// 'oauth_provider_id' => $provider
// ];
// $account = CreateAccount::dispatchNow($new_account);
// Auth::login($account->default_company->owner(), true);
// $cookie = cookie('db', $account->default_company->db);
// return redirect($this->redirectTo)->withCookie($cookie);
// }
}
}

View File

@ -164,7 +164,7 @@ class BaseController extends Controller
*/
public function notFoundClient()
{
return abort(404);
abort(404, 'Page not found in client portal.');
}
/**
@ -309,10 +309,6 @@ class BaseController extends Controller
},
'company.tax_rates' => function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at);
if(!$user->isAdmin())
$query->where('tax_rates.user_id', $user->id);
},
'company.vendors'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('contacts', 'documents');
@ -323,15 +319,9 @@ class BaseController extends Controller
},
'company.expense_categories'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at);
if(!$user->isAdmin())
$query->where('expense_categories.user_id', $user->id);
},
'company.task_statuses'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at);
},
'company.activities'=> function ($query) use($user) {

View File

@ -585,4 +585,61 @@ class ClientController extends BaseController
}
/**
* Update the specified resource in storage.
*
* @param UploadClientRequest $request
* @param Client $client
* @return Response
*
*
*
* @OA\Put(
* path="/api/v1/clients/{id}/adjust_ledger",
* operationId="adjustLedger",
* tags={"clients"},
* summary="Adjust the client ledger to rebalance",
* description="Adjust the client ledger to rebalance",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Parameter(
* name="id",
* in="path",
* description="The Client Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the client object",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Client"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function adjustLedger(Request $request, Client $client)
{
}
}

View File

@ -68,7 +68,7 @@ class DocumentController extends Controller
$documents->map(function ($document) {
if (auth()->user('contact')->client->id != $document->documentable->id) {
abort(401);
abort(401, 'Permission denied');
}
});

View File

@ -31,7 +31,7 @@ class EntityViewController extends Controller
public function index(string $entity_type, string $invitation_key)
{
if (! in_array($entity_type, $this->entity_types)) {
abort(404);
abort(404, 'Entity not found');
}
$invitation_entity = sprintf('App\\Models\\%sInvitation', ucfirst($entity_type));
@ -91,7 +91,7 @@ class EntityViewController extends Controller
public function handlePassword(string $entity_type, string $invitation_key)
{
if (! in_array($entity_type, $this->entity_types)) {
abort(404);
abort(404, 'Entity not found');
}
$invitation_entity = sprintf('App\\Models\\%sInvitation', ucfirst($entity_type));

View File

@ -57,7 +57,7 @@ class InvitationController extends Controller
/* Return early if we have the correct client_hash embedded */
if (request()->has('client_hash') && request()->input('client_hash') == $invitation->contact->client->client_hash) {
auth()->guard('contact')->login($invitation->contact, true);
auth()->guard('contact')->loginUsingId($invitation->contact->id, true);
} elseif ((bool) $invitation->contact->client->getSetting('enable_client_portal_password') !== false) {
@ -66,7 +66,7 @@ class InvitationController extends Controller
return redirect()->route('client.login');
} else {
auth()->guard('contact')->login($invitation->contact, true);
auth()->guard('contact')->loginUsingId($invitation->contact->id, true);
}

View File

@ -149,6 +149,6 @@ class PaymentMethodController extends Controller
return $gateway = auth()->user()->client->getBankTransferGateway();
}
return abort(404);
abort(404, 'Gateway not found.');
}
}

View File

@ -17,6 +17,7 @@ use App\Models\CompanyUser;
use App\Models\User;
use App\Transformers\CompanyUserTransformer;
use App\Transformers\UserTransformer;
use App\Utils\Traits\User\LoginCache;
use Google_Client;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
@ -24,6 +25,7 @@ use Illuminate\Support\Str;
class ConnectedAccountController extends BaseController
{
use LoginCache;
protected $entity_type = User::class;
@ -113,9 +115,8 @@ class ConnectedAccountController extends BaseController
auth()->user()->email_verified_at = now();
auth()->user()->save();
$timeout = auth()->user()->company()->default_password_timeout;
Cache::put(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in', Str::random(64), $timeout);
$this->setLoginCache(auth()->user());
return $this->itemResponse(auth()->user());
}

View File

@ -93,11 +93,16 @@ class LoginController extends BaseController
public function redirectToProvider(string $provider)
{
//'https://www.googleapis.com/auth/gmail.send','email','profile','openid'
//
$scopes = [];
if($provider == 'google'){
$scopes = ['https://www.googleapis.com/auth/gmail.send','email','profile','openid'];
}
if (request()->has('code')) {
return $this->handleProviderCallback($provider);
} else {
return Socialite::driver($provider)->scopes()->redirect();
return Socialite::driver($provider)->scopes($scopes)->redirect();
}
}
@ -231,43 +236,5 @@ class LoginController extends BaseController
}
}
/*
* Received the returning object from the provider
* which we will use to resolve the user, we return the response in JSON format
*
* @return json
public function handleProviderCallbackApiUser(string $provider)
{
$socialite_user = Socialite::driver($provider)->stateless()->user();
if($user = OAuth::handleAuth($socialite_user, $provider))
{
return $this->itemResponse($user);
}
else if(MultiDB::checkUserEmailExists($socialite_user->getEmail()))
{
return $this->errorResponse(['message'=>'User exists in system, but not with this authentication method'], 400);
}
else {
//todo
$name = OAuth::splitName($socialite_user->getName());
$new_account = [
'first_name' => $name[0],
'last_name' => $name[1],
'password' => '',
'email' => $socialite_user->getEmail(),
];
$account = CreateAccount::dispatchNow($new_account);
return $this->itemResponse($account->default_company->owner());
}
}
*/
}

View File

@ -0,0 +1,96 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Controllers;
use App\Http\Requests\Import\ImportJsonRequest;
use App\Jobs\Company\CompanyExport;
use App\Jobs\Company\CompanyImport;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
class ImportJsonController extends BaseController
{
use MakesHash;
public function __construct()
{
parent::__construct();
}
/**
* @OA\Post(
* path="/api/v1/import_json",
* operationId="getImportJson",
* tags={"import"},
* summary="Import data from the system",
* description="Import data from the system",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Response(
* response=200,
* description="success",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
public function index(ImportJsonRequest $request)
{
$import_file = $request->file('files');
$contents = $this->unzipFile($import_file->getPathname());
$hash = Str::random(32);
Cache::put( $hash, base64_encode( $contents ), 3600 );
CompanyImport::dispatch(auth()->user()->getCompany(), auth()->user(), $hash, $request->all());
return response()->json(['message' => 'Processing'], 200);
}
private function unzipFile($file_contents)
{
$zip = new ZipArchive();
$archive = $zip->open($file_contents);
$filename = pathinfo($file_contents, PATHINFO_FILENAME);
$zip->extractTo(public_path("storage/backups/{$filename}"));
$zip->close();
$file_location = public_path("storage/backups/$filename/backup.json");
if (! file_exists($file_location))
throw new NonExistingMigrationFile('Backup file does not exist, or it is corrupted.');
$data = json_decode(file_get_contents($file_location));
unlink($file_contents);
unlink($file_location);
return $data
}
}

View File

@ -845,13 +845,11 @@ class InvoiceController extends BaseController
*/
public function deliveryNote(ShowInvoiceRequest $request, Invoice $invoice)
{
$file = $invoice->service()->getInvoiceDeliveryNote($invoice, $invoice->invitations->first()->contact);
try {
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
} catch (\Exception $e) {
return response(['message' => 'Oops, something went wrong. Make sure you have symlink to storage/ in public/ directory.'], 500);
}
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
}
/**

View File

@ -631,6 +631,7 @@ class TaskController extends BaseController
$task_status = TaskStatus::where('id', $this->decodePrimaryKey($task_status_hashed_id))
->where('company_id', auth()->user()->company()->id)
->withTrashed()
->first();
$task_status->status_order = $key;
@ -643,18 +644,13 @@ class TaskController extends BaseController
$sort_status_id = $this->decodePrimaryKey($key);
// nlog($task_list);
foreach ($task_list as $key => $task)
{
// nlog($task);
$task_record = Task::where('id', $this->decodePrimaryKey($task))
->where('company_id', auth()->user()->company()->id)
->withTrashed()
->first();
// nlog($task_record->id);
$task_record->status_order = $key;
$task_record->status_id = $sort_status_id;
@ -663,6 +659,6 @@ class TaskController extends BaseController
}
return response()->json(['message' => 'Ok'],200);
return response()->json(['message' => 'Ok'], 200);
}
}

View File

@ -211,11 +211,12 @@ class UserController extends BaseController
$ct = CreateCompanyToken::dispatchNow($company, $user, $user_agent);
nlog("in the store method of the usercontroller class");
event(new UserWasCreated($user, auth()->user(), $company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
return $this->itemResponse($user->fresh());
$user->setCompany($company);
$user->company_id = $company->id;
return $this->itemResponse($user);
}
/**

View File

@ -16,6 +16,7 @@ use App\Http\Middleware\Authenticate;
use App\Http\Middleware\CheckClientExistence;
use App\Http\Middleware\CheckForMaintenanceMode;
use App\Http\Middleware\ClientPortalEnabled;
use App\Http\Middleware\ContactAccount;
use App\Http\Middleware\ContactKeyLogin;
use App\Http\Middleware\ContactRegister;
use App\Http\Middleware\ContactSetDb;
@ -141,6 +142,7 @@ class Kernel extends HttpKernel
'api_secret_check' => ApiSecretCheck::class,
'contact_token_auth' => ContactTokenAuth::class,
'contact_db' => ContactSetDb::class,
'contact_account' => ContactAccount::class,
'domain_db' => SetDomainNameDb::class,
'email_db' => SetEmailDb::class,
'invite_db' => SetInviteDb::class,
@ -182,5 +184,6 @@ class Kernel extends HttpKernel
PasswordProtection::class,
Locale::class,
SubstituteBindings::class,
ContactAccount::class,
];
}

View File

@ -181,14 +181,16 @@ class BillingPortalPurchase extends Component
{
$this->validate();
$contact = ClientContact::where('email', $this->email)->first();
$contact = ClientContact::where('email', $this->email)
->where('company_id', $this->subscription->company_id)
->first();
if ($contact && $this->steps['existing_user'] === false) {
return $this->steps['existing_user'] = true;
}
if ($contact && $this->steps['existing_user']) {
$attempt = Auth::guard('contact')->attempt(['email' => $this->email, 'password' => $this->password]);
$attempt = Auth::guard('contact')->attempt(['email' => $this->email, 'password' => $this->password, 'company_id' => $this->subscription->company_id]);
return $attempt
? $this->getPaymentMethods($contact)

View File

@ -0,0 +1,41 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Middleware;
use App\Libraries\MultiDB;
use App\Models\Account;
use App\Utils\Ninja;
use Closure;
use Illuminate\Http\Request;
class ContactAccount
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if(!Ninja::isHosted()) {
$account_id = Account::first()->id;
$request->attributes->add(['account_id' => $account_id]);
}
return $next($request);
}
}

View File

@ -18,6 +18,7 @@ use Auth;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
class ContactKeyLogin
{
@ -42,6 +43,9 @@ class ContactKeyLogin
if (MultiDB::findAndSetDbByContactKey($request->segment(3))) {
if($client_contact = ClientContact::where('contact_key', $request->segment(3))->first()){
if(empty($client_contact->email))
$client_contact->email = Str::random(6) . "@example.com"; $client_contact->save();
Auth::guard('contact')->login($client_contact, true);
return redirect()->to('client/dashboard');
}
@ -49,6 +53,10 @@ class ContactKeyLogin
}
} elseif ($request->segment(2) && $request->segment(2) == 'key_login' && $request->segment(3)) {
if ($client_contact = ClientContact::where('contact_key', $request->segment(3))->first()) {
if(empty($client_contact->email))
$client_contact->email = Str::random(6) . "@example.com"; $client_contact->save();
auth()->guard('contact')->login($client_contact, true);
return redirect()->to('client/dashboard');
}
@ -56,19 +64,36 @@ class ContactKeyLogin
if (MultiDB::findAndSetDbByClientHash($request->input('client_hash'))) {
if($client = Client::where('client_hash', $request->input('client_hash'))->first()){
auth()->guard('contact')->login($client->primary_contact()->first(), true);
$primary_contact = $client->primary_contact()->first();
if(empty($primary_contact->email))
$primary_contact->email = Str::random(6) . "@example.com"; $primary_contact->save();
auth()->guard('contact')->login($primary_contact, true);
return redirect()->to('client/dashboard');
}
}
} elseif ($request->has('client_hash')) {
if ($client = Client::where('client_hash', $request->input('client_hash'))->first()) {
Auth::guard('contact')->login($client->primary_contact()->first(), true);
$primary_contact = $client->primary_contact()->first();
if(empty($primary_contact->email))
$primary_contact->email = Str::random(6) . "@example.com"; $primary_contact->save();
auth()->guard('contact')->login($primary_contact, true);
return redirect()->to('client/dashboard');
}
} elseif ($request->segment(2) && $request->segment(2) == 'magic_link' && $request->segment(3)) {
$contact_email = Cache::get($request->segment(3));
if($client_contact = ClientContact::where('email', $contact_email)->first()){
Auth::guard('contact')->login($client_contact, true);
if(empty($client_contact->email))
$client_contact->email = Str::random(6) . "@example.com"; $client_contact->save();
auth()->guard('contact')->login($client_contact, true);
if ($request->query('redirect') && !empty($request->query('redirect'))) {
return redirect()->to($request->query('redirect'));

View File

@ -52,6 +52,6 @@ class ContactRegister
return $next($request);
}
return abort(404);
abort(404, 'ContactRegister Middlware');
}
}

View File

@ -48,7 +48,11 @@ class SetDomainNameDb
'portal_mode' => 'subdomain',
];
if(!MultiDB::findAndSetDbByDomain($query)){
if($company = MultiDB::findAndSetDbByDomain($query)){
$request->attributes->add(['account_id' => $company->account_id]);
}
else
{
if ($request->json) {
return response()->json($error, 403);
} else {
@ -66,7 +70,11 @@ class SetDomainNameDb
'portal_mode' => 'domain',
];
if(!MultiDB::findAndSetDbByDomain($query)){
if($company = MultiDB::findAndSetDbByDomain($query)){
$request->attributes->add(['account_id' => $company->account_id]);
}
else
{
if ($request->json) {
return response()->json($error, 403);
} else {

View File

@ -49,7 +49,6 @@ class TokenAuth
| us to decouple a $user and their attached companies completely.
|
*/
$user->setCompany($company_token->company);
app('queue')->createPayloadUsing(function () use ($company_token) {
return ['db' => $company_token->company->db];
@ -67,6 +66,7 @@ class TokenAuth
//stateless, don't remember the user.
auth()->login($user, false);
auth()->user()->setCompany($company_token->company);
} else {
$error = [

View File

@ -40,13 +40,13 @@ class CreateAccountRequest extends Request
'password' => 'required|string|min:6',
'email' => 'bail|required|email:rfc,dns',
'email' => new NewUniqueUserRule(),
'privacy_policy' => 'required',
'terms_of_service' => 'required',
'privacy_policy' => 'required|boolean',
'terms_of_service' => 'required|boolean',
];
}
protected function prepareForValidation()
{
{nlog($this->all());
$input = $this->all();
$input['user_agent'] = request()->server('HTTP_USER_AGENT');

View File

@ -0,0 +1,55 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\Client;
use App\Http\Requests\Request;
use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule;
class AdjustClientLedgerRequest extends Request
{
use MakesHash;
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->can('edit', $this->client);
}
public function rules()
{
/* Ensure we have a client name, and that all emails are unique*/
$rules = [];
return $rules;
}
public function messages()
{
return [
];
}
protected function prepareForValidation()
{
$input = $this->all();
$this->replace($input);
}
}

View File

@ -53,6 +53,6 @@ class RegisterRequest extends FormRequest
return $company;
}
abort(404);
abort(404, 'Register request not found.');
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Requests\Import;
use App\Http\Requests\Request;
class ImportJsonRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return auth()->user()->isAdmin();
}
public function rules()
{
return [
// 'import_type' => 'required',
// 'files' => 'required_without:hash|array|min:1|max:6',
// 'hash' => 'nullable|string',
// 'column_map' => 'required_with:hash|array',
// 'skip_header' => 'required_with:hash|boolean',
// 'files.*' => 'file|mimes:csv,txt',
];
}
}

View File

@ -45,9 +45,6 @@ class UpdateUserRequest extends Request
{
$input = $this->all();
// if (isset($input['company_user']) && ! auth()->user()->isAdmin()) {
// unset($input['company_user']);
// }
$this->replace($input);
}

View File

@ -57,13 +57,13 @@ class InvoiceBalanceSanity implements Rule
private function checkIfInvoiceBalanceIsSane() : bool
{
$this->invoice->line_items = $this->input['line_items'];
DB::connection(config('database.default'))->beginTransaction();
DB::beginTransaction();
$this->invoice = Invoice::on(config('database.default'))->find($this->invoice->id);
$this->invoice->line_items = $this->input['line_items'];
$temp_invoice = $this->invoice->calc()->getTempEntity();
$temp_invoice = $this->invoice->calc()->getTempEntity();
DB::rollBack();
DB::connection(config('database.default'))->rollBack();
if($temp_invoice->balance < 0){
$this->message = 'Invoice balance cannot go negative';
@ -71,7 +71,7 @@ class InvoiceBalanceSanity implements Rule
}
return true;
return true;
}
}

View File

@ -20,6 +20,7 @@ use Illuminate\Contracts\Validation\Rule;
*/
class AttachableUser implements Rule
{
public $message;
public function __construct()
{
@ -39,7 +40,7 @@ class AttachableUser implements Rule
*/
public function message()
{
return "Cannot add the same user to the same company";
return $this->message;
}
/**
@ -57,15 +58,22 @@ class AttachableUser implements Rule
if(!$user)
return true;
$user_already_attached = CompanyUser::query()
$user_already_attached = CompanyUser::query()
->where('user_id', $user->id)
->where('account_id',$user->account_id)
->where('company_id', auth()->user()->company()->id)
->exists();
if($user_already_attached)
//If the user is already attached or isn't link to this account - return false
if($user_already_attached) {
$this->message = ctrans('texts.user_duplicate_error');
return false;
}
if($user->account_id != auth()->user()->account_id){
$this->message = ctrans('texts.user_cross_linked_error');
return false;
}
return true;
}

View File

@ -25,11 +25,13 @@ use App\Jobs\Util\VersionCheck;
use App\Mail\Admin\AccountCreatedObject;
use App\Mail\Admin\VerifyUserObject;
use App\Models\Account;
use App\Models\Timezone;
use App\Notifications\Ninja\NewAccountCreated;
use App\Utils\Ninja;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;
use Turbo124\Beacon\Facades\LightLogs;
@ -40,9 +42,12 @@ class CreateAccount
protected $request;
public function __construct(array $sp660339)
protected $client_ip;
public function __construct(array $sp660339, $client_ip)
{
$this->request = $sp660339;
$this->client_ip = $client_ip;
}
public function handle()
@ -74,6 +79,7 @@ class CreateAccount
$sp035a66 = CreateCompany::dispatchNow($this->request, $sp794f3f);
$sp035a66->load('account');
$sp035a66->settings = $this->processSettings($sp035a66->settings);
$sp794f3f->default_company_id = $sp035a66->id;
$sp794f3f->save();
@ -107,4 +113,53 @@ class CreateAccount
return $sp794f3f;
}
private function processSettings($settings)
{
if(Ninja::isHosted() && Cache::get('currencies') && $data = unserialize(@file_get_contents('http://www.geoplugin.net/php.gp?ip=' . $this->client_ip)))
{
$currency_code = strtolower($data['geoplugin_currencyCode']);
$country_code = strtolower($data['geoplugin_countryCode']);
$currency = Cache::get('currencies')->filter(function ($item) use ($currency_code) {
return strtolower($item->code) == $currency_code;
})->first();
if ($currency) {
$settings->currency_id = (string)$currency->id;
}
$country = Cache::get('countries')->filter(function ($item) use ($country_code) {
return strtolower($item->iso_3166_2) == $country_code || strtolower($item->iso_3166_3) == $country_code;
})->first();
if ($country) {
$settings->country_id = (string)$country->id;
}
$language = Cache::get('languages')->filter(function ($item) use ($currency_code) {
return strtolower($item->locale) == $currency_code;
})->first();
if ($language) {
$settings->language_id = (string)$language->id;
}
$timezone = Timezone::where('name', $data['geoplugin_timezone'])->first();
if($timezone) {
$settings->timezone_id = (string)$timezone->id;
}
return $settings;
}
return $settings;
}
}

View File

@ -101,7 +101,7 @@ class CompanyExport implements ShouldQueue
return $activity;
})->makeHidden(['id'])->toArray();
})->makeHidden(['id'])->all();
$this->export_data['backups'] = $this->company->all_activities()->with('backup')->cursor()->map(function ($activity){
@ -114,43 +114,56 @@ class CompanyExport implements ShouldQueue
return $backup;
})->toArray();
})->all();
$this->export_data['users'] = $this->company->users()->withTrashed()->cursor()->map(function ($user){
$user->account_id = $this->encodePrimaryKey($user->account_id);
$user->id = $this->encodePrimaryKey($user->id);
// $user->id = $this->encodePrimaryKey($user->id);
return $user;
return $user->makeVisible(['id']);
})->toArray();
})->all();
$this->export_data['client_contacts'] = $this->company->client_contacts->map(function ($client_contact){
$client_contact = $this->transformArrayOfKeys($client_contact, ['id', 'company_id', 'user_id',' client_id']);
$client_contact = $this->transformArrayOfKeys($client_contact, ['company_id', 'user_id', 'client_id']);
return $client_contact;
return $client_contact->makeVisible([
'password',
'remember_token',
'user_id',
'company_id',
'client_id',
'google_2fa_secret',
'id',
'oauth_provider_id',
'oauth_user_id',
'token',
'hashed_id',
]);
})->toArray();
})->all();
$this->export_data['client_gateway_tokens'] = $this->company->client_gateway_tokens->map(function ($client_gateway_token){
$client_gateway_token = $this->transformArrayOfKeys($client_gateway_token, ['id', 'company_id', 'client_id']);
$client_gateway_token = $this->transformArrayOfKeys($client_gateway_token, ['company_id', 'client_id']);
return $client_gateway_token;
return $client_gateway_token->makeVisible(['id']);
})->toArray();
})->all();
$this->export_data['clients'] = $this->company->clients->map(function ($client){
$client = $this->transformArrayOfKeys($client, ['id', 'company_id', 'user_id',' assigned_user_id', 'group_settings_id']);
$client = $this->transformArrayOfKeys($client, ['company_id', 'user_id', 'assigned_user_id', 'group_settings_id']);
return $client;
return $client->makeVisible(['id','private_notes','user_id','company_id','last_login','hashed_id']);
})->all();
})->toArray();
$this->export_data['company'] = $this->company->toArray();
@ -159,9 +172,9 @@ class CompanyExport implements ShouldQueue
$company_gateway = $this->transformArrayOfKeys($company_gateway, ['company_id', 'user_id']);
$company_gateway->config = decrypt($company_gateway->config);
return $company_gateway;
return $company_gateway->makeVisible(['id']);
})->toArray();
})->all();
$this->export_data['company_tokens'] = $this->company->tokens->map(function ($token){
@ -169,7 +182,7 @@ class CompanyExport implements ShouldQueue
return $token;
})->toArray();
})->all();
$this->export_data['company_ledger'] = $this->company->ledger->map(function ($ledger){
@ -177,7 +190,7 @@ class CompanyExport implements ShouldQueue
return $ledger;
})->toArray();
})->all();
$this->export_data['company_users'] = $this->company->company_users->map(function ($company_user){
@ -185,43 +198,44 @@ class CompanyExport implements ShouldQueue
return $company_user;
})->toArray();
})->all();
$this->export_data['credits'] = $this->company->credits->map(function ($credit){
$credit = $this->transformBasicEntities($credit);
$credit = $this->transformArrayOfKeys($credit, ['recurring_id','client_id', 'vendor_id', 'project_id', 'design_id', 'subscription_id','invoice_id']);
return $credit;
return $credit->makeVisible(['id']);
})->toArray();
})->all();
$this->export_data['credit_invitations'] = CreditInvitation::where('company_id', $this->company->id)->withTrashed()->cursor()->map(function ($credit){
$credit = $this->transformArrayOfKeys($credit, ['company_id', 'user_id', 'client_contact_id', 'recurring_invoice_id']);
$credit = $this->transformArrayOfKeys($credit, ['company_id', 'user_id', 'client_contact_id', 'credit_id']);
return $credit;
return $credit->makeVisible(['id']);
})->toArray();
})->all();
$this->export_data['designs'] = $this->company->user_designs->makeHidden(['id'])->toArray();
$this->export_data['designs'] = $this->company->user_designs->makeHidden(['id'])->all();
$this->export_data['documents'] = $this->company->documents->map(function ($document){
$this->export_data['documents'] = $this->company->all_documents->map(function ($document){
$document = $this->transformArrayOfKeys($document, ['user_id', 'assigned_user_id', 'company_id', 'project_id', 'vendor_id']);
$document = $this->transformArrayOfKeys($document, ['user_id', 'assigned_user_id', 'company_id', 'project_id', 'vendor_id','documentable_id']);
$document->hashed_id = $this->encodePrimaryKey($document->id);
return $document;
return $document->makeVisible(['id']);
})->toArray();
})->all();
$this->export_data['expense_categories'] = $this->company->expenses->map(function ($expense_category){
$this->export_data['expense_categories'] = $this->company->expense_categories->map(function ($expense_category){
$expense_category = $this->transformArrayOfKeys($expense_category, ['user_id', 'company_id']);
return $expense_category;
return $expense_category->makeVisible(['id']);
})->toArray();
})->all();
$this->export_data['expenses'] = $this->company->expenses->map(function ($expense){
@ -229,17 +243,17 @@ class CompanyExport implements ShouldQueue
$expense = $this->transformBasicEntities($expense);
$expense = $this->transformArrayOfKeys($expense, ['vendor_id', 'invoice_id', 'client_id', 'category_id', 'recurring_expense_id','project_id']);
return $expense;
return $expense->makeVisible(['id']);
})->toArray();
})->all();
$this->export_data['group_settings'] = $this->company->group_settings->map(function ($gs){
$gs = $this->transformArrayOfKeys($gs, ['user_id', 'company_id']);
return $gs;
return $gs->makeVisible(['id']);
})->toArray();
})->all();
$this->export_data['invoices'] = $this->company->invoices->map(function ($invoice){
@ -247,18 +261,22 @@ class CompanyExport implements ShouldQueue
$invoice = $this->transformBasicEntities($invoice);
$invoice = $this->transformArrayOfKeys($invoice, ['recurring_id','client_id', 'vendor_id', 'project_id', 'design_id', 'subscription_id']);
return $invoice;
return $invoice->makeVisible(['id',
'private_notes',
'user_id',
'client_id',
'company_id',]);
})->toArray();
})->all();
$this->export_data['invoice_invitations'] = InvoiceInvitation::where('company_id', $this->company->id)->withTrashed()->cursor()->map(function ($invoice){
$invoice = $this->transformArrayOfKeys($invoice, ['company_id', 'user_id', 'client_contact_id', 'recurring_invoice_id']);
$invoice = $this->transformArrayOfKeys($invoice, ['company_id', 'user_id', 'client_contact_id', 'invoice_id']);
return $invoice;
return $invoice->makeVisible(['id']);
})->toArray();
})->all();
$this->export_data['payment_terms'] = $this->company->user_payment_terms->map(function ($term){
@ -266,61 +284,65 @@ class CompanyExport implements ShouldQueue
return $term;
})->makeHidden(['id'])->toArray();
})->makeHidden(['id'])->all();
$this->export_data['paymentables'] = $this->company->payments()->with('paymentables')->cursor()->map(function ($paymentable){
$paymentable = $this->transformArrayOfKeys($paymentable, ['payment_id','paymentable_id']);
return $paymentable;
})->toArray();
$this->export_data['payments'] = $this->company->payments->map(function ($payment){
$payment = $this->transformBasicEntities($payment);
$payment = $this->transformArrayOfKeys($payment, ['client_id','project_id', 'vendor_id', 'client_contact_id', 'invitation_id', 'company_gateway_id']);
return $payment;
$payment->paymentables = $this->transformPaymentable($payment);
})->toArray();
return $payment->makeVisible(['id']);
})->all();
$this->export_data['products'] = $this->company->products->map(function ($product){
$product = $this->transformBasicEntities($product);
$product = $this->transformArrayOfKeys($product, ['vendor_id','project_id']);
return $product->makeVisible(['id']);
})->all();
$this->export_data['projects'] = $this->company->projects->map(function ($project){
$project = $this->transformBasicEntities($project);
$project = $this->transformArrayOfKeys($project, ['client_id']);
return $project;
return $project->makeVisible(['id']);
})->toArray();
})->all();
$this->export_data['quotes'] = $this->company->quotes->map(function ($quote){
$quote = $this->transformBasicEntities($quote);
$quote = $this->transformArrayOfKeys($quote, ['invoice_id','recurring_id','client_id', 'vendor_id', 'project_id', 'design_id', 'subscription_id']);
return $quote;
return $quote->makeVisible(['id']);
})->toArray();
})->all();
$this->export_data['quote_invitations'] = QuoteInvitation::where('company_id', $this->company->id)->withTrashed()->cursor()->map(function ($quote){
$quote = $this->transformArrayOfKeys($quote, ['company_id', 'user_id', 'client_contact_id', 'recurring_invoice_id']);
$quote = $this->transformArrayOfKeys($quote, ['company_id', 'user_id', 'client_contact_id', 'quote_id']);
return $quote;
return $quote->makeVisible(['id']);
})->toArray();
})->all();
$this->export_data['recurring_invoices'] = $this->company->recurring_invoices->map(function ($ri){
$this->export_data['recurring_invoices'] = $this->company->recurring_invoices->makeVisible(['id'])->map(function ($ri){
$ri = $this->transformBasicEntities($ri);
$ri = $this->transformArrayOfKeys($ri, ['client_id', 'vendor_id', 'project_id', 'design_id', 'subscription_id']);
return $ri;
return $ri->makeVisible(['id']);
})->toArray();
})->all();
$this->export_data['recurring_invoice_invitations'] = RecurringInvoiceInvitation::where('company_id', $this->company->id)->withTrashed()->cursor()->map(function ($ri){
@ -329,16 +351,22 @@ class CompanyExport implements ShouldQueue
return $ri;
})->toArray();
})->all();
$this->export_data['subscriptions'] = $this->company->subscriptions->map(function ($subscription){
$subscription = $this->transformBasicEntities($subscription);
$subscription->group_id = $this->encodePrimaryKey($subscription->group_id);
return $subscription;
return $subscription->makeVisible([ 'id',
'user_id',
'assigned_user_id',
'company_id',
'product_ids',
'recurring_product_ids',
'group_id']);
})->toArray();
})->all();
$this->export_data['system_logs'] = $this->company->system_logs->map(function ($log){
@ -348,16 +376,16 @@ class CompanyExport implements ShouldQueue
return $log;
})->makeHidden(['id'])->toArray();
})->makeHidden(['id'])->all();
$this->export_data['tasks'] = $this->company->tasks->map(function ($task){
$task = $this->transformBasicEntities($task);
$task = $this->transformArrayOfKeys($task, ['client_id', 'invoice_id', 'project_id', 'status_id']);
return $task;
return $task->makeVisible(['id']);
})->toArray();
})->all();
$this->export_data['task_statuses'] = $this->company->task_statuses->map(function ($status){
@ -367,7 +395,7 @@ class CompanyExport implements ShouldQueue
return $status;
})->toArray();
})->all();
$this->export_data['tax_rates'] = $this->company->tax_rates->map(function ($rate){
@ -376,13 +404,13 @@ class CompanyExport implements ShouldQueue
return $rate;
})->makeHidden(['id'])->toArray();
})->makeHidden(['id'])->all();
$this->export_data['vendors'] = $this->company->vendors->map(function ($vendor){
return $this->transformBasicEntities($vendor);
return $this->transformBasicEntities($vendor)->makeVisible(['id']);
})->toArray();
})->all();
$this->export_data['vendor_contacts'] = VendorContact::where('company_id', $this->company->id)->withTrashed()->cursor()->map(function ($vendor){
@ -390,9 +418,9 @@ class CompanyExport implements ShouldQueue
$vendor = $this->transformBasicEntities($vendor);
$vendor->vendor_id = $this->encodePrimaryKey($vendor->vendor_id);
return $vendor;
return $vendor->makeVisible(['id']);
})->toArray();
})->all();
$this->export_data['webhooks'] = $this->company->webhooks->map(function ($hook){
@ -401,10 +429,10 @@ class CompanyExport implements ShouldQueue
return $hook;
})->makeHidden(['id'])->toArray();
})->makeHidden(['id'])->all();
//write to tmp and email to owner();
$this->zipAndSend();
return true;
@ -413,7 +441,7 @@ class CompanyExport implements ShouldQueue
private function transformBasicEntities($model)
{
return $this->transformArrayOfKeys($model, ['id', 'user_id', 'assigned_user_id', 'company_id']);
return $this->transformArrayOfKeys($model, ['user_id', 'assigned_user_id', 'company_id']);
}
@ -428,40 +456,48 @@ class CompanyExport implements ShouldQueue
}
private function transformPaymentable($payment)
{
$new_arr = [];
foreach($payment->paymentables as $paymentable)
{
$paymentable->payment_id = $this->encodePrimaryKey($paymentable->payment_id);
$paymentable->paymentable_id = $this->encodePrimaryKey($paymentable->paymentable_id);
$new_arr[] = $paymentable;
}
return $new_arr;
}
private function zipAndSend()
{
$tempStream = fopen('php://memory', 'w+');
$options = new Archive();
$options->setOutputStream($tempStream);
$file_name = date('Y-m-d').'_'.str_replace(' ', '_', $this->company->present()->name() . '_' . $this->company->company_key .'.zip');
$zip = new ZipStream($file_name, $options);
$zip_path = public_path('storage/backups/'.$file_name);
$zip = new \ZipArchive();
$fp = tmpfile();
fwrite($fp, json_encode($this->export_data));
rewind($fp);
$zip->addFileFromStream('backup.json', $fp);
if ($zip->open($zip_path, \ZipArchive::CREATE)!==TRUE) {
nlog("cannot open {$zip_path}");
}
$zip->finish();
$path = 'backups/';
Storage::disk(config('filesystems.default'))->put($path.$file_name, $tempStream);
fclose($tempStream);
$zip->addFromString("backup.json", json_encode($this->export_data));
$zip->close();
$nmo = new NinjaMailerObject;
$nmo->mailable = new DownloadBackup(Storage::disk(config('filesystems.default'))->url($path.$file_name), $this->company);
$nmo->mailable = new DownloadBackup(Storage::disk(config('filesystems.default'))->url('backups/'.$file_name), $this->company);
$nmo->to_user = $this->user;
$nmo->company = $this->company;
$nmo->settings = $this->company->settings;
NinjaMailerJob::dispatch($nmo);
UnlinkFile::dispatch(config('filesystems.default'), $path.$file_name)->delay(now()->addHours(1));
UnlinkFile::dispatch(config('filesystems.default'), 'backups/'.$file_name)->delay(now()->addHours(1));
}
}

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,9 @@
namespace App\Jobs\Company;
use App\DataMapper\CompanySettings;
use App\Libraries\MultiDB;
use App\Models\Company;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Http\Request;
@ -60,6 +62,12 @@ class CreateCompany
$company->subdomain = isset($this->request['subdomain']) ? $this->request['subdomain'] : '';
$company->custom_fields = new \stdClass;
$company->default_password_timeout = 1800000;
if(Ninja::isHosted())
$company->subdomain = MultiDB::randomSubdomainGenerator();
else
$company->subdomain = '';
$company->save();
return $company;

View File

@ -0,0 +1,101 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Jobs\Cron;
use App\Jobs\RecurringInvoice\SendRecurring;
use App\Libraries\MultiDB;
use App\Models\Invoice;
use App\Models\RecurringInvoice;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Carbon;
class AutoBillCron
{
use Dispatchable;
public $tries = 1;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct()
{
}
/**
* Execute the job.
*
* @return void
*/
public function handle() : void
{
set_time_limit(0);
/* Get all invoices where the send date is less than NOW + 30 minutes() */
nlog("Performing Autobilling ".Carbon::now()->format('Y-m-d h:i:s'));
if (! config('ninja.db.multi_db_enabled')) {
$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){
$this->runAutoBiller($invoice);
});
$auto_bill_invoices = Invoice::whereDate('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){
$this->runAutoBiller($invoice);
});
} 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){
$this->runAutoBiller($invoice);
});
$auto_bill_invoices = Invoice::whereDate('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){
$this->runAutoBiller($invoice);
});
}
}
}
private function runAutoBiller(Invoice $invoice)
{
nlog("Firing autobill for {$invoice->company_id} - {$invoice->number}");
$invoice->service()->autoBill()->save();
}
}

View File

@ -293,10 +293,14 @@ class CSVImport implements ShouldQueue {
],
];
$payment_repository->save(
$payment_data,
PaymentFactory::create( $this->company->id, $invoice->user_id, $invoice->client_id )
);
/* Make sure we don't apply any payments to invoices with a Zero Amount*/
if($invoice->amount > 0)
{
$payment_repository->save(
$payment_data,
PaymentFactory::create( $this->company->id, $invoice->user_id, $invoice->client_id )
);
}
}
}
}

View File

@ -47,9 +47,9 @@ class NinjaMailerJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesHash;
public $tries = 5; //number of retries
public $tries = 3; //number of retries
public $backoff = 5; //seconds to wait until retry
public $backoff = 10; //seconds to wait until retry
public $deleteWhenMissingModels = true;
@ -63,6 +63,7 @@ class NinjaMailerJob implements ShouldQueue
{
$this->nmo = $nmo;
$this->override = $override;
}
@ -105,7 +106,7 @@ class NinjaMailerJob implements ShouldQueue
//send email
try {
nlog("trying to send");
nlog("trying to send to {$this->nmo->to_user->email} ". now()->toDateTimeString());
Mail::to($this->nmo->to_user->email)
->send($this->nmo->mailable);

View File

@ -30,6 +30,7 @@ use App\Factory\TaxRateFactory;
use App\Factory\UserFactory;
use App\Factory\VendorFactory;
use App\Http\Requests\Company\UpdateCompanyRequest;
use App\Http\ValidationRules\User\AttachableUser;
use App\Http\ValidationRules\ValidCompanyGatewayFeesAndLimitsRule;
use App\Http\ValidationRules\ValidUserForCompany;
use App\Jobs\Company\CreateCompanyTaskStatuses;
@ -210,8 +211,8 @@ class Import implements ShouldQueue
$this->{$method}($data[$import]);
}
if(Ninja::isHosted())
$this->processNinjaTokens($data['ninja_tokens']);
// if(Ninja::isHosted() && array_key_exists('ninja_tokens', $data))
// $this->processNinjaTokens($data['ninja_tokens']);
$this->setInitialCompanyLedgerBalances();
@ -225,6 +226,7 @@ class Import implements ShouldQueue
->send(new MigrationCompleted($this->company, implode("<br>",$check_data)));
}
catch(\Exception $e) {
nlog($e->getMessage());
}
@ -296,6 +298,12 @@ class Import implements ShouldQueue
$data = $this->transformCompanyData($data);
if(Ninja::isHosted() && strlen($data['subdomain']) > 1) {
if(!MultiDB::checkDomainAvailable($data['subdomain']))
$data['subdomain'] = MultiDB::randomSubdomainGenerator();
}
$rules = (new UpdateCompanyRequest())->rules();
$validator = Validator::make($data, $rules);
@ -419,13 +427,10 @@ class Import implements ShouldQueue
$rules = [
'*.first_name' => ['string'],
'*.last_name' => ['string'],
'*.email' => ['distinct'],
//'*.email' => ['distinct'],
'*.email' => ['distinct', 'email', new ValidUserForCompany()],
];
// if (config('ninja.db.multi_db_enabled')) {
// array_push($rules['*.email'], new ValidUserForCompany());
// }
$validator = Validator::make($data, $rules);
if ($validator->fails()) {
@ -1647,53 +1652,65 @@ class Import implements ShouldQueue
private function buildNewUserPlan()
{
$local_company = Company::find($this->company->id);
$owner = $local_company->owner();
$current_db = config('database.default');
$ninja_company = Company::on('db-ninja-01')->find(config('ninja.ninja_default_company_id'));
nlog($this->company);
$local_company = Company::on($current_db)->where('company_key', $this->company->company_key)->first();
MultiDB::setDb('db-ninja-01');
$ninja_company = Company::find(config('ninja.ninja_default_company_id'));
/* If we already have a record of this user - move along. */
if($client_contact = ClientContact::on('db-ninja-01')->where(['email' => $owner->email, 'company_id' => $ninja_company->id])->exists())
if($client_contact = ClientContact::where(['email' => $this->user->email, 'company_id' => $ninja_company->id])->first())
return $client_contact->client;
$ninja_client = ClientFactory::create($ninja_company->id, $ninja_company->owner()->id);
$ninja_client->setConnection('db-ninja-01');
$ninja_client->name = $owner->present()->name();
$ninja_client->name = $this->user->present()->name();
$ninja_client->address1 = $local_company->settings->address1;
$ninja_client->address2 = $local_company->settings->address2;
$ninja_client->city = $local_company->settings->city;
$ninja_client->postal_code = $local_company->settings->postal_code;
$ninja_client->state = $local_company->settings->state;
$ninja_client->country_id = $local_company->settings->country_id;
$ninja_client->custom_value1 = $local_company->company_key;
$ninja_client->save();
$ninja_client_contact = ClientContactFactory::create($ninja_company->id, $ninja_company->owner()->id);
$ninja_client_contact->setConnection('db-ninja-01');
$ninja_client_contact->first_name = $owner->first_name;
$ninja_client_contact->last_name = $owner->last_name;
$ninja_client_contact->first_name = $this->user->first_name;
$ninja_client_contact->last_name = $this->user->last_name;
$ninja_client_contact->client_id = $ninja_client->id;
$ninja_client_contact->email = $owner->email;
$ninja_client_contact->phone = $owner->phone;
$ninja_client_contact->email = $this->user->email;
$ninja_client_contact->phone = $this->user->phone;
$ninja_client_contact->save();
MultiDB::setDb($current_db);
return $ninja_client;
}
private function processNinjaTokens(array $data)
{
if(count($data) == 0)
$current_db = config('database.default');
$local_company = Company::on($current_db)->where('company_key', $this->company->company_key)->first();
MultiDB::setDb('db-ninja-01');
if($existing_client = Client::where('custom_value1', $local_company->company_key)->first())
$ninja_client = $existing_client;
else
$ninja_client = $this->buildNewUserPlan();
foreach($data as $token)
{
//get invoiceninja company_id
$ninja_company = Company::on('db-ninja-01')->where('id', config('ninja.ninja_default_company_id'))->first();
$ninja_company = Company::where('id', config('ninja.ninja_default_company_id'))->first();
$token['company_id'] = $ninja_client->company_id;
$token['client_id'] = $ninja_client->id;
$token['user_id'] = $ninja_client->user_id;
$token['company_id'] = $ninja_company->id;
$token['client_id'] = $ninja_client->id;/////
$token['user_id'] = $ninja_company->owner()->id;
$token['company_gateway_id'] = config('ninja.ninja_default_company_gateway_id');
//todo
@ -1702,8 +1719,10 @@ class Import implements ShouldQueue
ClientGatewayToken::reguard();
}
MultiDB::setDb($current_db);
}
/* In V4 we use negative invoices (credits) and add then into the client balance. In V5, these sit off ledger and are applied later.
This next section will check for credit balances and reduce the client balance so that the V5 balances are correct
*/

View File

@ -16,6 +16,7 @@ use App\Jobs\Entity\EmailEntity;
use App\Libraries\MultiDB;
use App\Models\Invoice;
use App\Utils\Ninja;
use App\Utils\Traits\MakesReminders;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
@ -25,7 +26,7 @@ use Illuminate\Support\Carbon;
class ReminderJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesReminders;
public function __construct()
{
@ -39,40 +40,41 @@ class ReminderJob implements ShouldQueue
public function handle()
{
//always make sure you have set the company as this command is being
//run from the console so we have no awareness of the DB.
if (! config('ninja.db.multi_db_enabled')) {
$this->processReminders();
} else {
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$this->processReminders($db);
$this->processReminders();
}
}
}
private function processReminders($db = null)
private function processReminders()
{
Invoice::where('next_send_date', Carbon::today()->format('Y-m-d'))->with('invitations')->cursor()->each(function ($invoice) {
if ($invoice->isPayable()) {
$reminder_template = $invoice->calculateTemplate('invoice');
$invoice->service()->touchReminder($reminder_template)->save();
$invoice->invitations->each(function ($invitation) use ($invoice, $reminder_template) {
EmailEntity::dispatch($invitation, $invitation->company, $reminder_template);
nlog("Firing email for invoice {$invoice->number}");
nlog("Firing reminder email for invoice {$invoice->number}");
});
if ($invoice->invitations->count() > 0) {
event(new InvoiceWasEmailed($invoice->invitations->first(), $invoice->company, Ninja::eventVars(), $reminder_template));
}
$invoice->service()->setReminder()->save();
} else {
$invoice->next_send_date = null;
$invoice->save();
}
});
}
}

View File

@ -20,6 +20,7 @@ use App\Libraries\MultiDB;
use App\Mail\MigrationFailed;
use App\Models\Company;
use App\Models\User;
use App\Utils\Ninja;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
@ -139,7 +140,11 @@ class StartMigration implements ShouldQueue
$this->company->update_products = $update_product_flag;
$this->company->save();
Mail::to($this->user->email, $this->user->name())->send(new MigrationFailed($e, $e->getMessage()));
if(Ninja::isHosted())
app('sentry')->captureException($e);
Mail::to($this->user->email, $this->user->name())->send(new MigrationFailed($e, $this->company, $e->getMessage()));
if (app()->environment() !== 'production') {
info($e->getMessage());

View File

@ -35,9 +35,9 @@ class WebhookHandler implements ShouldQueue
private $company;
public $tries = 5; //number of retries
public $tries = 3; //number of retries
public $backoff = 5; //seconds to wait until retry
public $backoff = 10; //seconds to wait until retry
public $deleteWhenMissingModels = true;

View File

@ -52,37 +52,38 @@ class MultiDB
public static function checkDomainAvailable($subdomain) : bool
{
if (! config('ninja.db.multi_db_enabled')) {
if (! config('ninja.db.multi_db_enabled'))
return Company::whereSubdomain($subdomain)->get()->count() == 0;
}
$current_db = config('database.default');
//multi-db active
foreach (self::$dbs as $db) {
if (Company::on($db)->whereSubdomain($subdomain)->get()->count() >= 1) {
self::setDb($current_db);
return false;
}
}
//self::setDefaultDatabase();
self::setDb($current_db);
return true;
}
public static function checkUserEmailExists($email) : bool
{
if (! config('ninja.db.multi_db_enabled')) {
if (! config('ninja.db.multi_db_enabled'))
return User::where(['email' => $email])->get()->count() >= 1 ?? false; // true >= 1 emails found / false -> == emails found
}
$current_db = config('database.default');
//multi-db active
foreach (self::$dbs as $db) {
if (User::on($db)->where(['email' => $email])->get()->count() >= 1) { // if user already exists, validation will fail
self::setDb($current_db);
return true;
}
}
self::setDefaultDatabase();
self::setDb($current_db);
return false;
}
@ -102,19 +103,21 @@ class MultiDB
*/
public static function checkUserAndCompanyCoExist($email, $company_key) :bool
{
$current_db = config('database.default');
foreach (self::$dbs as $db) {
if (User::on($db)->where(['email' => $email])->get()->count() >= 1) { // if user already exists, validation will fail
if (Company::on($db)->where(['company_key' => $company_key])->get()->count() >= 1) {
if (User::on($db)->where(['email' => $email])->exists()) {
if (Company::on($db)->where(['company_key' => $company_key])->exists()) {
self::setDb($current_db);
return true;
} else {
self::setDefaultDatabase();
self::setDb($current_db);
return false;
}
}
}
self::setDefaultDatabase();
self::setDb($current_db);
return true;
}
@ -125,20 +128,21 @@ class MultiDB
*/
public static function hasUser(array $data) : ?User
{
if (! config('ninja.db.multi_db_enabled')) {
if (! config('ninja.db.multi_db_enabled'))
return User::where($data)->withTrashed()->first();
}
$current_db = config('database.default');
foreach (self::$dbs as $db) {
self::setDB($db);
if ($user = User::where($data)->withTrashed()->first())
if ($user = User::where($data)->withTrashed()->first()) {
return $user;
}
}
self::setDefaultDatabase();
self::setDb($current_db);
return null;
}
@ -149,125 +153,139 @@ class MultiDB
*/
public static function hasContact(string $email) : ?ClientContact
{
if (! config('ninja.db.multi_db_enabled')) {
if (! config('ninja.db.multi_db_enabled'))
return ClientContact::where('email', $email)->withTrashed()->first();
}
$current_db = config('database.default');
foreach (self::$dbs as $db) {
$user = ClientContact::on($db)->where('email', $email)->withTrashed()->first();
if ($user) {
self::setDB($db);
self::setDb($db);
return $user;
}
}
self::setDefaultDatabase();
self::setDB($current_db);
return null;
}
public static function contactFindAndSetDb($token) :bool
{
$current_db = config('database.default');
foreach (self::$dbs as $db) {
if ($ct = ClientContact::on($db)->whereRaw('BINARY `token`= ?', [$token])->first()) {
self::setDb($ct->company->db);
self::setDb($db);
return true;
}
}
self::setDefaultDatabase();
self::setDB($current_db);
return false;
}
public static function userFindAndSetDb($email) : bool
{
$current_db = config('database.default');
//multi-db active
foreach (self::$dbs as $db) {
if (User::on($db)->where('email', $email)->count() >= 1){
nlog("setting db {$db}");
self::setDb($db);
return true;
}
}
self::setDefaultDatabase();
self::setDB($current_db);
return false;
}
public static function findAndSetDb($token) :bool
{
$current_db = config('database.default');
foreach (self::$dbs as $db) {
if ($ct = CompanyToken::on($db)->whereRaw('BINARY `token`= ?', [$token])->first()) {
self::setDb($ct->company->db);
return true;
}
}
self::setDefaultDatabase();
self::setDB($current_db);
return false;
}
public static function findAndSetDbByCompanyKey($company_key) :bool
{
$current_db = config('database.default');
foreach (self::$dbs as $db) {
if ($company = Company::on($db)->where('company_key', $company_key)->first()) {
self::setDb($company->db);
return true;
}
}
self::setDefaultDatabase();
self::setDB($current_db);
return false;
}
public static function findAndSetDbByContactKey($contact_key) :bool
{
$current_db = config('database.default');
foreach (self::$dbs as $db) {
if ($client_contact = ClientContact::on($db)->where('contact_key', $contact_key)->first()) {
self::setDb($client_contact->company->db);
return true;
}
}
self::setDefaultDatabase();
self::setDB($current_db);
return false;
}
public static function findAndSetDbByClientHash($client_hash) :bool
{
$current_db = config('database.default');
foreach (self::$dbs as $db) {
if ($client = Client::on($db)->where('client_hash', $client_hash)->first()) {
self::setDb($client->company->db);
return true;
}
}
self::setDefaultDatabase();
self::setDB($current_db);
return false;
}
public static function findAndSetDbByDomain($query_array) :bool
public static function findAndSetDbByDomain($query_array)
{
if (! config('ninja.db.multi_db_enabled'))
return (Company::where($query_array)->exists() === true);
return (Company::where($query_array)->first());
$current_db = config('database.default');
foreach (self::$dbs as $db) {
if ($company = Company::on($db)->where($query_array)->first()) {
self::setDb($company->db);
return true;
return $company;
}
}
self::setDefaultDatabase();
self::setDB($current_db);
return false;
}
@ -275,20 +293,48 @@ class MultiDB
public static function findAndSetDbByInvitation($entity, $invitation_key)
{
$class = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation';
$current_db = config('database.default');
foreach (self::$dbs as $db) {
if ($invite = $class::on($db)->whereRaw('BINARY `key`= ?', [$invitation_key])->first()) {
self::setDb($db);
return true;
}
}
self::setDefaultDatabase();
self::setDB($current_db);
return false;
}
public static function randomSubdomainGenerator()
{
$current_db = config('database.default');
do {
$length = 8;
$string = '';
$vowels = array("a","e","i","o","u");
$consonants = array(
'b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm',
'n', 'p', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z'
);
$max = $length / 2;
for ($i = 1; $i <= $max; $i++)
{
$string .= $consonants[rand(0,19)];
$string .= $vowels[rand(0,4)];
}
}
while(!self::checkDomainAvailable($string));
self::setDb($current_db);
return $string;
}
/**
* @param $database
*/

View File

@ -45,7 +45,7 @@ class VendorUpdatedActivity implements ShouldQueue
$fields = new stdClass;
$user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars['user_id'] : $event->vendor->user_id;
$user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars['user_id'] : $event->vendor->user_id;
$fields->vendor_id = $vendor->id;
$fields->user_id = $user_id;

View File

@ -8,21 +8,23 @@ use Illuminate\Queue\SerializesModels;
class MigrationFailed extends Mailable
{
// use Queueable, SerializesModels;
public $exception;
public $content;
public $settings;
public $company;
/**
* Create a new message instance.
*
* @param $content
* @param $exception
*/
public function __construct($exception, $content = null)
public function __construct($exception, $company, $content = null)
{
$this->exception = $exception;
$this->content = $content;
$this->settings = $company->settings;
$this->company = $company;
}
/**
@ -33,6 +35,6 @@ class MigrationFailed extends Mailable
public function build()
{
return $this->from(config('mail.from.address'), config('mail.from.name'))
->view('email.migration.failed');
->view('email.migration.failed', ['settings' => $this->settings, 'company' => $this->company]);
}
}

View File

@ -59,12 +59,13 @@ class SupportMessageSent extends Mailable
$subject = "Customer MSG {$user->present()->name} - [{$plan} - DB:{$company->db}]";
return $this->from(config('mail.from.address'), config('mail.from.name')) //todo this needs to be fixed to handle the hosted version
->subject($subject)
->markdown('email.support.message', [
'message' => $this->message,
'system_info' => $system_info,
'laravel_log' => $log_lines,
]);
return $this->from(config('mail.from.address'), config('mail.from.name'))
->replyTo($user->email, $user->present()->name())
->subject($subject)
->markdown('email.support.message', [
'message' => $this->message,
'system_info' => $system_info,
'laravel_log' => $log_lines,
]);
}
}

View File

@ -103,6 +103,15 @@ class Activity extends StaticModel
'deleted_at' => 'timestamp',
];
protected $appends = [
'hashed_id',
];
public function getHashedIdAttribute()
{
return $this->encodePrimaryKey($this->id);
}
public function getEntityType()
{
return self::class;

View File

@ -36,10 +36,6 @@ class BaseModel extends Model
use UserSessionAttributes;
use HasFactory;
//todo customise names of archived_at / updated_at columns
///const CREATED_AT = 'creation_date';
//const UPDATED_AT = 'last_update';
protected $appends = [
'hashed_id',
];

View File

@ -40,7 +40,6 @@ class Client extends BaseModel implements HasLocalePreference
'private_notes',
'user_id',
'company_id',
// 'settings',
'last_login',
];

View File

@ -131,6 +131,11 @@ class Company extends BaseModel
return $this->morphMany(Document::class, 'documentable');
}
public function all_documents()
{
return $this->HasMany(Document::class);
}
public function getEntityType()
{
return self::class;

View File

@ -71,10 +71,6 @@ class Quote extends BaseModel
'custom_surcharge2',
'custom_surcharge3',
'custom_surcharge4',
// 'custom_surcharge_tax1',
// 'custom_surcharge_tax2',
// 'custom_surcharge_tax3',
// 'custom_surcharge_tax4',
'design_id',
'assigned_user_id',
'exchange_rate',

View File

@ -159,8 +159,6 @@ class User extends Authenticatable implements MustVerifyEmail
*/
public function setCompany($company)
{
// config(['ninja.company_id' => $company->id]);
$this->company = $company;
}
@ -170,16 +168,17 @@ class User extends Authenticatable implements MustVerifyEmail
public function getCompany()
{
if (request()->header('X-API-TOKEN')) {
$company_token = CompanyToken::with(['company'])->whereRaw('BINARY `token`= ?', [request()->header('X-API-TOKEN')])->first();
return $company_token->company;
}
elseif ($this->company){
if ($this->company){
return $this->company;
}
elseif (request()->header('X-API-TOKEN')) {
$company_token = CompanyToken::with(['company'])->whereRaw('BINARY `token`= ?', [request()->header('X-API-TOKEN')])->first();
return $company_token->company;
}
// return false;
throw new \Exception('No Company Found');
@ -408,7 +407,7 @@ class User extends Authenticatable implements MustVerifyEmail
$nmo->settings = $this->account->default_company->settings;
$nmo->company = $this->account->default_company;
NinjaMailerJob::dispatch($nmo);
NinjaMailerJob::dispatch($nmo, true);
//$this->notify(new ResetPasswordNotification($token));
}

View File

@ -39,10 +39,6 @@ class CompanyObserver
if(Ninja::isHosted() && $company->portal_mode == 'domain' && $company->isDirty('portal_domain'))
{
nlog('company observer - updated');
nlog($company->portal_domain);
nlog($company->getOriginal('portal_domain'));
//fire event to build new custom portal domain
\Modules\Admin\Jobs\Domain\CustomDomain::dispatch($company->getOriginal('portal_domain'), $company)->onQueue('domain');
}

View File

@ -52,6 +52,9 @@ class InvoiceObserver
WebhookHandler::dispatch(Webhook::EVENT_UPDATE_INVOICE, $invoice, $invoice->company);
}
// if($invoice->isDirty('date') || $invoice->isDirty('due_date'))
// $invoice->service()->setReminder()->save();
// UnlinkFile::dispatchNow(config('filesystems.default'), $invoice->client->invoice_filepath() . $invoice->numberFormatter().'.pdf');
}

View File

@ -118,7 +118,7 @@ class PayPalExpressPaymentDriver extends BaseDriver
$this->initializeOmnipayGateway();
$response = $this->omnipay_gateway
->completePurchase(['amount' => $this->payment_hash->data->amount])
->completePurchase(['amount' => $this->payment_hash->data->amount, 'currency' => $this->client->getCurrencyCode()])
->send();
if ($response->isCancelled()) {
@ -187,7 +187,7 @@ class PayPalExpressPaymentDriver extends BaseDriver
'cancelUrl' => $this->client->company->domain() . '/client/invoices',
'description' => implode(',', collect($this->payment_hash->data->invoices)
->map(function ($invoice) {
return sprintf('%s: %s', ctrans('texts.invoice_number'), $invoice->invoice_number);
return sprintf('%s: %s', ctrans('texts.invoice_number'), $invoice->number);
})->toArray()),
'transactionId' => $this->payment_hash->hash . '-' . time(),
'ButtonSource' => 'InvoiceNinja_SP',

View File

@ -193,7 +193,8 @@ class ACH
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_STRIPE,
$this->stripe->client
$this->stripe->client,
$this->stripe->client->company,
);
return redirect()->route('client.payments.show', ['payment' => $this->stripe->encodePrimaryKey($payment->id)]);

View File

@ -82,7 +82,7 @@ class ImportCustomers
}
nlog("inserting a customer");
nlog($customer);
//nlog($customer);
$client = ClientFactory::create($this->stripe->company_gateway->company_id, $this->stripe->company_gateway->user_id);

View File

@ -21,6 +21,6 @@ trait Utilities
public function convertToStripeAmount($amount, $precision)
{
return $amount * pow(10, $precision);
return (int)($amount * pow(10, $precision));
}
}

View File

@ -189,7 +189,7 @@ class StripePaymentDriver extends BaseDriver
if ($this->company_gateway->require_billing_address) {
$fields[] = ['name' => 'client_address_line_1', 'label' => ctrans('texts.address1'), 'type' => 'text', 'validation' => 'required'];
$fields[] = ['name' => 'client_address_line_2', 'label' => ctrans('texts.address2'), 'type' => 'text', 'validation' => 'sometimes'];
// $fields[] = ['name' => 'client_address_line_2', 'label' => ctrans('texts.address2'), 'type' => 'text', 'validation' => 'nullable'];
$fields[] = ['name' => 'client_city', 'label' => ctrans('texts.city'), 'type' => 'text', 'validation' => 'required'];
$fields[] = ['name' => 'client_state', 'label' => ctrans('texts.state'), 'type' => 'text', 'validation' => 'required'];
$fields[] = ['name' => 'client_country_id', 'label' => ctrans('texts.country'), 'type' => 'text', 'validation' => 'required'];
@ -197,7 +197,7 @@ class StripePaymentDriver extends BaseDriver
if ($this->company_gateway->require_shipping_address) {
$fields[] = ['name' => 'client_shipping_address_line_1', 'label' => ctrans('texts.shipping_address1'), 'type' => 'text', 'validation' => 'required'];
$fields[] = ['name' => 'client_shipping_address_line_2', 'label' => ctrans('texts.shipping_address2'), 'type' => 'text', 'validation' => 'sometimes'];
// $fields[] = ['name' => 'client_shipping_address_line_2', 'label' => ctrans('texts.shipping_address2'), 'type' => 'text', 'validation' => 'sometimes'];
$fields[] = ['name' => 'client_shipping_city', 'label' => ctrans('texts.shipping_city'), 'type' => 'text', 'validation' => 'required'];
$fields[] = ['name' => 'client_shipping_state', 'label' => ctrans('texts.shipping_state'), 'type' => 'text', 'validation' => 'required'];
$fields[] = ['name' => 'client_shipping_postal_code', 'label' => ctrans('texts.shipping_postal_code'), 'type' => 'text', 'validation' => 'required'];
@ -255,7 +255,7 @@ class StripePaymentDriver extends BaseDriver
public function createPaymentIntent($data): ?PaymentIntent
{
$this->init();
$meta = $this->stripe_connect_auth;
return PaymentIntent::create($data, $meta);
@ -299,7 +299,7 @@ class StripePaymentDriver extends BaseDriver
$customer = null;
$this->init();
$client_gateway_token = ClientGatewayToken::whereClientId($this->client->id)->whereCompanyGatewayId($this->company_gateway->id)->first();
if ($client_gateway_token && $client_gateway_token->gateway_customer_reference) {
@ -390,6 +390,13 @@ class StripePaymentDriver extends BaseDriver
$payment->save();
}
if ($request->type == 'charge.succeeded') {
$payment->status_id = Payment::STATUS_COMPLETED;
$payment->save();
}
// charge.failed, charge.refunded
return response([], 200);
}
@ -420,12 +427,12 @@ class StripePaymentDriver extends BaseDriver
nlog($e->getMessage());
SystemLogger::dispatch([
'server_response' => $e->getMessage(),
'server_response' => $e->getMessage(),
'data' => request()->all(),
],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_STRIPE,
],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_STRIPE,
$this->client, $this->client->company);
}
@ -444,7 +451,7 @@ class StripePaymentDriver extends BaseDriver
$this->init();
try{
$pm = $this->getStripePaymentMethod($token->token);
$pm->detach([], $this->stripe_connect_auth);
@ -453,12 +460,12 @@ class StripePaymentDriver extends BaseDriver
nlog($e->getMessage());
SystemLogger::dispatch([
'server_response' => $e->getMessage(),
'server_response' => $e->getMessage(),
'data' => request()->all(),
],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_STRIPE,
],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_STRIPE,
$this->client, $this->client->company);
}
@ -499,7 +506,7 @@ class StripePaymentDriver extends BaseDriver
/**
* Pull all client payment methods and update
* the respective tokens in the system.
*
*
*/
// public function updateAllPaymentMethods()
// {
@ -508,9 +515,9 @@ class StripePaymentDriver extends BaseDriver
/**
* Imports stripe customers and their payment methods
* Matches users in the system based on the $match_on_record
* Matches users in the system based on the $match_on_record
* ie. email
*
*
* Phone
* Email
*/

View File

@ -11,8 +11,8 @@
namespace App\Providers;
use App\Http\Middleware\SetDomainNameDb;
use App\Models\Account;
use App\Models\Subscription;
use App\Models\Client;
use App\Models\Company;
use App\Models\CompanyGateway;
@ -24,10 +24,10 @@ use App\Models\Payment;
use App\Models\Product;
use App\Models\Proposal;
use App\Models\Quote;
use App\Models\Subscription;
use App\Models\Task;
use App\Models\User;
use App\Observers\AccountObserver;
use App\Observers\SubscriptionObserver;
use App\Observers\ClientObserver;
use App\Observers\CompanyGatewayObserver;
use App\Observers\CompanyObserver;
@ -39,8 +39,10 @@ use App\Observers\PaymentObserver;
use App\Observers\ProductObserver;
use App\Observers\ProposalObserver;
use App\Observers\QuoteObserver;
use App\Observers\SubscriptionObserver;
use App\Observers\TaskObserver;
use App\Observers\UserObserver;
use App\Utils\Ninja;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Queue\Events\JobProcessing;
@ -49,6 +51,7 @@ use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider;
use Livewire\Livewire;
class AppServiceProvider extends ServiceProvider
{
@ -93,6 +96,15 @@ class AppServiceProvider extends ServiceProvider
Task::observe(TaskObserver::class);
User::observe(UserObserver::class);
/* Handles setting the correct database with livewire classes */
if(Ninja::isHosted())
{
Livewire::addPersistentMiddleware([
SetDomainNameDb::class,
]);
}
// Queue::before(function (JobProcessing $event) {
// // \Log::info('Event Job '.$event->connectionName);
// \Log::error('Event Job '.$event->job->getJobId);

View File

@ -169,10 +169,14 @@ class BaseRepository
*/
protected function alternativeSave($data, $model)
{
if (array_key_exists('client_id', $data)) //forces the client_id if it doesn't exist
//forces the client_id if it doesn't exist
if(array_key_exists('client_id', $data))
$model->client_id = $data['client_id'];
//pickup changes here to recalculate reminders
if($model instanceof Invoice && ($model->isDirty('date') || $model->isDirty('due_date')))
$model->service()->setReminder()->save();
$client = Client::where('id', $model->client_id)->withTrashed()->first();
$state = [];
@ -189,7 +193,7 @@ class BaseRepository
$data = array_merge($company_defaults, $data);
}
$tmp_data = $data; //preserves the $data arrayss
$tmp_data = $data; //preserves the $data array
/* We need to unset some variable as we sometimes unguard the model */
if (isset($tmp_data['invitations']))
@ -301,6 +305,10 @@ class BaseRepository
/* Perform model specific tasks */
if ($model instanceof Invoice) {
nlog("Finished amount = " . $state['finished_amount']);
nlog("Starting amount = " . $state['starting_amount']);
nlog("Diff = " . ($state['finished_amount'] - $state['starting_amount']));
if (($state['finished_amount'] != $state['starting_amount']) && ($model->status_id != Invoice::STATUS_DRAFT)) {

View File

@ -114,7 +114,7 @@ class UserRepository extends BaseRepository
}
$user->restore();
return $user;
return $user->fresh();
}
public function destroy(array $data, User $user)

View File

@ -40,6 +40,8 @@ class AutoBillInvoice extends AbstractService
public function run()
{
$is_partial = false;
/* Is the invoice payable? */
if (! $this->invoice->isPayable())
return $this->invoice;
@ -57,6 +59,8 @@ class AutoBillInvoice extends AbstractService
/* Determine $amount */
if ($this->invoice->partial > 0) {
$is_partial = true;
$invoice_total = $this->invoice->amount;
$amount = $this->invoice->partial;
} elseif ($this->invoice->balance > 0) {
$amount = $this->invoice->balance;
@ -77,7 +81,10 @@ class AutoBillInvoice extends AbstractService
//$fee = $gateway_token->gateway->calcGatewayFee($amount, $gateway_token->gateway_type_id, $this->invoice->uses_inclusive_taxes);
$this->invoice = $this->invoice->service()->addGatewayFee($gateway_token->gateway, $gateway_token->gateway_type_id, $amount)->save();
$fee = $this->invoice->amount - $amount;
if($is_partial)
$fee = $this->invoice->amount - $invoice_total;
else
$fee = $this->invoice->amount - $amount;
/* Build payment hash */
$payment_hash = PaymentHash::create([
@ -340,68 +347,4 @@ class AutoBillInvoice extends AbstractService
return $this;
}
/**
* Removes any existing unpaid gateway fees
* due to previous payment failure.
*
* @return $this
*/
// private function purgeStaleGatewayFees()
// {
// $starting_amount = $this->invoice->amount;
// $line_items = $this->invoice->line_items;
// $new_items = [];
// foreach($line_items as $item)
// {
// if($item->type_id != 3)
// $new_items[] = $item;
// }
// $this->invoice->line_items = $new_items;
// $this->invoice->save();
// $this->invoice = $this->invoice->calc()->getInvoice();
// if($starting_amount != $this->invoice->amount && $this->invoice->status_id != Invoice::STATUS_DRAFT){
// $this->invoice->client->service()->updateBalance($this->invoice->amount - $starting_amount)->save();
// $this->invoice->ledger()->updateInvoiceBalance($this->invoice->amount - $starting_amount, 'Invoice balance updated after stale gateway fee removed')->save();
// }
// return $this;
// }
// /**
// * Checks whether a given gateway token is able
// * to process the payment after passing through the
// * fees and limits check.
// *
// * @param CompanyGateway $cg The CompanyGateway instance
// * @param float $amount The amount to be paid
// * @return bool
// */
// public function validGatewayLimits($cg, $amount) : bool
// {
// if (isset($cg->fees_and_limits)) {
// $fees_and_limits = $cg->fees_and_limits->{'1'};
// } else {
// return true;
// }
// if ((property_exists($fees_and_limits, 'min_limit')) && $fees_and_limits->min_limit !== null && $amount < $fees_and_limits->min_limit) {
// info("amount {$amount} less than ".$fees_and_limits->min_limit);
// $passes = false;
// } elseif ((property_exists($fees_and_limits, 'max_limit')) && $fees_and_limits->max_limit !== null && $amount > $fees_and_limits->max_limit) {
// info("amount {$amount} greater than ".$fees_and_limits->max_limit);
// $passes = false;
// } else {
// $passes = true;
// }
// return $passes;
// }
}

View File

@ -16,6 +16,7 @@ use App\Factory\InvoiceInvitationFactory;
use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Services\AbstractService;
use Illuminate\Support\Str;
class CreateInvitations extends AbstractService
{

View File

@ -21,6 +21,8 @@ use App\Models\Invoice;
use App\Models\Payment;
use App\Models\Task;
use App\Services\Client\ClientService;
use App\Services\Invoice\UpdateReminder;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Storage;
@ -243,6 +245,13 @@ class InvoiceService
return $this;
}
public function setReminder($settings = null)
{
$this->invoice = (new UpdateReminder($this->invoice, $settings))->run();
return $this;
}
public function setStatus($status)
{
@ -301,6 +310,10 @@ class InvoiceService
//UnlinkFile::dispatchNow(config('filesystems.default'), $this->invoice->client->invoice_filepath() . $this->invoice->numberFormatter().'.pdf');
Storage::disk(config('filesystems.default'))->delete($this->invoice->client->invoice_filepath() . $this->invoice->numberFormatter().'.pdf');
if(Ninja::isHosted()) {
Storage::disk('public')->delete($this->invoice->client->invoice_filepath() . $this->invoice->numberFormatter().'.pdf');
}
return $this;
}
@ -419,7 +432,7 @@ class InvoiceService
return $this;
}
/**
* Saves the invoice.
* @return Invoice object

View File

@ -39,8 +39,6 @@ class MarkSent extends AbstractService
$this->invoice->markInvitationsSent();
$this->invoice->setReminder();
$this->invoice
->service()
->setStatus(Invoice::STATUS_SENT)
@ -48,6 +46,7 @@ class MarkSent extends AbstractService
->setDueDate()
->updateBalance($this->invoice->amount)
->deletePdf()
->setReminder()
->save();
$this->client->service()->updateBalance($this->invoice->balance)->save();

View File

@ -0,0 +1,131 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Services\Invoice;
use App\Models\Invoice;
use App\Services\AbstractService;
use Carbon\Carbon;
class UpdateReminder extends AbstractService
{
public $invoice;
public $settings;
public function __construct(Invoice $invoice, $settings = null)
{
$this->invoice = $invoice;
$this->settings = $settings;
}
public function run()
{
if (! $this->settings) {
$this->settings = $this->invoice->client->getMergedSettings();
}
if (! $this->invoice->isPayable()) {
$this->invoice->next_send_date = null;
$this->invoice->save();
return $this->invoice; //exit early
}
$date_collection = collect();
if (is_null($this->invoice->reminder1_sent) &&
$this->settings->schedule_reminder1 == 'after_invoice_date' &&
$this->settings->num_days_reminder1 > 0) {
$reminder_date = Carbon::parse($this->invoice->date)->addDays($this->settings->num_days_reminder1);
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date)));
$date_collection->push($reminder_date->format('Y-m-d'));
}
if (is_null($this->invoice->reminder1_sent) &&
$this->settings->schedule_reminder1 == 'before_due_date' &&
$this->settings->num_days_reminder1 > 0) {
$reminder_date = Carbon::parse($this->invoice->due_date)->subDays($this->settings->num_days_reminder1);
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date)));
$date_collection->push($reminder_date->format('Y-m-d'));
}
if (is_null($this->invoice->reminder1_sent) &&
$this->settings->schedule_reminder1 == 'after_due_date' &&
$this->settings->num_days_reminder1 > 0) {
$reminder_date = Carbon::parse($this->invoice->due_date)->addDays($this->settings->num_days_reminder1);
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date)));
$date_collection->push($reminder_date->format('Y-m-d'));
}
if (is_null($this->invoice->reminder2_sent) &&
$this->settings->schedule_reminder2 == 'after_invoice_date' &&
$this->settings->num_days_reminder2 > 0) {
$reminder_date = Carbon::parse($this->invoice->date)->addDays($this->settings->num_days_reminder2);
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date)));
$date_collection->push($reminder_date->format('Y-m-d'));
}
if (is_null($this->invoice->reminder2_sent) &&
$this->settings->schedule_reminder2 == 'before_due_date' &&
$this->settings->num_days_reminder2 > 0) {
$reminder_date = Carbon::parse($this->invoice->due_date)->subDays($this->settings->num_days_reminder2);
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date)));
$date_collection->push($reminder_date->format('Y-m-d'));
}
if (is_null($this->invoice->reminder2_sent) &&
$this->settings->schedule_reminder2 == 'after_due_date' &&
$this->settings->num_days_reminder2 > 0) {
$reminder_date = Carbon::parse($this->invoice->due_date)->addDays($this->settings->num_days_reminder2);
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date)));
$date_collection->push($reminder_date->format('Y-m-d'));
}
if (is_null($this->invoice->reminder3_sent) &&
$this->settings->schedule_reminder3 == 'after_invoice_date' &&
$this->settings->num_days_reminder3 > 0) {
$reminder_date = Carbon::parse($this->invoice->date)->addDays($this->settings->num_days_reminder3);
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date)));
$date_collection->push($reminder_date->format('Y-m-d'));
}
if (is_null($this->invoice->reminder3_sent) &&
$this->settings->schedule_reminder3 == 'before_due_date' &&
$this->settings->num_days_reminder3 > 0) {
$reminder_date = Carbon::parse($this->invoice->due_date)->subDays($this->settings->num_days_reminder3);
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date)));
$date_collection->push($reminder_date->format('Y-m-d'));
}
if (is_null($this->invoice->reminder3_sent) &&
$this->settings->schedule_reminder3 == 'after_due_date' &&
$this->settings->num_days_reminder3 > 0) {
$reminder_date = Carbon::parse($this->invoice->due_date)->addDays($this->settings->num_days_reminder3);
if ($reminder_date->gt(Carbon::parse($this->invoice->next_send_date)));
$date_collection->push($reminder_date->format('Y-m-d'));
}
$this->invoice->next_send_date = $date_collection->sort()->first();
return $this->invoice;
}
}

View File

@ -41,7 +41,7 @@ class GetInvoicePdf extends AbstractService
$file_path = $path.$this->entity->hashed_id.'.pdf';
$disk = config('filesystems.default');
$disk = 'public';
$file = Storage::disk($disk)->exists($file_path);
@ -49,12 +49,6 @@ class GetInvoicePdf extends AbstractService
$file_path = CreateEntityPdf::dispatchNow($invitation);
}
/* Copy from remote disk to local when using cloud file storage. */
if(config('filesystems.default') == 's3')
return TempFile::path(Storage::disk($disk)->url($file_path));
// return Storage::disk($disk)->url($file_path);
return Storage::disk($disk)->path($file_path);
}
}

View File

@ -108,7 +108,7 @@ class CreditTransformer extends EntityTransformer
'po_number' => $credit->po_number ?: '',
'date' => $credit->date ?: '',
'last_sent_date' => $credit->last_sent_date ?: '',
'next_send_date' => $credit->date ?: '',
'next_send_date' => $credit->next_send_date ?: '',
'reminder1_sent' => $credit->reminder1_sent ?: '',
'reminder2_sent' => $credit->reminder2_sent ?: '',
'reminder3_sent' => $credit->reminder3_sent ?: '',

View File

@ -114,7 +114,7 @@ class InvoiceTransformer extends EntityTransformer
'po_number' => $invoice->po_number ?: '',
'date' => $invoice->date ?: '',
'last_sent_date' => $invoice->last_sent_date ?: '',
'next_send_date' => $invoice->date ?: '',
'next_send_date' => $invoice->next_send_date ?: '',
'due_date' => $invoice->due_date ?: '',
'terms' => $invoice->terms ?: '',
'public_notes' => $invoice->public_notes ?: '',

View File

@ -108,7 +108,7 @@ class QuoteTransformer extends EntityTransformer
'po_number' => $quote->po_number ?: '',
'date' => $quote->date ?: '',
'last_sent_date' => $quote->last_sent_date ?: '',
'next_send_date' => $quote->date ?: '',
'next_send_date' => $quote->next_send_date ?: '',
'reminder1_sent' => $quote->reminder1_sent ?: '',
'reminder2_sent' => $quote->reminder2_sent ?: '',
'reminder3_sent' => $quote->reminder3_sent ?: '',

View File

@ -118,6 +118,8 @@ class Phantom
$finfo = new \finfo(FILEINFO_MIME);
nlog($pdf);
if($finfo->buffer($pdf) != 'application/pdf; charset=binary')
{
SystemLogger::dispatch(

View File

@ -25,7 +25,7 @@ use Illuminate\Support\Facades\Queue;
class SystemHealth
{
private static $extensions = [
'mysqli',
// 'mysqli',
'gd',
'curl',
'zip',
@ -34,7 +34,7 @@ class SystemHealth
'mbstring',
'xml',
'bcmath',
'mysqlnd',
// 'mysqlnd',
//'intl', //todo double check whether we need this for email dns validation
];

View File

@ -19,98 +19,7 @@ use Illuminate\Support\Carbon;
*/
trait MakesReminders
{
public function setReminder($settings = null)
{
if (! $settings) {
$settings = $this->client->getMergedSettings();
}
if (! $this->isPayable()) {
$this->next_send_date = null;
$this->save();
return; //exit early
}
$date_collection = collect();
if ($settings->schedule_reminder1 == 'after_invoice_date' &&
$settings->num_days_reminder1 > 0) {
$reminder_date = Carbon::parse($this->date)->addDays($settings->num_days_reminder1);
if ($reminder_date->gt(Carbon::parse($this->next_send_date)));
$date_collection->push($reminder_date->format('Y-m-d'));
}
if ($settings->schedule_reminder1 == 'before_due_date' &&
$settings->num_days_reminder1 > 0) {
$reminder_date = Carbon::parse($this->due_date)->subDays($settings->num_days_reminder1);
if ($reminder_date->gt(Carbon::parse($this->next_send_date)));
$date_collection->push($reminder_date->format('Y-m-d'));
}
if ($settings->schedule_reminder1 == 'after_due_date' &&
$settings->num_days_reminder1 > 0) {
$reminder_date = Carbon::parse($this->due_date)->addDays($settings->num_days_reminder1);
if ($reminder_date->gt(Carbon::parse($this->next_send_date)));
$date_collection->push($reminder_date->format('Y-m-d'));
}
if ($settings->schedule_reminder2 == 'after_invoice_date' &&
$settings->num_days_reminder2 > 0) {
$reminder_date = Carbon::parse($this->date)->addDays($settings->num_days_reminder2);
if ($reminder_date->gt(Carbon::parse($this->next_send_date)));
$date_collection->push($reminder_date->format('Y-m-d'));
}
if ($settings->schedule_reminder2 == 'before_due_date' &&
$settings->num_days_reminder2 > 0) {
$reminder_date = Carbon::parse($this->due_date)->subDays($settings->num_days_reminder2);
if ($reminder_date->gt(Carbon::parse($this->next_send_date)));
$date_collection->push($reminder_date->format('Y-m-d'));
}
if ($settings->schedule_reminder2 == 'after_due_date' &&
$settings->num_days_reminder2 > 0) {
$reminder_date = Carbon::parse($this->due_date)->addDays($settings->num_days_reminder2);
if ($reminder_date->gt(Carbon::parse($this->next_send_date)));
$date_collection->push($reminder_date->format('Y-m-d'));
}
if ($settings->schedule_reminder3 == 'after_invoice_date' &&
$settings->num_days_reminder3 > 0) {
$reminder_date = Carbon::parse($this->date)->addDays($settings->num_days_reminder3);
if ($reminder_date->gt(Carbon::parse($this->next_send_date)));
$date_collection->push($reminder_date->format('Y-m-d'));
}
if ($settings->schedule_reminder3 == 'before_due_date' &&
$settings->num_days_reminder3 > 0) {
$reminder_date = Carbon::parse($this->due_date)->subDays($settings->num_days_reminder3);
if ($reminder_date->gt(Carbon::parse($this->next_send_date)));
$date_collection->push($reminder_date->format('Y-m-d'));
}
if ($settings->schedule_reminder3 == 'after_due_date' &&
$settings->num_days_reminder3 > 0) {
$reminder_date = Carbon::parse($this->due_date)->addDays($settings->num_days_reminder3);
if ($reminder_date->gt(Carbon::parse($this->next_send_date)));
$date_collection->push($reminder_date->format('Y-m-d'));
}
$this->next_send_date = $date_collection->sort()->first();
$this->save();
}
public function inReminderWindow($schedule_reminder, $num_days_reminder)
{
switch ($schedule_reminder) {
@ -177,11 +86,9 @@ trait MakesReminders
private function addTimeInterval($date, $endless_reminder_frequency_id) :?Carbon
{
if (!$date) {
if (!$date)
return null;
}
switch ($endless_reminder_frequency_id) {
case RecurringInvoice::FREQUENCY_WEEKLY:
return Carbon::parse($date)->addWeek()->startOfDay();

View File

@ -0,0 +1,35 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Utils\Traits\User;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
trait LoginCache
{
public function setLoginCache($user)
{
$timeout = $user->company()->default_password_timeout;
if($timeout == 0)
$timeout = 30*60*1000*1000;
else
$timeout = $timeout/1000;
Cache::put($user->hashed_id.'_'.$user->account_id.'_logged_in', Str::random(64), $timeout);
}
}

113
config/livewire.php Normal file
View File

@ -0,0 +1,113 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Class Namespace
|--------------------------------------------------------------------------
|
| This value sets the root namespace for Livewire component classes in
| your application. This value affects component auto-discovery and
| any Livewire file helper commands, like `artisan make:livewire`.
|
| After changing this item, run: `php artisan livewire:discover`.
|
*/
'class_namespace' => 'App\\Http\\Livewire',
/*
|--------------------------------------------------------------------------
| View Path
|--------------------------------------------------------------------------
|
| This value sets the path for Livewire component views. This affects
| file manipulation helper commands like `artisan make:livewire`.
|
*/
'view_path' => resource_path('views/livewire'),
/*
|--------------------------------------------------------------------------
| Layout
|--------------------------------------------------------------------------
| The default layout view that will be used when rendering a component via
| Route::get('/some-endpoint', SomeComponent::class);. In this case the
| the view returned by SomeComponent will be wrapped in "layouts.app"
|
*/
'layout' => 'layouts.app',
/*
|--------------------------------------------------------------------------
| Livewire Assets URL
|--------------------------------------------------------------------------
|
| This value sets the path to Livewire JavaScript assets, for cases where
| your app's domain root is not the correct path. By default, Livewire
| will load its JavaScript assets from the app's "relative root".
|
| Examples: "/assets", "myurl.com/app".
|
*/
'asset_url' => env('ASSETS_URL', config('app.url')),
/*
|--------------------------------------------------------------------------
| Livewire Endpoint Middleware Group
|--------------------------------------------------------------------------
|
| This value sets the middleware group that will be applied to the main
| Livewire "message" endpoint (the endpoint that gets hit everytime
| a Livewire component updates). It is set to "web" by default.
|
*/
'middleware_group' => 'web',
/*
|--------------------------------------------------------------------------
| Livewire Temporary File Uploads Endpoint Configuration
|--------------------------------------------------------------------------
|
| Livewire handles file uploads by storing uploads in a temporary directory
| before the file is validated and stored permanently. All file uploads
| are directed to a global endpoint for temporary storage. The config
| items below are used for customizing the way the endpoint works.
|
*/
'temporary_file_upload' => [
'disk' => null, // Example: 'local', 's3' Default: 'default'
'rules' => null, // Example: ['file', 'mimes:png,jpg'] Default: ['required', 'file', 'max:12288'] (12MB)
'directory' => null, // Example: 'tmp' Default 'livewire-tmp'
'middleware' => null, // Example: 'throttle:5,1' Default: 'throttle:60,1'
'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs.
'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',
'mov', 'avi', 'wmv', 'mp3', 'm4a',
'jpg', 'jpeg', 'mpga', 'webp', 'wma',
],
'max_upload_time' => 5, // Max duration (in minutes) before an upload gets invalidated.
],
/*
|--------------------------------------------------------------------------
| Manifest File Path
|--------------------------------------------------------------------------
|
| This value sets the path to the Livewire manifest file.
| The default should work for most cases (which is
| "<app_root>/bootstrap/cache/livewire-components.php)", but for specific
| cases like when hosting on Laravel Vapor, it could be set to a different value.
|
| Example: for Laravel Vapor, it would be "/tmp/storage/bootstrap/cache/livewire-components.php".
|
*/
'manifest_path' => null,
];

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.1.65',
'app_tag' => '5.1.65-release',
'app_version' => '5.1.67',
'app_tag' => '5.1.67-release',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''),

View File

@ -0,0 +1,38 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace Database\Factories;
use App\Models\TaxRate;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
class TaxRateFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = TaxRate::class;
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'name' => $this->faker->word(3),
'rate' => rand(1,20)
];
}
}

View File

@ -0,0 +1,43 @@
<?php
use App\Libraries\MultiDB;
use App\Models\Document;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class MakeDocumentsAssignedUserNullable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('documents', function (Blueprint $table){
$table->unsignedInteger('assigned_user_id')->nullable()->change();
});
Document::where('assigned_user_id', 0)->update(['assigned_user_id' => null]);
if(config('ninja.db.multi_db_enabled')){
foreach (MultiDB::$dbs as $db) {
Document::on($db)->where('assigned_user_id', 0)->update(['assigned_user_id' => null]);
}
}
else{
Document::where('assigned_user_id', 0)->update(['assigned_user_id' => null]);
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

1
public/css/admin.css vendored Normal file

File diff suppressed because one or more lines are too long

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

View File

@ -3,9 +3,9 @@ const MANIFEST = 'flutter-app-manifest';
const TEMP = 'flutter-temp-cache';
const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = {
"version.json": "ea1781094b87723b953889a712b1feba",
"version.json": "9fe5b22a16f39b766c8fdc35a24b3efa",
"favicon.ico": "51636d3a390451561744c42188ccd628",
"main.dart.js": "f46c892fb39ce4151ac09e5794745701",
"main.dart.js": "55df523d1b81bba3d88e7b77511c7a87",
"/": "23224b5e03519aaa87594403d54412cf",
"assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "174c02fc4609e8fc4389f5d21f16a296",
"assets/AssetManifest.json": "659dcf9d1baf3aed3ab1b9c42112bf8f",

1
public/js/admin.js vendored Normal file
View File

@ -0,0 +1 @@
(()=>{var r,e={847:()=>{},113:()=>{}},o={};function n(r){var t=o[r];if(void 0!==t)return t.exports;var a=o[r]={exports:{}};return e[r](a,a.exports,n),a.exports}n.m=e,r=[],n.O=(e,o,t,a)=>{if(!o){var v=1/0;for(p=0;p<r.length;p++){for(var[o,t,a]=r[p],l=!0,i=0;i<o.length;i++)(!1&a||v>=a)&&Object.keys(n.O).every((r=>n.O[r](o[i])))?o.splice(i--,1):(l=!1,a<v&&(v=a));l&&(r.splice(p--,1),e=t())}return e}a=a||0;for(var p=r.length;p>0&&r[p-1][2]>a;p--)r[p]=r[p-1];r[p]=[o,t,a]},n.o=(r,e)=>Object.prototype.hasOwnProperty.call(r,e),(()=>{var r={467:0,703:0};n.O.j=e=>0===r[e];var e=(e,o)=>{var t,a,[v,l,i]=o,p=0;for(t in l)n.o(l,t)&&(n.m[t]=l[t]);if(i)var f=i(n);for(e&&e(o);p<v.length;p++)a=v[p],n.o(r,a)&&r[a]&&r[a][0](),r[v[p]]=0;return n.O(f)},o=self.webpackChunk=self.webpackChunk||[];o.forEach(e.bind(null,0)),o.push=e.bind(null,o.push.bind(o))})(),n.O(void 0,[703],(()=>n(847)));var t=n.O(void 0,[703],(()=>n(113)));t=n.O(t)})();

214752
public/main.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

216566
public/main.foss.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"/js/app.js": "/js/app.js?id=696e8203d5e8e7cf5ff5",
"/css/app.css": "/css/app.css?id=987a5ab343fc0d5c6cba",
"/css/app.css": "/css/app.css?id=14a824656f32eec8c2b1",
"/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=a09bb529b8e1826f13b4",
"/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=8ce8955ba775ea5f47d1",
"/js/clients/linkify-urls.js": "/js/clients/linkify-urls.js?id=0dc8c34010d09195d2f7",

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More