Merge branch 'v5-develop' into v5-1405-client-portal-settings

This commit is contained in:
Benjamin Beganović 2021-05-18 10:57:26 +02:00 committed by GitHub
commit 708073a83e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
86 changed files with 80900 additions and 79833 deletions

View File

@ -3,6 +3,7 @@
## [Unreleased (daily channel)](https://github.com/invoiceninja/invoiceninja/tree/v5-develop)
- Add Cache-control: no-cache to prevent overaggressive caching of assets
- Improved labelling in the settings (client portal)
- Client portal: Multiple accounts access improvements (#5703)
## [v5.1.56-release](https://github.com/invoiceninja/invoiceninja/releases/tag/v5.1.56-release)
## Fixed:

View File

@ -1 +1 @@
5.1.61
5.1.62

View File

@ -105,6 +105,8 @@ class CreateSingleAccount extends Command
'account_id' => $account->id,
'slack_webhook_url' => config('ninja.notification.slack'),
'default_password_timeout' => 30*60000,
'portal_mode' => 'domain',
'portal_domain' => 'http://ninja.test:8000',
]);
$account->default_company_id = $company->id;

View File

@ -128,7 +128,7 @@ class DemoMode extends Command
{
$faker = \Faker\Factory::create();
$this->count = 50;
$this->count = 25;
$this->info('Creating Small Account and Company');
@ -486,7 +486,7 @@ class DemoMode extends Command
if (rand(0, 1)) {
$invoice->assigned_user_id = $assigned_user_id;
}
$invoice->number = $this->getNextRecurringInvoiceNumber($client);
$invoice->save();
}

View File

@ -83,7 +83,7 @@ class Handler extends ExceptionHandler
'email' => 'anonymous@example.com',
'name' => 'Anonymous User',
]);
} elseif (auth()->guard('user') && auth()->guard('user')->user() && auth()->user()->company() && auth()->user()->company()->account->report_errors) {
} elseif (auth()->guard('user') && auth()->guard('user')->user() && auth()->user()->companyIsSet() && auth()->user()->company()->account->report_errors) {
$scope->setUser([
'id' => auth()->user()->account->key,
'email' => 'anonymous@example.com',

View File

@ -33,9 +33,6 @@ class ClientFactory
$client->client_hash = Str::random(40);
$client->settings = ClientSettings::defaults();
// $client_contact = ClientContactFactory::create($company_id, $user_id);
// $client->contacts->add($client_contact);
return $client;
}
}

View File

@ -21,6 +21,8 @@ class CompanyGatewayFactory
$company_gateway = new CompanyGateway;
$company_gateway->company_id = $company_id;
$company_gateway->user_id = $user_id;
$company_gateway->require_billing_address = false;
$company_gateway->require_shipping_address = false;
// $company_gateway->fees_and_limits = new FeesAndLimits;
return $company_gateway;

View File

@ -37,10 +37,10 @@ class CreditFactory
$credit->tax_rate1 = 0;
$credit->tax_name2 = '';
$credit->tax_rate2 = 0;
$credit->custom_value1 = 0;
$credit->custom_value2 = 0;
$credit->custom_value3 = 0;
$credit->custom_value4 = 0;
$credit->custom_value1 = '';
$credit->custom_value2 = '';
$credit->custom_value3 = '';
$credit->custom_value4 = '';
$credit->amount = 0;
$credit->balance = 0;
$credit->partial = 0;

View File

@ -38,10 +38,10 @@ class InvoiceFactory
$invoice->tax_rate2 = 0;
$invoice->tax_name3 = '';
$invoice->tax_rate3 = 0;
$invoice->custom_value1 = 0;
$invoice->custom_value2 = 0;
$invoice->custom_value3 = 0;
$invoice->custom_value4 = 0;
$invoice->custom_value1 = '';
$invoice->custom_value2 = '';
$invoice->custom_value3 = '';
$invoice->custom_value4 = '';
$invoice->amount = 0;
$invoice->balance = 0;
$invoice->paid_to_date = 0;

View File

@ -36,10 +36,10 @@ class RecurringInvoiceFactory
$invoice->tax_rate1 = 0;
$invoice->tax_name2 = '';
$invoice->tax_rate2 = 0;
$invoice->custom_value1 = 0;
$invoice->custom_value2 = 0;
$invoice->custom_value3 = 0;
$invoice->custom_value4 = 0;
$invoice->custom_value1 = '';
$invoice->custom_value2 = '';
$invoice->custom_value3 = '';
$invoice->custom_value4 = '';
$invoice->amount = 0;
$invoice->balance = 0;
$invoice->partial = 0;

View File

@ -35,10 +35,10 @@ class RecurringQuoteFactory
$quote->tax_rate1 = 0;
$quote->tax_name2 = '';
$quote->tax_rate2 = 0;
$quote->custom_value1 = 0;
$quote->custom_value2 = 0;
$quote->custom_value3 = 0;
$quote->custom_value4 = 0;
$quote->custom_value1 = '';
$quote->custom_value2 = '';
$quote->custom_value3 = '';
$quote->custom_value4 = '';
$quote->amount = 0;
$quote->balance = 0;
$quote->partial = 0;

View File

@ -164,10 +164,10 @@ class InvoiceController extends Controller
//if only 1 pdf, output to buffer for download
if ($invoices->count() == 1) {
return response()->streamDownload(function () use ($invoices) {
echo file_get_contents($invoices->first()->pdf_file_path());
}, basename($invoices->first()->pdf_file_path()), ['Cache-Control:' => 'no-cache']);
//return response()->download(TempFile::path($invoices->first()->pdf_file_path()), basename($invoices->first()->pdf_file_path()));
$file = $invoices->first()->pdf_file_path();
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);;
}
// enable output of HTTP headers

View File

@ -76,10 +76,9 @@ class QuoteController extends Controller
}
if ($quotes->count() == 1) {
return response()->streamDownload(function () use ($quotes) {
echo file_get_contents($quotes->first()->pdf_file_path());
}, basename($quotes->first()->pdf_file_path()), ['Cache-Control:' => 'no-cache']);
//return response()->download(TempFile::path($invoices->first()->pdf_file_path()), basename($quotes->first()->pdf_file_path()));
$file = $quotes->first()->pdf_file_path();
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
}
// enable output of HTTP headers

View File

@ -535,11 +535,9 @@ class CreditController extends BaseController
return $this->itemResponse($credit);
}
break;
case 'download':
return response()->streamDownload(function () use ($credit) {
echo file_get_contents($credit->pdf_file_path());
}, basename($credit->pdf_file_path()), ['Cache-Control:' => 'no-cache']);
//return response()->download(TempFile::path($credit->pdf_file_path()), basename($credit->pdf_file_path()));
case 'download':
$file = $credit->pdf_file_path();
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
break;
case 'archive':
$this->credit_repository->archive($credit);
@ -589,7 +587,7 @@ class CreditController extends BaseController
$file_path = $credit->service()->getCreditPdf($invitation);
return response()->download($file_path, basename($file_path), ['Cache-Control:' => 'no-cache']);
return response()->download($file_path, basename($file_path), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
}
/**

View File

@ -671,10 +671,10 @@ class InvoiceController extends BaseController
}
break;
case 'download':
return response()->streamDownload(function () use ($invoice) {
echo file_get_contents($invoice->pdf_file_path());
}, basename($invoice->pdf_file_path()), ['Cache-Control:' => 'no-cache']);
//return response()->download(TempFile::path($invoice->pdf_file_path()), basename($invoice->pdf_file_path()));
$file = $invoice->pdf_file_path();
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
break;
case 'restore':
$this->invoice_repo->restore($invoice);
@ -793,9 +793,10 @@ class InvoiceController extends BaseController
$contact = $invitation->contact;
$invoice = $invitation->invoice;
$file_path = $invoice->service()->getInvoicePdf($contact);
$file = $invoice->service()->getInvoicePdf($contact);
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);;
return response()->download($file_path, basename($file_path), ['Cache-Control:' => 'no-cache']);
}
/**
@ -844,13 +845,10 @@ class InvoiceController extends BaseController
*/
public function deliveryNote(ShowInvoiceRequest $request, Invoice $invoice)
{
$file_path = $invoice->service()->getInvoiceDeliveryNote($invoice, $invoice->invitations->first()->contact);
$file = $invoice->service()->getInvoiceDeliveryNote($invoice, $invoice->invitations->first()->contact);
try {
$file = public_path("storage/{$file_path}");
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache']);
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
} catch (\Exception $e) {
return response(['message' => 'Oops, something went wrong. Make sure you have symlink to storage/ in public/ directory.'], 500);
}

View File

@ -675,10 +675,10 @@ class QuoteController extends BaseController
// code...
break;
case 'download':
return response()->streamDownload(function () use ($quote) {
echo file_get_contents($quote->pdf_file_path());
}, basename($quote->pdf_file_path()), ['Cache-Control:' => 'no-cache']);
//return response()->download(TempFile::path($quote->pdf_file_path()), basename($quote->pdf_file_path()));
$file = $quote->pdf_file_path();
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
break;
case 'restore':
$this->quote_repo->restore($quote);
@ -730,7 +730,7 @@ class QuoteController extends BaseController
$file_path = $quote->service()->getQuotePdf($contact);
return response()->download($file_path, basename($file_path), ['Cache-Control:' => 'no-cache']);
return response()->download($file_path, basename($file_path), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
}
/**

View File

@ -497,7 +497,7 @@ class RecurringInvoiceController extends BaseController
$file_path = $recurring_invoice->service()->getInvoicePdf($contact);
return response()->download($file_path, basename($file_path), ['Cache-Control:' => 'no-cache']);
return response()->download($file_path, basename($file_path), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
}
/**

View File

@ -17,8 +17,11 @@ use App\Factory\CompanyGatewayFactory;
use App\Http\Requests\StripeConnect\InitializeStripeConnectRequest;
use App\Libraries\MultiDB;
use App\Models\Client;
use App\Models\Company;
use App\Models\CompanyGateway;
use App\Models\GatewayType;
use App\PaymentDrivers\Stripe\Connect\Account;
use Illuminate\Http\Request;
use Stripe\Exception\ApiErrorException;
class StripeConnectController extends BaseController
@ -38,6 +41,8 @@ class StripeConnectController extends BaseController
MultiDB::findAndSetDbByCompanyKey($request->getTokenContent()['company_key']);
$company = Company::where('company_key', $request->getTokenContent()['company_key'])->first();
$company_gateway = CompanyGateway::query()
->where('gateway_key', 'd14dd26a47cecc30fdd65700bfb67b34')
->where('company_id', $request->getCompany()->id)
@ -45,53 +50,75 @@ class StripeConnectController extends BaseController
if ($company_gateway) {
$config = decrypt($company_gateway->config);
$config = $company_gateway->getConfig();
if(property_exists($config, 'account_id'))
return render('gateways.stripe.connect.existing');
}
else
$company_gateway = CompanyGatewayFactory::create($request->getCompany()->id, $request->getContact()->id);
return view('auth.connect.existing');
/* Set Credit Card To Enabled */
$gateway_types = $company_gateway->driver(new Client)->gatewayTypes();
}
$stripe_client_id = config('ninja.ninja_stripe_client_id');
$redirect_uri = 'http://ninja.test:8000/stripe/completed';
$endpoint = "https://connect.stripe.com/oauth/authorize?response_type=code&client_id={$stripe_client_id}&redirect_uri={$redirect_uri}&scope=read_write&state={$token}";
if($email = $request->getContact()->email)
$endpoint .= "&stripe_user[email]={$email}";
$company_name = str_replace(" ", "_", $company->present()->name());
$endpoint .= "&stripe_user[business_name]={$company_name}";
return redirect($endpoint);
}
public function completed(InitializeStripeConnectRequest $request)
{
\Stripe\Stripe::setApiKey(config('ninja.ninja_stripe_key'));
$response = \Stripe\OAuth::token([
'grant_type' => 'authorization_code',
'code' => $request->input('code'),
]);
// nlog($response);
$company = Company::where('company_key', $request->getTokenContent()['company_key'])->first();
$company_gateway = CompanyGatewayFactory::create($company->id, $company->id);
$fees_and_limits = new \stdClass;
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
$fees_and_limits->{GatewayType::CREDIT_CARD} = new FeesAndLimits;
$company_gateway->gateway_key = 'd14dd26a47cecc30fdd65700bfb67b34';
$company_gateway->fees_and_limits = $fees_and_limits;
$company_gateway->setConfig([]);
$company_gateway->save();
$payload = [
'account_id' => $response->stripe_user_id,
"token_type" => 'bearer',
"stripe_publishable_key" => $response->stripe_publishable_key,
"scope" => $response->scope,
"livemode" => $response->livemode,
"stripe_user_id" => $response->stripe_user_id,
"refresh_token" => $response->refresh_token,
"access_token" => $response->access_token
];
/* Link account if existing account exists */
if($account_id = $this->checkAccountAlreadyLinkToEmail($company_gateway, $request->getContact()->email)) {
$config = json_decode(decrypt($company_gateway->config));
$config->account_id = $account_id;
$company_gateway->config = encrypt(json_encode($config));
$payload['account_id'] = $account_id;
$company_gateway->setConfig($payload);
$company_gateway->save();
return render('gateways.stripe.connect.existing');
return view('auth.connect.existing');
}
$data = [
'type' => 'standard',
'email' => $request->getContact()->email,
'country' => $request->getCompany()->country()->iso_3166_2,
];
$account = Account::create($data);
$link = Account::link($account->id, $token);
$company_gateway->config = encrypt(json_encode(['account_id' => $account->id]));
$company_gateway->setConfig($payload);
$company_gateway->save();
return redirect($link['url']);
}
public function completed()
{
return render('gateways.stripe.connect.completed');
//response here
return view('auth.connect.completed');
}
@ -111,4 +138,22 @@ class StripeConnectController extends BaseController
return false;
}
/*********************************
* Stripe OAuth
*/
// public function initialize(InitializeStripeConnectRequest $request, string $token)
// {
// $stripe_key = config('ninja.ninja_stripe_key');
// $endpoint = "https://connect.stripe.com/oauth/authorize?response_type=code&client_id={$stripe_key}&scope=read_write";
// return redirect($endpoint);
// }
}

View File

@ -0,0 +1,52 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Http\Controllers;
use App\Jobs\Util\ImportStripeCustomers;
use App\Jobs\Util\StripeUpdatePaymentMethods;
class StripeController extends BaseController
{
public function update()
{
if(auth()->user()->isAdmin())
{
StripeUpdatePaymentMethods::dispatch(auth()->user()->getCompany());
return response()->json(['message' => 'Processing'], 403);
}
return response()->json(['message' => 'Unauthorized'], 403);
}
public function import()
{
if(auth()->user()->isAdmin())
{
ImportStripeCustomers::dispatch(auth()->user()->getCompany());
return response()->json(['message' => 'Processing'], 403);
}
return response()->json(['message' => 'Unauthorized'], 403);
}
}

View File

@ -110,7 +110,6 @@ class Kernel extends HttpKernel
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
//\App\Http\Middleware\StartupCheck::class,
QueryLogging::class,
],
'shop' => [
@ -163,6 +162,7 @@ class Kernel extends HttpKernel
protected $middlewarePriority = [
SetDomainNameDb::class,
SetDb::class,
SetWebDb::class,
UrlSetDb::class,
@ -170,7 +170,6 @@ class Kernel extends HttpKernel
SetEmailDb::class,
SetInviteDb::class,
SetDbByCompanyKey::class,
SetDomainNameDb::class,
TokenAuth::class,
ContactTokenAuth::class,
ContactKeyLogin::class,

View File

@ -31,6 +31,7 @@ class CheckClientExistence
$multiple_contacts = ClientContact::query()
->where('email', auth('contact')->user()->email)
->whereNotNull('email')
->where('email', '<>', '')
->whereNull('deleted_at')
->distinct('company_id')
->distinct('email')
@ -38,6 +39,9 @@ class CheckClientExistence
->whereHas('client', function ($query) {
return $query->whereNull('deleted_at');
})
->whereHas('client.company', function ($query){
return $query->where('account_id', auth('contact')->user()->client->company->account->id);
})
->get();
if (count($multiple_contacts) == 0) {

View File

@ -34,14 +34,16 @@ class RedirectIfAuthenticated
}
break;
case 'user':
if (Auth::guard($guard)->check()) {
return redirect()->route('dashboard.index');
}
Auth::logout();
// if (Auth::guard($guard)->check()) {
// return redirect()->route('dashboard.index');
// }
break;
default:
if (Auth::guard($guard)->check()) {
return redirect('/');
}
Auth::logout();
// if (Auth::guard($guard)->check()) {
// return redirect('/');
// }
break;
}

View File

@ -34,13 +34,50 @@ class SetDomainNameDb
/*
* Use the host name to set the active DB
**/
if ($request->getSchemeAndHttpHost() && config('ninja.db.multi_db_enabled') && ! MultiDB::findAndSetDbByDomain($request->getSchemeAndHttpHost())) {
if (request()->json) {
return response()->json($error, 403);
} else {
abort(404);
if(!config('ninja.db.multi_db_enabled'))
return $next($request);
if (strpos($request->getHost(), 'invoicing.co') !== false)
{
$subdomain = explode('.', $request->getHost())[0];
$query = [
'subdomain' => $subdomain,
'portal_mode' => 'subdomain',
];
if(!MultiDB::findAndSetDbByDomain($query)){
if ($request->json) {
return response()->json($error, 403);
} else {
MultiDB::setDb('db-ninja-01');
nlog("I could not set the DB - defaulting to DB1");
//abort(400, 'Domain not found');
}
}
}
else {
$query = [
'portal_domain' => $request->getSchemeAndHttpHost(),
'portal_mode' => 'domain',
];
if(!MultiDB::findAndSetDbByDomain($query)){
if ($request->json) {
return response()->json($error, 403);
} else {
MultiDB::setDb('db-ninja-01');
nlog("I could not set the DB - defaulting to DB1");
//abort(400, 'Domain not found');
}
}
}
return $next($request);
}

View File

@ -28,7 +28,7 @@ class SetInviteDb
public function handle($request, Closure $next)
{
$error = [
'message' => 'Invalid URL',
'message' => 'I could not find the database for this object.',
'errors' => new stdClass,
];
/*
@ -46,7 +46,7 @@ class SetInviteDb
if (request()->json) {
return response()->json($error, 403);
} else {
abort(404);
abort(404,'I could not find the database for this object.');
}
}

View File

@ -55,8 +55,9 @@ class StoreCompanyRequest extends Request
{
$input = $this->all();
if(array_key_exists('portal_domain', $input) && strlen($input['portal_domain']) > 1)
$input['portal_domain'] = str_replace("http:", "https:", $input['portal_domain']);
//https not sure i should be forcing this.
// if(array_key_exists('portal_domain', $input) && strlen($input['portal_domain']) > 1)
// $input['portal_domain'] = str_replace("http:", "https:", $input['portal_domain']);
if (array_key_exists('google_analytics_url', $input)) {
$input['google_analytics_key'] = $input['google_analytics_url'];

View File

@ -60,8 +60,8 @@ class UpdateCompanyRequest extends Request
{
$input = $this->all();
if(array_key_exists('portal_domain', $input) && strlen($input['portal_domain']) > 1)
$input['portal_domain'] = str_replace("http:", "https:", $input['portal_domain']);
// if(array_key_exists('portal_domain', $input) && strlen($input['portal_domain']) > 1)
// $input['portal_domain'] = str_replace("http:", "https:", $input['portal_domain']);
if (array_key_exists('settings', $input)) {
$input['settings'] = $this->filterSaveableSettings($input['settings']);

View File

@ -49,6 +49,9 @@ class InitializeStripeConnectRequest extends FormRequest
*/
public function getTokenContent()
{
if($this->state)
$this->token = $this->state;
$data = Cache::get($this->token);
return $data;

View File

@ -71,6 +71,8 @@ class CompanyExport implements ShouldQueue
MultiDB::setDb($this->company->db);
$this->company = Company::where('company_key', $this->company->company_key)->first();
set_time_limit(0);
$this->export_data['app_version'] = config('ninja.app_version');
@ -114,6 +116,16 @@ class CompanyExport implements ShouldQueue
})->toArray();
$this->export_data['users'] = $this->company->users()->withTrashed()->cursor()->map(function ($user){
$user->account_id = $this->encodePrimaryKey($user->account_id);
$user->id = $this->encodePrimaryKey($user->id);
return $user;
})->toArray();
$this->export_data['client_contacts'] = $this->company->client_contacts->map(function ($client_contact){
$client_contact = $this->transformArrayOfKeys($client_contact, ['id', 'company_id', 'user_id',' client_id']);
@ -140,11 +152,7 @@ class CompanyExport implements ShouldQueue
})->toArray();
$temp_co = $this->company;
$temp_co->id = $this->encodePrimaryKey($temp_co->id);
$temp_co->account_id = $this->encodePrimaryKey($temp_co->account_id);
$this->export_data['company'] = $temp_co->toArray();
$this->export_data['company'] = $this->company->toArray();
$this->export_data['company_gateways'] = $this->company->company_gateways->map(function ($company_gateway){
@ -273,7 +281,7 @@ class CompanyExport implements ShouldQueue
$payment = $this->transformBasicEntities($payment);
$payment = $this->transformArrayOfKeys($payment, ['client_id','project_id', 'vendor_id', 'client_contact_id', 'invitation_id', 'company_gateway_id']);
return $project;
return $payment;
})->toArray();
@ -370,15 +378,6 @@ class CompanyExport implements ShouldQueue
})->makeHidden(['id'])->toArray();
$this->export_data['users'] = $this->company->users->map(function ($user){
$user->account_id = $this->encodePrimaryKey($user->account_id);
$user->id = $this->encodePrimaryKey($user->id);
return $user;
})->makeHidden(['ip'])->toArray();
$this->export_data['vendors'] = $this->company->vendors->map(function ($vendor){
return $this->transformBasicEntities($vendor);
@ -431,7 +430,6 @@ class CompanyExport implements ShouldQueue
private function zipAndSend()
{
nlog("zipping");
$tempStream = fopen('php://memory', 'w+');
@ -450,11 +448,8 @@ class CompanyExport implements ShouldQueue
$zip->finish();
$path = 'backups/';
nlog($path.$file_name);
Storage::disk(config('filesystems.default'))->put($path.$file_name, $tempStream);
// fclose($fp);
fclose($tempStream);

View File

@ -33,6 +33,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use ZipArchive;
use ZipStream\Option\Archive;
use ZipStream\ZipStream;
@ -41,15 +42,45 @@ class CompanyImport implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesHash;
protected $current_app_version;
public $company;
private $account;
public $file_path;
private $backup_file;
public $import_company;
public $ids = [];
private $options = '';
private $importables = [
'company',
'users',
// 'payment_terms',
// 'tax_rates',
// 'clients',
// 'company_gateways',
// 'client_gateway_tokens',
// 'vendors',
// 'projects',
// 'products',
// 'credits',
// 'invoices',
// 'recurring_invoices',
// 'quotes',
// 'payments',
// 'expense_categories',
// 'task_statuses',
// 'expenses',
// 'tasks',
// 'documents',
];
/**
* Create a new job instance.
*
@ -62,6 +93,7 @@ class CompanyImport implements ShouldQueue
$this->company = $company;
$this->file_path = $file_path;
$this->options = $options;
$this->current_app_version = config('ninja.app_version');
}
public function handle()
@ -69,10 +101,19 @@ class CompanyImport implements ShouldQueue
MultiDB::setDb($this->company->db);
$this->company =Company::where('company_key', $this->company->company_key)->firstOrFail();
$this->account = $this->company->account;
$this->unzipFile()
->preFlightChecks();
foreach($this->importables as $import){
$method = Str::ucfirst(Str::camel($import));
$this->{$method}();
}
}
@ -85,10 +126,12 @@ class CompanyImport implements ShouldQueue
private function preFlightChecks()
{
//check the file version and perform any necessary adjustments to the file in order to proceed - needed when we change schema
//
$app_version = $this->backup_file->app_version;
if($this->current_app_version != $this->backup_file->app_version)
{
//perform some magic here
}
nlog($app_version);
return $this;
}
@ -118,10 +161,39 @@ class CompanyImport implements ShouldQueue
return $this;
}
private function importData()
private function importUsers()
{
// $this->import_company = Company::where('company_key', $this->company->company_key)->firstOrFail();
User::unguard();
return $this;
foreach ($this->backup_file->users as $user)
{
$new_user = User::firstOrNew(
['email' => $user->email],
(array)$user,
);
$new_user->account_id = $this->account->id;
$new_user->save(['timestamps' => false]);
$this->ids['users']["{$user->id}"] = $new_user->id;
}
Expense::reguard();
}
public function transformId(string$resource, string $old): int
{
if (! array_key_exists($resource, $this->ids)) {
throw new \Exception("Resource {$resource} not available.");
}
if (! array_key_exists("{$old}", $this->ids[$resource])) {
throw new \Exception("Missing resource key: {$old}");
}
return $this->ids[$resource]["{$old}"];
}
}

View File

@ -64,7 +64,7 @@ class CreateEntityPdf implements ShouldQueue
*
* @param $invitation
*/
public function __construct($invitation)
public function __construct($invitation, $disk = 'public')
{
$this->invitation = $invitation;
@ -86,7 +86,9 @@ class CreateEntityPdf implements ShouldQueue
$this->contact = $invitation->contact;
$this->disk = $disk ?? config('filesystems.default');
$this->disk = $disk;
// $this->disk = $disk ?? config('filesystems.default');
}
public function handle()
@ -192,12 +194,12 @@ class CreateEntityPdf implements ShouldQueue
try{
Storage::disk($this->disk)->put($file_path, $pdf);
}
catch(\Exception $e)
{
throw new FilePermissionsFailure('Could not write the PDF, permission issues!');
throw new FilePermissionsFailure($e->getMessage());
}
}
@ -209,8 +211,5 @@ class CreateEntityPdf implements ShouldQueue
{
}
// public function failed(\Exception $exception)
// {
// nlog("help!");
// }
}

View File

@ -83,23 +83,24 @@ class ZipInvoices implements ShouldQueue
$zip = new ZipStream($file_name, $options);
foreach ($this->invoices as $invoice) {
$zip->addFileFromPath(basename($invoice->pdf_file_path()), TempFile::path($invoice->pdf_file_path()));
//$zip->addFileFromPath(basename($invoice->pdf_file_path()), TempFile::path($invoice->pdf_file_path()));
$zip->addFileFromPath(basename($invoice->pdf_file_path()), $invoice->pdf_file_path());
}
$zip->finish();
Storage::disk(config('filesystems.default'))->put($path.$file_name, $tempStream);
Storage::disk('public')->put($path.$file_name, $tempStream);
fclose($tempStream);
$nmo = new NinjaMailerObject;
$nmo->mailable = new DownloadInvoices(Storage::disk(config('filesystems.default'))->url($path.$file_name), $this->company);
$nmo->mailable = new DownloadInvoices(Storage::disk('public')->url($path.$file_name), $this->company);
$nmo->to_user = $this->user;
$nmo->settings = $this->settings;
$nmo->company = $this->company;
NinjaMailerJob::dispatch($nmo);
UnlinkFile::dispatch(config('filesystems.default'), $path.$file_name)->delay(now()->addHours(1));
UnlinkFile::dispatch('public', $path.$file_name)->delay(now()->addHours(1));
}
}

View File

@ -57,6 +57,8 @@ class NinjaMailerJob implements ShouldQueue
public $override;
public $company;
public function __construct(NinjaMailerObject $nmo, bool $override = false)
{
@ -73,7 +75,8 @@ class NinjaMailerJob implements ShouldQueue
/*Set the correct database*/
MultiDB::setDb($this->nmo->company->db);
$company = Company::where('company_key', $this->nmo->company->company_key)->first();
/* Serializing models from other jobs wipes the primary key */
$this->company = Company::where('company_key', $this->nmo->company->company_key)->first();
/* Set the email driver */
$this->setMailDriver();
@ -89,7 +92,7 @@ class NinjaMailerJob implements ShouldQueue
}
else {
$this->nmo->mailable->replyTo($company->owner()->email, $company->owner()->present()->name());
$this->nmo->mailable->replyTo($this->company->owner()->email, $this->company->owner()->present()->name());
}
@ -178,7 +181,15 @@ class NinjaMailerJob implements ShouldQueue
nlog("Sending via {$user->name()}");
$google = (new Google())->init();
$google->getClient()->setAccessToken(json_encode($user->oauth_user_token));
try{
$google->getClient()->setAccessToken(json_encode($user->oauth_user_token));
}
catch(\Exception $e) {
$this->logMailError('Gmail Token Invalid', $this->company->clients()->first());
$this->nmo->settings->email_sending_method = 'default';
return $this->setMailDriver();
}
if ($google->getClient()->isAccessTokenExpired()) {
$google->refreshToken($user);

View File

@ -1264,7 +1264,7 @@ class Import implements ShouldQueue
$modified['fees_and_limits'] = $this->cleanFeesAndLimits($modified['fees_and_limits']);
}
if(Ninja::isHosted() && $modified['gateway_key'] == 'd14dd26a37cecc30fdd65700bfb55b23'){
else if(Ninja::isHosted() && !Ninja::isPaidHostedClient() && $modified['gateway_key'] == 'd14dd26a37cecc30fdd65700bfb55b23'){
$modified['gateway_key'] = 'd14dd26a47cecc30fdd65700bfb67b34';
$modified['fees_and_limits'] = [];
}
@ -1296,6 +1296,8 @@ class Import implements ShouldQueue
$modified['company_id'] = $this->company->id;
$modified['client_id'] = $this->transformId('clients', $resource['client_id']);
$modified['company_gateway_id'] = $this->transformId('company_gateways', $resource['company_gateway_id']);
//$modified['user_id'] = $this->processUserId($resource);
$cgt = ClientGatewayToken::Create($modified);

View File

@ -0,0 +1,68 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Jobs\Util;
use App\Libraries\MultiDB;
use App\Models\Client;
use App\Models\CompanyGateway;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ImportStripeCustomers implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $company;
private $stripe_keys = ['d14dd26a47cecc30fdd65700bfb67b34', 'd14dd26a37cecc30fdd65700bfb55b23'];
/**
* Create a new job instance.
*
* @param $event_id
* @param $entity
*/
public function __construct($company)
{
$this->company = $company;
}
/**
* Execute the job.
*
* @return bool
*/
public function handle()
{
MultiDB::setDb($this->company->db);
$cgs = CompanyGateway::where('company_id', $this->company->id)
->whereIn('gateway_key', $this->stripe_keys)
->get();
$cgs->each(function ($company_gateway){
$company_gateway->driver(new Client)->importCustomers();
});
}
public function failed($exception)
{
nlog("Stripe import customer methods exception");
nlog($exception->getMessage());
}
}

View File

@ -0,0 +1,68 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Jobs\Util;
use App\Libraries\MultiDB;
use App\Models\Client;
use App\Models\CompanyGateway;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class StripeUpdatePaymentMethods implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $company;
private $stripe_keys = ['d14dd26a47cecc30fdd65700bfb67b34', 'd14dd26a37cecc30fdd65700bfb55b23'];
/**
* Create a new job instance.
*
* @param $event_id
* @param $entity
*/
public function __construct($company)
{
$this->company = $company;
}
/**
* Execute the job.
*
* @return bool
*/
public function handle()
{
MultiDB::setDb($this->company->db);
$cgs = CompanyGateway::where('company_id', $this->company->id)
->whereIn('gateway_key', $this->stripe_keys)
->get();
$cgs->each(function ($company_gateway){
$company_gateway->driver(new Client)->updateAllPaymentMethods();
});
}
public function failed($exception)
{
nlog("Stripe update payment methods exception");
nlog($exception->getMessage());
}
}

View File

@ -39,8 +39,6 @@ class UnlinkFile implements ShouldQueue
*/
public function handle()
{
// nlog("deleting");
// nlog($this->file_path);
Storage::disk($this->disk)->delete($this->file_path);
}
}

View File

@ -253,14 +253,14 @@ class MultiDB
return false;
}
public static function findAndSetDbByDomain($subdomain) :bool
public static function findAndSetDbByDomain($query_array) :bool
{
if (! config('ninja.db.multi_db_enabled'))
return (Company::whereSubdomain($subdomain)->exists() === true);
return (Company::where($query_array)->exists() === true);
foreach (self::$dbs as $db) {
if ($company = Company::on($db)->whereSubdomain($subdomain)->first()) {
if ($company = Company::on($db)->where($query_array)->first()) {
self::setDb($company->db);
return true;
}

View File

@ -1,4 +1,13 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Mail;
@ -9,7 +18,6 @@ use Illuminate\Queue\SerializesModels;
class DownloadBackup extends Mailable
{
// use Queueable, SerializesModels;
public $file_path;
@ -30,14 +38,15 @@ class DownloadBackup extends Mailable
$company = Company::where('company_key', $this->company->company_key)->first();
return $this->from(config('mail.from.address'), config('mail.from.name'))
->subject(ctrans('texts.download_backup_subject'))
->subject(ctrans('texts.download_backup_subject', ['company' => $company->present()->name()]))
->markdown(
'email.admin.download_files',
[
'url' => $this->file_path,
'logo' => $company->present()->logo,
'whitelabel' => $company->account->isPaid() ? true : false,
'settings' => $company->settings
'settings' => $company->settings,
'greeting' => $company->present()->name(),
]
);
}

View File

@ -12,6 +12,7 @@
namespace App\Mail\Engine;
use App\DataMapper\EmailTemplateDefaults;
use App\Models\Account;
use App\Utils\Helpers;
use App\Utils\Number;
use App\Utils\Traits\MakesDates;
@ -72,6 +73,16 @@ class PaymentEmailEngine extends BaseEmailEngine
->setViewLink('')
->setViewText('');
if ($this->client->getSetting('pdf_email_attachment') !== false && $this->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) {
$this->payment->invoices->each(function ($invoice){
$this->setAttachments([$invoice->pdf_file_path()]);
});
}
return $this;
}

View File

@ -13,12 +13,14 @@ namespace App\Models;
use App\DataMapper\ClientSettings;
use App\DataMapper\CompanySettings;
use App\Jobs\Entity\CreateEntityPdf;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\UserSessionAttributes;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Storage;
/**
@ -199,4 +201,5 @@ class BaseModel extends Model
return $formatted_number;
}
}

View File

@ -443,12 +443,12 @@ class Company extends BaseModel
public function domain()
{
if (Ninja::isNinja()) {
if (Ninja::isHosted()) {
if($this->portal_mode == 'domain')
return $this->portal_domain;
return "https://{$this->subdomain}" . config('ninja.app_domain');
return "https://{$this->subdomain}." . config('ninja.app_domain');
}
return config('ninja.app_url');

View File

@ -309,14 +309,14 @@ class CompanyGateway extends BaseModel
$fees_and_limits = $this->getFeesAndLimits($gateway_type_id);
if (! $fees_and_limits) {
return 0;
return false;
}
$fee = 0;
if ($fees_and_limits->fee_amount) {
$fee += $fees_and_limits->fee_amount;
// info("fee after adding fee amount = {$fee}");
nlog("fee after adding fee amount = {$fee}");
}
if ($fees_and_limits->fee_percent) {
@ -325,7 +325,7 @@ class CompanyGateway extends BaseModel
} else {
$fee += round(($amount * $fees_and_limits->fee_percent / 100), 2);
}
// info("fee after adding fee percent = {$fee}");
nlog("fee after adding fee percent = {$fee}");
}
/* Cap fee if we have to here. */
@ -334,6 +334,7 @@ class CompanyGateway extends BaseModel
}
$pre_tax_fee = $fee;
nlog("fee after adding fee percent = {$fee}");
/**/
if ($include_taxes) {
@ -352,6 +353,7 @@ class CompanyGateway extends BaseModel
// info("fee after adding fee tax 3 = {$fee}");
}
}
nlog("fee after adding fee percent = {$fee}");
return $fee;
}

View File

@ -251,23 +251,37 @@ class Credit extends BaseModel
$this->save();
}
public function pdf_file_path($invitation = null)
public function pdf_file_path($invitation = null, string $type = 'path', bool $portal = false)
{
$storage_path = Storage::url($this->client->credit_filepath().$this->numberFormatter().'.pdf');
if (Storage::exists($this->client->credit_filepath().$this->numberFormatter().'.pdf')) {
return $storage_path;
}
if (! $invitation) {
event(new CreditWasUpdated($this, $this->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
CreateEntityPdf::dispatchNow($this->invitations->first());
} else {
event(new CreditWasUpdated($this, $this->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
CreateEntityPdf::dispatchNow($invitation);
if($this->invitations()->exists())
$invitation = $this->invitations()->first();
else{
$this->service()->createInvitations();
$invitation = $this->invitations()->first();
}
}
return $storage_path;
if(!$invitation)
throw new \Exception('Hard fail, could not create an invitation - is there a valid contact?');
$file_path = $this->client->credit_filepath().$this->numberFormatter().'.pdf';
if(Ninja::isHosted() && $portal && Storage::disk(config('filesystems.default'))->exists($file_path)){
return Storage::disk(config('filesystems.default'))->{$type}($file_path);
}
elseif(Ninja::isHosted() && $portal){
$file_path = CreateEntityPdf::dispatchNow($invitation,config('filesystems.default'));
return Storage::disk(config('filesystems.default'))->{$type}($file_path);
}
if(Storage::disk('public')->exists($file_path))
return Storage::disk('public')->{$type}($file_path);
$file_path = CreateEntityPdf::dispatchNow($invitation);
return Storage::disk('public')->{$type}($file_path);
}
public function markInvitationsSent()

View File

@ -96,4 +96,9 @@ class Expense extends BaseModel
{
return $this->belongsTo(Vendor::class);
}
public function client()
{
return $this->belongsTo(Client::class);
}
}

View File

@ -392,7 +392,7 @@ class Invoice extends BaseModel
return $invoice_calc->build();
}
public function pdf_file_path($invitation = null, string $type = 'url')
public function pdf_file_path($invitation = null, string $type = 'path', bool $portal = false)
{
if (! $invitation) {
@ -408,21 +408,23 @@ class Invoice extends BaseModel
if(!$invitation)
throw new \Exception('Hard fail, could not create an invitation - is there a valid contact?');
$storage_path = Storage::$type($this->client->invoice_filepath().$this->numberFormatter().'.pdf');
$file_path = $this->client->invoice_filepath().$this->numberFormatter().'.pdf';
if (! Storage::exists($this->client->invoice_filepath().$this->numberFormatter().'.pdf')) {
event(new InvoiceWasUpdated($this, $this->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
CreateEntityPdf::dispatchNow($invitation);
if(Ninja::isHosted() && $portal && Storage::disk(config('filesystems.default'))->exists($file_path)){
return Storage::disk(config('filesystems.default'))->{$type}($file_path);
}
elseif(Ninja::isHosted() && $portal){
$file_path = CreateEntityPdf::dispatchNow($invitation,config('filesystems.default'));
return Storage::disk(config('filesystems.default'))->{$type}($file_path);
}
if(Storage::disk('public')->exists($file_path))
return Storage::disk('public')->{$type}($file_path);
nlog($storage_path);
return $storage_path;
$file_path = CreateEntityPdf::dispatchNow($invitation);
return Storage::disk('public')->{$type}($file_path);
}
/**
* Updates Invites to SENT.
*/
public function markInvitationsSent()
{
$this->invitations->each(function ($invitation) {

View File

@ -207,26 +207,39 @@ class Quote extends BaseModel
}
public function pdf_file_path($invitation = null, string $type = 'url')
public function pdf_file_path($invitation = null, string $type = 'path', bool $portal = false)
{
if (! $invitation) {
$invitation = $this->invitations->first();
if($this->invitations()->exists())
$invitation = $this->invitations()->first();
else{
$this->service()->createInvitations();
$invitation = $this->invitations()->first();
}
}
$storage_path = Storage::$type($this->client->quote_filepath().$this->numberFormatter().'.pdf');
if(!$invitation)
throw new \Exception('Hard fail, could not create an invitation - is there a valid contact?');
nlog($storage_path);
$file_path = $this->client->quote_filepath().$this->numberFormatter().'.pdf';
if (! Storage::exists($this->client->quote_filepath().$this->numberFormatter().'.pdf')) {
event(new QuoteWasUpdated($this, $this->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
CreateEntityPdf::dispatchNow($invitation);
if(Ninja::isHosted() && $portal && Storage::disk(config('filesystems.default'))->exists($file_path)){
return Storage::disk(config('filesystems.default'))->{$type}($file_path);
}
elseif(Ninja::isHosted() && $portal){
$file_path = CreateEntityPdf::dispatchNow($invitation,config('filesystems.default'));
return Storage::disk(config('filesystems.default'))->{$type}($file_path);
}
if(Storage::disk('public')->exists($file_path))
return Storage::disk('public')->{$type}($file_path);
return $storage_path;
$file_path = CreateEntityPdf::dispatchNow($invitation);
return Storage::disk('public')->{$type}($file_path);
}
/**
* @param int $status
* @return string

View File

@ -179,10 +179,19 @@ class User extends Authenticatable implements MustVerifyEmail
return $company_token->company;
}
// return false;
throw new \Exception('No Company Found');
//return Company::find(config('ninja.company_id'));
}
public function companyIsSet()
{
if($this->company)
return true;
return false;
}
/**
* Returns the current company.
*

View File

@ -0,0 +1,73 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers\Stripe\Connect;
use Stripe\Stripe;
class ConnectOauth
{
/**
* Response payload
* "token_type": "bearer",
"stripe_publishable_key": "{PUBLISHABLE_KEY}",
"scope": "read_write",
"livemode": false,
"stripe_user_id": "{ACCOUNT_ID}",
"refresh_token": "{REFRESH_TOKEN}",
"access_token": "{ACCESS_TOKEN}"
*/
public function getAccountId($code)
{
Stripe::setApiKey(config('ninja.ninja_stripe_key'));
$response = \Stripe\OAuth::token([
'grant_type' => 'authorization_code',
'code' => $code,
]);
// Access the connected account id in the response
$connected_account_id = $response->stripe_user_id;
return $response;
//return $connected_account_id;
}
/**
* Revokes access to Stripe from Invoice Ninja
* for the given account id
*/
public function revoke($account_id)
{
Stripe::setApiKey(config('ninja.ninja_stripe_key'));
\Stripe\OAuth::deauthorize([
'client_id' => config('ninja.ninja_stripe_key'),
'stripe_user_id' => $account_id,
]);
}
}

View File

@ -0,0 +1,121 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers\Stripe;
use App\Factory\ClientContactFactory;
use App\Factory\ClientFactory;
use App\Factory\ClientGatewayTokenFactory;
use App\Models\Client;
use App\Models\ClientGatewayToken;
use App\Models\Country;
use App\Models\Currency;
use App\Models\GatewayType;
use App\PaymentDrivers\StripePaymentDriver;
use App\Utils\Traits\MakesHash;
use Stripe\Customer;
use Stripe\PaymentMethod;
class ImportCustomers
{
use MakesHash;
/** @var StripePaymentDriver */
public $stripe;
public function __construct(StripePaymentDriver $stripe)
{
$this->stripe = $stripe;
}
public function run()
{
$this->stripe->init();
$customers = Customer::all();
foreach($customers as $customer)
{
$this->addCustomer($customer);
}
/* Now call the update payment methods handler*/
$this->stripe->updateAllPaymentMethods();
}
private function addCustomer(Customer $customer)
{
$account = $this->stripe->company_gateway->company->account;
$existing_customer = $this->stripe
->company_gateway
->client_gateway_tokens()
->where('gateway_customer_reference', $customer->id)
->exists();
if($existing_customer)
return;
$client = ClientFactory::create($this->stripe->company_gateway->company_id, $this->stripe->company_gateway->user_id);
if(property_exists($customer, 'address'))
{
$client->address1 = property_exists($customer->address, 'line1') ? $customer->address->line1 : '';
$client->address2 = property_exists($customer->address, 'line2') ? $customer->address->line2 : '';
$client->city = property_exists($customer->address, 'city') ? $customer->address->city : '';
$client->state = property_exists($customer->address, 'state') ? $customer->address->state : '';
$client->phone = property_exists($customer->address, 'phone') ? $customer->phone : '';
if(property_exists($customer->address, 'country')){
$country = Country::where('iso_3166_2', $customer->address->country)->first();
if($country)
$client->country_id = $country->id;
}
}
if($customer->currency) {
$currency = Currency::where('code', $customer->currency)->first();
if($currency){
$settings = $client->settings;
$settings->currency_id = (string)$currency->id;
$client->settings = $settings;
}
}
$client->name = property_exists($customer, 'name') ? $customer->name : '';
if(!$account->isPaidHostedClient() && Client::where('company_id', $this->stripe->company_gateway->company_id)->count() <= config('ninja.quotas.free.clients')){
$client->save();
$contact = ClientContactFactory::create($client->company_id, $client->user_id);
$contact->client_id = $client->id;
$contact->first_name = $client->name ?: '';
$contact->phone = $client->phone ?: '';
$contact->email = $client->email ?: '';
$contact->save();
}
}
}

View File

@ -0,0 +1,168 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\PaymentDrivers\Stripe;
use App\Factory\ClientGatewayTokenFactory;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\PaymentDrivers\StripePaymentDriver;
use App\Utils\Traits\MakesHash;
use Stripe\Customer;
use Stripe\PaymentMethod;
class UpdatePaymentMethods
{
use MakesHash;
/** @var StripePaymentDriver */
public $stripe;
public function __construct(StripePaymentDriver $stripe)
{
$this->stripe = $stripe;
}
public function run()
{
$this->stripe->init();
$this->stripe
->company_gateway
->client_gateway_tokens
->each(function ($token){
$card_methods = PaymentMethod::all([
'customer' => $token->gateway_customer_reference,
'type' => 'card',
],
$this->stripe->stripe_connect_auth);
foreach($card_methods as $method)
{
$this->addOrUpdateCard($method, $token, GatewayType::CREDIT_CARD);
}
$alipay_methods = PaymentMethod::all([
'customer' => $token->gateway_customer_reference,
'type' => 'alipay',
],
$this->stripe->stripe_connect_auth);
foreach($alipay_methods as $method)
{
$this->addOrUpdateCard($method, $token, GatewayType::ALIPAY);
}
$sofort_methods = PaymentMethod::all([
'customer' => $token->gateway_customer_reference,
'type' => 'sofort',
],
$this->stripe->stripe_connect_auth);
foreach($alipay_methods as $method)
{
$this->addOrUpdateCard($method, $token, GatewayType::SOFORT);
}
$bank_accounts = Customer::allSources(
$token->gateway_customer_reference,
['object' => 'bank_account', 'limit' => 300]
);
foreach($bank_accounts as $bank_account)
{
$this->addOrUpdateBankAccount($bank_account, $token);
}
});
}
private function addOrUpdateBankAccount($bank_account, ClientGatewayToken $token)
{
$token_exists = ClientGatewayToken::where([
'gateway_customer_reference' => $token->gateway_customer_reference,
'token' => $bank_account->id,
])->exists();
/* Already exists return */
if($token_exists)
return;
$cgt = ClientGatewayTokenFactory::create($token->company_id);
$cgt->client_id = $token->client_id;
$cgt->token = $bank_account->id;
$cgt->gateway_customer_reference = $token->gateway_customer_reference;
$cgt->company_gateway_id = $token->company_gateway_id;
$cgt->gateway_type_id = GatewayType::BANK_TRANSFER;
$cgt->meta = new \stdClass;
$cgt->routing_number = $bank_account->routing_number;
$cgt->save();
}
private function addOrUpdateCard(PaymentMethod $method, ClientGatewayToken $token, GatewayType $type_id)
{
$token_exists = ClientGatewayToken::where([
'gateway_customer_reference' => $token->gateway_customer_reference,
'token' => $method->id,
])->exists();
/* Already exists return */
if($token_exists)
return;
/* Ignore Expired cards */
if($method->card->exp_year <= date('Y') && $method->card->exp_month < date('m'))
return;
$cgt = ClientGatewayTokenFactory::create($token->company_id);
$cgt->client_id = $token->client_id;
$cgt->token = $method->id;
$cgt->gateway_customer_reference = $token->gateway_customer_reference;
$cgt->company_gateway_id = $token->company_gateway_id;
$cgt->gateway_type_id = $type_id;
$cgt->meta = $this->buildPaymentMethodMeta($method, $type_id);
$cgt->save();
}
private function buildPaymentMethodMeta(PaymentMethod $method, GatewayType $type_id)
{
switch ($type_id) {
case GatewayType::CREDIT_CARD:
$payment_meta = new \stdClass;
$payment_meta->exp_month = (string) $method->card->exp_month;
$payment_meta->exp_year = (string) $method->card->exp_year;
$payment_meta->brand = (string) $method->card->brand;
$payment_meta->last4 = (string) $method->card->last4;
$payment_meta->type = GatewayType::CREDIT_CARD;
return $payment_meta;
break;
case GatewayType::ALIPAY:
case GatewayType::SOFORT:
return new \stdClass;
default:
break;
}
}
}

View File

@ -25,7 +25,9 @@ use App\PaymentDrivers\Stripe\ACH;
use App\PaymentDrivers\Stripe\Alipay;
use App\PaymentDrivers\Stripe\Charge;
use App\PaymentDrivers\Stripe\CreditCard;
use App\PaymentDrivers\Stripe\ImportCustomers;
use App\PaymentDrivers\Stripe\SOFORT;
use App\PaymentDrivers\Stripe\UpdatePaymentMethods;
use App\PaymentDrivers\Stripe\Utilities;
use App\Utils\Traits\MakesHash;
use Exception;
@ -493,4 +495,30 @@ class StripePaymentDriver extends BaseDriver
return Account::all();
}
/**
* Pull all client payment methods and update
* the respective tokens in the system.
*
*/
public function updateAllPaymentMethods()
{
return (new UpdatePaymentMethods($this))->run();
}
/**
* Imports stripe customers and their payment methods
* Matches users in the system based on the $match_on_record
* ie. email
*
* Phone
* Email
*/
public function importCustomers()
{
return (new ImportCustomers($this))->run();
//match clients based on the gateway_customer_reference column
}
}

View File

@ -165,7 +165,7 @@ class PaymentMethod
foreach ($gateway->driver($this->client)->gatewayTypes() as $type) {
if (isset($gateway->fees_and_limits) && is_object($gateway->fees_and_limits) && property_exists($gateway->fees_and_limits, $type)) {
if (isset($gateway->fees_and_limits) && is_object($gateway->fees_and_limits) && property_exists($gateway->fees_and_limits, GatewayType::CREDIT_CARD)) {
if ($this->validGatewayForAmount($gateway->fees_and_limits->{GatewayType::CREDIT_CARD}, $this->amount))
$this->payment_methods[] = [$gateway->id => $type];

View File

@ -16,6 +16,7 @@ use App\Factory\CreditInvitationFactory;
use App\Models\Credit;
use App\Models\CreditInvitation;
use App\Services\AbstractService;
use Illuminate\Support\Str;
class CreateInvitations extends AbstractService
{

View File

@ -41,16 +41,9 @@ class GetCreditPdf extends AbstractService
$file_path = $path.$this->credit->numberFormatter().'.pdf';
$disk = config('filesystems.default');
$disk = 'public';
$file = Storage::disk($disk)->exists($file_path);
if (! $file) {
$file_path = CreateEntityPdf::dispatchNow($this->invitation);
}
if(config('filesystems.default') == 's3')
return TempFile::path(Storage::disk($disk)->url($file_path));
$file_path = CreateEntityPdf::dispatchNow($this->invitation);
return Storage::disk($disk)->path($file_path);
}

View File

@ -43,7 +43,7 @@ class AddGatewayFee extends AbstractService
{
$gateway_fee = round($this->company_gateway->calcGatewayFee($this->amount, $this->gateway_type_id, $this->invoice->uses_inclusive_taxes), $this->invoice->client->currency()->precision);
if ((int)$gateway_fee == 0)
if (!$gateway_fee)
return $this->invoice;
// Removes existing stale gateway fees

View File

@ -49,7 +49,9 @@ class GenerateDeliveryNote
$this->contact = $contact;
$this->disk = $disk ?? config('filesystems.default');
$this->disk = 'public';
// $this->disk = $disk ?? config('filesystems.default');
}
public function run()
@ -105,6 +107,7 @@ class GenerateDeliveryNote
Storage::disk($this->disk)->put($file_path, $pdf);
return $file_path;
return Storage::disk($this->disk)->path($file_path);
}
}

View File

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

View File

@ -62,32 +62,11 @@ trait PdfMakerUtilities
}
if (isset($element['elements'])) {
$sorted = $this->processChildrenOrder($element['elements']);
$this->createElementContent($node, $sorted);
$this->createElementContent($node, $element['elements']);
}
}
}
public function processChildrenOrder(array $children)
{
$processed = [];
foreach ($children as $child) {
if (!isset($child['order'])) {
$child['order'] = 0;
}
$processed[] = $child;
}
usort($processed, function ($a, $b) {
return $a['order'] <=> $b['order'];
});
return $processed;
}
public function updateElementProperty($element, string $attribute, ?string $value)
{
// We have exception for "hidden" property.
@ -154,9 +133,7 @@ trait PdfMakerUtilities
}
if (isset($child['elements'])) {
$sorted = $this->processChildrenOrder($child['elements']);
$this->createElementContent($_child, $sorted);
$this->createElementContent($_child, $child['elements']);
}
}
}

View File

@ -39,16 +39,9 @@ class GetQuotePdf extends AbstractService
$file_path = $path.$this->quote->numberFormatter().'.pdf';
$disk = config('filesystems.default');
$disk = 'public';
$file = Storage::disk($disk)->exists($file_path);
if (! $file) {
$file_path = CreateEntityPdf::dispatchNow($invitation);
}
if(config('filesystems.default') == 's3')
return TempFile::path(Storage::disk($disk)->url($file_path));
$file_path = CreateEntityPdf::dispatchNow($invitation);
return Storage::disk($disk)->path($file_path);
}

View File

@ -33,6 +33,7 @@ class SubscriptionTransformer extends EntityTransformer
public function transform(Subscription $subscription): array
{
$company = $subscription->company;
return [
'id' => $this->encodePrimaryKey($subscription->id),
@ -59,7 +60,8 @@ class SubscriptionTransformer extends EntityTransformer
'allow_plan_changes' => (bool)$subscription->allow_plan_changes,
'refund_period' => (int)$subscription->refund_period,
'webhook_configuration' => $subscription->webhook_configuration ?: [],
'purchase_page' => (string)route('client.subscription.purchase', $subscription->hashed_id),
'purchase_page' => (string)$company->domain() . "/client/subscriptions/{$subscription->hashed_id}/purchase",
//'purchase_page' => (string)route('client.subscription.purchase', $subscription->hashed_id),
'currency_id' => (string) $subscription->currency_id,
'is_deleted' => (bool)$subscription->is_deleted,
'created_at' => (int)$subscription->created_at,

View File

@ -11,6 +11,7 @@
namespace App\Utils\Traits;
use App\Utils\Ninja;
use Illuminate\Support\Str;
/**
@ -46,7 +47,10 @@ trait Inviteable
{
$entity_type = Str::snake(class_basename($this->entityType()));
$domain = isset($this->company->portal_domain) ? $this->company->portal_domain : $this->company->domain();
if(Ninja::isHosted())
$domain = isset($this->company->portal_domain) ? $this->company->portal_domain : $this->company->domain();
else
$domain = config('ninja.app_url');
switch ($this->company->portal_mode) {
case 'subdomain':
@ -69,7 +73,10 @@ trait Inviteable
public function getPortalLink() :string
{
$domain = isset($this->company->portal_domain) ? $this->company->portal_domain : $this->company->domain();
if(Ninja::isHosted())
$domain = isset($this->company->portal_domain) ? $this->company->portal_domain : $this->company->domain();
else
$domain = config('ninja.app_url');
switch ($this->company->portal_mode) {
case 'subdomain':

View File

@ -149,6 +149,7 @@ return [
'invoiceninja_hosted_pdf_generation' => env('NINJA_HOSTED_PDF', false),
'ninja_stripe_key' => env('NINJA_STRIPE_KEY', null),
'ninja_stripe_publishable_key' => env('NINJA_PUBLISHABLE_KEY', null),
'ninja_stripe_client_id' => env('NINJA_STRIPE_CLIENT_ID', null),
'pdf_generator' => env('PDF_GENERATOR', false),
'internal_queue_enabled' => env('INTERNAL_QUEUE_ENABLED', true),
];

View File

@ -30,7 +30,7 @@ const RESOURCES = {
"favicon.png": "dca91c54388f52eded692718d5a98b8b",
"version.json": "ea1781094b87723b953889a712b1feba",
"manifest.json": "ce1b79950eb917ea619a0a30da27c6a3",
"main.dart.js": "b6fdfe160593f3083646123daf386773",
"main.dart.js": "818f2adef47fec836b904d8b334de88d",
"favicon.ico": "51636d3a390451561744c42188ccd628",
"/": "23224b5e03519aaa87594403d54412cf"
};

78869
public/main.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

80241
public/main.foss.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4250,6 +4250,7 @@ $LANG = array(
'new_login_description' => 'You recently logged in to your Invoice Ninja account from a new location or device:<br><br><b>IP:</b> :ip<br><b>Time:</b> :time<br><b>Email:</b> :email',
'download_backup_subject' => 'Your company backup is ready for download',
'contact_details' => 'Contact Details',
'download_backup_subject' => ':company backup is ready for download',
);
return $LANG;

View File

@ -0,0 +1,13 @@
@extends('layouts.ninja')
@section('meta_title', ctrans('texts.success'))
@section('body')
<div class="flex flex-col justify-center items-center mt-10">
<div class="mb-4">
<svg height="60" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 468 222.5" xml:space="preserve"><style>.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#635bff}</style><path class="st0" d="M414 113.4c0-25.6-12.4-45.8-36.1-45.8-23.8 0-38.2 20.2-38.2 45.6 0 30.1 17 45.3 41.4 45.3 11.9 0 20.9-2.7 27.7-6.5v-20c-6.8 3.4-14.6 5.5-24.5 5.5-9.7 0-18.3-3.4-19.4-15.2h48.9c0-1.3.2-6.5.2-8.9zm-49.4-9.5c0-11.3 6.9-16 13.2-16 6.1 0 12.6 4.7 12.6 16h-25.8zM301.1 67.6c-9.8 0-16.1 4.6-19.6 7.8l-1.3-6.2h-22v116.6l25-5.3.1-28.3c3.6 2.6 8.9 6.3 17.7 6.3 17.9 0 34.2-14.4 34.2-46.1-.1-29-16.6-44.8-34.1-44.8zm-6 68.9c-5.9 0-9.4-2.1-11.8-4.7l-.1-37.1c2.6-2.9 6.2-4.9 11.9-4.9 9.1 0 15.4 10.2 15.4 23.3 0 13.4-6.2 23.4-15.4 23.4zM223.8 61.7l25.1-5.4V36l-25.1 5.3zM223.8 69.3h25.1v87.5h-25.1zM196.9 76.7l-1.6-7.4h-21.6v87.5h25V97.5c5.9-7.7 15.9-6.3 19-5.2v-23c-3.2-1.2-14.9-3.4-20.8 7.4zM146.9 47.6l-24.4 5.2-.1 80.1c0 14.8 11.1 25.7 25.9 25.7 8.2 0 14.2-1.5 17.5-3.3V135c-3.2 1.3-19 5.9-19-8.9V90.6h19V69.3h-19l.1-21.7zM79.3 94.7c0-3.9 3.2-5.4 8.5-5.4 7.6 0 17.2 2.3 24.8 6.4V72.2c-8.3-3.3-16.5-4.6-24.8-4.6C67.5 67.6 54 78.2 54 95.9c0 27.6 38 23.2 38 35.1 0 4.6-4 6.1-9.6 6.1-8.3 0-18.9-3.4-27.3-8v23.8c9.3 4 18.7 5.7 27.3 5.7 20.8 0 35.1-10.3 35.1-28.2-.1-29.8-38.2-24.5-38.2-35.7z"/></svg>
</div>
<p>Connecting your account using Stripe has been successfully completed.</p>
<span>Click <a class="font-semibold hover:underline" href="{{ url('/#/settings/company_gateways') }}">here</a> to continue.</span>
</div>
@endsection

View File

@ -0,0 +1,13 @@
@extends('layouts.ninja')
@section('meta_title', ctrans('texts.success'))
@section('body')
<div class="flex flex-col justify-center items-center mt-10">
<div class="mb-4">
<svg height="60" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 468 222.5" xml:space="preserve"><style>.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#635bff}</style><path class="st0" d="M414 113.4c0-25.6-12.4-45.8-36.1-45.8-23.8 0-38.2 20.2-38.2 45.6 0 30.1 17 45.3 41.4 45.3 11.9 0 20.9-2.7 27.7-6.5v-20c-6.8 3.4-14.6 5.5-24.5 5.5-9.7 0-18.3-3.4-19.4-15.2h48.9c0-1.3.2-6.5.2-8.9zm-49.4-9.5c0-11.3 6.9-16 13.2-16 6.1 0 12.6 4.7 12.6 16h-25.8zM301.1 67.6c-9.8 0-16.1 4.6-19.6 7.8l-1.3-6.2h-22v116.6l25-5.3.1-28.3c3.6 2.6 8.9 6.3 17.7 6.3 17.9 0 34.2-14.4 34.2-46.1-.1-29-16.6-44.8-34.1-44.8zm-6 68.9c-5.9 0-9.4-2.1-11.8-4.7l-.1-37.1c2.6-2.9 6.2-4.9 11.9-4.9 9.1 0 15.4 10.2 15.4 23.3 0 13.4-6.2 23.4-15.4 23.4zM223.8 61.7l25.1-5.4V36l-25.1 5.3zM223.8 69.3h25.1v87.5h-25.1zM196.9 76.7l-1.6-7.4h-21.6v87.5h25V97.5c5.9-7.7 15.9-6.3 19-5.2v-23c-3.2-1.2-14.9-3.4-20.8 7.4zM146.9 47.6l-24.4 5.2-.1 80.1c0 14.8 11.1 25.7 25.9 25.7 8.2 0 14.2-1.5 17.5-3.3V135c-3.2 1.3-19 5.9-19-8.9V90.6h19V69.3h-19l.1-21.7zM79.3 94.7c0-3.9 3.2-5.4 8.5-5.4 7.6 0 17.2 2.3 24.8 6.4V72.2c-8.3-3.3-16.5-4.6-24.8-4.6C67.5 67.6 54 78.2 54 95.9c0 27.6 38 23.2 38 35.1 0 4.6-4 6.1-9.6 6.1-8.3 0-18.9-3.4-27.3-8v23.8c9.3 4 18.7 5.7 27.3 5.7 20.8 0 35.1-10.3 35.1-28.2-.1-29.8-38.2-24.5-38.2-35.7z"/></svg>
</div>
<p>You have already configured a Stripe Connect account.</p>
<span>Click <a class="font-semibold hover:underline" href="{{ url('/#/settings/company_gateways') }}">here</a> to continue.</span>
</div>
@endsection

View File

@ -7,15 +7,20 @@
@endslot
@slot('greeting')
@endslot
@if(isset($greeting))
<p style="padding-top:20px">{{ $greeting }}</p>
@endif
<p style="padding-top:20px">
@lang('texts.download_timeframe')
</p>
<p style="padding-top:20px">
@component('email.components.button', ['url' => $url])
@lang('texts.download')
@endcomponent
</p>
@slot('signature')
InvoiceNinja (contact@invoiceninja.com)

View File

@ -0,0 +1,115 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<!-- Error: {{ session('error') }} -->
@if (config('services.analytics.tracking_id'))
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-122229484-1"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', '{{ config('services.analytics.tracking_id') }}', {'anonymize_ip': true});
function trackEvent(category, action) {
ga('send', 'event', category, action, this.src);
}
</script>
<script>
Vue.config.devtools = true;
</script>
@else
<script>
function gtag() {
}
</script>
@endif
<!-- Title -->
@auth()
<title></title>
@endauth
@guest
<title>@yield('meta_title', '') {{ config('app.name') }}</title>
@endguest
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="@yield('meta_description')"/>
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<!-- Scripts -->
<script src="{{ mix('js/app.js') }}" defer></script>
<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.7.x/dist/alpine.min.js" defer></script>
<!-- Fonts -->
<link rel="dns-prefetch" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css?family=Open+Sans&display=swap" rel="stylesheet" type="text/css">
<!-- Styles -->
<link href="{{ mix('css/app.css') }}" rel="stylesheet">
<link rel="canonical" href="{{ config('ninja.site_url') }}/{{ request()->path() }}"/>
@livewireStyles
{{-- Feel free to push anything to header using @push('header') --}}
@stack('head')
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/cookieconsent@3/build/cookieconsent.min.css" />
</head>
<body class="antialiased">
@if(session()->has('message'))
<div class="py-1 text-sm text-center text-white bg-primary disposable-alert">
{{ session('message') }}
</div>
@endif
@yield('body')
@livewireScripts
<script src="https://cdn.jsdelivr.net/npm/cookieconsent@3/build/cookieconsent.min.js" data-cfasync="false"></script>
<script>
window.addEventListener("load", function(){
if (! window.cookieconsent) {
return;
}
window.cookieconsent.initialise({
"palette": {
"popup": {
"background": "#000"
},
"button": {
"background": "#f1d600"
},
},
"content": {
"href": "https://www.invoiceninja.com/privacy-policy/",
"message": "This website uses cookies to ensure you get the best experience on our website.",
"dismiss": "Got it!",
"link": "Learn more",
}
})}
);
</script>
</body>
<footer>
@yield('footer')
@stack('footer')
</footer>
</html>

View File

@ -2,7 +2,7 @@
@section('meta_title', ctrans('texts.entity_number_placeholder', ['entity' => ctrans('texts.credit'), 'entity_number' => $credit->number]))
@push('head')
<meta name="pdf-url" content="{{ $credit->pdf_file_path() }}">
<meta name="pdf-url" content="{{ $credit->pdf_file_path(null, 'url', true) }}">
<script src="{{ asset('js/vendor/pdf.js/pdf.min.js') }}"></script>
@endpush

View File

@ -1,2 +1,2 @@
<iframe src="{{ $invoice->pdf_file_path() }}"
<iframe src="{{ $invoice->pdf_file_path(null, 'url', true) }}"
style="position:fixed; top:0; left:0; bottom:0; right:0; width:100%; height:100%; border:none; margin:0; padding:0; overflow:hidden; z-index:999999;"></iframe>

View File

@ -2,7 +2,7 @@
@section('meta_title', ctrans('texts.view_invoice'))
@push('head')
<meta name="pdf-url" content="{{ $invoice->pdf_file_path() }}">
<meta name="pdf-url" content="{{ $invoice->pdf_file_path(null, 'url', true) }}">
<meta name="show-invoice-terms" content="{{ $settings->show_accept_invoice_terms ? true : false }}">
<meta name="require-invoice-signature" content="{{ $client->user->account->hasFeature(\App\Models\Account::FEATURE_INVOICE_SETTINGS) && $settings->require_invoice_signature }}">
<script src="{{ asset('js/vendor/pdf.js/pdf.min.js') }}"></script>
@ -174,7 +174,7 @@
</section>
</div>
<iframe src="{{ $invoice->pdf_file_path() }}" class="h-screen w-full border-0 hidden lg:block mt-4"></iframe>
<iframe src="{{ $invoice->pdf_file_path(null, 'url', true) }}" class="h-screen w-full border-0 hidden lg:block mt-4"></iframe>
<div class="flex justify-center">
<canvas id="pdf-placeholder" class="shadow rounded-lg bg-white lg:hidden mt-4 p-4"></canvas>

View File

@ -45,7 +45,7 @@
<div class="col-span-6 sm:col-span-3">
<div class="inline-flex items-center">
<label for="website" class="input-label">{{ ctrans('texts.website') }}</label>
<span class="text-xs ml-2 text-gray-600">E.g. https://invoiceninja.com</span>
<span class="text-xs ml-2 text-gray-600">E.g. https://example.com</span>
</div>
<input id="website" class="input w-full" name="website" wire:model.defer="website"/>
@error('website')

View File

@ -1,2 +1,2 @@
<iframe src="{{ $quote->pdf_file_path() }}"
<iframe src="{{ $quote->pdf_file_path(null,'url',true) }}"
style="position:fixed; top:0; left:0; bottom:0; right:0; width:100%; height:100%; border:none; margin:0; padding:0; overflow:hidden; z-index:999999;"></iframe>

View File

@ -2,7 +2,7 @@
@section('meta_title', ctrans('texts.entity_number_placeholder', ['entity' => ctrans('texts.quote'), 'entity_number' => $quote->number]))
@push('head')
<meta name="pdf-url" content="{{ asset($quote->pdf_file_path()) }}">
<meta name="pdf-url" content="{{ asset($quote->pdf_file_path(null, 'url', true)) }}">
<script src="{{ asset('js/vendor/pdf.js/pdf.min.js') }}"></script>
<meta name="show-quote-terms" content="{{ $settings->show_accept_quote_terms ? true : false }}">
@ -78,7 +78,7 @@
<canvas id="pdf-placeholder" class="shadow rounded-lg bg-white lg:hidden mt-4 p-4"></canvas>
</div>
<iframe src="{{ $quote->pdf_file_path() }}" class="h-screen w-full border-0 hidden lg:block mt-4"></iframe>
<iframe src="{{ $quote->pdf_file_path(null, 'url',true) }}" class="h-screen w-full border-0 hidden lg:block mt-4"></iframe>
@include('portal.ninja2020.invoices.includes.terms', ['entities' => [$quote], 'entity_type' => ctrans('texts.quote')])
@include('portal.ninja2020.invoices.includes.signature')

View File

@ -1,7 +1,7 @@
@extends('portal.ninja2020.layout.clean')
@push('head')
<meta name="pdf-url" content="{{ asset($entity->pdf_file_path()) }}">
<meta name="pdf-url" content="{{ asset($entity->pdf_file_path(null, 'url',true)) }}">
<script src="{{ asset('js/vendor/pdf.js/pdf.min.js') }}"></script>
<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.7.x/dist/alpine.min.js" defer></script>
@endpush
@ -39,7 +39,7 @@
<div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="transform opacity-0 scale-95" x-transition:enter-end="transform opacity-100 scale-100" x-transition:leave="transition ease-in duration-75" x-transition:leave-start="transform opacity-100 scale-100" x-transition:leave-end="transform opacity-0 scale-95" class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg">
<div class="rounded-md bg-white shadow-xs">
<div class="py-1">
<a target="_blank" href="{{ asset($entity->pdf_file_path()) }}" class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900">{{ ctrans('texts.open_in_new_tab') }}</a>
<a target="_blank" href="{{ asset($entity->pdf_file_path(null, 'url',true)) }}" class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900">{{ ctrans('texts.open_in_new_tab') }}</a>
</div>
</div>
</div>

View File

@ -183,6 +183,9 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a
// Route::post('hooks', 'SubscriptionController@subscribe')->name('hooks.subscribe');
// Route::delete('hooks/{subscription_id}', 'SubscriptionController@unsubscribe')->name('hooks.unsubscribe');
Route::post('stripe/update_payment_methods', 'StripeController@update')->middleware('password_protected')->name('stripe.update');
Route::post('stripe/import_customers', 'StripeController@import')->middleware('password_protected')->name('stripe.import');
Route::resource('subscriptions', 'SubscriptionController');
Route::post('subscriptions/bulk', 'SubscriptionController@bulk')->name('subscriptions.bulk');
@ -193,9 +196,7 @@ Route::match(['get', 'post'], 'payment_webhook/{company_key}/{company_gateway_id
->name('payment_webhook');
Route::post('api/v1/postmark_webhook', 'PostMarkController@webhook');
Route::get('token_hash_router', 'OneTimeTokenController@router');
Route::get('webcron', 'WebCronController@index');
Route::fallback('BaseController@notFound');

View File

@ -26,8 +26,7 @@ Route::get('client/magic_link/{magic_link}', 'ClientPortal\ContactHashLoginContr
Route::get('documents/{document_hash}', 'ClientPortal\DocumentController@publicDownload')->name('documents.public_download');
Route::get('error', 'ClientPortal\ContactHashLoginController@errorPage')->name('client.error');
//todo implement domain DB
Route::group(['middleware' => ['auth:contact', 'locale', 'check_client_existence'], 'prefix' => 'client', 'as' => 'client.'], function () {
Route::group(['middleware' => ['auth:contact', 'locale', 'check_client_existence','domain_db'], 'prefix' => 'client', 'as' => 'client.'], function () {
Route::get('dashboard', 'ClientPortal\DashboardController@index')->name('dashboard'); // name = (dashboard. index / create / show / update / destroy / edit
Route::get('invoices', 'ClientPortal\InvoiceController@index')->name('invoices.index')->middleware('portal_enabled');
@ -81,7 +80,7 @@ Route::group(['middleware' => ['auth:contact', 'locale', 'check_client_existence
Route::get('logout', 'Auth\ContactLoginController@logout')->name('logout');
});
Route::get('client/subscription/{subscription}/purchase/', 'ClientPortal\SubscriptionPurchaseController@index')->name('client.subscription.purchase');
Route::get('client/subscriptions/{subscription}/purchase', 'ClientPortal\SubscriptionPurchaseController@index')->name('client.subscription.purchase')->middleware('domain_db');
Route::group(['middleware' => ['invite_db'], 'prefix' => 'client', 'as' => 'client.'], function () {
/*Invitation catches*/

View File

@ -11,11 +11,13 @@
namespace Tests\Feature\Import;
use App\Jobs\Import\CSVImport;
use App\Models\Account;
use App\Models\Client;
use App\Models\Expense;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\Product;
use App\Models\User;
use App\Models\Vendor;
use App\Utils\Traits\MakesHash;
use Illuminate\Routing\Middleware\ThrottleRequests;
@ -34,6 +36,8 @@ class ImportCompanyTest extends TestCase
{
use MakesHash;
public $account;
public function setUp() :void
{
parent::setUp();
@ -44,13 +48,76 @@ class ImportCompanyTest extends TestCase
$this->withoutExceptionHandling();
$this->account = Account::factory()->create();
}
public function testBackupJsonRead()
{
$backup_json_file = base_path().'/tests/Feature/Import/backup.json';
$backup_json_file_zip = base_path().'/tests/Feature/Import/backup.zip';
$zip = new \ZipArchive;
$res = $zip->open($backup_json_file_zip);
if ($res === TRUE) {
$zip->extractTo(sys_get_temp_dir());
$zip->close();
}
$backup_json_file = sys_get_temp_dir() . "/backup/backup.json";
$this->assertTrue(is_array(json_decode(file_get_contents($backup_json_file),1)));
unlink($backup_json_file);
}
public function testImportUsers()
{
$backup_json_file_zip = base_path().'/tests/Feature/Import/backup.zip';
$zip = new \ZipArchive;
$res = $zip->open($backup_json_file_zip);
if ($res === TRUE) {
$zip->extractTo(sys_get_temp_dir());
$zip->close();
}
$backup_json_file = sys_get_temp_dir() . "/backup/backup.json";
$backup_json_object = json_decode(file_get_contents($backup_json_file));
$this->assertTrue(property_exists($backup_json_object, 'app_version'));
$this->assertTrue(property_exists($backup_json_object, 'users'));
unlink($backup_json_file);
User::all()->each(function ($user){
$user->forceDelete();
});
User::unguard();
foreach ($backup_json_object->users as $user)
{
$user_array = (array)$user;
unset($user_array['laravel_through_key']);
unset($user_array['hashed_id']);
$new_user = User::firstOrNew(
['email' => $user->email],
array_merge($user_array, ['account_id' => $this->account->id]),
);
$new_user->save(['timestamps' => false]);
$this->ids['users']["{$user->hashed_id}"] = $new_user->id;
}
User::reguard();
$this->assertEquals(2, User::count());
}
}

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -40,20 +40,20 @@ class PdfCreatorTest extends TestCase
{
$credit_path = CreateEntityPdf::dispatchNow($this->credit->invitations->first());
$this->assertTrue(Storage::exists($this->client->credit_filepath().$this->credit->number.'.pdf'));
$this->assertTrue(Storage::disk('public')->exists($credit_path));
}
public function testInvoicePdfCreated()
{
$invoice_path = CreateEntityPdf::dispatchNow($this->invoice->invitations->first());
$this->assertTrue(Storage::exists($this->client->invoice_filepath().$this->invoice->number.'.pdf'));
}
$this->assertTrue(Storage::disk('public')->exists($invoice_path));
}
public function testQuotePdfCreated()
{
$quote_path = CreateEntityPdf::dispatchNow($this->quote->invitations->first());
$this->assertTrue(Storage::exists($this->client->quote_filepath().$this->quote->number.'.pdf'));
$this->assertTrue(Storage::disk('public')->exists($quote_path));
}
}

View File

@ -209,65 +209,6 @@ class PdfMakerTest extends TestCase
$this->assertNotSame($output1, $output2);
}
public function testOrderingElements()
{
$design = new Design('example', ['custom_path' => base_path('tests/Feature/PdfMaker/')]);
$maker = new PdfMaker([
'template' => [
'header' => [
'id' => 'header',
'properties' => [],
'elements' => [
['element' => 'h1', 'content' => 'h1-element'],
['element' => 'span', 'content' => 'span-element'],
],
],
],
]);
$maker
->design($design)
->build();
$node = $maker->getSectionNode('header');
$before = [];
foreach ($node->childNodes as $child) {
$before[] = $child->nodeName;
}
$this->assertEquals('h1', $before[1]);
$maker = new PdfMaker([
'template' => [
'header' => [
'id' => 'header',
'properties' => [],
'elements' => [
['element' => 'h1', 'content' => 'h1-element', 'order' => 1],
['element' => 'span', 'content' => 'span-element', 'order' => 0],
],
],
],
]);
$maker
->design($design)
->build();
$node = $maker->getSectionNode('header');
$after = [];
foreach ($node->childNodes as $child) {
$after[] = $child->nodeName;
}
$this->assertEquals('span', $after[1]);
}
public function testGeneratingPdf()
{
$state = [