Merge remote-tracking branch 'upstream/v5-develop' into v5-client-portal-tests-dusk

This commit is contained in:
Benjamin Beganović 2021-07-12 15:58:53 +02:00
commit 2be806bb8e
136 changed files with 178775 additions and 176414 deletions

View File

@ -4,14 +4,19 @@
![v5-develop phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-develop)
![v5-stable phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-stable)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/d39acb4bf0f74a0698dc77f382769ba5)](https://www.codacy.com/app/turbo124/invoiceninja?utm_source=github.com&utm_medium=referral&utm_content=invoiceninja/invoiceninja&utm_campaign=Badge_Grade)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/d16c78aad8574466bf83232b513ef4fb)](https://www.codacy.com/gh/turbo124/invoiceninja/dashboard?utm_source=github.com&utm_medium=referral&utm_content=turbo124/invoiceninja&utm_campaign=Badge_Grade)
# Invoice Ninja version 5!
## Quick Start
## Preamble
Currently the client portal and API are of alpha quality, to get started:
Version 5 of Invoice Ninja is here! We've taken the best parts of version 4 and bolted on all of the most requested features to produce a invoicing application like no other.
The new interface has a lot more functionality so it isn't a carbon copy of v4, but once you get used to the new layout and functionality we are sure you will love it!
If you have any questions, please join us on our [forum](https://forum.invoiceninja.com) or on [slack](https://invoiceninja.slack.com)
## Quick Start
```bash
git clone https://github.com/invoiceninja/invoiceninja.git
@ -69,6 +74,8 @@ To improve chances of PRs being merged please include tests to ensure your code
API documentation is hosted using Swagger and can be found [HERE](https://app.swaggerhub.com/apis/invoiceninja/invoiceninja)
Installation, Configuration and Troubleshooting documentation can be found [HERE] (https://invoiceninja.github.io)
## Credits
* [Hillel Coren](https://hillelcoren.com/)
* [David Bomba](https://github.com/turbo124)
@ -85,10 +92,18 @@ API documentation is hosted using Swagger and can be found [HERE](https://app.sw
## Current work in progress
Invoice Ninja is currently being written in a combination of Laravel for the API and Client Portal and Flutter for the front end management console. This will allow an immersive and consistent experience across any device: mobile, tablet or desktop.
Invoice Ninja is written in a combination of technologies:
To manage our workflow we will be creating separate branches for the client (Flutter) and server (Laravel API / Client Portal) and merge these into a release branch for deployments.
API - Laravel
Client Portal - Laravel + Tailwind
Admin Portal - Flutter
This allows an immersive and consistent experience across any device: mobile, tablet or desktop.
## Security
If you find a security issue with this application please send an email to contact@invoiceninja.com Please follow responsible disclosure procedures if you detect an issue. For further information on responsible disclosure please read [here](https://cheatsheetseries.owasp.org/cheatsheets/Vulnerability_Disclosure_Cheat_Sheet.html)
## License
Invoice Ninja is released under the Attribution Assurance License.
Invoice Ninja is released under the Elastic License.
See [LICENSE](LICENSE) for details.

View File

@ -1 +1 @@
5.2.7
5.2.11

View File

@ -234,38 +234,38 @@ class CheckData extends Command
}
}
// check for more than one primary contact
$clients = DB::table('clients')
->leftJoin('client_contacts', function ($join) {
$join->on('client_contacts.client_id', '=', 'clients.id')
->where('client_contacts.is_primary', '=', true)
->whereNull('client_contacts.deleted_at');
})
->groupBy('clients.id')
->havingRaw('count(client_contacts.id) != 1');
// // check for more than one primary contact
// $clients = DB::table('clients')
// ->leftJoin('client_contacts', function ($join) {
// $join->on('client_contacts.client_id', '=', 'clients.id')
// ->where('client_contacts.is_primary', '=', true)
// ->whereNull('client_contacts.deleted_at');
// })
// ->groupBy('clients.id')
// ->havingRaw('count(client_contacts.id) != 1');
if ($this->option('client_id')) {
$clients->where('clients.id', '=', $this->option('client_id'));
}
// if ($this->option('client_id')) {
// $clients->where('clients.id', '=', $this->option('client_id'));
// }
$clients = $clients->get(['clients.id', 'clients.user_id', 'clients.company_id']);
$this->logMessage($clients->count().' clients without a single primary contact');
// $clients = $clients->get(['clients.id', 'clients.user_id', 'clients.company_id']);
// // $this->logMessage($clients->count().' clients without a single primary contact');
if ($this->option('fix') == 'true') {
foreach ($clients as $client) {
$this->logMessage("Fixing missing primary contacts #{$client->id}");
// // if ($this->option('fix') == 'true') {
// // foreach ($clients as $client) {
// // $this->logMessage("Fixing missing primary contacts #{$client->id}");
$new_contact = ClientContactFactory::create($client->company_id, $client->user_id);
$new_contact->client_id = $client->id;
$new_contact->contact_key = Str::random(40);
$new_contact->is_primary = true;
$new_contact->save();
}
}
// // $new_contact = ClientContactFactory::create($client->company_id, $client->user_id);
// // $new_contact->client_id = $client->id;
// // $new_contact->contact_key = Str::random(40);
// // $new_contact->is_primary = true;
// // $new_contact->save();
// // }
// // }
if ($clients->count() > 0) {
$this->isValid = false;
}
// if ($clients->count() > 0) {
// $this->isValid = false;
// }
}
private function checkFailedJobs()
@ -365,7 +365,7 @@ class CheckData extends Command
/* Due to accounting differences we need to perform a second loop here to ensure there actually is an issue */
$clients->each(function ($client_record) use ($credit_total_applied) {
$client = Client::find($client_record->id);
$client = Client::withTrashed()->find($client_record->id);
$total_invoice_payments = 0;
@ -594,6 +594,7 @@ class CheckData extends Command
'client',
'client_contact',
'payment',
'recurring_invoice',
],
'invoices' => [
'client',

View File

@ -64,7 +64,7 @@ class Kernel extends ConsoleKernel
$schedule->job(new AutoBillCron)->dailyAt('00:30')->withoutOverlapping();
$schedule->job(new SchedulerCheck)->everyFiveMinutes();
$schedule->job(new SchedulerCheck)->daily()->withoutOverlapping();
/* Run hosted specific jobs */
if (Ninja::isHosted()) {

View File

@ -243,7 +243,7 @@ class CompanySettings extends BaseSettings
public $font_size = 7; //@implemented
public $primary_font = 'Roboto';
public $secondary_font = 'Roboto';
public $primary_color = '#142cb5';
public $primary_color = '#298AAB';
public $secondary_color = '#7081e0';
public $hide_paid_to_date = false; //@TODO where?

View File

@ -19,6 +19,8 @@ class InvoiceItem
public $product_key = '';
public $product_cost = 0;
public $notes = '';
public $discount = 0;
@ -57,6 +59,7 @@ class InvoiceItem
'type_id' => 'string',
'quantity' => 'float',
'cost' => 'float',
'product_cost' => 'float',
'product_key' => 'string',
'notes' => 'string',
'discount' => 'float',

View File

@ -81,17 +81,23 @@ class Handler extends ExceptionHandler
app('sentry')->configureScope(function (Scope $scope): void {
if(auth()->guard('contact') && auth()->guard('contact')->user())
$name = 'hosted@invoiceninja.com';
if(auth()->guard('contact') && auth()->guard('contact')->user()){
$name = "Contact = ".auth()->guard('contact')->user()->email;
$key = auth()->guard('contact')->user()->company->account->key;
elseif (auth()->guard('user') && auth()->guard('user')->user())
}
elseif (auth()->guard('user') && auth()->guard('user')->user()){
$name = "Admin = ".auth()->guard('user')->user()->email;
$key = auth()->user()->account->key;
}
else
$key = 'Anonymous';
$scope->setUser([
'id' => 'Hosted_User',
'id' => $key,
'email' => 'hosted@invoiceninja.com',
'name' => $key,
'name' => $name,
]);
});
@ -120,8 +126,7 @@ class Handler extends ExceptionHandler
}
}
// if(config('ninja.expanded_logging'))
parent::report($exception);
parent::report($exception);
}
@ -191,7 +196,7 @@ class Handler extends ExceptionHandler
} elseif ($exception instanceof GenericPaymentDriverFailure && $request->expectsJson()) {
return response()->json(['message' => $exception->getMessage()], 400);
} elseif ($exception instanceof GenericPaymentDriverFailure) {
$data['message'] = $exception->getMessage();
return response()->json(['message' => $exception->getMessage()], 400);
}
return parent::render($request, $exception);

View File

@ -22,7 +22,7 @@ class ExpenseCategoryFactory
$expense->company_id = $company_id;
$expense->name = '';
$expense->is_deleted = false;
$expense->color = '#fff';
$expense->color = '';
return $expense;
}

View File

@ -21,7 +21,7 @@ class TaskStatusFactory
$task_status->user_id = $user_id;
$task_status->company_id = $company_id;
$task_status->name = '';
$task_status->color = '#fff';
$task_status->color = '';
$task_status->status_order = 9999;
return $task_status;

View File

@ -12,6 +12,7 @@
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\ClientPortal\Contact\ContactPasswordResetRequest;
use App\Libraries\MultiDB;
use App\Models\Account;
use Illuminate\Contracts\View\Factory;
@ -73,9 +74,8 @@ class ContactForgotPasswordController extends Controller
return Password::broker('contacts');
}
public function sendResetLinkEmail(Request $request)
public function sendResetLinkEmail(ContactPasswordResetRequest $request)
{
//MultiDB::userFindAndSetDb($request->input('email'));
$user = MultiDB::hasContact($request->input('email'));

View File

@ -35,10 +35,16 @@ class ContactLoginController extends Controller
public function showLoginForm(Request $request)
{
if (strpos($request->getHost(), 'invoicing.co') !== false) {
//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) {
$subdomain = explode('.', $request->getHost())[0];
$company = Company::where('subdomain', $subdomain)->first();
} elseif (Ninja::isSelfHost()) {
} elseif(Ninja::isHosted() && $company = Company::where('portal_domain', $request->getSchemeAndHttpHost())->first()){
}
elseif (Ninja::isSelfHost()) {
$company = Account::first()->default_company;
} else {
$company = null;

View File

@ -488,6 +488,8 @@ class LoginController extends BaseController
auth()->user()->email_verified_at = now();
auth()->user()->save();
auth()->user()->setCompany(auth()->user()->account->default_company);
$this->setLoginCache(auth()->user());
$cu = CompanyUser::whereUserId(auth()->user()->id);

View File

@ -24,6 +24,7 @@ use Illuminate\Contracts\View\Factory;
use Illuminate\View\View;
use ZipStream\Option\Archive;
use ZipStream\ZipStream;
use Illuminate\Support\Facades\Storage;
class InvoiceController extends Controller
{
@ -170,8 +171,10 @@ class InvoiceController extends Controller
$invitation = $invoice->invitations->first();
//$file = $invoice->pdf_file_path($invitation);
$file = $invoice->service()->getInvoicePdf(auth()->user());
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);;
// return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);;
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file));
}
// enable output of HTTP headers

View File

@ -27,6 +27,7 @@ use Illuminate\View\View;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use ZipStream\Option\Archive;
use ZipStream\ZipStream;
use Illuminate\Support\Facades\Storage;
class QuoteController extends Controller
{
@ -89,8 +90,11 @@ class QuoteController extends Controller
if ($quotes->count() == 1) {
$file = $quotes->first()->pdf_file_path();
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
$file = $quotes->first()->service()->getQuotePdf();
// return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file));
}
// enable output of HTTP headers

View File

@ -474,6 +474,10 @@ class CompanyController extends BaseController
*/
public function destroy(DestroyCompanyRequest $request, Company $company)
{
if(Ninja::isHosted() && config('ninja.ninja_default_company_id') == $company->id)
return response()->json(['message' => 'Cannot purge this company'], 400);
$company_count = $company->account->companies->count();
$account = $company->account;
$account_key = $account->key;

View File

@ -37,6 +37,7 @@ use App\Utils\TempFile;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SavesDocuments;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
/**
* Class CreditController.
@ -536,8 +537,14 @@ class CreditController extends BaseController
}
break;
case 'download':
$file = $credit->pdf_file_path();
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
// $file = $credit->pdf_file_path();
$file = $credit->service()->getCreditPdf($credit->invitations->first());
// return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file));
break;
case 'archive':
$this->credit_repository->archive($credit);
@ -585,9 +592,12 @@ class CreditController extends BaseController
// $contact = $invitation->contact;
$credit = $invitation->credit;
$file_path = $credit->service()->getCreditPdf($invitation);
return response()->download($file_path, basename($file_path), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
$file = $credit->service()->getCreditPdf($invitation);
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file));
// return response()->download($file_path, basename($file_path), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
}
/**

View File

@ -672,8 +672,17 @@ class InvoiceController extends BaseController
break;
case 'download':
$file = $invoice->pdf_file_path();
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
// $file = $invoice->pdf_file_path();
// return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
$file = $invoice->service()->getInvoicePdf();
// return response()->download(Storage::get($file), basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file));
break;
case 'restore':
@ -722,10 +731,11 @@ class InvoiceController extends BaseController
}
//touch reminder1,2,3_sent + last_sent here if the email is a reminder.
$invoice->service()->touchReminder($this->reminder_template)->deletePdf()->save();
//$invoice->service()->touchReminder($this->reminder_template)->deletePdf()->save();
$invoice->service()->touchReminder($this->reminder_template)->markSent()->save();
$invoice->invitations->load('contact.client.country', 'invoice.client.country', 'invoice.company')->each(function ($invitation) use ($invoice) {
EmailEntity::dispatch($invitation, $invoice->company, $this->reminder_template);
EmailEntity::dispatch($invitation, $invoice->company, $this->reminder_template)->delay(now()->addSeconds(30));
});
if ($invoice->invitations->count() >= 1) {
@ -795,8 +805,11 @@ class InvoiceController extends BaseController
$file = $invoice->service()->getInvoicePdf($contact);
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
// return response()->download(Storage::get($file), basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file));
}
/**
@ -848,7 +861,10 @@ class InvoiceController extends BaseController
$file = $invoice->service()->getInvoiceDeliveryNote($invoice, $invoice->invitations->first()->contact);
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
// return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file));
}

View File

@ -16,6 +16,7 @@ use App\Utils\CurlUtils;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use stdClass;
use Carbon\Carbon;
class LicenseController extends BaseController
{
@ -152,7 +153,7 @@ class LicenseController extends BaseController
{
$account = auth()->user()->company()->account;
if($account->plan == 'white_label' && $account->plan_expires->lt(now())){
if($account->plan == 'white_label' && Carbon::parse($account->plan_expires)->lt(now())){
$account->plan = null;
$account->plan_expires = null;
$account->save();

View File

@ -82,6 +82,9 @@ class MigrationController extends BaseController
*/
public function purgeCompany(Company $company)
{
if(Ninja::isHosted() && config('ninja.ninja_default_company_id') == $company->id)
return response()->json(['message' => 'Cannot purge this company'], 400);
$account = $company->account;
$company_id = $company->id;
@ -102,6 +105,9 @@ class MigrationController extends BaseController
private function purgeCompanyWithForceFlag(Company $company)
{
if(Ninja::isHosted() && config('ninja.ninja_default_company_id') == $company->id)
return response()->json(['message' => 'Cannot purge this company'], 400);
$account = $company->account;
$company_id = $company->id;

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\Controllers;
use App\Http\Requests\Payments\PaymentNotificationWebhookRequest;
use App\Libraries\MultiDB;
use App\Models\Client;
use App\Models\CompanyGateway;
use App\Utils\Traits\MakesHash;
use Auth;
class PaymentNotificationWebhookController extends Controller
{
use MakesHash;
public function __invoke(PaymentNotificationWebhookRequest $request, string $company_key, string $company_gateway_id, string $client_hash)
{
$company_gateway = CompanyGateway::find($this->decodePrimaryKey($company_gateway_id));
$client = Client::find($this->decodePrimaryKey($client_hash));
return $company_gateway
->driver($client)
->processWebhookRequest($request);
}
}

View File

@ -39,6 +39,7 @@ use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SavesDocuments;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
/**
* Class QuoteController.
@ -676,8 +677,14 @@ class QuoteController extends BaseController
break;
case 'download':
$file = $quote->pdf_file_path();
return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
//$file = $quote->pdf_file_path();
$file = $quote->service()->getQuotePdf();
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file));
//return response()->download($file, basename($file), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
break;
case 'restore':
@ -728,9 +735,14 @@ class QuoteController extends BaseController
$contact = $invitation->contact;
$quote = $invitation->quote;
$file_path = $quote->service()->getQuotePdf($contact);
$file = $quote->service()->getQuotePdf($contact);
nlog($file);
return response()->download($file_path, basename($file_path), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file));
// return response()->download($file_path, basename($file_path), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
}
/**

View File

@ -33,6 +33,7 @@ use App\Utils\Traits\SavesDocuments;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
/**
* Class RecurringInvoiceController.
@ -500,9 +501,12 @@ class RecurringInvoiceController extends BaseController
$contact = $invitation->contact;
$recurring_invoice = $invitation->recurring_invoice;
$file_path = $recurring_invoice->service()->getInvoicePdf($contact);
$file = $recurring_invoice->service()->getInvoicePdf($contact);
return response()->streamDownload(function () use($file) {
echo Storage::get($file);
}, basename($file));
return response()->download($file_path, basename($file_path), ['Cache-Control:' => 'no-cache'])->deleteFileAfterSend(true);
}
/**

View File

@ -241,6 +241,11 @@ class SetupController extends Controller
$pdf->setChromiumPath(config('ninja.snappdf_chromium_path'));
}
if (config('ninja.snappdf_chromium_arguments')) {
$pdf->clearChromiumArguments();
$pdf->addChromiumArguments(config('ninja.snappdf_chromium_arguments'));
}
$pdf = $pdf
->setHtml('GENERATING PDFs WORKS! Thank you for using Invoice Ninja!')
->generate();

View File

@ -60,12 +60,6 @@ class StripeConnectController extends BaseController
$redirect_uri = 'https://invoicing.co/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);
}
@ -87,18 +81,24 @@ class StripeConnectController extends BaseController
}
// nlog($response);
$company = Company::where('company_key', $request->getTokenContent()['company_key'])->first();
$company_gateway = CompanyGatewayFactory::create($company->id, $company->owner()->id);
$fees_and_limits = new \stdClass;
$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->token_billing = 'always';
// $company_gateway->save();
$company_gateway = CompanyGateway::query()
->where('gateway_key', 'd14dd26a47cecc30fdd65700bfb67b34')
->where('company_id', $company->id)
->first();
if(!$company_gateway)
{
$company_gateway = CompanyGatewayFactory::create($company->id, $company->owner()->id);
$fees_and_limits = new \stdClass;
$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->token_billing = 'always';
// $company_gateway->save();
}
$payload = [
'account_id' => $response->stripe_user_id,
@ -111,18 +111,6 @@ class StripeConnectController extends BaseController
"access_token" => $response->access_token
];
/* Link account if existing account exists */
// if($account_id = $this->checkAccountAlreadyLinkToEmail($company_gateway, $request->getContact()->email)) {
// $payload['account_id'] = $account_id;
// $payload['stripe_user_id'] = $account_id;
// $company_gateway->setConfig($payload);
// $company_gateway->save();
// return view('auth.connect.existing');
// }
$company_gateway->setConfig($payload);
$company_gateway->save();

View File

@ -35,6 +35,9 @@ class StripeController extends BaseController
public function import()
{
return response()->json(['message' => 'Processing'], 200);
if(auth()->user()->isAdmin())
{

View File

@ -34,11 +34,15 @@ class CreditsTable extends Component
public function render()
{
$query = Credit::query()
->where('client_id', auth('contact')->user()->client->id)
->where('company_id', $this->company->id)
->where('status_id', '<>', Credit::STATUS_DRAFT)
->whereDate('due_date', '<=', now())
->orWhere('due_date', NULL)
->where(function ($query){
$query->whereDate('due_date', '<=', now())
->orWhereNull('due_date');
})
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->paginate($this->per_page);

View File

@ -44,6 +44,7 @@ class InvoicesTable extends Component
$query = Invoice::query()
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->where('company_id', $this->company->id)
->where('is_deleted', false);
if (in_array('paid', $this->status)) {

View File

@ -34,6 +34,7 @@ class PaymentMethodsTable extends Component
{
$query = ClientGatewayToken::query()
->with('gateway_type')
->where('company_id', $this->company->id)
->where('client_id', $this->client->id)
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->paginate($this->per_page);

View File

@ -41,6 +41,7 @@ class PaymentsTable extends Component
{
$query = Payment::query()
->with('type', 'client')
->where('company_id', $this->company->id)
->where('client_id', auth('contact')->user()->client->id)
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->paginate($this->per_page);

View File

@ -45,6 +45,7 @@ class QuotesTable extends Component
}
$query = $query
->where('company_id', $this->company->id)
->where('client_id', auth('contact')->user()->client->id)
->where('status_id', '<>', Quote::STATUS_DRAFT)
->paginate($this->per_page);

View File

@ -12,6 +12,7 @@
namespace App\Http\Livewire;
use App\Libraries\MultiDB;
use App\Models\RecurringInvoice;
use App\Utils\Traits\WithSorting;
use Livewire\Component;
@ -23,8 +24,12 @@ class RecurringInvoicesTable extends Component
public $per_page = 10;
public $company;
public function mount()
{
MultiDB::setDb($this->company->db);
$this->sort_asc = false;
$this->sort_field = 'date';
@ -36,6 +41,7 @@ class RecurringInvoicesTable extends Component
$query = $query
->where('client_id', auth('contact')->user()->client->id)
->where('company_id', $this->company->id)
->whereIn('status_id', [RecurringInvoice::STATUS_PENDING, RecurringInvoice::STATUS_ACTIVE, RecurringInvoice::STATUS_PAUSED,RecurringInvoice::STATUS_COMPLETED])
->orderBy('status_id', 'asc')
->with('client')

View File

@ -36,6 +36,7 @@ class SubscriptionRecurringInvoicesTable extends Component
{
$query = RecurringInvoice::query()
->where('client_id', auth('contact')->user()->client->id)
->where('company_id', $this->company->id)
->whereNotNull('subscription_id')
->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc')
->paginate($this->per_page);

View File

@ -35,6 +35,7 @@ class TasksTable extends Component
public function render()
{
$query = Task::query()
->where('company_id', $this->company->id)
->where('client_id', auth('contact')->user()->client->id);
if ($this->company->getSetting('show_all_tasks_client_portal') === 'invoiced') {

View File

@ -51,10 +51,10 @@ class QueryLogging
$count = count($queries);
$timeEnd = microtime(true);
$time = $timeEnd - $timeStart;
//nlog($request->method().' - '.urldecode($request->url()).": $count queries - ".$time);
// if($count > 50)
//nlog($queries);
if($count > 150)
nlog($queries);
$ip = '';
if(request()->header('Cf-Connecting-Ip'))

View File

@ -30,17 +30,22 @@ class UrlSetDb
*/
public function handle($request, Closure $next)
{
if (config('ninja.db.multi_db_enabled')) {
$hashids = new Hashids('', 10); //decoded output is _always_ an array.
$hashids = new Hashids(config('ninja.hash_salt'), 10);
//parse URL hash and set DB
$segments = explode('-', $request->route('confirmation_code'));
if(!is_array($segments))
return response()->json(['message' => 'Invalid confirmation code'], 403);
$hashed_db = $hashids->decode($segments[0]);
MultiDB::setDB(MultiDB::DB_PREFIX.str_pad($hashed_db[0], 2, '0', STR_PAD_LEFT));
}
return $next($request);
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests\ClientPortal\Contact;
use Illuminate\Foundation\Http\FormRequest;
class ContactPasswordResetRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'email' => 'required',
];
}
}

View File

@ -67,8 +67,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(Ninja::isHosted() && array_key_exists('portal_domain', $input) && strlen($input['portal_domain']) > 1)
$input['portal_domain'] = $this->addScheme($input['portal_domain']);
if (array_key_exists('settings', $input)) {
$input['settings'] = $this->filterSaveableSettings($input['settings']);
@ -105,4 +105,15 @@ class UpdateCompanyRequest extends Request
return $settings;
}
private function addScheme($url, $scheme = 'https://')
{
$url = str_replace("http://", "", $url);
$url = parse_url($url, PHP_URL_SCHEME) === null ? $scheme . $url : $url;
return rtrim($url, '/');
}
}

View File

@ -59,7 +59,7 @@ class StoreExpenseRequest extends Request
}
if(array_key_exists('color', $input) && is_null($input['color']))
$input['color'] = '#fff';
$input['color'] = '';
$this->replace($input);
}

View File

@ -43,7 +43,7 @@ class StoreExpenseCategoryRequest extends Request
$input = $this->decodePrimaryKeys($input);
if(array_key_exists('color', $input) && is_null($input['color']))
$input['color'] = '#fff';
$input['color'] = '';
$this->replace($input);
}

View File

@ -47,7 +47,7 @@ class UpdateExpenseCategoryRequest extends Request
$input = $this->all();
if(array_key_exists('color', $input) && is_null($input['color']))
$input['color'] = '#fff';
$input['color'] = '';
$this->replace($input);
}

View File

@ -28,12 +28,7 @@ class ImportJsonRequest extends Request
public function rules()
{
return [
// 'import_type' => 'required',
// 'files' => 'required_without:hash|array|min:1|max:6',
// 'hash' => 'nullable|string',
// 'column_map' => 'required_with:hash|array',
// 'skip_header' => 'required_with:hash|boolean',
// 'files.*' => 'file|mimes:csv,txt',
'files' => 'file|mimes:zip',
];
}
}

View File

@ -0,0 +1,42 @@
<?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\Payments;
use App\Http\Requests\Request;
use App\Libraries\MultiDB;
use App\Models\Client;
use App\Models\Company;
use App\Models\CompanyGateway;
use App\Models\Payment;
use App\Models\PaymentHash;
use App\Utils\Traits\MakesHash;
class PaymentNotificationWebhookRequest extends Request
{
use MakesHash;
public function authorize()
{
MultiDB::findAndSetDbByCompanyKey($this->company_key);
return true;
}
public function rules()
{
return [
//
];
}
}

View File

@ -51,7 +51,7 @@ class StoreProjectRequest extends Request
if(array_key_exists('color', $input) && is_null($input['color']))
$input['color'] = '#fff';
$input['color'] = '';
$this->replace($input);
}

View File

@ -49,7 +49,7 @@ class UpdateProjectRequest extends Request
}
if(array_key_exists('color', $input) && is_null($input['color']))
$input['color'] = '#fff';
$input['color'] = '';
$this->replace($input);
}

View File

@ -51,7 +51,7 @@ class UpdateTaskRequest extends Request
}
if(array_key_exists('color', $input) && is_null($input['color']))
$input['color'] = '#fff';
$input['color'] = '';
$this->replace($input);
}

View File

@ -33,7 +33,7 @@ class StoreTaskStatusRequest extends Request
$input = $this->all();
if(array_key_exists('color', $input) && is_null($input['color']))
$input['color'] = '#fff';
$input['color'] = '';
$this->replace($input);
}

View File

@ -46,7 +46,7 @@ class UpdateTaskStatusRequest extends Request
$input = $this->all();
if(array_key_exists('color', $input) && is_null($input['color']))
$input['color'] = '#fff';
$input['color'] = '';
$this->replace($input);
}

View File

@ -28,6 +28,7 @@ use App\Models\Account;
use App\Models\Timezone;
use App\Notifications\Ninja\NewAccountCreated;
use App\Utils\Ninja;
use App\Utils\Traits\User\LoginCache;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -39,6 +40,7 @@ use Turbo124\Beacon\Facades\LightLogs;
class CreateAccount
{
use Dispatchable;
use LoginCache;
protected $request;
@ -77,9 +79,6 @@ class CreateAccount
$sp794f3f->save();
if(Ninja::isHosted())
$sp794f3f->startTrial('pro');
$sp035a66 = CreateCompany::dispatchNow($this->request, $sp794f3f);
$sp035a66->load('account');
$sp794f3f->default_company_id = $sp035a66->id;
@ -95,6 +94,8 @@ class CreateAccount
}
$spaa9f78->setCompany($sp035a66);
$this->setLoginCache($spaa9f78);
$spafe62e = isset($this->request['token_name']) ? $this->request['token_name'] : request()->server('HTTP_USER_AGENT');
$sp2d97e8 = CreateCompanyToken::dispatchNow($sp035a66, $spaa9f78, $spafe62e);

View File

@ -221,8 +221,8 @@ class CompanyImport implements ShouldQueue
private function unzipFile()
{
if(mime_content_type(Storage::path($this->file_location)) == 'text/plain')
return Storage::path($this->file_location);
// 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));

View File

@ -62,6 +62,7 @@ class SubscriptionCron
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->where('balance', '>', 0)
->whereDate('due_date', '<=', now()->addDay()->startOfDay())
->whereNull('deleted_at')
->whereNotNull('subscription_id')
->cursor();

View File

@ -86,9 +86,8 @@ class CreateEntityPdf implements ShouldQueue
$this->contact = $invitation->contact;
$this->disk = $disk;
// $this->disk = $disk ?? config('filesystems.default');
$this->disk = Ninja::isHosted() ? config('filesystems.default') : $disk;
}
public function handle()
@ -201,11 +200,9 @@ class CreateEntityPdf implements ShouldQueue
if(!Storage::disk($this->disk)->exists($path))
Storage::disk($this->disk)->makeDirectory($path, 0775);
Storage::disk($this->disk)->put($file_path, $pdf);
nlog($file_path);
Storage::disk($this->disk)->put($file_path, $pdf);
}
catch(\Exception $e)
{

View File

@ -190,6 +190,9 @@ class Import implements ShouldQueue
{
set_time_limit(0);
nlog("Starting Migration");
nlog($this->user->email);
auth()->login($this->user, false);
auth()->user()->setCompany($this->company);
@ -333,7 +336,7 @@ class Import implements ShouldQueue
$data = $this->transformCompanyData($data);
if(Ninja::isHosted() && strlen($data['subdomain']) > 1) {
if(Ninja::isHosted()) {
if(!MultiDB::checkDomainAvailable($data['subdomain']))
$data['subdomain'] = MultiDB::randomSubdomainGenerator();
@ -364,6 +367,10 @@ class Import implements ShouldQueue
unset($data['referral_code']);
}
if (isset($data['custom_fields']) && is_array($data['custom_fields'])) {
$data['custom_fields'] = $this->parseCustomFields($data['custom_fields']);
}
$company_repository = new CompanyRepository();
$company_repository->save($data, $this->company);
@ -385,6 +392,34 @@ class Import implements ShouldQueue
$company_repository = null;
}
private function parseCustomFields($fields) :array
{
if(array_key_exists('account1', $fields))
$fields['company1'] = $fields['account1'];
if(array_key_exists('account2', $fields))
$fields['company2'] = $fields['account2'];
if(array_key_exists('invoice1', $fields))
$fields['surcharge1'] = $fields['invoice1'];
if(array_key_exists('invoice2', $fields))
$fields['surcharge2'] = $fields['invoice2'];
if(array_key_exists('invoice_text1', $fields))
$fields['invoice1'] = $fields['invoice_text1'];
if(array_key_exists('invoice_text2', $fields))
$fields['invoice2'] = $fields['invoice_text2'];
foreach ($fields as &$value) {
$value = (string) $value;
}
return $fields;
}
private function transformCompanyData(array $data): array
{
$company_settings = CompanySettings::defaults();
@ -1321,7 +1356,7 @@ class Import implements ShouldQueue
$modified['fees_and_limits'] = $this->cleanFeesAndLimits($modified['fees_and_limits']);
}
else if(Ninja::isHosted() && $modified['gateway_key'] == 'd14dd26a37cecc30fdd65700bfb55b23'){
if(Ninja::isHosted() && $modified['gateway_key'] == 'd14dd26a37cecc30fdd65700bfb55b23'){
$modified['gateway_key'] = 'd14dd26a47cecc30fdd65700bfb67b34';
$modified['fees_and_limits'] = [];
}

View File

@ -18,6 +18,7 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Carbon\Carbon;
class VersionCheck implements ShouldQueue
{
@ -49,7 +50,7 @@ class VersionCheck implements ShouldQueue
if(!$account)
return;
if($account->plan == 'white_label' && $account->plan_expires && $account->plan_expires->lt(now())){
if($account->plan == 'white_label' && $account->plan_expires && Carbon::parse($account->plan_expires)->lt(now())){
$account->plan = null;
$account->plan_expires = null;
$account->save();

View File

@ -50,7 +50,7 @@ $user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars[
$fields->subscription_id = $subscription->id;
$fields->user_id = $user_id;
$fields->company_id = $subscription->company_id;
$fields->activity_type_id = Activity::ARCHIVE_SUBSCRIPTIOn;
$fields->activity_type_id = Activity::ARCHIVE_SUBSCRIPTION;
$this->activity_repo->save($fields, $subscription, $event->event_vars);
}

View File

@ -71,7 +71,6 @@ class PaymentNotification implements ShouldQueue
}
/*Google Analytics Track Revenue*/
if (isset($payment->company->google_analytics_key)) {
$this->trackRevenue($event);

View File

@ -11,6 +11,7 @@
namespace App\Mail\Engine;
use App\Models\Account;
use App\Utils\HtmlEngine;
use App\Utils\Ninja;
use App\Utils\Number;

View File

@ -12,6 +12,7 @@
namespace App\Mail\Engine;
use App\DataMapper\EmailTemplateDefaults;
use App\Jobs\Entity\CreateEntityPdf;
use App\Models\Account;
use App\Utils\HtmlEngine;
use App\Utils\Ninja;
@ -116,8 +117,6 @@ class InvoiceEmailEngine extends BaseEmailEngine
else
$this->setAttachments([$this->invoice->pdf_file_path($this->invitation)]);
// $this->setAttachments(['path' => $this->invoice->pdf_file_path(), 'name' => basename($this->invoice->pdf_file_path())]);
}
//attach third party documents

View File

@ -52,7 +52,7 @@ class SupportMessageSent extends Mailable
$account = auth()->user()->account;
$plan = $account->plan ?: 'Free Self Hosted';
$plan = $account->plan ?: 'Forever Free';
$company = auth()->user()->company();
$user = auth()->user();

View File

@ -55,7 +55,7 @@ class Account extends BaseModel
'promo_expires',
'discount_expires',
'trial_started',
'plan_expires'
// 'plan_expires'
];
const PLAN_FREE = 'free';
@ -120,6 +120,11 @@ class Account extends BaseModel
return $this->hasMany(CompanyUser::class);
}
public function owner()
{
return $this->hasMany(CompanyUser::class)->where('is_owner', true)->first() ? $this->hasMany(CompanyUser::class)->where('is_owner', true)->first()->user : false;
}
public function getPlan()
{
return $this->plan ?: '';

View File

@ -13,6 +13,8 @@ namespace App\Models;
use App\DataMapper\ClientSettings;
use App\DataMapper\CompanySettings;
use App\DataMapper\FeesAndLimits;
use App\Models\CompanyGateway;
use App\Models\Presenters\ClientPresenter;
use App\Services\Client\ClientService;
use App\Utils\Traits\AppSetup;
@ -396,61 +398,131 @@ class Client extends BaseModel implements HasLocalePreference
*/
public function getCreditCardGateway() :?CompanyGateway
{
$company_gateways = $this->getSetting('company_gateway_ids');
// $company_gateways = $this->getSetting('company_gateway_ids');
/* It is very important to respect the order of the company_gateway_ids as they are ordered by priority*/
if (strlen($company_gateways) >= 1) {
$transformed_ids = $this->transformKeys(explode(',', $company_gateways));
$gateways = $this->company
->company_gateways
->whereIn('id', $transformed_ids)
->sortby(function ($model) use ($transformed_ids) {
return array_search($model->id, $transformed_ids);
});
} else {
$gateways = $this->company->company_gateways;
}
// /* It is very important to respect the order of the company_gateway_ids as they are ordered by priority*/
// if (strlen($company_gateways) >= 1) {
// $transformed_ids = $this->transformKeys(explode(',', $company_gateways));
// $gateways = $this->company
// ->company_gateways
// ->whereIn('id', $transformed_ids)
// ->sortby(function ($model) use ($transformed_ids) {
// return array_search($model->id, $transformed_ids);
// });
// } else {
// $gateways = $this->company->company_gateways;
// }
foreach ($gateways as $gateway) {
if (in_array(GatewayType::CREDIT_CARD, $gateway->driver($this)->gatewayTypeEnabled(GatewayType::CREDIT_CARD))) {
return $gateway;
// foreach ($gateways as $gateway) {
// if (in_array(GatewayType::CREDIT_CARD, $gateway->driver($this)->gatewayTypeEnabled($gateway, GatewayType::CREDIT_CARD))) {
// return $gateway;
// }
// }
// return null;
//
$pms = $this->service()->getPaymentMethods(0);
foreach($pms as $pm)
{
if($pm['gateway_type_id'] == GatewayType::CREDIT_CARD)
{
$cg = CompanyGateway::find($pm['company_gateway_id']);
if($cg && !property_exists($cg->fees_and_limits, GatewayType::CREDIT_CARD)){
$fees_and_limits = $cg->fees_and_limits;
$fees_and_limits->{GatewayType::CREDIT_CARD} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
}
if($cg && $cg->fees_and_limits->{GatewayType::CREDIT_CARD}->is_enabled)
return $cg;
}
}
}
return null;
return null;
}
//todo refactor this - it is only searching for existing tokens
public function getBankTransferGateway() :?CompanyGateway
{
$company_gateways = $this->getSetting('company_gateway_ids');
$pms = $this->service()->getPaymentMethods(0);
if($this->currency()->code == 'USD' && in_array(GatewayType::BANK_TRANSFER, array_column($pms, 'gateway_type_id'))){
foreach($pms as $pm){
if($pm['gateway_type_id'] == GatewayType::BANK_TRANSFER)
{
$cg = CompanyGateway::find($pm['company_gateway_id']);
if($cg && !property_exists($cg->fees_and_limits, GatewayType::BANK_TRANSFER)){
$fees_and_limits = $cg->fees_and_limits;
$fees_and_limits->{GatewayType::BANK_TRANSFER} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
}
if($cg && $cg->fees_and_limits->{GatewayType::BANK_TRANSFER}->is_enabled)
return $cg;
}
}
if (strlen($company_gateways) >= 1) {
$transformed_ids = $this->transformKeys(explode(',', $company_gateways));
$gateways = $this->company
->company_gateways
->whereIn('id', $transformed_ids)
->sortby(function ($model) use ($transformed_ids) {
return array_search($model->id, $transformed_ids);
});
} else {
$gateways = $this->company->company_gateways;
}
foreach ($gateways as $gateway) {
if ($this->currency()->code == 'USD' && in_array(GatewayType::BANK_TRANSFER, $gateway->driver($this)->gatewayTypeEnabled(GatewayType::BANK_TRANSFER))) {
return $gateway;
if($this->currency()->code == 'EUR' && in_array(GatewayType::BANK_TRANSFER, array_column($pms, 'gateway_type_id'))){
foreach($pms as $pm){
if($pm['gateway_type_id'] == GatewayType::SEPA)
{
$cg = CompanyGateway::find($pm['company_gateway_id']);
if($cg && $cg->fees_and_limits->{GatewayType::SEPA}->is_enabled)
return $cg;
}
}
if ($this->currency()->code == 'EUR' && in_array(GatewayType::SEPA, $gateway->driver($this)->gatewayTypeEnabled(GatewayType::SEPA))) {
return $gateway;
}
}
return null;
// $company_gateways = $this->getSetting('company_gateway_ids');
// if (strlen($company_gateways) >= 1) {
// $transformed_ids = $this->transformKeys(explode(',', $company_gateways));
// $gateways = $this->company
// ->company_gateways
// ->whereIn('id', $transformed_ids)
// ->sortby(function ($model) use ($transformed_ids) {
// return array_search($model->id, $transformed_ids);
// });
// } else {
// $gateways = $this->company->company_gateways;
// }
// foreach ($gateways as $gateway) {
// if ($this->currency()->code == 'USD' && in_array(GatewayType::BANK_TRANSFER, $gateway->driver($this)->gatewayTypeEnabled(GatewayType::BANK_TRANSFER))) {
// return $gateway;
// }
// if ($this->currency()->code == 'EUR' && in_array(GatewayType::SEPA, $gateway->driver($this)->gatewayTypeEnabled(GatewayType::SEPA))) {
// return $gateway;
// }
// }
// return null;
}
public function getBankTransferMethodType()
{
if ($this->currency()->code == 'USD') {
return GatewayType::BANK_TRANSFER;
}

View File

@ -16,6 +16,7 @@ use App\Models\Presenters\CompanyPresenter;
use App\Models\User;
use App\Services\Notification\NotificationService;
use App\Utils\Ninja;
use App\Utils\Traits\AppSetup;
use App\Utils\Traits\CompanySettingsSaver;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\ThrottlesEmail;
@ -31,6 +32,7 @@ class Company extends BaseModel
use MakesHash;
use CompanySettingsSaver;
use ThrottlesEmail;
use AppSetup;
const ENTITY_RECURRING_INVOICE = 'recurring_invoice';
const ENTITY_CREDIT = 'credit';
@ -311,7 +313,17 @@ class Company extends BaseModel
public function timezone()
{
return Timezone::find($this->settings->timezone_id);
$timezones = Cache::get('timezones');
if(!$timezones)
$this->buildCache(true);
return $timezones->filter(function ($item) {
return $item->id == $this->settings->timezone_id;
})->first();
// return Timezone::find($this->settings->timezone_id);
}
public function designs()
@ -339,7 +351,18 @@ class Company extends BaseModel
*/
public function language()
{
return Language::find($this->settings->language_id);
$languages = Cache::get('languages');
if(!$languages)
$this->buildCache(true);
return $languages->filter(function ($item) {
return $item->id == $this->settings->language_id;
})->first();
// return Language::find($this->settings->language_id);
}
public function getLocale()

View File

@ -59,6 +59,17 @@ class CompanyGateway extends BaseModel
16 => ['card' => 'images/credit_cards/Test-Discover-Icon.png', 'text' => 'Discover'],
];
// const TYPE_PAYPAL = 300;
// const TYPE_STRIPE = 301;
// const TYPE_LEDGER = 302;
// const TYPE_FAILURE = 303;
// const TYPE_CHECKOUT = 304;
// const TYPE_AUTHORIZE = 305;
// const TYPE_CUSTOM = 306;
// const TYPE_BRAINTREE = 307;
// const TYPE_WEPAY = 309;
public $gateway_consts = [
'38f2c48af60c7dd69e04248cbb24c36e' => 300,
'd14dd26a37cecc30fdd65700bfb55b23' => 301,
@ -66,6 +77,8 @@ class CompanyGateway extends BaseModel
'3b6621f970ab18887c4f6dca78d3f8bb' => 305,
'54faab2ab6e3223dbe848b1686490baa' => 306,
'd14dd26a47cecc30fdd65700bfb67b34' => 301,
'8fdeed552015b3c7b44ed6c8ebd9e992' => 309,
'f7ec488676d310683fb51802d076d713' => 307,
];
protected $touches = [];
@ -358,6 +371,11 @@ class CompanyGateway extends BaseModel
return $fee;
}
public function webhookUrl()
{
return route('payment_webhook', ['company_key' => $this->company->company_key, 'company_gateway_id' => $this->hashed_id]);
}
/**
* we need to average out the gateway fees across all the invoices
* so lets iterate.
@ -399,4 +417,6 @@ class CompanyGateway extends BaseModel
return $this
->where('id', $this->decodePrimaryKey($value))->firstOrFail();
}
}

View File

@ -21,6 +21,10 @@ class UserPresenter extends EntityPresenter
*/
public function name()
{
if(!$this->entity)
return "No User Object Available";
$first_name = isset($this->entity->first_name) ? $this->entity->first_name : '';
$last_name = isset($this->entity->last_name) ? $this->entity->last_name : '';

View File

@ -67,6 +67,7 @@ class SystemLog extends Model
const TYPE_CUSTOM = 306;
const TYPE_BRAINTREE = 307;
const TYPE_WEPAY = 309;
const TYPE_PAYFAST = 310;
const TYPE_QUOTA_EXCEEDED = 400;

View File

@ -161,6 +161,8 @@ class User extends Authenticatable implements MustVerifyEmail
public function setCompany($company)
{
$this->company = $company;
return $this;
}
/**

View File

@ -119,7 +119,7 @@ class AuthorizeCreditCard
'data' => $this->formatGatewayResponse($data, $vars),
];
SystemLogger::dispatch($logger_message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_AUTHORIZE, $this->authorize->client);
SystemLogger::dispatch($logger_message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_AUTHORIZE, $this->authorize->client, $this->authorize->client->company);
return true;
} else {
@ -202,6 +202,7 @@ class AuthorizeCreditCard
private function processFailedResponse($data, $request)
{
$response = $data['response'];
$amount = array_key_exists('amount_with_fee', $data) ? $data['amount_with_fee'] : 0;
PaymentFailureMailer::dispatch($this->authorize->client, $response->getTransactionResponse()->getTransId(), $this->authorize->client->company, $data['amount_with_fee']);

View File

@ -548,6 +548,15 @@ class BaseDriver extends AbstractPaymentDriver
);
}
public function genericWebhookUrl()
{
return route('payment_notification_webhook', [
'company_key' => $this->client->company->company_key,
'company_gateway_id' => $this->encodePrimaryKey($this->company_gateway->id),
'client' => $this->encodePrimaryKey($this->client->id),
]);
}
/* Performs an extra iterate on the gatewayTypes() array and passes back only the enabled gateways*/
public function gatewayTypeEnabled($type)
{

View File

@ -11,6 +11,7 @@
namespace App\PaymentDrivers;
use App\Http\Requests\Payments\PaymentWebhookRequest;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\Models\Payment;
@ -39,6 +40,22 @@ class DriverTemplate extends BaseDriver
const SYSTEM_LOG_TYPE = SystemLog::TYPE_STRIPE; //define a constant for your gateway ie TYPE_YOUR_CUSTOM_GATEWAY - set the const in the SystemLog model
public function init()
{
return $this; /* This is where you boot the gateway with your auth credentials*/
}
/* Returns an array of gateway types for the payment gateway */
public function gatewayTypes(): array
{
$types = [];
$types[] = GatewayType::CREDIT_CARD;
return $types;
}
/* Sets the payment method initialized */
public function setPaymentMethod($payment_method_id)
{
$class = self::$methods[$payment_method_id];
@ -75,4 +92,8 @@ class DriverTemplate extends BaseDriver
{
return $this->payment_method->yourTokenBillingImplmentation(); //this is your custom implementation from here
}
public function processWebhookRequest(PaymentWebhookRequest $request, Payment $payment = null)
{
}
}

View File

@ -0,0 +1,282 @@
<?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\PaymentDrivers\PayFast;
use App\Exceptions\PaymentFailed;
use App\Jobs\Mail\PaymentFailureMailer;
use App\Jobs\Util\SystemLogger;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\PayFastPaymentDriver;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
class CreditCard
{
public $payfast;
public function __construct(PayFastPaymentDriver $payfast)
{
$this->payfast = $payfast;
}
/*
$data = array();
$data['merchant_id'] = $this->getMerchantId();
$data['merchant_key'] = $this->getMerchantKey();
$data['return_url'] = $this->getReturnUrl();
$data['cancel_url'] = $this->getCancelUrl();
$data['notify_url'] = $this->getNotifyUrl();
if ($this->getCard()) {
$data['name_first'] = $this->getCard()->getFirstName();
$data['name_last'] = $this->getCard()->getLastName();
$data['email_address'] = $this->getCard()->getEmail();
}
$data['m_payment_id'] = $this->getTransactionId();
$data['amount'] = $this->getAmount();
$data['item_name'] = $this->getDescription();
$data['custom_int1'] = $this->getCustomInt1();
$data['custom_int2'] = $this->getCustomInt2();
$data['custom_int3'] = $this->getCustomInt3();
$data['custom_int4'] = $this->getCustomInt4();
$data['custom_int5'] = $this->getCustomInt5();
$data['custom_str1'] = $this->getCustomStr1();
$data['custom_str2'] = $this->getCustomStr2();
$data['custom_str3'] = $this->getCustomStr3();
$data['custom_str4'] = $this->getCustomStr4();
$data['custom_str5'] = $this->getCustomStr5();
if ($this->getPaymentMethod()) {
$data['payment_method'] = $this->getPaymentMethod();
}
if (1 == $this->getSubscriptionType()) {
$data['subscription_type'] = $this->getSubscriptionType();
$data['billing_date'] = $this->getBillingDate();
$data['recurring_amount'] = $this->getRecurringAmount();
$data['frequency'] = $this->getFrequency();
$data['cycles'] = $this->getCycles();
}
if (2 == $this->getSubscriptionType()) {
$data['subscription_type'] = $this->getSubscriptionType();
}
$data['passphrase'] = $this->getParameter('passphrase'); 123456789012aV
$data['signature'] = $this->generateSignature($data);
*/
public function authorizeView($data)
{
$hash = Str::random(32);
Cache::put($hash, 'cc_auth', 300);
$data = [
'merchant_id' => $this->payfast->company_gateway->getConfigField('merchantId'),
'merchant_key' => $this->payfast->company_gateway->getConfigField('merchantKey'),
'return_url' => route('client.payment_methods.index'),
'cancel_url' => route('client.payment_methods.index'),
'notify_url' => $this->payfast->genericWebhookUrl(),
'm_payment_id' => $hash,
'amount' => 5,
'item_name' => 'pre-auth',
'item_description' => 'Credit Card Pre Authorization',
'subscription_type' => 2,
'passphrase' => $this->payfast->company_gateway->getConfigField('passphrase'),
];
$data['signature'] = $this->payfast->generateSignature($data);
$data['gateway'] = $this->payfast;
$data['payment_endpoint_url'] = $this->payfast->endpointUrl();
return render('gateways.payfast.authorize', $data);
}
/*
'm_payment_id' => NULL,
'pf_payment_id' => '1409993',
'payment_status' => 'COMPLETE',
'item_name' => 'pre-auth',
'item_description' => NULL,
'amount_gross' => '5.00',
'amount_fee' => '-2.53',
'amount_net' => '2.47',
'custom_str1' => NULL,
'custom_str2' => NULL,
'custom_str3' => NULL,
'custom_str4' => NULL,
'custom_str5' => NULL,
'custom_int1' => NULL,
'custom_int2' => NULL,
'custom_int3' => NULL,
'custom_int4' => NULL,
'custom_int5' => NULL,
'name_first' => NULL,
'name_last' => NULL,
'email_address' => NULL,
'merchant_id' => '10023100',
'token' => '34b66bc2-3c54-9590-03ea-42ee8b89922a',
'billing_date' => '2021-07-05',
'signature' => 'ebdb4ca937d0e3f43462841c0afc6ad9',
'q' => '/payment_notification_webhook/EhbnVYyzJZyccY85hcHIkIzNPI2rtHzznAyyyG73oSxZidAdN9gf8BvAKDomqeHp/4openRe7Az/WPe99p3eLy',
*/
public function authorizeResponse($request)
{
$data = $request->all();
$cgt = [];
$cgt['token'] = $data['token'];
$cgt['payment_method_id'] = GatewayType::CREDIT_CARD;
$payment_meta = new \stdClass;
$payment_meta->exp_month = 'xx';
$payment_meta->exp_year = 'xx';
$payment_meta->brand = 'CC';
$payment_meta->last4 = 'xxxx';
$payment_meta->type = GatewayType::CREDIT_CARD;
$cgt['payment_meta'] = $payment_meta;
$token = $this->payfast->storeGatewayToken($cgt, []);
return response()->json([], 200);
}
public function paymentView($data)
{
$payfast_data = [
'merchant_id' => $this->payfast->company_gateway->getConfigField('merchantId'),
'merchant_key' => $this->payfast->company_gateway->getConfigField('merchantKey'),
'return_url' => route('client.payments.index'),
'cancel_url' => route('client.payment_methods.index'),
'notify_url' => $this->payfast->genericWebhookUrl(),
'm_payment_id' => $data['payment_hash'],
'amount' => $data['amount_with_fee'],
'item_name' => 'purchase',
'item_description' => ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number'),
'passphrase' => $this->payfast->company_gateway->getConfigField('passphrase'),
];
$payfast_data['signature'] = $this->payfast->generateSignature($payfast_data);
$payfast_data['gateway'] = $this->payfast;
$payfast_data['payment_endpoint_url'] = $this->payfast->endpointUrl();
return render('gateways.payfast.pay', array_merge($data, $payfast_data));
}
/*
[2021-07-05 11:21:24] local.INFO: array (
'm_payment_id' => 'B7G9Q2vPhqkLEoMwwY1paXvPGuFxpbDe',
'pf_payment_id' => '1410364',
'payment_status' => 'COMPLETE',
'item_name' => 'purchase',
'item_description' => 'Invoices: ["0001"]',
'amount_gross' => '100.00',
'amount_fee' => '-2.30',
'amount_net' => '97.70',
'custom_str1' => NULL,
'custom_str2' => NULL,
'custom_str3' => NULL,
'custom_str4' => NULL,
'custom_str5' => NULL,
'custom_int1' => NULL,
'custom_int2' => NULL,
'custom_int3' => NULL,
'custom_int4' => NULL,
'custom_int5' => NULL,
'name_first' => NULL,
'name_last' => NULL,
'email_address' => NULL,
'merchant_id' => '10023100',
'signature' => '3ed27638479fd65cdffb0f4910679d10',
'q' => '/payment_notification_webhook/EhbnVYyzJZyccY85hcHIkIzNPI2rtHzznAyyyG73oSxZidAdN9gf8BvAKDomqeHp/4openRe7Az/WPe99p3eLy',
)
*/
public function paymentResponse(Request $request)
{
$response_array = $request->all();
$state = [
'server_response' => $request->all(),
'payment_hash' => $request->input('m_payment_id'),
];
$this->payfast->payment_hash->data = array_merge((array) $this->payfast->payment_hash->data, $state);
$this->payfast->payment_hash->save();
if($response_array['payment_status'] == 'COMPLETE') {
$this->payfast->logSuccessfulGatewayResponse(['response' => $response_array, 'data' => $this->payfast->payment_hash], SystemLog::TYPE_PAYFAST);
return $this->processSuccessfulPayment($response_array);
}
else {
$this->processUnsuccessfulPayment($response_array);
}
}
private function processSuccessfulPayment($response_array)
{
$payment_record = [];
$payment_record['amount'] = $response_array['amount_gross'];
$payment_record['payment_type'] = PaymentType::CREDIT_CARD_OTHER;
$payment_record['gateway_type_id'] = GatewayType::CREDIT_CARD;
$payment_record['transaction_reference'] = $response_array['pf_payment_id'];
$payment = $this->payfast->createPayment($payment_record, Payment::STATUS_COMPLETED);
return redirect()->route('client.payments.show', ['payment' => $this->payfast->encodePrimaryKey($payment->id)]);
}
private function processUnsuccessfulPayment($server_response)
{
PaymentFailureMailer::dispatch($this->payfast->client, $server_response->cancellation_reason, $this->payfast->client->company, $server_response->amount);
PaymentFailureMailer::dispatch(
$this->payfast->client,
$server_response,
$this->payfast->client->company,
$server_response['amount_gross']
);
$message = [
'server_response' => $server_response,
'data' => $this->payfast->payment_hash->data,
];
SystemLogger::dispatch(
$message,
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_PAYFAST,
$this->payfast->client,
$this->payfast->client->company,
);
throw new PaymentFailed('Failed to process the payment.', 500);
}
}

View File

@ -0,0 +1,184 @@
<?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\PaymentDrivers\PayFast;
use App\Exceptions\PaymentFailed;
use App\Jobs\Mail\PaymentFailureMailer;
use App\Jobs\Util\SystemLogger;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\PayFastPaymentDriver;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use GuzzleHttp\RequestOptions;
class Token
{
public $payfast;
//https://api.payfast.co.za/subscriptions/dc0521d3-55fe-269b-fa00-b647310d760f/adhoc
public function __construct(PayFastPaymentDriver $payfast)
{
$this->payfast = $payfast;
}
// Attributes
// merchant-id
// integer, 8 char | REQUIRED
// Header, the Merchant ID as given by the PayFast system.
// version
// string | REQUIRED
// Header, the PayFast API version (i.e. v1).
// timestamp
// ISO-8601 date and time | REQUIRED
// Header, the current timestamp (YYYY-MM-DDTHH:MM:SS[+HH:MM]).
// signature
// string | REQUIRED
// Header, MD5 hash of the alphabetised submitted header and body variables, as well as the passphrase. Characters must be in lower case.
// amount
// integer | REQUIRED
// Body, the amount which the buyer must pay, in cents (ZAR), no decimals.
// item_name
// string, 100 char | REQUIRED
// Body, the name of the item being charged for.
// item_description
// string, 255 char | OPTIONAL
// Body, the description of the item being charged for.
// itn
// boolean | OPTIONAL
// Body, specify whether an ITN must be sent for the tokenization payment (true by default).
// m_payment_id
// string, 100 char | OPTIONAL
// Body, unique payment ID on the merchants system.
// cc_cvv
// numeric | OPTIONAL
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
{
$amount = array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total;
$amount = round(($amount * pow(10, $this->payfast->client->currency()->precision)),0);
$header =[
'merchant-id' => $this->payfast->company_gateway->getConfigField('merchantId'),
'timestamp' => now()->format('c'),
'version' => 'v1',
];
nlog($header);
$body = [
'amount' => $amount,
'item_name' => 'purchase',
'item_description' => ctrans('texts.invoices') . ': ' . collect($payment_hash->invoices())->pluck('invoice_number'),
'm_payment_id' => $payment_hash->hash,
'passphrase' => $this->payfast->company_gateway->getConfigField('passphrase'),
];
$header['signature'] = $this->genSig(array_merge($header, $body));
nlog($header['signature']);
nlog($header['timestamp']);
nlog($this->payfast->company_gateway->getConfigField('merchantId'));
$result = $this->send($header, $body, $cgt->token);
nlog($result);
// /*Refactor and push to BaseDriver*/
// if ($data['response'] != null && $data['response']->getMessages()->getResultCode() == 'Ok') {
// $response = $data['response'];
// $this->storePayment($payment_hash, $data);
// $vars = [
// 'invoices' => $payment_hash->invoices(),
// 'amount' => $amount,
// ];
// $logger_message = [
// 'server_response' => $response->getTransactionResponse()->getTransId(),
// 'data' => $this->formatGatewayResponse($data, $vars),
// ];
// SystemLogger::dispatch($logger_message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_AUTHORIZE, $this->authorize->client, $this->authorize->client->company);
// return true;
// } else {
// $vars = [
// 'invoices' => $payment_hash->invoices(),
// 'amount' => $amount,
// ];
// $logger_message = [
// 'server_response' => $response->getTransactionResponse()->getTransId(),
// 'data' => $this->formatGatewayResponse($data, $vars),
// ];
// PaymentFailureMailer::dispatch($this->authorize->client, $response->getTransactionResponse()->getTransId(), $this->authorize->client->company, $amount);
// SystemLogger::dispatch($logger_message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_AUTHORIZE, $this->authorize->client, $this->authorize->client->company);
// return false;
// }
}
private function genSig($data)
{
$fields = [];
ksort($data);
foreach($data as $key => $value)
{
if (!empty($data[$key])) {
$fields[$key] = $data[$key];
}
}
return md5(http_build_query($fields));
}
private function send($headers, $body, $token)
{
$client = new \GuzzleHttp\Client(
[
'headers' => $headers,
]);
try {
$response = $client->post("https://api.payfast.co.za/subscriptions/{$token}/adhoc?testing=true",[
RequestOptions::JSON => ['body' => $body], RequestOptions::ALLOW_REDIRECTS => false
]);
return json_decode($response->getBody(),true);
}
catch(\Exception $e)
{
nlog($e->getMessage());
}
}
}

View File

@ -0,0 +1,202 @@
<?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\PaymentDrivers;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\SystemLog;
use App\PaymentDrivers\PayFast\CreditCard;
use App\PaymentDrivers\PayFast\Token;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class PayFastPaymentDriver extends BaseDriver
{
use MakesHash;
public $refundable = false; //does this gateway support refunds?
public $token_billing = false; //does this gateway support token billing?
public $can_authorise_credit_card = true; //does this gateway support authorizations?
public $payfast; //initialized gateway
public $payment_method; //initialized payment method
public static $methods = [
GatewayType::CREDIT_CARD => CreditCard::class,
];
const SYSTEM_LOG_TYPE = SystemLog::TYPE_PAYFAST;
//developer resources
//https://sandbox.payfast.co.za/
public function gatewayTypes(): array
{
$types = [];
if($this->client->currency()->code == 'ZAR')
$types[] = GatewayType::CREDIT_CARD;
return $types;
}
public function endpointUrl()
{
if($this->company_gateway->getConfigField('testMode'))
return 'https://sandbox.payfast.co.za/eng/process';
return 'https://www.payfast.co.za/eng/process';
}
public function init()
{
try{
$this->payfast = new \PayFast\PayFastPayment(
[
'merchantId' => $this->company_gateway->getConfigField('merchantId'),
'merchantKey' => $this->company_gateway->getConfigField('merchantKey'),
'passPhrase' => $this->company_gateway->getConfigField('passPhrase'),
'testMode' => $this->company_gateway->getConfigField('testMode')
]
);
} catch(Exception $e) {
echo '##PAYFAST## There was an exception: '.$e->getMessage();
}
return $this;
}
public function setPaymentMethod($payment_method_id)
{
$class = self::$methods[$payment_method_id];
$this->payment_method = new $class($this);
return $this;
}
public function authorizeView(array $data)
{
return $this->payment_method->authorizeView($data);
}
public function authorizeResponse($request)
{
return $this->payment_method->authorizeResponse($request);
}
public function processPaymentView(array $data)
{
return $this->payment_method->paymentView($data); //this is your custom implementation from here
}
public function processPaymentResponse($request)
{
return $this->payment_method->paymentResponse($request); //this is your custom implementation from here
}
public function refund(Payment $payment, $amount, $return_client_response = false)
{
return false;
}
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
{
return (new Token($this))->tokenBilling($cgt, $payment_hash);
}
public function generateSignature($data)
{
$fields = array();
// specific order required by PayFast
// @see https://developers.payfast.co.za/documentation/#checkout-page
foreach (array('merchant_id', 'merchant_key', 'return_url', 'cancel_url', 'notify_url', 'name_first',
'name_last', 'email_address', 'cell_number',
/**
* Transaction Details
*/
'm_payment_id', 'amount', 'item_name', 'item_description',
/**
* Custom return data
*/
'custom_int1', 'custom_int2', 'custom_int3', 'custom_int4', 'custom_int5',
'custom_str1', 'custom_str2', 'custom_str3', 'custom_str4', 'custom_str5',
/**
* Email confirmation
*/
'email_confirmation', 'confirmation_address',
/**
* Payment Method
*/
'payment_method',
/**
* Subscriptions
*/
'subscription_type', 'billing_date', 'recurring_amount', 'frequency', 'cycles',
/**
* Passphrase for md5 signature generation
*/
'passphrase') as $key) {
if (!empty($data[$key])) {
$fields[$key] = $data[$key];
}
}
return md5(http_build_query($fields));
}
public function processWebhookRequest(Request $request, Payment $payment = null)
{
$data = $request->all();
nlog($data);
if(array_key_exists('m_payment_id', $data))
{
$hash = Cache::get($data['m_payment_id']);
switch ($hash)
{
case 'cc_auth':
return $this->setPaymentMethod(GatewayType::CREDIT_CARD)
->authorizeResponse($request);
break;
default:
$payment_hash = PaymentHash::whereRaw('BINARY `hash`= ?', [$data['m_payment_id']])->first();
return $this->setPaymentMethod(GatewayType::CREDIT_CARD)
->setPaymentHash($payment_hash)
->processPaymentResponse($request);
break;
}
}
return response()->json([], 200);
}
}

View File

@ -114,7 +114,7 @@ class StripePaymentDriver extends BaseDriver
public function gatewayTypes(): array
{
$types = [
GatewayType::CRYPTO,
// GatewayType::CRYPTO,
GatewayType::CREDIT_CARD
];

View File

@ -117,7 +117,6 @@ use WePayCommon;
nlog("authorize the card first!");
$response = $this->wepay_payment_driver->wepay->request('credit_card/authorize', array(
// 'callback_uri' => route('payment_webhook', ['company_key' => $this->wepay_payment_driver->company_gateway->company->company_key, 'company_gateway_id' => $this->wepay_payment_driver->company_gateway->hashed_id]),
'client_id' => config('ninja.wepay.client_id'),
'client_secret' => config('ninja.wepay.client_secret'),
'credit_card_id' => (int)$request->input('credit_card_id'),

View File

@ -49,28 +49,11 @@ class CompanyRepository extends BaseRepository
private function parseCustomFields($fields) :array
{
if(array_key_exists('account1', $fields))
$fields['company1'] = $fields['account1'];
if(array_key_exists('account2', $fields))
$fields['company2'] = $fields['account2'];
if(array_key_exists('invoice1', $fields))
$fields['surcharge1'] = $fields['invoice1'];
if(array_key_exists('invoice2', $fields))
$fields['surcharge2'] = $fields['invoice2'];
if(array_key_exists('invoice_text1', $fields))
$fields['invoice1'] = $fields['invoice_text1'];
if(array_key_exists('invoice_text2', $fields))
$fields['invoice2'] = $fields['invoice_text2'];
foreach ($fields as &$value) {
$value = (string) $value;
}
return $fields;
}
}

View File

@ -51,21 +51,25 @@ class ClientService
$credits = $this->client->credits()
->where('is_deleted', false)
->where('balance', '>', 0)
->whereDate('due_date', '<=', now()->format('Y-m-d'))
->orWhere('due_date', NULL)
->where(function ($query){
$query->whereDate('due_date', '<=', now()->format('Y-m-d'))
->orWhereNull('due_date');
})
->orderBy('created_at','ASC');
return Number::roundValue($credits->sum('balance'), $this->client->currency()->precision);
}
public function getCredits() :Collection
public function getCredits()
{
return $this->client->credits()
->where('is_deleted', false)
->where('balance', '>', 0)
->whereDate('due_date', '<=', now()->format('Y-m-d'))
->orWhere('due_date', NULL)
->orderBy('created_at','ASC');
->where(function ($query){
$query->whereDate('due_date', '<=', now()->format('Y-m-d'))
->orWhereNull('due_date');
})
->orderBy('created_at','ASC')->get();
}
public function getPaymentMethods(float $amount)

View File

@ -41,10 +41,13 @@ class GetCreditPdf extends AbstractService
$file_path = $path.$this->credit->numberFormatter().'.pdf';
$disk = 'public';
// $disk = 'public';
$disk = config('filesystems.default');
$file_path = CreateEntityPdf::dispatchNow($this->invitation);
return Storage::disk($disk)->path($file_path);
nlog($file_path);
return $file_path;
// return Storage::disk($disk)->path($file_path);
}
}

View File

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

View File

@ -39,7 +39,8 @@ class GetInvoicePdf extends AbstractService
$file_path = $path.$this->invoice->numberFormatter().'.pdf';
$disk = 'public';
// $disk = 'public';
$disk = config('filesystems.default');
$file = Storage::disk($disk)->exists($file_path);
@ -47,6 +48,8 @@ class GetInvoicePdf extends AbstractService
$file_path = CreateEntityPdf::dispatchNow($invitation);
}
return Storage::disk($disk)->path($file_path);
// return Storage::disk($disk)->path($file_path);
//
return $file_path;
}
}

View File

@ -71,7 +71,8 @@ class HandleReversal extends AbstractService
$credit = CreditFactory::create($this->invoice->company_id, $this->invoice->user_id);
$credit->client_id = $this->invoice->client_id;
$credit->invoice_id = $this->invoice->id;
$credit->date = now();
$item = InvoiceItemFactory::create();
$item->quantity = 1;
$item->cost = (float) $total_paid;

View File

@ -119,6 +119,7 @@ class PaymentService
->service()
->getCredits();
foreach ($credits as $credit) {
//starting invoice balance
$invoice_balance = $invoice->balance;

View File

@ -39,10 +39,12 @@ class GetQuotePdf extends AbstractService
$file_path = $path.$this->quote->numberFormatter().'.pdf';
$disk = 'public';
// $disk = 'public';
$disk = config('filesystems.default');
$file_path = CreateEntityPdf::dispatchNow($invitation);
return Storage::disk($disk)->path($file_path);
return $file_path;
//return Storage::disk($disk)->path($file_path);
}
}

View File

@ -51,6 +51,11 @@ class QuoteService
$this->quote->fresh();
if ($this->quote->client->getSetting('auto_archive_quote')) {
$quote_repo = new QuoteRepository();
$quote_repo->archive($this->quote);
}
return $this;
}
@ -129,10 +134,6 @@ class QuoteService
public function convertToInvoice()
{
//to prevent circular references we need to explicit call this here.
// $mark_approved = new MarkApproved($this->quote->client);
// $this->quote = $mark_approved->run($this->quote);
$this->convert();
$this->invoice->service()->createInvitations();

View File

@ -41,14 +41,14 @@ class GetInvoicePdf extends AbstractService
$file_path = $path.$this->entity->hashed_id.'.pdf';
$disk = 'public';
$disk = config('filesystems.default');
$file = Storage::disk($disk)->exists($file_path);
if (! $file) {
$file_path = CreateEntityPdf::dispatchNow($invitation);
}
return Storage::disk($disk)->path($file_path);
return $file_path;
}
}

View File

@ -77,7 +77,8 @@ class SubscriptionService
$recurring_invoice->next_send_date = now();
$recurring_invoice = $recurring_invoice_repo->save([], $recurring_invoice);
$recurring_invoice->next_send_date = $recurring_invoice->nextSendDate();
$recurring_invoice->auto_bill = $this->subscription->auto_bill;
/* Start the recurring service */
$recurring_invoice->service()
->start()

View File

@ -26,9 +26,9 @@ use App\Utils\CurlUtils;
use App\Utils\HtmlEngine;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
class Phantom

View File

@ -48,7 +48,7 @@ trait AppSetup
$orderBy = 'num_days';
} elseif ($name == 'fonts') {
$orderBy = 'sort_order';
} elseif (in_array($name, ['currencies', 'industries', 'languages', 'countries', 'banks'])) {
} elseif (in_array($name, ['currencies', 'industries', 'languages', 'countries', 'banks', 'timezones'])) {
$orderBy = 'name';
} else {
$orderBy = 'id';

View File

@ -34,6 +34,11 @@ trait PdfMaker
$pdf->setChromiumPath(config('ninja.snappdf_chromium_path'));
}
if (config('ninja.snappdf_chromium_arguments')) {
$pdf->clearChromiumArguments();
$pdf->addChromiumArguments(config('ninja.snappdf_chromium_arguments'));
}
$generated = $pdf
->setHtml($html)
->generate();

View File

@ -35,8 +35,15 @@ trait SubscriptionHooker
'headers' => $headers,
]);
nlog("method name must be a string");
nlog($subscription->webhook_configuration['post_purchase_rest_method']);
nlog($subscription->webhook_configuration['post_purchase_url']);
$post_purchase_rest_method = (string)$subscription->webhook_configuration['post_purchase_rest_method'];
$post_purchase_url = (string)$subscription->webhook_configuration['post_purchase_url'];
try {
$response = $client->{$subscription->webhook_configuration['post_purchase_rest_method']}($subscription->webhook_configuration['post_purchase_url'],[
$response = $client->{$post_purchase_rest_method}($post_purchase_url,[
RequestOptions::JSON => ['body' => $body], RequestOptions::ALLOW_REDIRECTS => false
]);

View File

@ -33,7 +33,7 @@
"asm/php-ansible": "dev-main",
"authorizenet/authorizenet": "^2.0",
"bacon/bacon-qr-code": "^2.0",
"beganovich/snappdf": "^1.0",
"beganovich/snappdf": "^1.7",
"braintree/braintree_php": "^6.0",
"checkout/checkout-sdk-php": "^1.0",
"cleverit/ubl_invoice": "^1.3",
@ -47,6 +47,7 @@
"google/apiclient": "^2.7",
"guzzlehttp/guzzle": "^7.0.1",
"hashids/hashids": "^4.0",
"hedii/laravel-gelf-logger": "^6.0",
"intervention/image": "^2.5",
"laracasts/presenter": "^0.2.1",
"laravel/framework": "^8.0",
@ -63,6 +64,7 @@
"maennchen/zipstream-php": "^1.2",
"nwidart/laravel-modules": "^8.0",
"omnipay/paypal": "^3.0",
"payfast/payfast-php-sdk": "^1.1",
"pragmarx/google2fa": "^8.0",
"predis/predis": "^1.1",
"sentry/sentry-laravel": "^2",

772
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -102,8 +102,71 @@ return [
'invoiceninja' => [
'driver' => 'single',
'level' => 'debug',
'path' => storage_path('logs/invoiceninja.log'),
],
'gelf' => [
'driver' => 'custom',
'via' => \Hedii\LaravelGelfLogger\GelfLoggerFactory::class,
// This optional option determines the processors that should be
// pushed to the handler. This option is useful to modify a field
// in the log context (see NullStringProcessor), or to add extra
// data. Each processor must be a callable or an object with an
// __invoke method: see monolog documentation about processors.
// Default is an empty array.
'processors' => [
\Hedii\LaravelGelfLogger\Processors\NullStringProcessor::class,
// another processor...
],
// This optional option determines the minimum "level" a message
// must be in order to be logged by the channel. Default is 'debug'
'level' => 'debug',
// This optional option determines the channel name sent with the
// message in the 'facility' field. Default is equal to app.env
// configuration value
'name' => 'my-custom-name',
// This optional option determines the system name sent with the
// message in the 'source' field. When forgotten or set to null,
// the current hostname is used.
'system_name' => null,
// This optional option determines if you want the UDP, TCP or HTTP
// transport for the gelf log messages. Default is UDP
'transport' => 'udp',
// This optional option determines the host that will receive the
// gelf log messages. Default is 127.0.0.1
'host' => env('GRAYLOG_SERVER', '127.0.0.1'),
// This optional option determines the port on which the gelf
// receiver host is listening. Default is 12201
'port' => 12201,
// This optional option determines the path used for the HTTP
// transport. When forgotten or set to null, default path '/gelf'
// is used.
'path' => null,
// This optional option determines the maximum length per message
// field. When forgotten or set to null, the default value of
// \Monolog\Formatter\GelfMessageFormatter::DEFAULT_MAX_LENGTH is
// used (currently this value is 32766)
'max_length' => null,
// This optional option determines the prefix for 'context' fields
// from the Monolog record. Default is null (no context prefix)
'context_prefix' => null,
// This optional option determines the prefix for 'extra' fields
// from the Monolog record. Default is null (no extra prefix)
'extra_prefix' => null,
],
],
];

View File

@ -14,8 +14,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => '5.2.8',
'app_tag' => '5.2.8',
'app_version' => '5.2.11',
'app_tag' => '5.2.11',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''),
@ -144,6 +144,7 @@ return [
'log_pdf_html' => env('LOG_PDF_HTML', false),
'expanded_logging' => env('EXPANDED_LOGGING', false),
'snappdf_chromium_path' => env('SNAPPDF_CHROMIUM_PATH', false),
'snappdf_chromium_arguments' => env('SNAPPDF_CHROMIUM_ARGUMENTS', false),
'v4_migration_version' => '4.5.35',
'flutter_renderer' => env('FLUTTER_RENDERER', 'selfhosted-html'),
'webcron_secret' => env('WEBCRON_SECRET', false),

View File

@ -1,8 +1,6 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class UpdateDesigns extends Migration
{

View File

@ -0,0 +1,29 @@
<?php
use App\Models\Gateway;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class ActivatePayfastPaymentDriver extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Gateway::whereIn('id', [11])->update(['visible' => 1]);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
}

File diff suppressed because it is too large Load Diff

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

View File

@ -4,7 +4,7 @@ const TEMP = 'flutter-temp-cache';
const CACHE_NAME = 'flutter-app-cache';
const RESOURCES = {
"favicon.png": "dca91c54388f52eded692718d5a98b8b",
"main.dart.js": "ec0d23274ffa0ea4b6743fe88c16ccaa",
"main.dart.js": "3b486fb6ff9ff8c688f498feb859535a",
"/": "23224b5e03519aaa87594403d54412cf",
"manifest.json": "ce1b79950eb917ea619a0a30da27c6a3",
"assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "174c02fc4609e8fc4389f5d21f16a296",
@ -29,7 +29,7 @@ const RESOURCES = {
"assets/assets/images/payment_types/solo.png": "2030c3ccaccf5d5e87916a62f5b084d6",
"assets/assets/images/payment_types/visa.png": "3ddc4a4d25c946e8ad7e6998f30fd4e3",
"assets/AssetManifest.json": "7e49562f32e24a9e2557fe4178a84b79",
"version.json": "4d10e2258012cbb88b24009334a24f24",
"version.json": "d5a91f8fb6852f3ca9a9199e65eb6dbd",
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"favicon.ico": "51636d3a390451561744c42188ccd628"

175643
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

176083
public/main.foss.dart.js vendored

File diff suppressed because one or more lines are too long

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