Fixes for recurring expenses

This commit is contained in:
David Bomba 2021-09-14 18:52:54 +10:00
commit 02de2607e0
242 changed files with 1141910 additions and 687679 deletions

View File

@ -1 +1 @@
5.3.0
5.3.10

View File

@ -24,12 +24,14 @@ use App\Models\Invoice;
use App\Models\InvoiceInvitation;
use App\Models\Payment;
use App\Models\Paymentable;
use App\Models\QuoteInvitation;
use App\Models\RecurringInvoiceInvitation;
use App\Utils\Ninja;
use DB;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use Mail;
use Symfony\Component\Console\Input\InputOption;
/*
@ -103,6 +105,7 @@ class CheckData extends Command
// $this->checkPaidToCompanyDates();
$this->checkClientBalances();
$this->checkContacts();
$this->checkEntityInvitations();
$this->checkCompanyData();
@ -197,7 +200,7 @@ class CheckData extends Command
->where('id', '=', $contact->id)
->whereNull('contact_key')
->update([
'contact_key' => str_random(config('ninja.key_length')),
'contact_key' => Str::random(config('ninja.key_length')),
]);
}
}
@ -307,13 +310,73 @@ class CheckData extends Command
$invitation->company_id = $invoice->company_id;
$invitation->user_id = $invoice->user_id;
$invitation->invoice_id = $invoice->id;
$invitation->contact_id = ClientContact::whereClientId($invoice->client_id)->whereIsPrimary(true)->first()->id;
$invitation->invitation_key = str_random(config('ninja.key_length'));
$invitation->contact_id = ClientContact::whereClientId($invoice->client_id)->first()->id;
$invitation->invitation_key = Str::random(config('ninja.key_length'));
$invitation->save();
}
}
}
private function checkEntityInvitations()
{
RecurringInvoiceInvitation::where('deleted_at',"0000-00-00 00:00:00.000000")->withTrashed()->update(['deleted_at' => null]);
InvoiceInvitation::where('deleted_at',"0000-00-00 00:00:00.000000")->withTrashed()->update(['deleted_at' => null]);
QuoteInvitation::where('deleted_at',"0000-00-00 00:00:00.000000")->withTrashed()->update(['deleted_at' => null]);
$entities = ['invoice', 'quote', 'credit', 'recurring_invoice'];
foreach($entities as $entity)
{
$table = "{$entity}s";
$invitation_table = "{$entity}_invitations";
$entities = DB::table($table)
->leftJoin($invitation_table, function ($join) use($invitation_table, $table, $entity){
$join->on("{$invitation_table}.{$entity}_id", '=', "{$table}.id");
// ->whereNull("{$invitation_table}.deleted_at");
})
->groupBy("{$table}.id", "{$table}.user_id", "{$table}.company_id", "{$table}.client_id")
->havingRaw("count({$invitation_table}.id) = 0")
->get(["{$table}.id", "{$table}.user_id", "{$table}.company_id", "{$table}.client_id"]);
$this->logMessage($entities->count()." {$table} without any invitations");
if ($this->option('fix') == 'true')
$this->fixInvitations($entities, $entity);
}
}
private function fixInvitations($entities, $entity)
{
$entity_key = "{$entity}_id";
$entity_obj = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation';
foreach($entities as $entity)
{
$invitation = new $entity_obj();
$invitation->company_id = $entity->company_id;
$invitation->user_id = $entity->user_id;
$invitation->{$entity_key} = $entity->id;
$invitation->client_contact_id = ClientContact::whereClientId($entity->client_id)->first()->id;
$invitation->key = Str::random(config('ninja.key_length'));
try{
$invitation->save();
}
catch(\Exception $e){
$invitation = null;
}
}
}
// private function checkPaidToCompanyDates()
// {
// Company::cursor()->each(function ($company){

View File

@ -22,6 +22,7 @@ use App\Factory\RecurringInvoiceFactory;
use App\Factory\SubscriptionFactory;
use App\Helpers\Invoice\InvoiceSum;
use App\Jobs\Company\CreateCompanyTaskStatuses;
use App\Libraries\MultiDB;
use App\Models\Account;
use App\Models\Client;
use App\Models\ClientContact;
@ -62,7 +63,7 @@ class CreateSingleAccount extends Command
/**
* @var string
*/
protected $signature = 'ninja:create-single-account {gateway=all}';
protected $signature = 'ninja:create-single-account {gateway=all} {--database=db-ninja-01}';
protected $invoice_repo;
@ -89,6 +90,8 @@ class CreateSingleAccount extends Command
*/
public function handle()
{
MultiDB::setDb($this->option('database'));
$this->info(date('r').' Create Single Sample Account...');
$this->count = 1;
$this->gateway = $this->argument('gateway');

View File

@ -24,6 +24,7 @@ use App\Models\Company;
use App\Models\CompanyToken;
use App\Models\Country;
use App\Models\Credit;
use App\Models\Document;
use App\Models\Expense;
use App\Models\Product;
use App\Models\Project;
@ -230,6 +231,7 @@ class CreateTestData extends Command
'company_id' => $company->id,
]);
$this->count = $this->count * 10;
$this->info('Creating '.$this->count.' clients');
@ -387,6 +389,14 @@ class CreateTestData extends Command
'company_id' => $company->id,
]);
Document::factory()->count(50)->create([
'user_id' => $user->id,
'company_id' => $company->id,
'documentable_type' => Client::class,
'documentable_id' => $client->id
]);
ClientContact::factory()->create([
'user_id' => $user->id,
'client_id' => $client->id,
@ -428,6 +438,13 @@ class CreateTestData extends Command
'company_id' => $client->company->id,
]);
Document::factory()->count(50)->create([
'user_id' => $client->user->id,
'company_id' => $client->company_id,
'documentable_type' => Vendor::class,
'documentable_id' => $vendor->id
]);
VendorContact::factory()->create([
'user_id' => $client->user->id,
'vendor_id' => $vendor->id,
@ -449,6 +466,14 @@ class CreateTestData extends Command
'user_id' => $client->user->id,
'company_id' => $client->company->id,
]);
Document::factory()->count(5)->create([
'user_id' => $client->user->id,
'company_id' => $client->company_id,
'documentable_type' => Task::class,
'documentable_id' => $vendor->id
]);
}
private function createProject($client)
@ -457,6 +482,13 @@ class CreateTestData extends Command
'user_id' => $client->user->id,
'company_id' => $client->company->id,
]);
Document::factory()->count(5)->create([
'user_id' => $client->user->id,
'company_id' => $client->company_id,
'documentable_type' => Project::class,
'documentable_id' => $vendor->id
]);
}
private function createInvoice($client)
@ -506,6 +538,13 @@ class CreateTestData extends Command
$invoice = $invoice->service()->markPaid()->save();
}
Document::factory()->count(5)->create([
'user_id' => $invoice->user->id,
'company_id' => $invoice->company_id,
'documentable_type' => Invoice::class,
'documentable_id' => $invoice->id
]);
event(new InvoiceWasCreated($invoice, $invoice->company, Ninja::eventVars()));
}

View File

@ -0,0 +1,128 @@
<?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://www.elastic.co/licensing/elastic-license
*/
namespace App\Console\Commands;
use App\DataMapper\CompanySettings;
use App\Exceptions\MigrationValidatorFailed;
use App\Exceptions\NonExistingMigrationFile;
use App\Exceptions\ProcessingMigrationArchiveFailed;
use App\Exceptions\ResourceDependencyMissing;
use App\Exceptions\ResourceNotAvailableForMigration;
use App\Jobs\Util\Import;
use App\Jobs\Util\StartMigration;
use App\Libraries\MultiDB;
use App\Mail\MigrationFailed;
use App\Models\Account;
use App\Models\Company;
use App\Models\CompanyToken;
use App\Models\User;
use App\Utils\Traits\AppSetup;
use App\Utils\Traits\MakesHash;
use DirectoryIterator;
use Faker\Factory;
use Faker\Generator;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
use ZipArchive;
class HostedMigrations extends Command
{
use MakesHash;
use AppSetup;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'ninja:import {--email=}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Import a v4 migration file';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->buildCache();
if(!MultiDB::userFindAndSetDb($this->option('email'))){
$this->info("Could not find a user with that email address");
return;
}
$user = User::where('email', $this->option('email'))->first();
if(!$user){
$this->info("There was a problem getting the user, did you set the right DB?");
return;
}
$path = public_path('storage/migrations/import');
nlog(public_path('storage/migrations/import'));
$directory = new DirectoryIterator($path);
foreach ($directory as $file) {
if ($file->getExtension() === 'zip') {
$company = $user->companies()->first();
$this->info('Started processing: '.$file->getBasename().' at '.now());
$zip = new ZipArchive();
$archive = $zip->open($file->getRealPath());
try {
if (! $archive) {
throw new ProcessingMigrationArchiveFailed('Processing migration archive failed. Migration file is possibly corrupted.');
}
$filename = pathinfo($file->getRealPath(), PATHINFO_FILENAME);
$zip->extractTo(public_path("storage/migrations/{$filename}"));
$zip->close();
$import_file = public_path("storage/migrations/$filename/migration.json");
Import::dispatch($import_file, $user->companies()->first(), $user);
} catch (NonExistingMigrationFile | ProcessingMigrationArchiveFailed | ResourceNotAvailableForMigration | MigrationValidatorFailed | ResourceDependencyMissing $e) {
\Mail::to($this->user)->send(new MigrationFailed($e, $e->getMessage()));
if (app()->environment() !== 'production') {
info($e->getMessage());
}
}
}
}
}
}

View File

@ -43,7 +43,7 @@ class ImportMigrations extends Command
*
* @var string
*/
protected $signature = 'migrations:import {--path=}';
protected $signature = 'ninja:old-import {--path=}';
/**
* The console command description.

View File

@ -15,12 +15,14 @@ class S3Cleanup extends Command
*/
protected $signature = 'ninja:s3-cleanup';
protected $log = '';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Remove orphan folders';
protected $description = 'Remove orphan folders/files';
/**
* Create a new command instance.
@ -54,7 +56,11 @@ class S3Cleanup extends Command
if(!in_array($dir, $merged))
{
$this->logMessage("Deleting $dir");
Storage::disk(config('filesystems.default'))->deleteDirectory($dir);
/* Ensure we are not deleting the root folder */
if(strlen($dir) > 1)
Storage::disk(config('filesystems.default'))->deleteDirectory($dir);
}
}

View File

@ -74,7 +74,8 @@ class Kernel extends ConsoleKernel
$schedule->job(new AdjustEmailQuota)->dailyAt('23:00')->withoutOverlapping();
$schedule->job(new SendFailedEmails)->daily()->withoutOverlapping();
$schedule->command('ninja:check-data --database=db-ninja-02')->daily()->withoutOverlapping();
$schedule->command('ninja:check-data --database=db-ninja-02')->dailyAt('00:15')->withoutOverlapping();
$schedule->command('ninja:s3-cleanup')->dailyAt('23:15')->withoutOverlapping();
}

View File

@ -273,8 +273,10 @@ class CompanySettings extends BaseSettings
public $use_credits_payment = 'off'; //always, option, off //@implemented
public $hide_empty_columns_on_pdf = false;
public $email_from_name = '';
public static $casts = [
'email_from_name' => 'string',
'show_all_tasks_client_portal' => 'string',
'entity_send_time' => 'int',
'shared_invoice_credit_counter' => 'bool',
@ -602,7 +604,7 @@ class CompanySettings extends BaseSettings
*
* @return stdClass The stdClass of PDF variables
*/
private static function getEntityVariableDefaults() :stdClass
public static function getEntityVariableDefaults() :stdClass
{
$variables = [
'client_details' => [
@ -684,6 +686,19 @@ class CompanySettings extends BaseSettings
'$paid_to_date',
'$outstanding',
],
'statement_invoice_columns' => [
'$invoice.number',
'$invoice.date',
'$due_date',
'$total',
'$outstanding',
],
'statement_payment_columns' => [
'$invoice.number',
'$payment.date',
'$method',
'$outstanding',
],
];
return json_decode(json_encode($variables));

View File

@ -0,0 +1,75 @@
<?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://www.elastic.co/licensing/elastic-license
*/
namespace App\DataProviders;
class USStates
{
protected static array $states = [
'AL' => 'Alabama',
'AK' => 'Alaska',
'AZ' => 'Arizona',
'AR' => 'Arkansas',
'CA' => 'California',
'CO' => 'Colorado',
'CT' => 'Connecticut',
'DE' => 'Delaware',
'DC' => 'District Of Columbia',
'FL' => 'Florida',
'GA' => 'Georgia',
'HI' => 'Hawaii',
'ID' => 'Idaho',
'IL' => 'Illinois',
'IN' => 'Indiana',
'IA' => 'Iowa',
'KS' => 'Kansas',
'KY' => 'Kentucky',
'LA' => 'Louisiana',
'ME' => 'Maine',
'MD' => 'Maryland',
'MA' => 'Massachusetts',
'MI' => 'Michigan',
'MN' => 'Minnesota',
'MS' => 'Mississippi',
'MO' => 'Missouri',
'MT' => 'Montana',
'NE' => 'Nebraska',
'NV' => 'Nevada',
'NH' => 'New Hampshire',
'NJ' => 'New Jersey',
'NM' => 'New Mexico',
'NY' => 'New York',
'NC' => 'North Carolina',
'ND' => 'North Dakota',
'OH' => 'Ohio',
'OK' => 'Oklahoma',
'OR' => 'Oregon',
'PA' => 'Pennsylvania',
'RI' => 'Rhode Island',
'SC' => 'South Carolina',
'SD' => 'South Dakota',
'TN' => 'Tennessee',
'TX' => 'Texas',
'UT' => 'Utah',
'VT' => 'Vermont',
'VA' => 'Virginia',
'WA' => 'Washington',
'WV' => 'West Virginia',
'WI' => 'Wisconsin',
'WY' => 'Wyoming',
];
public static function get(): array
{
return self::$states;
}
}

View File

@ -26,8 +26,16 @@ class PaymentRefundFailed extends Exception
*/
public function render($request)
{
// $msg = 'Unable to refund the transaction';
$msg = ctrans('texts.warning_local_refund');
if($this->getMessage() && strlen($this->getMessage()) >=1 )
$msg = $this->getMessage();
return response()->json([
'message' => 'Unable to refund the transaction',
'message' => $msg,
], 401);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Exceptions;
use Exception;
class SystemError extends Exception
{
public function report()
{
// ..
}
public function render($request)
{
return view('errors.guest', [
'message' => $this->getMessage(),
'code' => $this->getCode(),
]);
}
}

View File

@ -67,18 +67,6 @@ class InvoiceFilters extends QueryFilters
return $this->builder;
}
public function client_id(string $client_id = '') :Builder
{
if (strlen($client_id) == 0) {
return $this->builder;
}
$this->builder->where('client_id', $this->decodePrimaryKey($client_id));
return $this->builder;
}
public function number(string $number) :Builder
{
return $this->builder->where('number', $number);

View File

@ -12,6 +12,7 @@
namespace App\Filters;
//use Illuminate\Database\Query\Builder;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
@ -20,6 +21,8 @@ use Illuminate\Http\Request;
*/
abstract class QueryFilters
{
use MakesHash;
/**
* active status.
*/
@ -177,6 +180,18 @@ abstract class QueryFilters
}
public function client_id(string $client_id = '') :Builder
{
if (strlen($client_id) == 0) {
return $this->builder;
}
$this->builder->where('client_id', $this->decodePrimaryKey($client_id));
return $this->builder;
}
public function filter_deleted_clients($value)
{

View File

@ -34,11 +34,6 @@ class SystemLogFilters extends QueryFilters
return $this->builder->where('event_id', $event_id);
}
public function client_id(int $client_id) :Builder
{
return $this->builder->where('client_id', $client_id);
}
/**
* Filter based on search text.
*

View File

@ -30,7 +30,7 @@ class InvoiceSum
public $invoice_item;
public $total_taxes;
public $total_taxes = 0;
private $total;

View File

@ -15,6 +15,7 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\Contact\ContactPasswordResetRequest;
use App\Libraries\MultiDB;
use App\Models\Account;
use App\Utils\Ninja;
use Illuminate\Contracts\View\Factory;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Http\Request;
@ -56,11 +57,13 @@ class ContactForgotPasswordController extends Controller
{
$account_id = $request->get('account_id');
$account = Account::find($account_id);
$company = $account->companies->first();
return $this->render('auth.passwords.request', [
'title' => 'Client Password Reset',
'passwordEmailRoute' => 'client.password.email',
'account' => $account
'account' => $account,
'company' => $company
]);
}
@ -76,7 +79,11 @@ class ContactForgotPasswordController extends Controller
public function sendResetLinkEmail(ContactPasswordResetRequest $request)
{
$user = MultiDB::hasContact($request->input('email'));
if(Ninja::isHosted() && $request->has('db'))
MultiDB::setDb($request->input('db'));
// $user = MultiDB::hasContact($request->input('email'));
$this->validateEmail($request);

View File

@ -13,6 +13,7 @@ namespace App\Http\Controllers\Auth;
use App\Events\Contact\ContactLoggedIn;
use App\Http\Controllers\Controller;
use App\Libraries\MultiDB;
use App\Models\Account;
use App\Models\ClientContact;
use App\Models\Company;
@ -36,12 +37,22 @@ class ContactLoginController extends Controller
public function showLoginForm(Request $request)
{
//if we are on the root domain invoicing.co do not show any company logos
if(Ninja::isHosted() && count(explode('.', request()->getHost())) == 2){
$company = null;
}elseif (strpos($request->getHost(), 'invoicing.co') !== false) {
// if(Ninja::isHosted() && count(explode('.', request()->getHost())) == 2){
// $company = null;
// }else
if (strpos($request->getHost(), 'invoicing.co') !== false) {
$subdomain = explode('.', $request->getHost())[0];
MultiDB::findAndSetDbByDomain(['subdomain' => $subdomain]);
$company = Company::where('subdomain', $subdomain)->first();
} elseif(Ninja::isHosted() && $company = Company::where('portal_domain', $request->getSchemeAndHttpHost())->first()){
} elseif(Ninja::isHosted()){
MultiDB::findAndSetDbByDomain(['portal_domain' => $request->getSchemeAndHttpHost()]);
$company = Company::where('portal_domain', $request->getSchemeAndHttpHost())->first();
}
elseif (Ninja::isSelfHost()) {
@ -61,6 +72,9 @@ class ContactLoginController extends Controller
{
Auth::shouldUse('contact');
if(Ninja::isHosted() && $request->has('db'))
MultiDB::setDb($request->input('db'));
$this->validateLogin($request);
// If the class is using the ThrottlesLogins trait, we can automatically throttle
// the login attempts for this application. We'll key this by the username and

View File

@ -12,6 +12,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\ResetsPasswords;
@ -65,14 +66,18 @@ class ContactResetPasswordController extends Controller
{
$account_id = $request->get('account_id');
$account = Account::find($account_id);
$db = $account->companies->first()->db;
return $this->render('auth.passwords.reset')->with(
['token' => $token, 'email' => $request->email, 'account' => $account]
['token' => $token, 'email' => $request->email, 'account' => $account, 'db' => $db]
);
}
public function reset(Request $request)
{
if($request->has('db'))
MultiDB::setDb($request->input('db'));
$request->validate($this->rules(), $this->validationErrorMessages());
// Here we will attempt to reset the user's password. If it is successful we

View File

@ -44,61 +44,6 @@ class ForgotPasswordController extends Controller
}
/**
* Password Reset.
*
*
* @OA\Post(
* path="/api/v1/reset_password",
* operationId="reset_password",
* tags={"reset_password"},
* summary="Attempts to reset the users password",
* description="Resets a users email password",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\RequestBody(
* description="Password reset email",
* required=true,
* @OA\MediaType(
* mediaType="application/json",
* @OA\Schema(
* type="object",
* @OA\Property(
* property="email",
* description="The user email address",
* type="string",
* )
* )
* )
* ),
* @OA\Response(
* response=201,
* description="The Reset response",
* @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(
* @OA\Items(
* type="string",
* example="Reset link send to your email.",
* )
* ),
* ),
* @OA\Response(
* response=401,
* description="Validation error",
* @OA\JsonContent(
* @OA\Items(
* type="string",
* example="Unable to send password reset link",
* ),
* ),
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
* @param Request $request
* @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse
* @throws \Illuminate\Validation\ValidationException

View File

@ -221,13 +221,29 @@ class LoginController extends BaseController
return response()->json(['message' => 'User not linked to any companies'], 403);
/* Ensure the user has a valid token */
$user->company_users->each(function ($company_user) use($request){
if($user->company_users()->count() != $user->tokens()->count())
{
$user->companies->each(function($company) use($user, $request){
if(!CompanyToken::where('user_id', $user->id)->where('company_id', $company->id)->exists()){
CreateCompanyToken::dispatchNow($company, $user, $request->server('HTTP_USER_AGENT'));
if($company_user->tokens->count() == 0){
CreateCompanyToken::dispatchNow($company_user->company, $company_user->user, $request->server('HTTP_USER_AGENT'));
}
});
});
}
//method above override this
// $user->company_users->each(function ($company_user) use($request){
// if($company_user->tokens->count() == 0){
// CreateCompanyToken::dispatchNow($company_user->company, $company_user->user, $request->server('HTTP_USER_AGENT'));
// }
// });
/*On the hosted platform, only owners can login for free/pro accounts*/
if(Ninja::isHosted() && !$cu->first()->is_owner && !$user->account->isEnterpriseClient())

View File

@ -107,13 +107,16 @@ class BaseController extends Controller
'user.company_user',
'token',
'company.activities',
'company.documents',
'company.users.company_user',
'company.tax_rates',
'company.groups',
'company.documents',
'company.company_gateways.gateway',
'company.users.company_user',
'company.task_statuses',
'company.payment_terms',
'company.groups',
'company.designs.company',
'company.expense_categories',
'company.subscriptions',
];
public function __construct()
@ -213,7 +216,7 @@ class BaseController extends Controller
$query->with(
[
'company' => function ($query) use ($updated_at, $user) {
$query->whereNotNull('updated_at')->with('documents');
$query->whereNotNull('updated_at')->with('documents')->with('users');
},
'company.clients' => function ($query) use ($updated_at, $user) {
$query->where('clients.updated_at', '>=', $updated_at)->with('contacts.company', 'gateway_tokens', 'documents');
@ -252,7 +255,7 @@ class BaseController extends Controller
$query->where('expenses.user_id', $user->id)->orWhere('expenses.assigned_user_id', $user->id);
},
'company.groups' => function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at);
$query->where('updated_at', '>=', $updated_at)->with('documents');
if(!$user->isAdmin())
$query->where('group_settings.user_id', $user->id);
@ -300,7 +303,7 @@ class BaseController extends Controller
},
'company.recurring_invoices'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('invitations', 'documents');
$query->where('updated_at', '>=', $updated_at)->with('invitations', 'documents', 'client.gateway_tokens', 'client.group_settings', 'client.company');
if(!$user->hasPermission('view_recurring_invoice'))
$query->where('recurring_invoices.user_id', $user->id)->orWhere('recurring_invoices.assigned_user_id', $user->id);
@ -320,8 +323,8 @@ class BaseController extends Controller
$query->where('tasks.user_id', $user->id)->orWhere('tasks.assigned_user_id', $user->id);
},
'company.tax_rates' => function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at);
'company.tax_rates'=> function ($query) use ($updated_at, $user) {
$query->whereNotNull('updated_at');
},
'company.vendors'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at)->with('contacts', 'documents');
@ -334,7 +337,7 @@ class BaseController extends Controller
$query->where('updated_at', '>=', $updated_at);
},
'company.task_statuses'=> function ($query) use ($updated_at, $user) {
$query->where('updated_at', '>=', $updated_at);
$query->whereNotNull('updated_at');
},
'company.activities'=> function ($query) use($user) {
@ -396,16 +399,16 @@ class BaseController extends Controller
'company.documents'=> function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at);
},
'company.groups' => function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at);
'company.groups'=> function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at)->with('documents');
},
'company.payment_terms'=> function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at);
},
'company.tax_rates' => function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at);
'company.tax_rates'=> function ($query) use ($created_at, $user) {
$query->whereNotNull('created_at');
},
'company.activities'=> function ($query) use($user) {
@ -484,12 +487,6 @@ class BaseController extends Controller
$query->where('credits.user_id', $user->id)->orWhere('credits.assigned_user_id', $user->id);
},
// 'company.designs'=> function ($query) use ($created_at, $user) {
// $query->where('created_at', '>=', $created_at)->with('company');
// if(!$user->isAdmin())
// $query->where('designs.user_id', $user->id);
// },
'company.documents'=> function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at);
},
@ -500,7 +497,7 @@ class BaseController extends Controller
$query->where('expenses.user_id', $user->id)->orWhere('expenses.assigned_user_id', $user->id);
},
'company.groups' => function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at);
$query->where('created_at', '>=', $created_at)->with('documents');
if(!$user->isAdmin())
$query->where('group_settings.user_id', $user->id);
@ -546,7 +543,7 @@ class BaseController extends Controller
},
'company.recurring_invoices'=> function ($query) use ($created_at, $user) {
$query->where('created_at', '>=', $created_at)->with('invitations', 'documents');
$query->where('created_at', '>=', $created_at)->with('invitations', 'documents', 'client.gateway_tokens', 'client.group_settings', 'client.company');
if(!$user->hasPermission('view_recurring_invoice'))
$query->where('recurring_invoices.user_id', $user->id)->orWhere('recurring_invoices.assigned_user_id', $user->id);
@ -777,7 +774,13 @@ class BaseController extends Controller
return 'main.last.dart.js';
case 'next':
return 'main.next.dart.js';
case 'profile':
return 'main.profile.dart.js';
default:
if(Ninja::isSelfHost())
return 'main.foss.dart.js';
return 'main.dart.js';
}

View File

@ -521,16 +521,6 @@ class ClientController extends BaseController
return $this->listResponse(Client::withTrashed()->whereIn('id', $this->transformKeys($ids)));
}
/**
* Returns a client statement.
*
* @return void [type] [description]
*/
public function statement()
{
//todo
}
/**
* Update the specified resource in storage.
*
@ -595,64 +585,5 @@ 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"),
* ),
* )
*/
//@deprecated - not available
public function adjustLedger(AdjustClientLedgerRequest $request, Client $client)
{
// $adjustment = $request->input('adjustment');
// $notes = $request->input('notes');
// $client->service()->updateBalance
}
}

View File

@ -79,7 +79,7 @@ class DocumentController extends Controller
$zip = new ZipStream(now() . '-documents.zip', $options);
foreach ($documents as $document) {
$zip->addFileFromPath(basename($document->diskPath()), TempFile::path($document->diskPath()));
$zip->addFileFromPath(basename($document->diskPath()), TempFile::path($document->filePath()));
}
$zip->finish();

View File

@ -16,6 +16,9 @@ use App\Events\Invoice\InvoiceWasViewed;
use App\Events\Misc\InvitationWasViewed;
use App\Events\Quote\QuoteWasViewed;
use App\Http\Controllers\Controller;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Payment;
use App\Utils\Ninja;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
@ -43,6 +46,21 @@ class InvitationController extends Controller
return $this->genericRouter('recurring_invoice', $invitation_key);
}
public function invoiceRouter(string $invitation_key)
{
return $this->genericRouter('invoice', $invitation_key);
}
public function quoteRouter(string $invitation_key)
{
return $this->genericRouter('quote', $invitation_key);
}
public function creditRouter(string $invitation_key)
{
return $this->genericRouter('credit', $invitation_key);
}
private function genericRouter(string $entity, string $invitation_key)
{
@ -113,4 +131,18 @@ class InvitationController extends Controller
public function routerForIframe(string $entity, string $client_hash, string $invitation_key)
{
}
public function paymentRouter(string $contact_key, string $payment_id)
{
$contact = ClientContact::where('contact_key', $contact_key)->firstOrFail();
$payment = Payment::find($this->decodePrimaryKey($payment_id));
if($payment->client_id != $contact->client_id)
abort(403, 'You are not authorized to view this resource');
auth()->guard('contact')->login($contact, true);
return redirect()->route('client.payments.show', $payment->hashed_id);
}
}

View File

@ -20,7 +20,9 @@ use App\Utils\Number;
use App\Utils\TempFile;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\RedirectResponse;
use Illuminate\View\View;
use ZipStream\Option\Archive;
use ZipStream\ZipStream;
@ -86,6 +88,10 @@ class InvoiceController extends Controller
->with('message', ctrans('texts.no_action_provided'));
}
/**
* @param array $ids
* @return Factory|View|RedirectResponse
*/
private function makePayment(array $ids)
{
$invoices = Invoice::whereIn('id', $ids)
@ -119,8 +125,8 @@ class InvoiceController extends Controller
//format data
$invoices->map(function ($invoice) {
$invoice->service()->removeUnpaidGatewayFees()->save();
$invoice->balance = Number::formatValue($invoice->balance, $invoice->client->currency());
$invoice->partial = Number::formatValue($invoice->partial, $invoice->client->currency());
$invoice->balance = $invoice->balance > 0 ? Number::formatValue($invoice->balance, $invoice->client->currency()) : 0;
$invoice->partial = $invoice->partial > 0 ? Number::formatValue($invoice->partial, $invoice->client->currency()) : 0;
return $invoice;
});

View File

@ -0,0 +1,56 @@
<?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://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers\ClientPortal;
use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\Uploads\StoreUploadRequest;
use App\Libraries\MultiDB;
use App\Models\ClientContact;
use App\Models\Company;
use App\Utils\Ninja;
use Illuminate\Contracts\Routing\ResponseFactory;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
class NinjaPlanController extends Controller
{
public function index(string $contact_key, string $company_key)
{
MultiDB::findAndSetDbByCompanyKey($company_key);
$company = Company::where('company_key', $company_key)->first();
nlog("Ninja Plan Controller Company key found {$company->company_key}");
$account = $company->account;
if (MultiDB::findAndSetDbByContactKey($contact_key) && $client_contact = ClientContact::where('contact_key', $contact_key)->first())
{
nlog("Ninja Plan Controller - Found and set Client Contact");
Auth::guard('contact')->login($client_contact,true);
/* Current paid users get pushed straight to subscription overview page*/
if($account->isPaidHostedClient())
return redirect('/client/subscriptions');
/* Users that are not paid get pushed to a custom purchase page */
return $this->render('subscriptions.ninja_plan', ['settings' => $client_contact->company->settings]);
}
return redirect()->route('client.catchall');
}
}

View File

@ -11,31 +11,197 @@
namespace App\Http\Controllers;
/**
* Class ClientStatementController.
*/
use App\Http\Requests\Statements\CreateStatementRequest;
use App\Models\Design;
use App\Models\InvoiceInvitation;
use App\Services\PdfMaker\Design as PdfDesignModel;
use App\Services\PdfMaker\Design as PdfMakerDesign;
use App\Services\PdfMaker\PdfMaker as PdfMakerService;
use App\Utils\HostedPDF\NinjaPdf;
use App\Utils\HtmlEngine;
use App\Utils\PhantomJS\Phantom;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\Pdf\PdfMaker;
class ClientStatementController extends BaseController
{
use MakesHash, PdfMaker;
/** @var \App\Models\Invoice|\App\Models\Payment */
protected $entity;
public function __construct()
{
parent::__construct();
}
/**
* Displays a client statement view for a given
* client_id.
* @return void
* Update the specified resource in storage.
*
* @param CreateStatementRequest $request
* @return Response
*
* @OA\Post(
* path="/api/v1/client_statement",
* operationId="clientStatement",
* tags={"clients"},
* summary="Return a PDF of the client statement",
* description="Return a PDF of the client statement",
* @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\RequestBody(
* description="Statment Options",
* required=true,
* @OA\MediaType(
* mediaType="application/json",
* @OA\Schema(
* type="object",
* @OA\Property(
* property="start_date",
* description="The start date of the statement period - format Y-m-d",
* type="string",
* ),
* @OA\Property(
* property="end_date",
* description="The start date of the statement period - format Y-m-d",
* type="string",
* ),
* @OA\Property(
* property="client_id",
* description="The hashed ID of the client",
* type="string",
* ),
* @OA\Property(
* property="show_payments_table",
* description="Flag which determines if the payments table is shown",
* type="boolean",
* ),
* @OA\Property(
* property="show_aging_table",
* description="Flag which determines if the aging table is shown",
* type="boolean",
* )
* )
* )
* ),
* @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 show()
public function statement(CreateStatementRequest $request)
{
$pdf = $this->createStatement($request);
if ($pdf) {
return response()->streamDownload(function () use ($pdf) {
echo $pdf;
}, 'statement.pdf', ['Content-Type' => 'application/pdf']);
}
return response()->json(['message' => 'Something went wrong. Please check logs.']);
}
/**
* Updates the show view data dependent on
* configured variables.
* @return void
*/
public function update()
protected function createStatement(CreateStatementRequest $request): ?string
{
$invitation = false;
if ($request->getInvoices()->count() >= 1) {
$this->entity = $request->getInvoices()->first();
$invitation = $this->entity->invitations->first();
}
else if ($request->getPayments()->count() >= 1) {
$this->entity = $request->getPayments()->first()->invoices->first()->invitations->first();
$invitation = $this->entity->invitations->first();
}
$entity_design_id = 1;
$entity_design_id = $this->entity->design_id
? $this->entity->design_id
: $this->decodePrimaryKey($this->entity->client->getSetting('invoice_design_id'));
$design = Design::find($entity_design_id);
if (!$design) {
$design = Design::find($entity_design_id);
}
$html = new HtmlEngine($invitation);
$options = [
'start_date' => $request->start_date,
'end_date' => $request->end_date,
'show_payments_table' => $request->show_payments_table,
'show_aging_table' => $request->show_aging_table,
];
if ($design->is_custom) {
$options['custom_partials'] = \json_decode(\json_encode($design->design), true);
$template = new PdfMakerDesign(PdfDesignModel::CUSTOM, $options);
} else {
$template = new PdfMakerDesign(strtolower($design->name), $options);
}
$variables = $html->generateLabelsAndValues();
$state = [
'template' => $template->elements([
'client' => $this->entity->client,
'entity' => $this->entity,
'pdf_variables' => (array)$this->entity->company->settings->pdf_variables,
'$product' => $design->design->product,
'variables' => $variables,
'invoices' => $request->getInvoices(),
'payments' => $request->getPayments(),
'aging' => $request->getAging(),
], \App\Services\PdfMaker\Design::STATEMENT),
'variables' => $variables,
'options' => [],
'process_markdown' => $this->entity->client->company->markdown_enabled,
];
$maker = new PdfMakerService($state);
$maker
->design($template)
->build();
$pdf = null;
try {
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
$pdf = (new Phantom)->convertHtmlToPdf($maker->getCompiledHTML(true));
}
else if (config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja') {
$pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true));
} else {
$pdf = $this->makePdf(null, null, $maker->getCompiledHTML(true));
}
} catch (\Exception $e) {
nlog(print_r($e->getMessage(), 1));
}
return $pdf;
}
}

View File

@ -15,6 +15,7 @@ use App\DataMapper\Analytics\AccountDeleted;
use App\DataMapper\CompanySettings;
use App\DataMapper\DefaultSettings;
use App\Http\Requests\Company\CreateCompanyRequest;
use App\Http\Requests\Company\DefaultCompanyRequest;
use App\Http\Requests\Company\DestroyCompanyRequest;
use App\Http\Requests\Company\EditCompanyRequest;
use App\Http\Requests\Company\ShowCompanyRequest;
@ -69,9 +70,13 @@ class CompanyController extends BaseController
*/
public function __construct(CompanyRepository $company_repo)
{
parent::__construct();
$this->company_repo = $company_repo;
$this->middleware('password_protected')->only(['destroy']);
}
/**
@ -594,8 +599,66 @@ class CompanyController extends BaseController
}
// public function default(DefaultCompanyRequest $request, Company $company)
// {
/**
* Update the specified resource in storage.
*
* @param UploadCompanyRequest $request
* @param Company $client
* @return Response
*
*
*
* @OA\Post(
* path="/api/v1/companies/{company}/default",
* operationId="setDefaultCompany",
* tags={"companies"},
* summary="Sets the company as the default company.",
* description="Sets the company as the default company.",
* @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="company",
* in="path",
* description="The Company Hashed ID",
* example="D2J234DFA",
* required=true,
* @OA\Schema(
* type="string",
* format="string",
* ),
* ),
* @OA\Response(
* response=200,
* description="Returns the company 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/Company"),
* ),
* @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 default(DefaultCompanyRequest $request, Company $company)
{
$account = $company->account;
$account->default_company_id = $company->id;
$account->save();
return $this->itemResponse($company->fresh());
}
// }
}

View File

@ -127,12 +127,11 @@ class EmailController extends BaseController
$entity_obj->invitations->each(function ($invitation) use ($data, $entity_string, $entity_obj, $template) {
if ($invitation->contact->send_email && $invitation->contact->email) {
if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) {
$entity_obj->service()->markSent()->save();
EmailEntity::dispatch($invitation->fresh(), $invitation->company, $template, $data);
// ->delay(now()->addSeconds(45));
}

View File

@ -66,7 +66,8 @@ class ImportJsonController extends BaseController
$file_location = $request->file('files')
->storeAs(
'migrations',
$request->file('files')->getClientOriginalName()
$request->file('files')->getClientOriginalName(),
config('filesystems.default'),
);
if(Ninja::isHosted())

View File

@ -25,6 +25,7 @@ use App\Utils\Ninja;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\App;
class MigrationController extends BaseController
{
@ -263,6 +264,10 @@ class MigrationController extends BaseController
// Look for possible existing company (based on company keys).
$existing_company = Company::whereRaw('BINARY `company_key` = ?', [$company->company_key])->first();
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations($user->account->companies()->first()->settings));
if(!$existing_company && $company_count >=10) {
$nmo = new NinjaMailerObject;

View File

@ -213,7 +213,7 @@ class PostMarkController extends BaseController
$request->input('MessageID')
);
LightLogs::create($bounce)->batch();
LightLogs::create($spam)->batch();
SystemLogger::dispatch($request->all(), SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_SPAM_COMPLAINT, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company);
}

View File

@ -73,55 +73,69 @@ class SetupController extends Controller
return response('Oops, something went wrong. Check your logs.'); /* We should never reach this block, but just in case. */
}
try {
$db = SystemHealth::dbCheck($request);
// try {
// $db = SystemHealth::dbCheck($request);
if ($db['success'] == false) {
throw new Exception($db['message']);
}
} catch (Exception $e) {
return response([
'message' => 'Oops, connection to database was not successful.',
'error' => $e->getMessage(),
]);
}
// if ($db['success'] == false) {
// throw new Exception($db['message']);
// }
// } catch (Exception $e) {
// return response([
// 'message' => 'Oops, connection to database was not successful.',
// 'error' => $e->getMessage(),
// ]);
// }
try {
if ($request->mail_driver != 'log') {
$smtp = SystemHealth::testMailServer($request);
// try {
// if ($request->mail_driver != 'log') {
// $smtp = SystemHealth::testMailServer($request);
if ($smtp['success'] == false) {
throw new Exception($smtp['message']);
}
}
} catch (Exception $e) {
return response([
'message' => 'Oops, connection to mail server was not successful.',
'error' => $e->getMessage(),
]);
}
// if ($smtp['success'] == false) {
// throw new Exception($smtp['message']);
// }
// }
// } catch (Exception $e) {
// return response([
// 'message' => 'Oops, connection to mail server was not successful.',
// 'error' => $e->getMessage(),
// ]);
// }
$mail_driver = $request->input('mail_driver');
$url = $request->input('url');
$db_host = $request->input('db_host');
$db_port = $request->input('db_port');
$db_database = $request->input('db_database');
$db_username = $request->input('db_username');
$db_password = $request->input('db_password');
$mail_port = $request->input('mail_port');
$encryption = $request->input('encryption');
$mail_host = $request->input('mail_host');
$mail_username = $request->input('mail_username');
$mail_name = $request->input('mail_name');
$mail_address = $request->input('mail_address');
$mail_password = $request->input('mail_password');
$env_values = [
'APP_URL' => $request->input('url'),
'APP_URL' => $url,
'REQUIRE_HTTPS' => $request->input('https') ? 'true' : 'false',
'APP_DEBUG' => 'false',
'DB_HOST' => $request->input('db_host'),
'DB_PORT' => $request->input('db_port'),
'DB_DATABASE' => $request->input('db_database'),
'DB_USERNAME' => $request->input('db_username'),
'DB_PASSWORD' => $request->input('db_password'),
'DB_HOST' => $db_host,
'DB_PORT' => $db_port,
'DB_DATABASE' => $db_database,
'DB_USERNAME' => $db_username,
'DB_PASSWORD' => $db_password,
'MAIL_MAILER' => $mail_driver,
'MAIL_PORT' => $request->input('mail_port'),
'MAIL_ENCRYPTION' => $request->input('encryption'),
'MAIL_HOST' => $request->input('mail_host'),
'MAIL_USERNAME' => $request->input('mail_username'),
'MAIL_FROM_NAME' => $request->input('mail_name'),
'MAIL_FROM_ADDRESS' => $request->input('mail_address'),
'MAIL_PASSWORD' => $request->input('mail_password'),
'MAIL_PORT' => $mail_port,
'MAIL_ENCRYPTION' => $encryption,
'MAIL_HOST' => $mail_host,
'MAIL_USERNAME' => $mail_username,
'MAIL_FROM_NAME' => $mail_name,
'MAIL_FROM_ADDRESS' => $mail_address,
'MAIL_PASSWORD' => $mail_password,
'NINJA_ENVIRONMENT' => 'selfhost',
'DB_CONNECTION' => 'mysql',
@ -150,6 +164,7 @@ class SetupController extends Controller
/* Make sure no stale connections are cached */
DB::purge('db-ninja-01');
//DB::reconnect('db-ninja-01');
/* Run migrations */
if (!config('ninja.disable_auto_update')) {

View File

@ -12,6 +12,7 @@
namespace App\Http\Controllers;
use App\DataMapper\FeesAndLimits;
use App\Exceptions\SystemError;
use App\Factory\CompanyGatewayFactory;
use App\Http\Requests\StripeConnect\InitializeStripeConnectRequest;
use App\Libraries\MultiDB;
@ -20,6 +21,7 @@ use App\Models\Company;
use App\Models\CompanyGateway;
use App\Models\GatewayType;
use App\PaymentDrivers\Stripe\Connect\Account;
use Exception;
use Illuminate\Http\Request;
use Stripe\Exception\ApiErrorException;
@ -78,7 +80,7 @@ class StripeConnectController extends BaseController
{
nlog($e->getMessage());
throw new SystemError($e->getMessage(), 500);
}
MultiDB::findAndSetDbByCompanyKey($request->getTokenContent()['company_key']);

View File

@ -14,6 +14,7 @@ namespace App\Http\Controllers;
use App\Jobs\Util\ImportStripeCustomers;
use App\Jobs\Util\StripeUpdatePaymentMethods;
use App\Libraries\MultiDB;
use App\Models\Client;
use App\Models\CompanyGateway;
@ -60,6 +61,8 @@ class StripeController extends BaseController
if(auth()->user()->isAdmin())
{
MultiDB::findAndSetDbByCompanyKey(auth()->user()->company()->company_key);
$company_gateway = CompanyGateway::where('company_id', auth()->user()->company()->id)
->where('is_deleted',0)
->whereIn('gateway_key', $this->stripe_keys)

View File

@ -44,6 +44,7 @@ class CreditsTable extends Component
->orWhereNull('due_date');
})
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->withTrashed()
->paginate($this->per_page);
return render('components.livewire.credits-table', [

View File

@ -14,6 +14,15 @@ namespace App\Http\Livewire;
use App\Libraries\MultiDB;
use App\Models\Client;
use App\Models\Credit;
use App\Models\Document;
use App\Models\Expense;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\Project;
use App\Models\Quote;
use App\Models\RecurringInvoice;
use App\Models\Task;
use App\Utils\Traits\WithSorting;
use Livewire\Component;
use Livewire\WithPagination;
@ -28,23 +37,142 @@ class DocumentsTable extends Component
public $company;
public string $tab = 'documents';
protected $query;
public function mount($client)
{
MultiDB::setDb($this->company->db);
$this->client = $client;
$this->query = $this->documents();
}
public function render()
{
$query = $this->client
->documents()
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->paginate($this->per_page);
return render('components.livewire.documents-table', [
'documents' => $query,
'documents' => $this->query
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->withTrashed()
->paginate($this->per_page),
]);
}
public function updateResources(string $resource)
{
$this->tab = $resource;
switch ($resource) {
case 'documents':
$this->query = $this->documents();
break;
case 'credits':
$this->query = $this->credits();
break;
case 'expenses':
$this->query = $this->expenses();
break;
case 'invoices':
$this->query = $this->invoices();
break;
case 'payments':
$this->query = $this->payments();
break;
case 'projects':
$this->query = $this->projects();
break;
case 'quotes':
$this->query = $this->quotes();
break;
case 'recurringInvoices':
$this->query = $this->recurringInvoices();
break;
case 'tasks':
$this->query = $this->tasks();
break;
default:
$this->query = $this->documents();
break;
}
}
protected function documents()
{
return $this->client->documents();
}
protected function credits()
{
return Document::query()
->whereHasMorph('documentable', [Credit::class], function ($query) {
$query->where('client_id', $this->client->id);
});
}
protected function expenses()
{
return Document::query()
->whereHasMorph('documentable', [Expense::class], function ($query) {
$query->where('client_id', $this->client->id);
});
}
protected function invoices()
{
return Document::query()
->whereHasMorph('documentable', [Invoice::class], function ($query) {
$query->where('client_id', $this->client->id);
});
}
protected function payments()
{
return Document::query()
->whereHasMorph('documentable', [Payment::class], function ($query) {
$query->where('client_id', $this->client->id);
});
}
protected function projects()
{
return Document::query()
->whereHasMorph('documentable', [Project::class], function ($query) {
$query->where('client_id', $this->client->id);
});
}
protected function quotes()
{
return Document::query()
->whereHasMorph('documentable', [Quote::class], function ($query) {
$query->where('client_id', $this->client->id);
});
}
protected function recurringInvoices()
{
return Document::query()
->whereHasMorph('documentable', [RecurringInvoice::class], function ($query) {
$query->where('client_id', $this->client->id);
});
}
protected function tasks()
{
return Document::query()
->whereHasMorph('documentable', [Task::class], function ($query) {
$query->where('client_id', $this->client->id);
});
}
}

View File

@ -76,6 +76,7 @@ class InvoicesTable extends Component
$query = $query
->where('client_id', auth('contact')->user()->client->id)
->where('status_id', '<>', Invoice::STATUS_DRAFT)
->where('status_id', '<>', Invoice::STATUS_CANCELLED)
->withTrashed()
->paginate($this->per_page);

View File

@ -37,6 +37,7 @@ class PaymentMethodsTable extends Component
->where('company_id', $this->company->id)
->where('client_id', $this->client->id)
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->withTrashed()
->paginate($this->per_page);
return render('components.livewire.payment-methods-table', [

View File

@ -44,6 +44,7 @@ class PaymentsTable extends Component
->where('company_id', $this->company->id)
->where('client_id', auth('contact')->user()->client->id)
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->withTrashed()
->paginate($this->per_page);
return render('components.livewire.payments-table', [

View File

@ -48,6 +48,7 @@ class QuotesTable extends Component
->where('company_id', $this->company->id)
->where('client_id', auth('contact')->user()->client->id)
->where('status_id', '<>', Quote::STATUS_DRAFT)
->withTrashed()
->paginate($this->per_page);
return render('components.livewire.quotes-table', [

View File

@ -21,7 +21,7 @@ class UpdateAutoBilling extends Component
public function updateAutoBilling(): void
{
if ($this->invoice->auto_bill === 'optin' || $this->invoice->auto_bill === 'optout') {
if ($this->invoice->auto_bill == 'optin' || $this->invoice->auto_bill == 'optout') {
$this->invoice->auto_bill_enabled = !$this->invoice->auto_bill_enabled;
$this->invoice->save();
}

View File

@ -46,6 +46,7 @@ class RecurringInvoicesTable extends Component
->orderBy('status_id', 'asc')
->with('client')
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->withTrashed()
->paginate($this->per_page);
return render('components.livewire.recurring-invoices-table', [

View File

@ -39,6 +39,7 @@ class SubscriptionRecurringInvoicesTable extends Component
->where('company_id', $this->company->id)
->whereNotNull('subscription_id')
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->withTrashed()
->paginate($this->per_page);
return render('components.livewire.subscriptions-recurring-invoices-table', [

View File

@ -48,6 +48,7 @@ class TasksTable extends Component
$query = $query
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->withTrashed()
->paginate($this->per_page);
return render('components.livewire.tasks-table', [

View File

@ -41,6 +41,10 @@ class ContactKeyLogin
if ($request->segment(2) && $request->segment(2) == 'magic_link' && $request->segment(3)) {
$payload = Cache::get($request->segment(3));
if(!$payload)
abort(403, 'Link expired.');
$contact_email = $payload['email'];
if($client_contact = ClientContact::where('email', $contact_email)->where('company_id', $payload['company_id'])->first()){
@ -58,13 +62,20 @@ class ContactKeyLogin
}
}
elseif ($request->segment(3) && config('ninja.db.multi_db_enabled')) {
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);
auth()->guard('contact')->login($client_contact, true);
if ($request->query('next')) {
return redirect()->to($request->query('next'));
}
return redirect()->to('client/dashboard');
}

View File

@ -52,7 +52,8 @@ class PasswordProtection
$x_api_password = base64_decode($request->header('X-API-PASSWORD-BASE64'));
}
if (Cache::get(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in')) {
// If no password supplied - then we just check if their authentication is in cache //
if (Cache::get(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in') && !$x_api_password) {
Cache::put(auth()->user()->hashed_id.'_'.auth()->user()->account_id.'_logged_in', Str::random(64), $timeout);

View File

@ -52,8 +52,12 @@ class QueryLogging
$timeEnd = microtime(true);
$time = $timeEnd - $timeStart;
// if($count > 150)
// nlog($queries);
// nlog("Query count = {$count}");
if($count > 175){
nlog("Query count = {$count}");
nlog($queries);
}
$ip = '';

View File

@ -74,7 +74,6 @@ class StoreClientRequest extends Request
$rules['number'] = ['nullable',Rule::unique('clients')->where('company_id', auth()->user()->company()->id)];
$rules['id_number'] = ['nullable',Rule::unique('clients')->where('company_id', auth()->user()->company()->id)];
return $rules;
}

View File

@ -0,0 +1,36 @@
<?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://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\Company;
use App\Http\Requests\Request;
class DefaultCompanyRequest 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()
{
$rules = [];
return $rules;
}
}

View File

@ -2,6 +2,7 @@
namespace App\Http\Requests\Gateways\Checkout3ds;
use App\Libraries\MultiDB;
use App\Models\Client;
use App\Models\Company;
use App\Models\CompanyGateway;
@ -37,6 +38,7 @@ class Checkout3dsRequest extends FormRequest
public function getCompany()
{
MultiDB::findAndSetDbByCompanyKey($this->company_key);
return Company::where('company_key', $this->company_key)->first();
}

View File

@ -101,8 +101,8 @@ class UpdateRecurringInvoiceRequest extends Request
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
}
if (isset($input['auto_bill'])) {
$input['auto_bill_enabled'] = $this->setAutoBillFlag($input['auto_bill']);
if (array_key_exists('auto_bill', $input) && isset($input['auto_bill']) && $this->setAutoBillFlag($input['auto_bill'])) {
$input['auto_bill_enabled'] = true;
}
if (array_key_exists('documents', $input)) {
@ -123,13 +123,8 @@ class UpdateRecurringInvoiceRequest extends Request
*/
private function setAutoBillFlag($auto_bill) :bool
{
if ($auto_bill == 'always') {
if ($auto_bill == 'always')
return true;
}
// if($auto_bill == '')
// off / optin / optout will reset the status of this field to off to allow
// the client to choose whether to auto_bill or not.
return false;
}

View File

@ -136,6 +136,10 @@ class Request extends FormRequest
if (isset($input['contacts']) && is_array($input['contacts'])) {
foreach ($input['contacts'] as $key => $contact) {
if(!is_array($contact))
continue;
if (array_key_exists('id', $contact) && is_numeric($contact['id'])) {
unset($input['contacts'][$key]['id']);
} elseif (array_key_exists('id', $contact) && is_string($contact['id'])) {
@ -154,6 +158,7 @@ class Request extends FormRequest
}
}
}
}
}

View File

@ -0,0 +1,169 @@
<?php
namespace App\Http\Requests\Statements;
use App\Http\Requests\Request;
use App\Models\Client;
use App\Models\Invoice;
use App\Models\Payment;
use App\Utils\Number;
use App\Utils\Traits\MakesHash;
use Carbon\Carbon;
class CreateStatementRequest extends Request
{
use MakesHash;
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return auth()->user()->isAdmin();
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'start_date' => 'required|date_format:Y-m-d',
'end_date' => 'required|date_format:Y-m-d',
'client_id' => 'bail|required|exists:clients,id,company_id,'.auth()->user()->company()->id,
'show_payments_table' => 'boolean',
'show_aging_table' => 'boolean',
];
}
protected function prepareForValidation()
{
$input = $this->all();
$input = $this->decodePrimaryKeys($input);
$this->replace($input);
}
/**
* The collection of invoices for the statement.
*
* @return Invoice[]|\Illuminate\Database\Eloquent\Collection
*/
public function getInvoices()
{
$input = $this->all();
// $input['start_date & $input['end_date are available.
$client = Client::where('id', $input['client_id'])->first();
$from = Carbon::parse($input['start_date']);
$to = Carbon::parse($input['end_date']);
return Invoice::where('company_id', auth()->user()->company()->id)
->where('client_id', $client->id)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL, Invoice::STATUS_PAID])
->whereBetween('date',[$from, $to])
->get();
}
/**
* The collection of payments for the statement.
*
* @return Payment[]|\Illuminate\Database\Eloquent\Collection
*/
public function getPayments()
{
// $input['start_date & $input['end_date are available.
$input = $this->all();
$client = Client::where('id', $input['client_id'])->first();
$from = Carbon::parse($input['start_date']);
$to = Carbon::parse($input['end_date']);
return Payment::where('company_id', auth()->user()->company()->id)
->where('client_id', $client->id)
->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment::STATUS_PARTIALLY_REFUNDED, Payment::STATUS_REFUNDED])
->whereBetween('date',[$from, $to])
->get();
}
/**
* The array of aging data.
*/
public function getAging(): array
{
return [
'0-30' => $this->getAgingAmount('30'),
'30-60' => $this->getAgingAmount('60'),
'60-90' => $this->getAgingAmount('90'),
'90-120' => $this->getAgingAmount('120'),
'120+' => $this->getAgingAmount('120+'),
];
}
private function getAgingAmount($range)
{
$input = $this->all();
$ranges = $this->calculateDateRanges($range);
$from = $ranges[0];
$to = $ranges[1];
$client = Client::where('id', $input['client_id'])->first();
$amount = Invoice::where('company_id', auth()->user()->company()->id)
->where('client_id', $client->id)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->where('balance', '>', 0)
->whereBetween('date',[$from, $to])
->sum('balance');
return Number::formatMoney($amount, $client);
}
private function calculateDateRanges($range)
{
$ranges = [];
switch ($range) {
case '30':
$ranges[0] = now();
$ranges[1] = now()->subDays(30);
return $ranges;
break;
case '60':
$ranges[0] = now()->subDays(30);
$ranges[1] = now()->subDays(60);
return $ranges;
break;
case '90':
$ranges[0] = now()->subDays(60);
$ranges[1] = now()->subDays(90);
return $ranges;
break;
case '120':
$ranges[0] = now()->subDays(90);
$ranges[1] = now()->subDays(120);
return $ranges;
break;
case '120+':
$ranges[0] = now()->subDays(120);
$ranges[1] = now()->subYears(40);
return $ranges;
break;
default:
$ranges[0] = now()->subDays(0);
$ranges[1] = now()->subDays(30);
return $ranges;
break;
}
}
}

View File

@ -60,6 +60,8 @@ class StoreUserRequest extends Request
//unique user rule - check company_user table for user_id / company_id / account_id if none exist we can add the user. ELSE return false
if(array_key_exists('email', $input))
$input['email'] = trim($input['email']);
if (isset($input['company_user'])) {
if (! isset($input['company_user']['is_admin'])) {

View File

@ -45,6 +45,8 @@ class UpdateUserRequest extends Request
{
$input = $this->all();
if(array_key_exists('email', $input))
$input['email'] = trim($input['email']);
$this->replace($input);
}

View File

@ -48,12 +48,13 @@ class PortalComposer
*/
public function compose(View $view) :void
{
$view->with($this->portalData());
if (auth()->user()) {
if (auth('contact')->user()) {
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations(auth()->user()->client->getMergedSettings()));
$t->replace(Ninja::transformTranslations(auth('contact')->user()->client->getMergedSettings()));
}
}
@ -62,23 +63,23 @@ class PortalComposer
*/
private function portalData() :array
{
if (! auth()->user()) {
if (! auth('contact')->user()) {
return [];
}
$this->settings = auth()->user()->client->getMergedSettings();
$this->settings = auth('contact')->user()->client->getMergedSettings();
$data['sidebar'] = $this->sidebarMenu();
$data['header'] = [];
$data['footer'] = [];
$data['countries'] = TranslationHelper::getCountries();
$data['company'] = auth()->user()->company;
$data['client'] = auth()->user()->client;
$data['company'] = auth('contact')->user()->company;
$data['client'] = auth('contact')->user()->client;
$data['settings'] = $this->settings;
$data['currencies'] = TranslationHelper::getCurrencies();
$data['contact'] = auth('contact')->user();
$data['multiple_contacts'] = session()->get('multiple_contacts');
$data['multiple_contacts'] = session()->get('multiple_contacts') ?: collect();
return $data;
}
@ -114,7 +115,7 @@ class PortalComposer
$data[] = ['title' => ctrans('texts.documents'), 'url' => 'client.documents.index', 'icon' => 'download'];
$data[] = ['title' => ctrans('texts.subscriptions'), 'url' => 'client.subscriptions.index', 'icon' => 'calendar'];
if (auth()->user('contact')->client->getSetting('enable_client_portal_tasks')) {
if (auth('contact')->user()->client->getSetting('enable_client_portal_tasks')) {
$data[] = ['title' => ctrans('texts.tasks'), 'url' => 'client.tasks.index', 'icon' => 'clock'];
}

View File

@ -62,7 +62,7 @@ class BaseTransformer
public function getClient($client_name, $client_email) {
$clients = $this->maps['company']->clients;
$clients = $clients->where( 'name', $client_name );
$clients = $clients->where( 'id_number', $client_name );
if ( $clients->count() >= 1 ) {
return $clients->first()->id;
@ -146,7 +146,7 @@ class BaseTransformer
$number = 0;
}
return Number::parseStringFloat($number);
return Number::parseFloat($number);
}
/**

View File

@ -52,10 +52,10 @@ class ClientTransformer extends BaseTransformer
'website' => $this->getString( $data, 'client.website' ),
'vat_number' => $this->getString( $data, 'client.vat_number' ),
'id_number' => $this->getString( $data, 'client.id_number' ),
'custom_value1' => $this->getString( $data, 'client.custom1' ),
'custom_value2' => $this->getString( $data, 'client.custom2' ),
'custom_value3' => $this->getString( $data, 'client.custom3' ),
'custom_value4' => $this->getString( $data, 'client.custom4' ),
'custom_value1' => $this->getString( $data, 'client.custom_value1' ),
'custom_value2' => $this->getString( $data, 'client.custom_value2' ),
'custom_value3' => $this->getString( $data, 'client.custom_value3' ),
'custom_value4' => $this->getString( $data, 'client.custom_value4' ),
'balance' => preg_replace( '/[^0-9,.]+/', '', $this->getFloat( $data, 'client.balance' ) ),
'paid_to_date' => preg_replace( '/[^0-9,.]+/', '', $this->getFloat( $data, 'client.paid_to_date' ) ),
'credit_balance' => 0,
@ -67,10 +67,10 @@ class ClientTransformer extends BaseTransformer
'last_name' => $this->getString( $data, 'contact.last_name' ),
'email' => $this->getString( $data, 'contact.email' ),
'phone' => $this->getString( $data, 'contact.phone' ),
'custom_value1' => $this->getString( $data, 'contact.custom1' ),
'custom_value2' => $this->getString( $data, 'contact.custom2' ),
'custom_value3' => $this->getString( $data, 'contact.custom3' ),
'custom_value4' => $this->getString( $data, 'contact.custom4' ),
'custom_value1' => $this->getString( $data, 'contact.custom_value1' ),
'custom_value2' => $this->getString( $data, 'contact.custom_value2' ),
'custom_value3' => $this->getString( $data, 'contact.custom_value3' ),
'custom_value4' => $this->getString( $data, 'contact.custom_value4' ),
],
],
'country_id' => isset( $data['client.country'] ) ? $this->getCountryId( $data['client.country']) : null,

View File

@ -25,10 +25,14 @@ class ExpenseTransformer extends BaseTransformer {
'date' => isset( $data['expense.date'] ) ? date( 'Y-m-d', strtotime( $data['expense.date'] ) ) : null,
'public_notes' => $this->getString( $data, 'expense.public_notes' ),
'private_notes' => $this->getString( $data, 'expense.private_notes' ),
'expense_category_id' => isset( $data['expense.category'] ) ? $this->getExpenseCategoryId( $data['expense.category'] ) : null,
'category_id' => isset( $data['expense.category'] ) ? $this->getExpenseCategoryId( $data['expense.category'] ) : null,
'project_id' => isset( $data['expense.project'] ) ? $this->getProjectId( $data['expense.project'] ) : null,
'payment_type_id' => isset( $data['expense.payment_type'] ) ? $this->getPaymentTypeId( $data['expense.payment_type'] ) : null,
'payment_date' => isset( $data['expense.payment_date'] ) ? date( 'Y-m-d', strtotime( $data['expense.payment_date'] ) ) : null,
'custom_value1' => $this->getString( $data, 'expense.custom_value1' ),
'custom_value2' => $this->getString( $data, 'expense.custom_value2' ),
'custom_value3' => $this->getString( $data, 'expense.custom_value3' ),
'custom_value4' => $this->getString( $data, 'expense.custom_value4' ),
'transaction_reference' => $this->getString( $data, 'expense.transaction_reference' ),
'should_be_invoiced' => $clientId ? true : false,
];

View File

@ -33,6 +33,10 @@ class VendorTransformer extends BaseTransformer {
'city' => $this->getString( $data, 'vendor.city' ),
'state' => $this->getString( $data, 'vendor.state' ),
'postal_code' => $this->getString( $data, 'vendor.postal_code' ),
'custom_value1' => $this->getString( $data, 'vendor.custom_value1' ),
'custom_value2' => $this->getString( $data, 'vendor.custom_value2' ),
'custom_value3' => $this->getString( $data, 'vendor.custom_value3' ),
'custom_value4' => $this->getString( $data, 'vendor.custom_value4' ),
'vendor_contacts' => [
[
'first_name' => $this->getString( $data, 'vendor.first_name' ),

View File

@ -42,7 +42,7 @@ class ClientTransformer extends BaseTransformer {
'work_phone' => $this->getString( $data, 'Phone' ),
'private_notes' => $this->getString( $data, 'Notes' ),
'website' => $this->getString( $data, 'Website' ),
'id_number' => $this->getString( $data, 'Customer ID'),
'address1' => $this->getString( $data, 'Billing Address' ),
'address2' => $this->getString( $data, 'Billing Street2' ),
'city' => $this->getString( $data, 'Billing City' ),

View File

@ -38,7 +38,7 @@ class InvoiceTransformer extends BaseTransformer {
$transformed = [
'company_id' => $this->maps['company']->id,
'client_id' => $this->getClient( $this->getString( $invoice_data, 'Company Name' ), null ),
'client_id' => $this->getClient( $this->getString( $invoice_data, 'Customer ID' ), null ),
'number' => $this->getString( $invoice_data, 'Invoice Number' ),
'date' => isset( $invoice_data['Invoice Date'] ) ? date( 'Y-m-d', strtotime( $invoice_data['Invoice Date'] ) ) : null,
'due_date' => isset( $invoice_data['Due Date'] ) ? date( 'Y-m-d', strtotime( $invoice_data['Due Date'] ) ) : null,
@ -59,7 +59,7 @@ class InvoiceTransformer extends BaseTransformer {
'notes' => $this->getString( $record, 'Item Description' ),
'cost' => $this->getFloat( $record, 'Item Price' ),
'quantity' => $this->getFloat( $record, 'Quantity' ),
'discount' => $this->getFloat( $record, 'Discount Amount' ),
'discount' => $this->getString( $record, 'Discount Amount' ),
'is_amount_discount' => true,
];
}
@ -67,7 +67,7 @@ class InvoiceTransformer extends BaseTransformer {
if ( $transformed['balance'] < $transformed['amount'] ) {
$transformed['payments'] = [[
'date' => date( 'Y-m-d' ),
'date' => isset( $invoice_data['Last Payment Date'] ) ? date( 'Y-m-d', strtotime( $invoice_data['Invoice Date'] ) ) : date( 'Y-m-d' ),
'amount' => $transformed['amount'] - $transformed['balance'],
]];
}
@ -75,3 +75,4 @@ class InvoiceTransformer extends BaseTransformer {
return $transformed;
}
}

View File

@ -36,6 +36,7 @@ use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;
use Turbo124\Beacon\Facades\LightLogs;
use Illuminate\Support\Facades\App;
class CreateAccount
{
@ -114,11 +115,22 @@ class CreateAccount
$spaa9f78->fresh();
//todo implement SLACK notifications
//$sp035a66->notification(new NewAccountCreated($spaa9f78, $sp035a66))->ninja();
if(Ninja::isHosted()){
nlog("welcome");
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations($sp035a66->settings));
$nmo = new NinjaMailerObject;
$nmo->mailable = new \Modules\Admin\Mail\Welcome($sp035a66->owner());
$nmo->company = $sp035a66;
$nmo->settings = $sp035a66->settings;
$nmo->to_user = $sp035a66->owner();
NinjaMailerJob::dispatch($nmo);
if(Ninja::isHosted())
\Modules\Admin\Jobs\Account\NinjaUser::dispatch([], $sp035a66);
}
VersionCheck::dispatch();
@ -126,6 +138,9 @@ class CreateAccount
->increment()
->batch();
return $sp794f3f;
}

View File

@ -35,6 +35,7 @@ use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
use ZipStream\Option\Archive;
use ZipStream\ZipStream;
use Illuminate\Support\Facades\App;
class CompanyExport implements ShouldQueue
{
@ -478,7 +479,7 @@ class CompanyExport implements ShouldQueue
private function zipAndSend()
{
$file_name = date('Y-m-d').'_'.str_replace(' ', '_', $this->company->present()->name() . '_' . $this->company->company_key .'.zip');
$file_name = date('Y-m-d').'_'.str_replace([" ", "/"],["_",""], $this->company->present()->name() . '_' . $this->company->company_key .'.zip');
$path = 'backups';
@ -497,8 +498,13 @@ class CompanyExport implements ShouldQueue
if(Ninja::isHosted()) {
Storage::disk(config('filesystems.default'))->put('backups/'.$file_name, file_get_contents($zip_path));
unlink($zip_path);
}
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations($this->company->settings));
$nmo = new NinjaMailerObject;
$nmo->mailable = new DownloadBackup(Storage::disk(config('filesystems.default'))->url('backups/'.$file_name), $this->company);
$nmo->to_user = $this->user;

View File

@ -224,7 +224,7 @@ class CompanyImport implements ShouldQueue
// if(mime_content_type(Storage::path($this->file_location)) == 'text/plain')
// return Storage::path($this->file_location);
$path = TempFile::filePath(Storage::get($this->file_location), basename($this->file_location));
$path = TempFile::filePath(Storage::disk(config('filesystems.default'))->get($this->file_location), basename($this->file_location));
$zip = new ZipArchive();
$archive = $zip->open($path);
@ -235,7 +235,7 @@ class CompanyImport implements ShouldQueue
$zip->close();
$file_location = "{$file_path}/backup.json";
if (! file_exists($file_location))
if (! file_exists($file_path))
throw new NonExistingMigrationFile('Backup file does not exist, or is corrupted.');
return $file_location;
@ -483,7 +483,7 @@ class CompanyImport implements ShouldQueue
{
$this->genericImport(Client::class,
['user_id', 'assigned_user_id', 'company_id', 'id', 'hashed_id', 'gateway_tokens', 'contacts', 'documents'],
['user_id', 'assigned_user_id', 'company_id', 'id', 'hashed_id', 'gateway_tokens', 'contacts', 'documents','country'],
[['users' => 'user_id'], ['users' => 'assigned_user_id']],
'clients',
'number');
@ -496,7 +496,7 @@ class CompanyImport implements ShouldQueue
{
$this->genericImport(ClientContact::class,
['user_id', 'company_id', 'id', 'hashed_id'],
['user_id', 'company_id', 'id', 'hashed_id','company'],
[['users' => 'user_id'], ['clients' => 'client_id']],
'client_contacts',
'email');
@ -568,7 +568,7 @@ class CompanyImport implements ShouldQueue
{
$this->genericImport(GroupSetting::class,
['user_id', 'company_id', 'id', 'hashed_id',],
['user_id', 'company_id', 'id', 'hashed_id'],
[['users' => 'user_id']],
'group_settings',
'name');
@ -580,7 +580,7 @@ class CompanyImport implements ShouldQueue
{
$this->genericImport(Subscription::class,
['user_id', 'assigned_user_id', 'company_id', 'id', 'hashed_id',],
['user_id', 'assigned_user_id', 'company_id', 'id', 'hashed_id'],
[['group_settings' => 'group_id'], ['users' => 'user_id'], ['users' => 'assigned_user_id']],
'subscriptions',
'name');
@ -875,7 +875,7 @@ class CompanyImport implements ShouldQueue
{
$this->genericImport(Design::class,
['company_id', 'user_id'],
['company_id', 'user_id', 'hashed_id'],
[
['users' => 'user_id'],
],
@ -984,6 +984,8 @@ class CompanyImport implements ShouldQueue
$cu_array = (array)$cu;
unset($cu_array['id']);
unset($cu_array['company_id']);
unset($cu_array['user_id']);
$new_cu = CompanyUser::firstOrNew(
['user_id' => $user_id, 'company_id' => $this->company->id],
@ -1102,6 +1104,18 @@ class CompanyImport implements ShouldQueue
unset($obj_array[$un]);
}
if($class instanceof CompanyGateway){
if(Ninja::isHosted() && $obj_array['gateway_key'] == 'd14dd26a37cecc30fdd65700bfb55b23'){
$obj_array['gateway_key'] = 'd14dd26a47cecc30fdd65700bfb67b34';
}
if(Ninja::isSelfHost() && $obj_array['gateway_key'] == 'd14dd26a47cecc30fdd65700bfb67b34'){
$obj_array['gateway_key'] = 'd14dd26a37cecc30fdd65700bfb55b23';
}
}
$activity_invitation_key = false;
if($class == 'App\Models\Activity'){
@ -1227,8 +1241,11 @@ class CompanyImport implements ShouldQueue
/* New to convert product ids from old hashes to new hashes*/
if($class == 'App\Models\Subscription'){
$obj_array['product_ids'] = $this->recordProductIds($obj_array['product_ids']);
$obj_array['recurring_product_ids'] = $this->recordProductIds($obj_array['recurring_product_ids']);
//$obj_array['product_ids'] = $this->recordProductIds($obj_array['product_ids']);
//$obj_array['recurring_product_ids'] = $this->recordProductIds($obj_array['recurring_product_ids']);
//
$obj_array['recurring_product_ids'] = '';
$obj_array['product_ids'] = '';
}
$new_obj = $class::firstOrNew(
@ -1258,6 +1275,12 @@ class CompanyImport implements ShouldQueue
foreach($id_array as $id) {
if(!$id)
continue;
$id = $this->decodePrimaryKey($id);
nlog($id);
$tmp_arr[] = $this->encodePrimaryKey($this->transformId('products', $id));
}

View File

@ -51,51 +51,79 @@ class AutoBillCron
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->where('auto_bill_enabled', true)
->where('balance', '>', 0)
->with('company')
->cursor()->each(function ($invoice){
$this->runAutoBiller($invoice);
->where('is_deleted', false)
->with('company');
nlog($auto_bill_partial_invoices->count(). " partial invoices to auto bill");
$auto_bill_partial_invoices->cursor()->each(function ($invoice){
$this->runAutoBiller($invoice, false);
});
$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);
->where('is_deleted', false)
->with('company');
nlog($auto_bill_invoices->count(). " full invoices to auto bill");
$auto_bill_invoices->cursor()->each(function ($invoice){
$this->runAutoBiller($invoice, false);
});
} 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);
->where('is_deleted', false)
->with('company');
nlog($auto_bill_partial_invoices->count(). " partial invoices to auto bill db = {$db}");
$auto_bill_partial_invoices->cursor()->each(function ($invoice) use($db){
$this->runAutoBiller($invoice, $db);
});
$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);
->where('is_deleted', false)
->with('company');
nlog($auto_bill_invoices->count(). " full invoices to auto bill db = {$db}");
$auto_bill_invoices->cursor()->each(function ($invoice) use($db){
$this->runAutoBiller($invoice, $db);
});
}
}
}
private function runAutoBiller(Invoice $invoice)
private function runAutoBiller(Invoice $invoice, $db)
{
info("Firing autobill for {$invoice->company_id} - {$invoice->number}");
$invoice->service()->autoBill()->save();
try{
if($db)
MultiDB::setDB($db);
$invoice->service()->autoBill()->save();
}
catch(\Exception $e) {
nlog("Failed to capture payment for {$invoice->company_id} - {$invoice->number} ->" . $e->getMessage());
}
}
}

View File

@ -46,6 +46,7 @@ class RecurringInvoicesCron
$recurring_invoices = RecurringInvoice::where('next_send_date', '<=', now()->toDateTimeString())
->whereNotNull('next_send_date')
->whereNull('deleted_at')
->where('is_deleted', false)
->where('status_id', RecurringInvoice::STATUS_ACTIVE)
->where('remaining_cycles', '!=', '0')
->whereHas('client', function ($query) {
@ -61,7 +62,13 @@ class RecurringInvoicesCron
nlog("Current date = " . now()->format("Y-m-d") . " Recurring date = " .$recurring_invoice->next_send_date);
if (!$recurring_invoice->company->is_disabled) {
SendRecurring::dispatchNow($recurring_invoice, $recurring_invoice->company->db);
try{
SendRecurring::dispatchNow($recurring_invoice, $recurring_invoice->company->db);
}
catch(\Exception $e){
nlog("Unable to sending recurring invoice {$recurring_invoice->id}");
}
}
});
} else {
@ -72,6 +79,7 @@ class RecurringInvoicesCron
$recurring_invoices = RecurringInvoice::where('next_send_date', '<=', now()->toDateTimeString())
->whereNotNull('next_send_date')
->whereNull('deleted_at')
->where('is_deleted', false)
->where('status_id', RecurringInvoice::STATUS_ACTIVE)
->where('remaining_cycles', '!=', '0')
->whereHas('client', function ($query) {
@ -81,13 +89,19 @@ class RecurringInvoicesCron
->with('company')
->cursor();
nlog(now()->format('Y-m-d') . ' Sending Recurring Invoices. Count = '.$recurring_invoices->count().' On Database # '.$db);
nlog(now()->format('Y-m-d') . ' Sending Recurring Invoices. Count = '.$recurring_invoices->count());
$recurring_invoices->each(function ($recurring_invoice, $key) {
nlog("Current date = " . now()->format("Y-m-d") . " Recurring date = " .$recurring_invoice->next_send_date ." Recurring #id = ". $recurring_invoice->id);
if (!$recurring_invoice->company->is_disabled) {
SendRecurring::dispatchNow($recurring_invoice, $recurring_invoice->company->db);
try{
SendRecurring::dispatchNow($recurring_invoice, $recurring_invoice->company->db);
}
catch(\Exception $e){
nlog("Unable to sending recurring invoice {$recurring_invoice->id}");
}
}
});
}

View File

@ -30,6 +30,7 @@ use App\Providers\MailServiceProvider;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use Dacastro4\LaravelGmail\Facade\LaravelGmail;
use GuzzleHttp\Exception\ClientException;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
@ -118,10 +119,31 @@ class NinjaMailerJob implements ShouldQueue
nlog("error failed with {$e->getMessage()}");
if($this->nmo->entity)
$this->entityEmailFailed($e->getMessage());
$message = $e->getMessage();
if(Ninja::isHosted())
/**
* Post mark buries the proper message in a a guzzle response
* this merges a text string with a json object
* need to harvest the ->Message property using the following
*/
if($e instanceof ClientException) { //postmark specific failure
$response = $e->getResponse();
$message_body = json_decode($response->getBody()->getContents());
if(property_exists($message_body, 'Message')){
$message = $message_body->Message;
nlog($message);
}
}
/* If the is an entity attached to the message send a failure mailer */
if($this->nmo->entity)
$this->entityEmailFailed($message);
/* Don't send postmark failures to Sentry */
if(Ninja::isHosted() && (!$e instanceof ClientException))
app('sentry')->captureException($e);
}
}
@ -224,7 +246,7 @@ class NinjaMailerJob implements ShouldQueue
return true;
/* On the hosted platform we set default contacts a @example.com email address - we shouldn't send emails to these types of addresses */
if(Ninja::isHosted() && strpos($this->nmo->to_user->email, '@example.com') !== false)
if(Ninja::isHosted() && $this->nmo->to_user && strpos($this->nmo->to_user->email, '@example.com') !== false)
return true;
/* GMail users are uncapped */
@ -241,6 +263,7 @@ class NinjaMailerJob implements ShouldQueue
private function logMailError($errors, $recipient_object)
{
SystemLogger::dispatch(
$errors,
SystemLog::CATEGORY_MAIL,
@ -249,19 +272,18 @@ class NinjaMailerJob implements ShouldQueue
$recipient_object,
$this->nmo->company
);
}
public function failed($exception = null)
{
nlog('mailer job failed');
nlog($exception->getMessage());
$job_failure = new EmailFailure($this->nmo->company->company_key);
$job_failure->string_metric5 = 'failed_email';
$job_failure->string_metric6 = substr($exception->getMessage(), 0, 150);
$job_failure->string_metric6 = substr($errors, 0, 150);
LightLogs::create($job_failure)
->batch();
}
public function failed($exception = null)
{
}
}

View File

@ -55,8 +55,8 @@ class QuoteWorkflowSettings implements ShouldQueue
});
}
if ($this->client->getSetting('auto_archive_quote')) {
$this->base_repository->archive($this->quote);
}
// if ($this->client->getSetting('auto_archive_quote')) {
// $this->base_repository->archive($this->quote);
// }
}
}

View File

@ -25,6 +25,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Turbo124\Beacon\Facades\LightLogs;
use Carbon\Carbon;
class SendRecurring implements ShouldQueue
{
@ -72,6 +73,7 @@ class SendRecurring implements ShouldQueue
$invoice->date = now()->format('Y-m-d');
$invoice->due_date = $this->recurring_invoice->calculateDueDate(now()->format('Y-m-d'));
$invoice->recurring_id = $this->recurring_invoice->id;
if($invoice->client->getSetting('auto_email_invoice'))
{
@ -115,7 +117,7 @@ class SendRecurring implements ShouldQueue
nlog("Invoice {$invoice->number} created");
$invoice->invitations->each(function ($invitation) use ($invoice) {
if ($invitation->contact && strlen($invitation->contact->email) >=1 && $invoice->client->getSetting('auto_email_invoice')) {
if ($invitation->contact && !$invitation->contact->trashed() && strlen($invitation->contact->email) >=1 && $invoice->client->getSetting('auto_email_invoice')) {
try{
EmailEntity::dispatch($invitation, $invoice->company);
@ -128,11 +130,25 @@ class SendRecurring implements ShouldQueue
}
});
if ($invoice->client->getSetting('auto_bill_date') == 'on_send_date' && $this->recurring_invoice->auto_bill_enabled) {
if ($invoice->client->getSetting('auto_bill_date') == 'on_send_date' && $invoice->auto_bill_enabled) {
nlog("attempting to autobill {$invoice->number}");
$invoice->service()->autoBill()->save();
}
elseif($invoice->client->getSetting('auto_bill_date') == 'on_due_date' && $invoice->auto_bill_enabled) {
if($invoice->due_date && Carbon::parse($invoice->due_date)->startOfDay()->lte(now()->startOfDay())) {
nlog("attempting to autobill {$invoice->number}");
$invoice->service()->autoBill()->save();
}
}
//important catch all here - we should never leave contacts send_email to false incase they are permanently set to false in the future.
$this->recurring_invoice->client->contacts()->update(['send_email' => true]);
}

View File

@ -82,8 +82,7 @@ class CreateUser
'settings' => null,
]);
if(!Ninja::isSelfHost()){
nlog("in the create user class");
if(!Ninja::isSelfHost()) {
event(new UserWasCreated($user, $user, $this->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
}

View File

@ -235,7 +235,7 @@ class Import implements ShouldQueue
$account->save();
//company size check
if ($this->company->invoices()->count() > 1000 || $this->company->products()->count() > 1000 || $this->company->clients()->count() > 1000) {
if ($this->company->invoices()->count() > 500 || $this->company->products()->count() > 500 || $this->company->clients()->count() > 500) {
$this->company->is_large = true;
$this->company->save();
}
@ -264,11 +264,6 @@ class Import implements ShouldQueue
/*After a migration first some basic jobs to ensure the system is up to date*/
VersionCheck::dispatch();
// CreateCompanyPaymentTerms::dispatchNow($sp035a66, $spaa9f78);
// CreateCompanyTaskStatuses::dispatchNow($this->company, $this->user);
info('Completed🚀🚀🚀🚀🚀 at '.now());
unlink($this->file_path);
@ -644,6 +639,7 @@ class Import implements ShouldQueue
$client->updated_at = Carbon::parse($modified['updated_at']);
$client->save(['timestamps' => false]);
$client->fresh();
$client->contacts()->forceDelete();
@ -654,7 +650,7 @@ class Import implements ShouldQueue
$modified_contacts[$key]['company_id'] = $this->company->id;
$modified_contacts[$key]['user_id'] = $this->processUserId($resource);
$modified_contacts[$key]['client_id'] = $client->id;
$modified_contacts[$key]['password'] = 'mysuperpassword'; // @todo, and clean up the code..
$modified_contacts[$key]['password'] = Str::random(8);
unset($modified_contacts[$key]['id']);
}
@ -689,6 +685,8 @@ class Import implements ShouldQueue
'old' => $resource['id'],
'new' => $client->id,
];
$client = null;
}
Client::reguard();
@ -1466,7 +1464,7 @@ class Import implements ShouldQueue
$modified['fees_and_limits'] = $this->cleanFeesAndLimits($modified['fees_and_limits']);
}
/* On Hosted platform we need to advise Stripe users to connect with Stripe Connect */
// /* On Hosted platform we need to advise Stripe users to connect with Stripe Connect */
if(Ninja::isHosted() && $modified['gateway_key'] == 'd14dd26a37cecc30fdd65700bfb55b23'){
$nmo = new NinjaMailerObject;
@ -1483,6 +1481,13 @@ class Import implements ShouldQueue
}
if(Ninja::isSelfHost() && $modified['gateway_key'] == 'd14dd26a47cecc30fdd65700bfb67b34'){
$modified['gateway_key'] = 'd14dd26a37cecc30fdd65700bfb55b23';
}
$company_gateway = CompanyGateway::create($modified);
$key = "company_gateways_{$resource['id']}";

View File

@ -63,7 +63,7 @@ class SendFailedEmails implements ShouldQueue
$invitation = $job_meta_array['entity_name']::where('key', $job_meta_array['invitation_key'])->with('contact')->first();
if ($invitation->invoice) {
if ($invitation->contact->send_email && $invitation->contact->email) {
if (!$invitation->contact->trashed() && $invitation->contact->send_email && $invitation->contact->email) {
EmailEntity::dispatch($invitation, $invitation->company, $job_meta_array['reminder_template']);
}
}

View File

@ -29,6 +29,7 @@ use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use ZipArchive;
use Illuminate\Support\Facades\App;
class StartMigration implements ShouldQueue
{
@ -122,6 +123,10 @@ class StartMigration implements ShouldQueue
$this->company->update_products = $update_product_flag;
$this->company->save();
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations($this->company->settings));
} catch (NonExistingMigrationFile | ProcessingMigrationArchiveFailed | ResourceNotAvailableForMigration | MigrationValidatorFailed | ResourceDependencyMissing | \Exception $e) {
$this->company->update_products = $update_product_flag;

View File

@ -55,6 +55,10 @@ class SystemLogger implements ShouldQueue
MultiDB::setDb($this->company->db);
$client_id = $this->client ? $this->client->id : null;
if(!$this->client && !$this->company->owner())
return;
$user_id = $this->client ? $this->client->user_id : $this->company->owner()->id;
$sl = [

View File

@ -73,12 +73,12 @@ class MultiDB
public static function checkUserEmailExists($email) : bool
{
if (! config('ninja.db.multi_db_enabled'))
return User::where(['email' => $email])->exists(); // true >= 1 emails found / false -> == emails found
return User::where(['email' => $email])->withTrashed()->exists(); // true >= 1 emails found / false -> == emails found
$current_db = config('database.default');
foreach (self::$dbs as $db) {
if (User::on($db)->where(['email' => $email])->exists()) { // if user already exists, validation will fail
if (User::on($db)->where(['email' => $email])->withTrashed()->exists()) { // if user already exists, validation will fail
self::setDb($current_db);
return true;
}
@ -107,7 +107,7 @@ class MultiDB
$current_db = config('database.default');
foreach (self::$dbs as $db) {
if (User::on($db)->where(['email' => $email])->exists()) {
if (User::on($db)->where(['email' => $email])->withTrashed()->exists()) {
if (Company::on($db)->where(['company_key' => $company_key])->exists()) {
self::setDb($current_db);
return true;
@ -196,7 +196,7 @@ class MultiDB
//multi-db active
foreach (self::$dbs as $db) {
if (User::on($db)->where('email', $email)->exists()){
if (User::on($db)->where('email', $email)->withTrashed()->exists()){
self::setDb($db);
return true;
}

View File

@ -88,7 +88,7 @@ class PaymentNotification implements ShouldQueue
$client = $payment->client;
$amount = $payment->amount;
if ($invoice) {
if ($invoice && $invoice->line_items) {
$items = $invoice->line_items;
$item = end($items)->product_key;
$entity_number = $invoice->number;

View File

@ -0,0 +1,47 @@
<?php
/**
* Quote Ninja (https://quoteninja.com).
*
* @link https://github.com/quoteninja/quoteninja source repository
*
* @copyright Copyright (c) 2021. Quote Ninja LLC (https://quoteninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Listeners\Quote;
use App\Jobs\Util\WebhookHandler;
use App\Libraries\MultiDB;
use App\Models\Webhook;
use Illuminate\Contracts\Queue\ShouldQueue;
class QuoteApprovedWebhook implements ShouldQueue
{
public function __construct()
{
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
MultiDB::setDb($event->company->db);
$quote = $event->quote;
$subscriptions = Webhook::where('company_id', $quote->company_id)
->where('event_id', Webhook::EVENT_APPROVE_QUOTE)
->exists();
if ($subscriptions) {
WebhookHandler::dispatch(Webhook::EVENT_APPROVE_QUOTE, $quote, $quote->company);
}
}
}

View File

@ -17,13 +17,14 @@ use App\Jobs\Mail\NinjaMailerObject;
use App\Libraries\MultiDB;
use App\Mail\Admin\VerifyUserObject;
use App\Mail\User\UserAdded;
use App\Notifications\Ninja\VerifyUser;
use App\Utils\Ninja;
use Exception;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Carbon;
class SendVerificationNotification implements ShouldQueue
{
@ -53,13 +54,20 @@ class SendVerificationNotification implements ShouldQueue
$event->user->service()->invite($event->company);
$nmo = new NinjaMailerObject;
$nmo->mailable = new UserAdded($event->company, $event->creating_user, $event->user);
$nmo->company = $event->company;
$nmo->settings = $event->company->settings;
$nmo->to_user = $event->creating_user;
NinjaMailerJob::dispatch($nmo);
if(Carbon::parse($event->company->created_at)->lt(now()->subDay()))
{
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations($event->company->settings));
$nmo = new NinjaMailerObject;
$nmo->mailable = new UserAdded($event->company, $event->creating_user, $event->user);
$nmo->company = $event->company;
$nmo->settings = $event->company->settings;
$nmo->to_user = $event->creating_user;
NinjaMailerJob::dispatch($nmo);
}
}
}

View File

@ -132,7 +132,6 @@ class InvoiceEmailEngine extends BaseEmailEngine
}
}
return $this;

View File

@ -45,8 +45,8 @@ class SupportMessageSent extends Mailable
$log_file->seek(PHP_INT_MAX);
$last_line = $log_file->key();
$lines = new LimitIterator($log_file, $last_line - 100, $last_line);
$lines = new LimitIterator($log_file, $last_line - 100, $last_line);
$log_lines = iterator_to_array($lines);
}
@ -62,9 +62,10 @@ class SupportMessageSent extends Mailable
$company = auth()->user()->company();
$user = auth()->user();
$db = str_replace("db-ninja-", "", $company->db);
$is_large = $company->is_large ? "L" : "";
if(Ninja::isHosted())
$subject = "{$priority}Hosted-{$db} :: {$plan} :: ".date('M jS, g:ia');
$subject = "{$priority}Hosted-{$db}{$is_large} :: {$plan} :: ".date('M jS, g:ia');
else
$subject = "{$priority}Self Hosted :: {$plan} :: ".date('M jS, g:ia');
@ -76,6 +77,7 @@ class SupportMessageSent extends Mailable
'system_info' => $system_info,
'laravel_log' => $log_lines,
'logo' => $company->present()->logo(),
'settings' => $company->settings
]);
}
}

View File

@ -11,6 +11,8 @@
namespace App\Mail;
use App\Jobs\Invoice\CreateUbl;
use App\Models\Account;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\User;
@ -77,7 +79,12 @@ class TemplateEmail extends Mailable
else
$signature = $settings->email_signature;
$this->from(config('mail.from.address'), $this->company->present()->name());
if(property_exists($settings, 'email_from_name') && strlen($settings->email_from_name) > 1)
$email_from_name = $settings->email_from_name;
else
$email_from_name = $this->company->present()->name();
$this->from(config('mail.from.address'), $email_from_name);
if (strlen($settings->bcc_email) > 1)
$this->bcc(explode(",",$settings->bcc_email));
@ -116,6 +123,17 @@ class TemplateEmail extends Mailable
}
if($this->invitation && $this->invitation->invoice && $settings->ubl_email_attachment && $this->company->account->hasFeature(Account::FEATURE_DOCUMENTS)){
$ubl_string = CreateUbl::dispatchNow($this->invitation->invoice);
nlog($ubl_string);
if($ubl_string)
$this->attachData($ubl_string, $this->invitation->invoice->getFileName('xml'));
}
return $this;
}
}

View File

@ -29,6 +29,7 @@ class TestMailServer extends Mailable
$this->from_email = $from_email;
}
/**
* Test Server mail.
*
@ -36,12 +37,18 @@ class TestMailServer extends Mailable
*/
public function build()
{
$settings = new \stdClass;
$settings->primary_color = "#4caf50";
$settings->email_style = 'dark';
return $this->from(config('mail.from.address'), config('mail.from.name'))
->subject(ctrans('texts.email'))
->markdown('email.support.message', [
'support_message' => $this->support_messages,
'system_info' => '',
'laravel_log' => [],
'settings' => $settings,
]);
}
}

View File

@ -15,11 +15,13 @@ use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Mail\Ninja\EmailQuotaExceeded;
use App\Models\Presenters\AccountPresenter;
use App\Notifications\Ninja\EmailQuotaNotification;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use Carbon\Carbon;
use DateTime;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache;
use Laracasts\Presenter\PresentableTrait;
@ -356,11 +358,11 @@ class Account extends BaseModel
if($this->isPaid()){
$limit = $this->paid_plan_email_quota;
$limit += Carbon::createFromTimestamp($this->created_at)->diffInMonths() * 50;
$limit += Carbon::createFromTimestamp($this->created_at)->diffInMonths() * 100;
}
else{
$limit = $this->free_plan_email_quota;
$limit += Carbon::createFromTimestamp($this->created_at)->diffInMonths() * 100;
$limit += Carbon::createFromTimestamp($this->created_at)->diffInMonths() * 50;
}
return min($limit, 5000);
@ -384,6 +386,10 @@ class Account extends BaseModel
if(is_null(Cache::get("throttle_notified:{$this->key}"))) {
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations($this->companies()->first()->settings));
$nmo = new NinjaMailerObject;
$nmo->mailable = new EmailQuotaExceeded($this->companies()->first());
$nmo->company = $this->companies()->first();
@ -392,6 +398,9 @@ class Account extends BaseModel
NinjaMailerJob::dispatch($nmo);
Cache::put("throttle_notified:{$this->key}", true, 60 * 24);
if(config('ninja.notification.slack'))
$this->companies()->first()->notification(new EmailQuotaNotification($this))->ninja();
}
return true;

View File

@ -134,6 +134,13 @@ class Activity extends StaticModel
return $this->hasOne(Backup::class);
}
public function history()
{
return $this->hasOne(Backup::class);
}
/**
* @return mixed
*/

View File

@ -90,7 +90,7 @@ class Client extends BaseModel implements HasLocalePreference
'contacts.company',
// 'currency',
// 'primary_contact',
// 'country',
'country',
// 'contacts',
// 'shipping_country',
// 'company',
@ -213,12 +213,12 @@ class Client extends BaseModel implements HasLocalePreference
public function user()
{
return $this->belongsTo(User::class);
return $this->belongsTo(User::class)->withTrashed();
}
public function assigned_user()
{
return $this->belongsTo(User::class, 'assigned_user_id', 'id');
return $this->belongsTo(User::class, 'assigned_user_id', 'id')->withTrashed();
}
public function country()
@ -361,6 +361,9 @@ class Client extends BaseModel implements HasLocalePreference
if (is_string($this->settings->{$setting}) && (iconv_strlen($this->settings->{$setting}) >= 1)) {
return $this->settings->{$setting};
}
elseif(is_bool($this->settings->{$setting})){
return $this->settings->{$setting};
}
}
/*Group Settings*/

View File

@ -92,7 +92,7 @@ class ClientContact extends Authenticatable implements HasLocalePreference
'custom_value4',
'email',
'is_primary',
'client_id',
// 'client_id',
];
/**

View File

@ -13,10 +13,12 @@ namespace App\Models;
use App\Utils\Traits\MakesDates;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class ClientGatewayToken extends BaseModel
{
use MakesDates;
use SoftDeletes;
protected $casts = [
'meta' => 'object',
@ -45,7 +47,7 @@ class ClientGatewayToken extends BaseModel
public function client()
{
return $this->hasOne(Client::class)->withTrashed();
return $this->belongsTo(Client::class)->withTrashed();
}
public function gateway()
@ -60,12 +62,12 @@ class ClientGatewayToken extends BaseModel
public function company()
{
return $this->hasOne(Company::class);
return $this->belongsTo(Company::class);
}
public function user()
{
return $this->hasOne(User::class)->withTrashed();
return $this->belongsTo(User::class)->withTrashed();
}
/**

View File

@ -95,6 +95,7 @@ class Company extends BaseModel
'default_password_timeout',
'show_task_end_date',
'use_comma_as_decimal_place',
'report_include_drafts',
];
protected $hidden = [
@ -288,7 +289,7 @@ class Company extends BaseModel
*/
public function company_gateways()
{
return $this->hasMany(CompanyGateway::class);
return $this->hasMany(CompanyGateway::class)->withTrashed();
}
/**
@ -296,7 +297,7 @@ class Company extends BaseModel
*/
public function tax_rates()
{
return $this->hasMany(TaxRate::class);
return $this->hasMany(TaxRate::class)->withTrashed();
}
/**
@ -304,7 +305,7 @@ class Company extends BaseModel
*/
public function products()
{
return $this->hasMany(Product::class);
return $this->hasMany(Product::class)->withTrashed();
}
/**
@ -318,7 +319,7 @@ class Company extends BaseModel
public function group_settings()
{
return $this->hasMany(GroupSetting::class);
return $this->hasMany(GroupSetting::class)->withTrashed();
}
public function timezone()

View File

@ -36,7 +36,7 @@ class CompanyLedger extends Model
public function user()
{
return $this->belongsTo(User::class);
return $this->belongsTo(User::class)->withTrashed();
}
public function company()

View File

@ -23,6 +23,8 @@ class CompanyToken extends BaseModel
];
protected $with = [
'company',
'user'
];
protected $touches = [];

View File

@ -56,10 +56,10 @@ class CompanyUser extends Pivot
return self::class;
}
public function tax_rates()
{
return $this->hasMany(TaxRate::class, 'company_id', 'company_id');
}
// public function tax_rates()
// {
// return $this->hasMany(TaxRate::class, 'company_id', 'company_id');
// }
public function account()
{
@ -78,7 +78,7 @@ class CompanyUser extends Pivot
public function user()
{
return $this->belongsTo(User::class);
return $this->belongsTo(User::class)->withTrashed();
}
public function company()

View File

@ -120,7 +120,7 @@ class Credit extends BaseModel
public function assigned_user()
{
return $this->belongsTo(User::class, 'assigned_user_id', 'id');
return $this->belongsTo(User::class, 'assigned_user_id', 'id')->withTrashed();
}
public function history()

View File

@ -84,7 +84,7 @@ class Expense extends BaseModel
public function assigned_user()
{
return $this->belongsTo(User::class, 'assigned_user_id', 'id');
return $this->belongsTo(User::class, 'assigned_user_id', 'id')->withTrashed();
}
public function company()

View File

@ -110,7 +110,8 @@ class Gateway extends StaticModel
case 50:
return [
GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true], //Braintree
GatewayType::PAYPAL => ['refund' => true, 'token_billing' => true]
GatewayType::PAYPAL => ['refund' => true, 'token_billing' => true],
GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true],
];
break;
case 7:

View File

@ -34,6 +34,15 @@ class GroupSetting extends StaticModel
'settings',
];
protected $appends = [
'hashed_id',
];
public function getHashedIdAttribute()
{
return $this->encodePrimaryKey($this->id);
}
protected $touches = [];
public function company()
@ -43,7 +52,7 @@ class GroupSetting extends StaticModel
public function user()
{
return $this->belongsTo(User::class);
return $this->belongsTo(User::class)->withTrashed();
}
public function clients()

View File

@ -84,10 +84,6 @@ class Invoice 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

@ -287,8 +287,24 @@ class Payment extends BaseModel
event(new PaymentWasVoided($this, $this->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
}
public function getLink()
// public function getLink()
// {
// return route('client.payments.show', $this->hashed_id);
// }
public function getLink() :string
{
return route('client.payments.show', $this->hashed_id);
if(Ninja::isHosted()){
$domain = isset($this->company->portal_domain) ? $this->company->portal_domain : $this->company->domain();
}
else
$domain = config('ninja.app_url');
return $domain.'/client/payment/'. $this->client->contacts()->first()->contact_key .'/'. $this->hashed_id."?next=/client/payments/".$this->hashed_id;
}
}

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