1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-21 00:41:34 +02:00

Merge pull request #4831 from turbo124/v5-stable

V5.1.0 RC2
This commit is contained in:
David Bomba 2021-02-04 09:24:40 +11:00 committed by GitHub
commit f8b99a1cea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 123045 additions and 121997 deletions

View File

@ -10,8 +10,6 @@ DB_DATABASE1=ninja
DB_USERNAME1=root
DB_PASSWORD1=ninja
DB_HOST1=127.0.0.1
DB_PORT1=32768
DB_PORT=32768
DB_DATABASE=ninja
DB_USERNAME=root
DB_PASSWORD=ninja

View File

@ -67,7 +67,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
extensions: mysql, mysqlnd, sqlite3, bcmath, gmp, gd, curl, zip, openssl, mbstring, xml
extensions: mysql, sqlite3, bcmath, gmp, gd, curl, zip, openssl, mbstring, xml
- uses: actions/checkout@v1
with:

View File

@ -1 +1 @@
5.0.56
5.1.0

View File

@ -289,30 +289,6 @@ class CheckData extends Command
}
}
private function checkInvoiceBalances()
{
$wrong_balances = 0;
$wrong_paid_to_dates = 0;
foreach (Client::where('is_deleted', 0)->cursor() as $client) {
$invoice_balance = $client->invoices->where('is_deleted', false)->where('status_id', '>', 1)->sum('balance');
$credit_balance = $client->credits->where('is_deleted', false)->sum('balance');
// $invoice_balance -= $credit_balance;//doesn't make sense to remove the credit amount
$ledger = CompanyLedger::where('client_id', $client->id)->orderBy('id', 'DESC')->first();
if ($ledger && number_format($invoice_balance, 4) != number_format($client->balance, 4)) {
$wrong_balances++;
$this->logMessage("# {$client->id} " . $client->present()->name.' - '.$client->number." - Balance Failure - Invoice Balances = {$invoice_balance} Client Balance = {$client->balance} Ledger Balance = {$ledger->balance}");
$this->isValid = false;
}
}
$this->logMessage("{$wrong_balances} clients with incorrect balances");
}
private function checkPaidToDates()
{
$wrong_paid_to_dates = 0;
@ -390,7 +366,9 @@ class CheckData extends Command
$invoice_balance = Invoice::where('client_id', $client->id)->where('is_deleted', false)->where('status_id', '>', 1)->withTrashed()->sum('balance');
$credit_balance = Credit::where('client_id', $client->id)->where('is_deleted', false)->withTrashed()->sum('balance');
// $invoice_balance -= $credit_balance;
/*Legacy - V4 will add credits to the balance - we may need to reverse engineer this and remove the credits from the client balance otherwise we need this hack here and in the invoice balance check.*/
if($client->balance != $invoice_balance)
$invoice_balance -= $credit_balance;
$ledger = CompanyLedger::where('client_id', $client->id)->orderBy('id', 'DESC')->first();
@ -405,6 +383,32 @@ class CheckData extends Command
$this->logMessage("{$wrong_paid_to_dates} clients with incorrect client balances");
}
private function checkInvoiceBalances()
{
$wrong_balances = 0;
$wrong_paid_to_dates = 0;
foreach (Client::where('is_deleted', 0)->cursor() as $client) {
$invoice_balance = $client->invoices->where('is_deleted', false)->where('status_id', '>', 1)->sum('balance');
$credit_balance = $client->credits->where('is_deleted', false)->sum('balance');
if($client->balance != $invoice_balance)
$invoice_balance -= $credit_balance;//doesn't make sense to remove the credit amount
$ledger = CompanyLedger::where('client_id', $client->id)->orderBy('id', 'DESC')->first();
if ($ledger && number_format($invoice_balance, 4) != number_format($client->balance, 4)) {
$wrong_balances++;
$this->logMessage("# {$client->id} " . $client->present()->name.' - '.$client->number." - Balance Failure - Invoice Balances = {$invoice_balance} Client Balance = {$client->balance} Ledger Balance = {$ledger->balance}");
$this->isValid = false;
}
}
$this->logMessage("{$wrong_balances} clients with incorrect balances");
}
private function checkLogoFiles()
{
// $accounts = DB::table('accounts')

View File

@ -80,6 +80,8 @@ class ImportMigrations extends Command
$path = $this->option('path') ?? public_path('storage/migrations/import');
nlog(public_path('storage/migrations/import'));
$directory = new DirectoryIterator($path);
foreach ($directory as $file) {

View File

@ -53,6 +53,7 @@ class PostUpdate extends Command
nlog("finished migrating");
exec('vendor/bin/composer install --no-dev');
exec('vendor/bin/composer dump');
nlog("finished running composer install ");

View File

@ -35,10 +35,10 @@ function nlog($output, $context = []): void
}
if (!function_exists('ray')) {
function ray($payload)
{
return true;
}
}
// if (!function_exists('ray')) {
// function ray($payload)
// {
// return true;
// }
// }

View File

@ -32,7 +32,9 @@ class InvitationController extends Controller
use MakesDates;
public function router(string $entity, string $invitation_key)
{
{
Auth::logout();
return $this->genericRouter($entity, $invitation_key);
}
@ -43,6 +45,7 @@ class InvitationController extends Controller
private function genericRouter(string $entity, string $invitation_key)
{
$key = $entity.'_id';
$entity_obj = 'App\Models\\'.ucfirst(Str::camel($entity)).'Invitation';
@ -51,17 +54,22 @@ class InvitationController extends Controller
->with('contact.client')
->firstOrFail();
/* Return early if we have the correct client_hash embedded */
if (request()->has('client_hash') && request()->input('client_hash') == $invitation->contact->client->client_hash) {
auth()->guard('contact')->login($invitation->contact, true);
} elseif ((bool) $invitation->contact->client->getSetting('enable_client_portal_password') !== false) {
//If no contact password is set - this will cause a 401 error - instead redirect to the client.login route
$this->middleware('auth:contact');
return redirect()->route('client.login');
} else {
auth()->guard('contact')->login($invitation->contact, true);
}
if (auth()->guard('contact') && ! request()->has('silent') && ! $invitation->viewed_date) {
$invitation->markViewed();

View File

@ -266,6 +266,10 @@ class ProjectController extends BaseController
$project->number = empty($project->number) ? $this->getNextProjectNumber($project) : $project->number;
$project->save();
if ($request->has('documents')) {
$this->saveDocuments($request->input('documents'), $project);
}
return $this->itemResponse($project->fresh());
}

View File

@ -122,7 +122,7 @@ class SetupController extends Controller
];
try {
foreach ($env_values as $property => $value) {
$this->updateEnvironmentProperty($property, $value);
}

View File

@ -111,7 +111,7 @@ class EmailEntity extends BaseMailerJob implements ShouldQueue
->send(
new TemplateEmail(
$this->email_entity_builder,
$this->invitation->contact->client
$this->invitation->contact
)
);
} catch (\Exception $e) {

View File

@ -0,0 +1,109 @@
<?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\Mail;
use App\Libraries\MultiDB;
use App\Mail\Admin\AutoBillingFailureObject;
use App\Mail\Admin\EntityNotificationMailer;
use App\Mail\Admin\PaymentFailureObject;
use App\Models\User;
use App\Utils\Traits\Notifications\UserNotifies;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
/*Multi Mailer implemented*/
class AutoBillingFailureMailer extends BaseMailerJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, UserNotifies;
public $client;
public $error;
public $company;
public $payment_hash;
public $settings;
/**
* Create a new job instance.
*
* @param $client
* @param $message
* @param $company
* @param $amount
*/
public function __construct($client, $error, $company, $payment_hash)
{
$this->client = $client;
$this->error = $error;
$this->company = $company;
$this->payment_hash = $payment_hash;
$this->company = $company;
$this->settings = $client->getMergedSettings();
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
/*If we are migrating data we don't want to fire these notification*/
if ($this->company->is_disabled) {
return true;
}
//Set DB
MultiDB::setDb($this->company->db);
//if we need to set an email driver do it now
$this->setMailDriver();
//iterate through company_users
$this->company->company_users->each(function ($company_user) {
//determine if this user has the right permissions
$methods = $this->findCompanyUserNotificationType($company_user, ['payment_failure','all_notifications']);
//if mail is a method type -fire mail!!
if (($key = array_search('mail', $methods)) !== false) {
unset($methods[$key]);
$mail_obj = (new AutoBillingFailureObject($this->client, $this->error, $this->company, $this->payment_hash))->build();
$mail_obj->from = [config('mail.from.address'), config('mail.from.name')];
//send email
try {
Mail::to($company_user->user->email)
->send(new EntityNotificationMailer($mail_obj));
} catch (\Exception $e) {
//$this->failed($e);
$this->logMailError($e->getMessage(), $this->client);
}
}
});
}
}

View File

@ -0,0 +1,111 @@
<?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\Mail;
use App\Libraries\MultiDB;
use App\Mail\Admin\ClientPaymentFailureObject;
use App\Mail\Admin\EntityNotificationMailer;
use App\Mail\Admin\PaymentFailureObject;
use App\Models\Invoice;
use App\Models\User;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\Notifications\UserNotifies;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
/*Multi Mailer implemented*/
class ClientPaymentFailureMailer extends BaseMailerJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, UserNotifies, MakesHash;
public $client;
public $error;
public $company;
public $payment_hash;
public $settings;
/**
* Create a new job instance.
*
* @param $client
* @param $message
* @param $company
* @param $amount
*/
public function __construct($client, $error, $company, $payment_hash)
{
$this->company = $company;
$this->error = $error;
$this->client = $client;
$this->payment_hash = $payment_hash;
$this->company = $company;
$this->settings = $client->getMergedSettings();
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
/*If we are migrating data we don't want to fire these notification*/
if ($this->company->is_disabled) {
return true;
}
//Set DB
MultiDB::setDb($this->company->db);
//if we need to set an email driver do it now
$this->setMailDriver();
$this->invoices = Invoice::whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->get();
$this->invoices->first()->invitations->each(function ($invitation) {
if ($invitation->contact->send_email && $invitation->contact->email) {
$mail_obj = (new ClientPaymentFailureObject($this->client, $this->error, $this->company, $this->payment_hash))->build();
$mail_obj->from = [config('mail.from.address'), config('mail.from.name')];
//send email
try {
Mail::to($invitation->contact->email)
->send(new EntityNotificationMailer($mail_obj));
} catch (\Exception $e) {
$this->logMailError($e->getMessage(), $this->client);
}
}
});
}
}

View File

@ -31,11 +31,11 @@ class PaymentFailureMailer extends BaseMailerJob implements ShouldQueue
public $client;
public $message;
public $error;
public $company;
public $amount;
public $payment_hash;
public $settings;
@ -47,15 +47,15 @@ class PaymentFailureMailer extends BaseMailerJob implements ShouldQueue
* @param $company
* @param $amount
*/
public function __construct($client, $message, $company, $amount)
public function __construct($client, $error, $company, $payment_hash)
{
$this->company = $company;
$this->message = $message;
$this->error = $error;
$this->client = $client;
$this->amount = $amount;
$this->payment_hash = $payment_hash;
$this->company = $company;
@ -69,6 +69,7 @@ class PaymentFailureMailer extends BaseMailerJob implements ShouldQueue
*/
public function handle()
{
/*If we are migrating data we don't want to fire these notification*/
if ($this->company->is_disabled) {
return true;
@ -81,16 +82,17 @@ class PaymentFailureMailer extends BaseMailerJob implements ShouldQueue
$this->setMailDriver();
//iterate through company_users
$this->company->company_users->each(function ($company_user) {
$this->company->company_users->each(function ($company_user) {
//determine if this user has the right permissions
$methods = $this->findCompanyUserNotificationType($company_user, ['payment_failure']);
$methods = $this->findCompanyUserNotificationType($company_user, ['payment_failure','all_notifications']);
//if mail is a method type -fire mail!!
if (($key = array_search('mail', $methods)) !== false) {
unset($methods[$key]);
$mail_obj = (new PaymentFailureObject($this->client, $this->message, $this->amount, $this->company))->build();
$mail_obj = (new PaymentFailureObject($this->client, $this->error, $this->company, $this->payment_hash))->build();
$mail_obj->from = [config('mail.from.address'), config('mail.from.name')];
//send email

View File

@ -80,7 +80,7 @@ class EmailPayment extends BaseMailerJob implements ShouldQueue
try {
$mail = Mail::to($this->contact->email, $this->contact->present()->name());
$mail->send(new TemplateEmail($email_builder, $this->contact->client));
$mail->send(new TemplateEmail($email_builder, $this->contact));
} catch (\Exception $e) {
nlog("mailing failed with message " . $e->getMessage());
event(new PaymentWasEmailedAndFailed($this->payment, $this->company, Mail::failures(), Ninja::eventVars()));

View File

@ -205,8 +205,10 @@ class Import implements ShouldQueue
$this->setInitialCompanyLedgerBalances();
$this->fixClientBalances();
Mail::to($this->user)
->send(new MigrationCompleted());
->send(new MigrationCompleted($this->company));
/*After a migration first some basic jobs to ensure the system is up to date*/
VersionCheck::dispatch();
@ -649,7 +651,8 @@ class Import implements ShouldQueue
unset($resource['invitations'][$key]['recurring_invoice_id']);
}
$modified['invitations'] = $resource['invitations'];
$modified['invitations'] = $this->deDuplicateInvitations($resource['invitations']);
}
$invoice = $invoice_repository->save(
@ -710,8 +713,10 @@ class Import implements ShouldQueue
unset($resource['invitations'][$key]['invoice_id']);
}
$modified['invitations'] = $resource['invitations'];
$modified['invitations'] = $this->deDuplicateInvitations($resource['invitations']);
}
$invoice = $invoice_repository->save(
$modified,
InvoiceFactory::create($this->company->id, $modified['user_id'])
@ -732,6 +737,13 @@ class Import implements ShouldQueue
$invoice_repository = null;
}
/* Prevent edge case where V4 has inserted multiple invitations for a resource for a client contact */
private function deDuplicateInvitations($invitations)
{
return array_intersect_key($invitations, array_unique(array_column($invitations, 'client_contact_id')));
}
private function processCredits(array $data): void
{
Credit::unguard();
@ -779,6 +791,7 @@ class Import implements ShouldQueue
/*Improve memory handling by setting everything to null when we have finished*/
$data = null;
$credit_repository = null;
}
private function processQuotes(array $data): void
@ -811,6 +824,19 @@ class Import implements ShouldQueue
unset($modified['id']);
if (array_key_exists('invitations', $resource)) {
foreach ($resource['invitations'] as $key => $invite) {
$resource['invitations'][$key]['client_contact_id'] = $this->transformId('client_contacts', $invite['client_contact_id']);
$resource['invitations'][$key]['user_id'] = $modified['user_id'];
$resource['invitations'][$key]['company_id'] = $this->company->id;
unset($resource['invitations'][$key]['invoice_id']);
}
$modified['invitations'] = $this->deDuplicateInvitations($resource['invitations']);
}
$quote = $quote_repository->save(
$modified,
QuoteFactory::create($this->company->id, $modified['user_id'])
@ -950,6 +976,7 @@ class Import implements ShouldQueue
/* No validators since data provided by database is already valid. */
foreach ($data as $resource) {
$modified = $resource;
if (array_key_exists('invoice_id', $resource) && $resource['invoice_id'] && ! array_key_exists('invoices', $this->ids)) {
@ -974,20 +1001,27 @@ class Import implements ShouldQueue
$file_name = $resource['name'];
$file_path = sys_get_temp_dir().'/'.$file_name;
file_put_contents($file_path, $this->curlGet($file_url));
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$file_info = $finfo->file($file_path);
try {
file_put_contents($file_path, $this->curlGet($file_url));
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$file_info = $finfo->file($file_path);
$uploaded_file = new UploadedFile(
$file_path,
$file_name,
$file_info,
filesize($file_path),
0,
false
);
$uploaded_file = new UploadedFile(
$file_path,
$file_name,
$file_info,
filesize($file_path),
0,
false
);
$this->saveDocument($uploaded_file, $entity, $is_public = true);
$this->saveDocument($uploaded_file, $entity, $is_public = true);
}
catch(\Exception $e) {
//do nothing, gracefully :)
}
}
@ -1394,4 +1428,23 @@ class Import implements ShouldQueue
return $response->getBody();
}
/* In V4 we use negative invoices (credits) and add then into the client balance. In V5, these sit off ledger and are applied later.
This next section will check for credit balances and reduce the client balance so that the V5 balances are correct
*/
private function fixClientBalances()
{
Client::cursor()->each(function ($client) {
$credit_balance = $client->credits->where('is_deleted', false)->sum('balance');
if($credit_balance > 0){
$client->balance += $credit_balance;
$client->save();
}
});
}
}

View File

@ -0,0 +1,104 @@
<?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\Admin;
use App\Models\Invoice;
use App\Utils\Number;
use App\Utils\Traits\MakesHash;
use stdClass;
class AutoBillingFailureObject
{
use MakesHash;
public $client;
public $error;
public $company;
public $payment_hash;
private $invoices;
/**
* Create a new job instance.
*
* @param $client
* @param $message
* @param $company
* @param $amount
*/
public function __construct($client, $error, $company, $payment_hash)
{
$this->client = $client;
$this->error = $error;
$this->company = $company;
$this->payment_hash = $payment_hash;
$this->company = $company;
}
public function build()
{
$this->$invoices = Invoice::whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->get();
$mail_obj = new stdClass;
$mail_obj->amount = $this->getAmount();
$mail_obj->subject = $this->getSubject();
$mail_obj->data = $this->getData();
$mail_obj->markdown = 'email.admin.generic';
$mail_obj->tag = $this->company->company_key;
return $mail_obj;
}
private function getAmount()
{
return array_sum(array_column($this->payment_hash->invoices(), 'amount')) + $this->payment_hash->fee_total;
}
private function getSubject()
{
return
ctrans(
'texts.auto_bill_failed',
['invoice_number' => $this->invoices->first()->number]
);
}
private function getData()
{
$signature = $this->client->getSetting('email_signature');
$data = [
'title' => ctrans(
'texts.auto_bill_failed',
['invoice_number' => $this->invoices->first()->number]
),
'message' => $this->error,
'signature' => $signature,
'logo' => $this->company->present()->logo(),
'settings' => $this->client->getMergedSettings(),
'whitelabel' => $this->company->account->isPaid() ? true : false,
'url' => config('ninja.app_url'),
'button' => ctrans('texts.login'),
];
return $data;
}
}

View File

@ -0,0 +1,114 @@
<?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\Admin;
use App\Models\Invoice;
use App\Utils\Number;
use App\Utils\Traits\MakesHash;
use stdClass;
class ClientPaymentFailureObject
{
use MakesHash;
public $client;
public $error;
public $company;
public $payment_hash;
private $invoices;
/**
* Create a new job instance.
*
* @param $client
* @param $message
* @param $company
* @param $amount
*/
public function __construct($client, $error, $company, $payment_hash)
{
$this->client = $client;
$this->error = $error;
$this->company = $company;
$this->payment_hash = $payment_hash;
$this->company = $company;
}
public function build()
{
$this->invoices = Invoice::whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->get();
$mail_obj = new stdClass;
$mail_obj->amount = $this->getAmount();
$mail_obj->subject = $this->getSubject();
$mail_obj->data = $this->getData();
$mail_obj->markdown = 'email.admin.generic';
$mail_obj->tag = $this->company->company_key;
return $mail_obj;
}
private function getAmount()
{
return array_sum(array_column($this->payment_hash->invoices(), 'amount')) + $this->payment_hash->fee_total;
}
private function getSubject()
{
return
ctrans(
'texts.notification_invoice_payment_failed_subject',
['invoice' => $this->client->present()->name()]
);
}
private function getData()
{
$signature = $this->client->getSetting('email_signature');
$data = [
'title' => ctrans(
'texts.notification_invoice_payment_failed_subject',
[
'invoice' => $this->invoices->first()->number
]
),
'greeting' => ctrans('texts.email_salutation', ['name' => $this->client->present()->name]),
'message' => $this->error,
'signature' => $signature,
'logo' => $this->company->present()->logo(),
'settings' => $this->client->getMergedSettings(),
'whitelabel' => $this->company->account->isPaid() ? true : false,
'url' => route('client.login'),
'button' => ctrans('texts.login'),
'additional_info' => false
];
return $data;
}
}

View File

@ -11,29 +11,52 @@
namespace App\Mail\Admin;
use App\Models\Invoice;
use App\Utils\Number;
use App\Utils\Traits\MakesHash;
use stdClass;
class PaymentFailureObject
{
use MakesHash;
public $client;
public $message;
public $error;
public $company;
public $amount;
public $payment_hash;
public function __construct($client, $message, $amount, $company)
private $invoices;
/**
* Create a new job instance.
*
* @param $client
* @param $message
* @param $company
* @param $amount
*/
public function __construct($client, $error, $company, $payment_hash)
{
$this->client = $client;
$this->message = $message;
$this->amount = $amount;
$this->error = $error;
$this->company = $company;
$this->payment_hash = $payment_hash;
$this->company = $company;
}
public function build()
{
$this->invoices = Invoice::whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->get();
$mail_obj = new stdClass;
$mail_obj->amount = $this->getAmount();
$mail_obj->subject = $this->getSubject();
@ -46,16 +69,20 @@ class PaymentFailureObject
private function getAmount()
{
return Number::formatMoney($this->amount, $this->client);
return array_sum(array_column($this->payment_hash->invoices(), 'amount')) + $this->payment_hash->fee_total;
}
private function getSubject()
{
return
ctrans(
'texts.payment_failed_subject',
['client' => $this->payment->client->present()->name()]
['client' => $this->client->present()->name()]
);
}
private function getData()
@ -65,21 +92,36 @@ class PaymentFailureObject
$data = [
'title' => ctrans(
'texts.payment_failed_subject',
['client' => $this->client->present()->name()]
),
'message' => ctrans(
'texts.notification_payment_paid',
['amount' => $this->getAmount(),
'client' => $this->client->present()->name(),
'message' => $this->message,
]
[
'client' => $this->client->present()->name()
]
),
'message' => $this->error,
'signature' => $signature,
'logo' => $this->company->present()->logo(),
'settings' => $this->client->getMergedSettings(),
'whitelabel' => $this->company->account->isPaid() ? true : false,
'url' => config('ninja.app_url'),
'button' => ctrans('texts.login'),
'additional_info' => $this->buildFailedInvoices()
];
return $data;
}
private function buildFailedInvoices()
{
$text = '';
foreach($this->invoices as $invoice)
{
$text .= ctrans('texts.notification_invoice_payment_failed_subject', ['invoice' => $invoice->number]) . "\n";
}
return $text;
}
}

View File

@ -31,14 +31,14 @@ class DownloadInvoices extends Mailable
public function build()
{
return $this->from(config('mail.from.address'), config('mail.from.name'))
->subject(ctrans('texts.download_files'))
->markdown(
'email.admin.download_files',
[
'url' => $this->file_path,
'logo' => $this->company->present()->logo,
]
);
->subject(ctrans('texts.download_files'))
->markdown(
'email.admin.download_files',
[
'url' => $this->file_path,
'logo' => $this->company->present()->logo,
'whitelabel' => $this->company->account->isPaid() ? true : false,
]
);
}
}

View File

@ -2,6 +2,7 @@
namespace App\Mail;
use App\Models\Company;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
@ -10,14 +11,16 @@ class MigrationCompleted extends Mailable
{
use Queueable, SerializesModels;
public $company;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct()
public function __construct(Company $company)
{
//
$this->company = $company;
}
/**
@ -27,9 +30,16 @@ class MigrationCompleted extends Mailable
*/
public function build()
{
$data['settings'] = auth()->user()->company()->settings;
$data['settings'] = $this->company->settings;
$data['company'] = $this->company;
$data['whitelabel'] = $this->company->account->isPaid() ? true : false;
$result = $this->from(config('mail.from.address'), config('mail.from.name'))
->view('email.import.completed', $data);
return $this->from(config('mail.from.address'), config('mail.from.name'))
->view('email.migration.completed', $data);
if($this->company->invoices->count() >=1)
$result->attach($this->company->invoices->first()->pdf_file_path());
return $result;
}
}

View File

@ -32,7 +32,6 @@ class MigrationFailed extends Mailable
public function build()
{
return $this->from(config('mail.from.address'), config('mail.from.name'))
->view('email.migration.failed');
}
}

View File

@ -12,6 +12,7 @@
namespace App\Mail;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
@ -25,11 +26,15 @@ class TemplateEmail extends Mailable
private $client;
public function __construct($build_email, Client $client)
private $contact;
public function __construct($build_email, ClientContact $contact)
{
$this->build_email = $build_email;
$this->client = $client;
$this->contact = $contact;
$this->client = $contact->client;
}
/**
@ -64,12 +69,12 @@ class TemplateEmail extends Mailable
'settings' => $settings,
])
->view($template_name, [
'greeting' => ctrans('texts.email_salutation', ['name' => $this->contact->present()->name()]),
'body' => $this->build_email->getBody(),
'footer' => $this->build_email->getFooter(),
'view_link' => $this->build_email->getViewLink(),
'view_text' => $this->build_email->getViewText(),
'title' => '',
// 'title' => $this->build_email->getSubject(),
'signature' => $settings->email_signature,
'settings' => $settings,
'company' => $company,

View File

@ -373,6 +373,11 @@ class Company extends BaseModel
return $this->hasMany(CompanyToken::class);
}
public function client_gateway_tokens()
{
return $this->hasMany(ClientGatewayToken::class);
}
public function system_logs()
{
return $this->hasMany(SystemLog::class)->orderBy('id', 'DESC')->take(50);

View File

@ -16,6 +16,8 @@ use App\Events\Payment\PaymentWasCreated;
use App\Exceptions\PaymentFailed;
use App\Factory\PaymentFactory;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Jobs\Mail\AutoBillingFailureMailer;
use App\Jobs\Mail\ClientPaymentFailureMailer;
use App\Jobs\Mail\PaymentFailureMailer;
use App\Jobs\Util\SystemLogger;
use App\Models\Client;
@ -333,21 +335,30 @@ class BaseDriver extends AbstractPaymentDriver
public function processInternallyFailedPayment($gateway, $e)
{
if ($e instanceof Exception) {
$error = $e->getMessage();
}
$this->unWindGatewayFees($this->payment_hash);
if ($e instanceof CheckoutHttpException) {
$error = $e->getBody();
}
$amount = optional($this->payment_hash->data)->value ?? optional($this->payment_hash->data)->amount;
else if ($e instanceof Exception) {
$error = $e->getMessage();
}
else
$error = $e->getMessage();
PaymentFailureMailer::dispatch(
$gateway->client,
$error,
$gateway->client->company,
$amount
$this->payment_hash
);
ClientPaymentFailureMailer::dispatch(
$gateway->client,
$error,
$gateway->client->company,
$this->payment_hash
);
SystemLogger::dispatch(

View File

@ -82,15 +82,6 @@ class CreditCard
{
$this->checkout->init();
$cgt = ClientGatewayToken::query()
->where('id', $this->decodePrimaryKey($request->input('token')))
->where('company_id', auth('contact')->user()->client->company->id)
->first();
if (!$cgt) {
throw new PaymentFailed(ctrans('texts.payment_token_not_found'), 401);
}
$state = [
'server_response' => json_decode($request->gateway_response),
'value' => $request->value,
@ -103,12 +94,11 @@ class CreditCard
$state = array_merge($state, $request->all());
$state['store_card'] = boolval($state['store_card']);
$state['token'] = $cgt;
$this->checkout->payment_hash->data = array_merge((array)$this->checkout->payment_hash->data, $state);
$this->checkout->payment_hash->save();
if ($request->has('token')) {
if ($request->has('token') && !is_null($request->token) && !empty($request->token)) {
return $this->attemptPaymentUsingToken($request);
}
@ -117,7 +107,16 @@ class CreditCard
private function attemptPaymentUsingToken(PaymentResponseRequest $request)
{
$method = new IdSource($this->checkout->payment_hash->data->token->token);
$cgt = ClientGatewayToken::query()
->where('id', $this->decodePrimaryKey($request->input('token')))
->where('company_id', auth('contact')->user()->client->company->id)
->first();
if (!$cgt) {
throw new PaymentFailed(ctrans('texts.payment_token_not_found'), 401);
}
$method = new IdSource($cgt->token);
return $this->completePayment($method, $request);
}
@ -161,7 +160,7 @@ class CreditCard
}
if ($response->status == 'Pending') {
$this->checkout->confirmGatewayFee($request);
$this->checkout->confirmGatewayFee();
return $this->processPendingPayment($response);
}
@ -171,7 +170,6 @@ class CreditCard
PaymentFailureMailer::dispatch($this->checkout->client, $response->response_summary, $this->checkout->client->company, $this->checkout->payment_hash->data->value);
return $this->processUnsuccessfulPayment($response);
}
} catch (CheckoutHttpException $e) {

View File

@ -66,6 +66,7 @@ trait Utilities
'payment_type' => PaymentType::parseCardType(strtolower($_payment->source['scheme'])),
'amount' => $this->getParent()->payment_hash->data->raw_value,
'transaction_reference' => $_payment->id,
'gateway_type_id' => GatewayType::CREDIT_CARD,
];
$payment = $this->getParent()->createPayment($data, \App\Models\Payment::STATUS_COMPLETED);

View File

@ -79,89 +79,47 @@ class Charge
]);
SystemLogger::dispatch($response, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_STRIPE, $this->stripe->client);
} catch (CardException $e) {
// Since it's a decline, \Stripe\Exception\CardException will be caught
} catch (\Exception $e) {
$data = [
'status' => $e->getHttpStatus(),
'error_type' => $e->getError()->type,
'error_code' => $e->getError()->code,
'param' => $e->getError()->param,
'message' => $e->getError()->message,
];
$data =[
'status' => '',
'error_type' => '',
'error_code' => '',
'param' => '',
'message' => '',
];
switch ($e) {
case ($e instanceof CardException):
$data['status'] = $e->getHttpStatus();
$data['error_type'] = $e->getError()->type;
$data['error_code'] = $e->getError()->code;
$data['param'] = $e->getError()->param;
$data['message'] = $e->getError()->message;
break;
case ($e instanceof RateLimitException):
$data['message'] = 'Too many requests made to the API too quickly';
break;
case ($e instanceof InvalidRequestException):
$data['message'] = 'Invalid parameters were supplied to Stripe\'s API';
break;
case ($e instanceof AuthenticationException):
$data['message'] = 'Authentication with Stripe\'s API failed';
break;
case ($e instanceof ApiErrorException):
$data['message'] = 'Network communication with Stripe failed';
break;
default:
$data['message'] = $e->getMessage();
break;
}
$this->stripe->processInternallyFailedPayment($this->stripe, $e);
SystemLogger::dispatch($data, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_STRIPE, $this->stripe->client);
} catch (RateLimitException $e) {
// Too many requests made to the API too quickly
$data = [
'status' => '',
'error_type' => '',
'error_code' => '',
'param' => '',
'message' => 'Too many requests made to the API too quickly',
];
SystemLogger::dispatch($data, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_STRIPE, $this->stripe->client);
} catch (InvalidRequestException $e) {
// Invalid parameters were supplied to Stripe's API
//
$data = [
'status' => '',
'error_type' => '',
'error_code' => '',
'param' => '',
'message' => 'Invalid parameters were supplied to Stripe\'s API',
];
SystemLogger::dispatch($data, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_STRIPE, $this->stripe->client);
} catch (AuthenticationException $e) {
// Authentication with Stripe's API failed
$data = [
'status' => '',
'error_type' => '',
'error_code' => '',
'param' => '',
'message' => 'Authentication with Stripe\'s API failed',
];
SystemLogger::dispatch($data, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_STRIPE, $this->stripe->client);
} catch (ApiConnectionException $e) {
// Network communication with Stripe failed
$data = [
'status' => '',
'error_type' => '',
'error_code' => '',
'param' => '',
'message' => 'Network communication with Stripe failed',
];
SystemLogger::dispatch($data, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_STRIPE, $this->stripe->client);
} catch (ApiErrorException $e) {
$data = [
'status' => '',
'error_type' => '',
'error_code' => '',
'param' => '',
'message' => 'API Error',
];
SystemLogger::dispatch($data, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_STRIPE, $this->stripe->client);
} catch (Exception $e) {
// Something else happened, completely unrelated to Stripe
//
$data = [
'status' => '',
'error_type' => '',
'error_code' => '',
'param' => '',
'message' => $e->getMessage(),
];
SystemLogger::dispatch($data, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_STRIPE, $this->stripe->client);
}
}
if (! $response) {
return false;

View File

@ -86,6 +86,10 @@ class CreditCard
$state = array_merge($state, $request->all());
$state['store_card'] = boolval($state['store_card']);
if ($request->has('token') && !is_null($request->token)) {
$state['store_card'] = false;
}
$state['payment_intent'] = PaymentIntent::retrieve($state['server_response']->id);
$state['customer'] = $state['payment_intent']->customer;
@ -116,7 +120,6 @@ class CreditCard
'gateway_type_id' => GatewayType::CREDIT_CARD,
];
$this->stripe->payment_hash->data = array_merge((array) $this->stripe->payment_hash->data, ['amount' => $data['amount']]);
$this->stripe->payment_hash->save();

View File

@ -310,7 +310,7 @@ class BaseRepository
$model = $model->calc()->getCredit();
$model->ledger()->updateCreditBalance(($state['finished_amount'] - $state['starting_amount']));
// $model->ledger()->updateCreditBalance(-1*($state['finished_amount'] - $state['starting_amount']));
if (! $model->design_id)
$model->design_id = $this->decodePrimaryKey($client->getSetting('credit_design_id'));

View File

@ -182,6 +182,14 @@ class InvoiceMigrationRepository extends BaseRepository
}
}
if($data['is_deleted']){
$model->is_deleted = true;
$model->save();
}
if($data['deleted_at'])
$model->delete();
$model->save();
return $model->fresh();

View File

@ -114,11 +114,18 @@ class PaymentMigrationRepository extends BaseRepository
$payment->invoices()->saveMany($invoices);
$payment->invoices->each(function ($inv) use ($invoice_totals, $refund_totals) {
$inv->pivot->amount = $invoice_totals;
$inv->pivot->refunded = $refund_totals;
$inv->pivot->save();
$inv->paid_to_date += $invoice_totals;
if($inv->balance > 0)
$inv->balance -= $invoice_totals;
$inv->balance = max(0, $inv->balance);
$inv->save();
});
@ -135,7 +142,8 @@ class PaymentMigrationRepository extends BaseRepository
$cre->pivot->amount = $credit_totals;
$cre->pivot->save();
$cre->paid_to_date += $invoice_totals;
$cre->paid_to_date += $credit_totals;
$cre->balance -= $credit_totals;
$cre->save();
});
}

View File

@ -33,7 +33,8 @@ class RecurringService
*/
public function stop()
{
$this->status_id = RecurringInvoice::STATUS_PAUSED;
if($this->recurring_entity->status_id < RecurringInvoice::STATUS_PAUSED)
$this->recurring_entity->status_id = RecurringInvoice::STATUS_PAUSED;
return $this;
}

View File

@ -16,6 +16,7 @@ use App\Models\CreditInvitation;
use App\Models\Design;
use App\Models\InvoiceInvitation;
use App\Models\QuoteInvitation;
use App\Models\RecurringInvoiceInvitation;
use App\Models\SystemLog;
use App\Services\PdfMaker\Design as PdfDesignModel;
use App\Services\PdfMaker\Design as PdfMakerDesign;
@ -52,6 +53,9 @@ class Phantom
} elseif ($invitation instanceof QuoteInvitation) {
$entity = 'quote';
$entity_design_id = 'quote_design_id';
} elseif ($invitation instanceof RecurringInvoiceInvitation) {
$entity = 'recurring_invoice';
$entity_design_id = 'invoice_design_id';
}
$entity_obj = $invitation->{$entity};
@ -68,6 +72,10 @@ class Phantom
$path = $entity_obj->client->credit_filepath();
}
if ($entity == 'recurring_invoice') {
$path = $entity_obj->client->recurring_invoice_filepath();
}
$file_path = $path.$entity_obj->number.'.pdf';
$url = config('ninja.app_url').'/phantom/'.$entity.'/'.$invitation->key.'?phantomjs_secret='.config('ninja.phantomjs_secret');
@ -147,6 +155,10 @@ class Phantom
App::setLocale($invitation->contact->preferredLocale());
$entity_design_id = $entity . '_design_id';
if($entity == 'recurring_invoice')
$entity_design_id = 'invoice_design_id';
$design_id = $entity_obj->design_id ? $entity_obj->design_id : $this->decodePrimaryKey($entity_obj->client->getSetting($entity_design_id));
$design = Design::find($design_id);
@ -181,6 +193,9 @@ class Phantom
->build()
->getCompiledHTML(true);
if (config('ninja.log_pdf_html')) {
info($data['html']);
}
return view('pdf.html', $data);
}

View File

@ -118,6 +118,10 @@ trait AppSetup
private function updateEnvironmentProperty(string $property, $value): void
{
if (Str::contains($value, '#')) {
$value = sprintf('"%s"', $value);
}
$env = file(base_path('.env'));
$position = null;
@ -135,7 +139,7 @@ trait AppSetup
} elseif ($words_count > 1) {
$env[$position] = "{$property}=" . '"' . $value . '"' . "\n"; // If value of variable is more than one word, surround with quotes.
} else {
$env[$position] = "{$property}=" . $value . "\n"; // Just a normal variable update, with prexisting keys.
$env[$position] = "{$property}=" . $value . "\n"; // Just a normal variable update, with pre-existing keys.
}
try {

View File

@ -25,6 +25,7 @@ trait UserNotifies
$notifiable_methods = [];
$notifications = $company_user->notifications;
//if a user owns this record or is assigned to it, they are attached the permission for notification.
if ($invitation->{$entity_name}->user_id == $company_user->_user_id || $invitation->{$entity_name}->assigned_user_id == $company_user->user_id) {
array_push($required_permissions, 'all_user_notifications');
}
@ -65,6 +66,7 @@ trait UserNotifies
public function findCompanyUserNotificationType($company_user, $required_permissions) :array
{
if ($company_user->company->is_disabled) {
return [];
}

View File

@ -188,7 +188,7 @@ trait Refundable
$client_balance_adjustment = $this->adjustInvoices($data);
$credit_note->ledger()->updateCreditBalance($client_balance_adjustment, $ledger_string);
// $credit_note->ledger()->updateCreditBalance($client_balance_adjustment, $ledger_string);
$this->client->paid_to_date -= $data['amount'];
$this->client->save();

View File

@ -79,7 +79,6 @@
"mockery/mockery": "^1.3.1",
"nunomaduro/collision": "^5.0",
"phpunit/phpunit": "^9.0",
"spatie/laravel-ray": "^1.3",
"vimeo/psalm": "^4.0",
"wildbit/postmark-php": "^4.0"
},

909
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', ''),
'app_version' => '5.0.56',
'app_version' => '5.1.0',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', false),
@ -139,4 +139,5 @@ return [
'log_pdf_html' => env('LOG_PDF_HTML', false),
'expanded_logging' => env('EXPANDED_LOGGING', false),
'snappdf_chromium_path' => env('SNAPPDF_CHROMIUM_PATH', false),
'v4_migration_version' => '4.5.31',
];

View File

@ -1,5 +1,8 @@
{
"video": false,
"baseUrl": "http://localhost:8000/",
"chromeWebSecurity": false
"baseUrl": "https://localhost:8000/",
"chromeWebSecurity": false,
"env": {
"runningEnvironment": "native"
}
}

View File

@ -0,0 +1,87 @@
context('Checkout.com: Credit card testing', () => {
before(() => {
cy.artisan('migrate:fresh --seed');
cy.artisan('ninja:create-single-account checkout');
});
beforeEach(() => {
cy.viewport('macbook-13');
cy.clientLogin();
});
afterEach(() => {
cy.visit('/client/logout');
});
it('should not be able to add payment method', function () {
cy.visit('/client/payment_methods');
cy.get('[data-cy=add-payment-method]').click();
cy.get('[data-cy=add-credit-card-link]').click();
cy.get('[data-ref=gateway-container]')
.contains('Checkout.com can be can saved as payment method for future use, once you complete your first transaction. Don\'t forget to check "Store credit card details" during payment process.');
});
it('should pay with new card', function () {
cy.visit('/client/invoices');
cy.get('[data-cy=pay-now]').first().click();
cy.get('[data-cy=pay-now-dropdown]').click();
cy.get('[data-cy=pay-with-0]').click();
cy.getWithinIframe('#checkout-frames-card-number').type('4658584090000001');
cy.getWithinIframe('#checkout-frames-expiry-date').type('12/22');
cy.getWithinIframe('#checkout-frames-cvv').type('257');
cy.get('#pay-button').click();
cy.url().should('contain', '/client/payments/VolejRejNm');
});
it('should pay with new card & save credit card for future use', function () {
cy.visit('/client/invoices');
cy.get('[data-cy=pay-now]').first().click();
cy.get('[data-cy=pay-now-dropdown]').click();
cy.get('[data-cy=pay-with-0]').click();
cy.get('[name=token-billing-checkbox]').first().check();
cy.getWithinIframe('#checkout-frames-card-number').type('4543474002249996');
cy.getWithinIframe('#checkout-frames-expiry-date').type('12/22');
cy.getWithinIframe('#checkout-frames-cvv').type('956');
cy.get('#pay-button').click();
cy.url().should('contain', '/client/payments/Wpmbk5ezJn');
});
it('should pay with saved card (token)', function () {
cy.visit('/client/invoices');
cy.get('[data-cy=pay-now]').first().click();
cy.get('[data-cy=pay-now-dropdown]').click();
cy.get('[data-cy=pay-with-0]').click();
cy.get('[name=payment-type]').first().check();
cy.get('#pay-now-with-token').click();
cy.url().should('contain', '/client/payments/Opnel5aKBz');
});
it('should be able to remove payment method', function () {
cy.visit('/client/payment_methods');
cy.get('[data-cy=view-payment-method]').click();
cy.get('#open-delete-popup').click();
cy.get('[data-cy=confirm-payment-removal]').click();
cy.url().should('contain', '/client/payment_methods');
cy.get('body').contains('Payment method has been successfully removed.');
});
});

View File

@ -1,47 +1,98 @@
describe('Stripe Credit Card Payments', () => {
describe('Stripe: Credit card testing', () => {
before(() => {
cy.artisan('migrate:fresh --seed');
cy.artisan('ninja:create-single-account stripe');
});
beforeEach(() => {
cy.viewport('macbook-13');
cy.clientLogin();
});
it('should be able to add credit card using Stripe', () => {
afterEach(() => {
cy.visit('/client/logout');
});
it('should pay with new card', function () {
cy.visit('/client/invoices');
cy.get('[data-cy=pay-now]').first().click();
cy.get('[data-cy=pay-now-dropdown]').click();
cy.get('[data-cy=pay-with-0]').click();
cy.get('#cardholder-name').type('Invoice Ninja Rocks');
cy.getWithinIframe('[name=cardnumber]').type('4242424242424242');
cy.getWithinIframe('[name=exp-date]').type('04/24');
cy.getWithinIframe('[name=cvc]').type('242');
cy.getWithinIframe('[name=postal]').type('42424');
cy.get('#pay-now').click();
cy.url().should('contain', '/client/payments/VolejRejNm');
});
it('should pay with new card & save credit card for future use', function () {
cy.visit('/client/invoices');
cy.get('[data-cy=pay-now]').first().click();
cy.get('[data-cy=pay-now-dropdown]').click();
cy.get('[data-cy=pay-with-0]').click();
cy.get('#cardholder-name').type('Invoice Ninja Rocks');
cy.getWithinIframe('[name=cardnumber]').type('4242424242424242');
cy.getWithinIframe('[name=exp-date]').type('04/24');
cy.getWithinIframe('[name=cvc]').type('242');
cy.getWithinIframe('[name=postal]').type('42424');
cy.get('[name=token-billing-checkbox]').first().check();
cy.get('#pay-now').click();
cy.url().should('contain', '/client/payments/Wpmbk5ezJn');
});
it('should pay with saved card (token)', function () {
cy.visit('/client/invoices');
cy.get('[data-cy=pay-now]').first().click();
cy.get('[data-cy=pay-now-dropdown]').click();
cy.get('[data-cy=pay-with-0]').click();
cy.get('[name=payment-type]').first().check();
cy.get('#pay-now').click();
cy.url().should('contain', '/client/payments/Opnel5aKBz');
});
it('should be able to remove payment method', function () {
cy.visit('/client/payment_methods');
cy.get('[data-cy=view-payment-method]').click();
cy.get('#open-delete-popup').click();
cy.get('[data-cy=confirm-payment-removal]').click();
cy.url().should('contain', '/client/payment_methods');
cy.get('body').contains('Payment method has been successfully removed.');
});
it('should be able to add credit card (standalone)', function () {
cy.visit('/client/payment_methods');
cy.get('[data-cy=add-payment-method]').click();
cy.get('[data-cy=add-credit-card-link]').click();
cy.get('#cardholder-name').type('Invoice Ninja');
cy.get('#cardholder-name').type('Invoice Ninja Rocks');
cy.getWithinIframe('[name=cardnumber]').type('4242424242424242');
cy.getWithinIframe('[name=exp-date]').type('04/24');
cy.getWithinIframe('[name=cvc]').type('242');
cy.getWithinIframe('[name=postal]').type('42424');
cy.getWithinIframe('[name="cardnumber"]').type('4242424242424242');
cy.getWithinIframe('[name="exp-date"]').type('1230');
cy.getWithinIframe('[name="cvc"]').type('100');
cy.getWithinIframe('[name="postal"]').type('12345');
cy.get('#authorize-card').click();
cy.get('#card-button').click();
cy.get('#errors').should('be.empty');
cy.location('pathname').should('eq', '/client/payment_methods');
});
it('should be able to complete payment with added credit card', () => {
cy.visit('/client/invoices');
cy.get('#unpaid-checkbox').click();
cy.get('[data-cy=pay-now')
.first()
.click();
cy.location('pathname').should('eq', '/client/invoices/payment');
cy.get('[data-cy=payment-methods-dropdown').click();
cy.get('[data-cy=payment-method')
.first()
.click();
cy.get('#pay-now-with-token').click();
cy.url().should('contain', '/client/payments');
cy.url().should('contain', '/client/payment_methods');
});
});

35
cypress/support/account.js vendored Normal file
View File

@ -0,0 +1,35 @@
import axios from 'axios';
const baseUrl = Cypress.config().baseUrl.endsWith('/')
? Cypress.config().baseUrl.slice(0, -1)
: Cypress.config().baseUrl;
Cypress.Commands.add('createAdminAccount', () => {
let body = {
first_name: "Cypress",
last_name: "Testing",
email: "cypress_testing@example.com",
password: "password",
terms_of_service: true,
privacy_policy: true,
report_errors: true,
};
let headers = {
"Content-Type": "application/json",
"X-Requested-With": "XMLHttpRequest"
};
return axios.post(`${baseUrl}/api/v1/signup?first_load=true`, body, headers)
.then(response => {
console.log('Data from the request', response.data.data[0]);
return response.data.data[0];
})
.catch(e => {
throw "Unable to create an account for admin.";
});
});
Cypress.Commands.add('createClientAccount', () => {
// ..
});

6
cypress/support/artisan.js vendored Normal file
View File

@ -0,0 +1,6 @@
Cypress.Commands.add('artisan', (cmd) => {
let environment = Cypress.env('runningEnvironment');
let prefix = environment === 'docker' ? 'docker-compose run --rm artisan' : 'php artisan';
return cy.exec(`${prefix} ${cmd}`);
});

View File

@ -14,7 +14,9 @@
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
import './commands';
import './artisan';
import './account';
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@ -5141,7 +5141,6 @@ glob
http
http_multi_server
http_parser
json_rpc_2
matcher
path
pool
@ -8152,6 +8151,33 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
fuchsia_sdk
Copyright 2021 The Fuchsia Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
fuchsia_sdk
The majority of files in this project use the Apache 2.0 License.
There are a few exceptions and their license can be found in the source.
Any license deviations from Apache 2.0 are "more permissive" licenses.

View File

@ -20,18 +20,18 @@ const RESOURCES = {
"assets/assets/images/payment_types/carteblanche.png": "d936e11fa3884b8c9f1bd5c914be8629",
"assets/assets/images/payment_types/dinerscard.png": "06d85186ba858c18ab7c9caa42c92024",
"assets/assets/images/logo.png": "090f69e23311a4b6d851b3880ae52541",
"assets/NOTICES": "c3e1cbfaeb1a4f54fadae1bd6558d91b",
"assets/NOTICES": "6f1d482736ec8b2a4aad78b5c5c69235",
"assets/FontManifest.json": "cf3c681641169319e61b61bd0277378f",
"assets/AssetManifest.json": "659dcf9d1baf3aed3ab1b9c42112bf8f",
"assets/packages/material_design_icons_flutter/lib/fonts/materialdesignicons-webfont.ttf": "3e722fd57a6db80ee119f0e2c230ccff",
"assets/fonts/MaterialIcons-Regular.otf": "1288c9e28052e028aba623321f7826ac",
"/": "23224b5e03519aaa87594403d54412cf",
"main.dart.js": "419ce42069c50ba32d64d01d76373c60",
"main.dart.js": "de56853e220a15bb1f361a7bcc8e396a",
"icons/Icon-512.png": "0f9aff01367f0a0c69773d25ca16ef35",
"icons/Icon-192.png": "bb1cf5f6982006952211c7c8404ffbed",
"manifest.json": "77215c1737c7639764e64a192be2f7b8",
"favicon.ico": "51636d3a390451561744c42188ccd628",
"version.json": "24380404aa64649901a0878a4f6aae18",
"version.json": "661ce90abb75e6e3549b3204793a7e38",
"favicon.png": "dca91c54388f52eded692718d5a98b8b"
};

File diff suppressed because one or more lines are too long

241246
public/main.dart.js vendored

File diff suppressed because one or more lines are too long

View File

@ -9,7 +9,7 @@
"/js/clients/payments/checkout-credit-card.js": "/js/clients/payments/checkout-credit-card.js?id=98e406fa8e4db0e93427",
"/js/clients/payments/stripe-ach.js": "/js/clients/payments/stripe-ach.js?id=c4012ad90f17d60432ad",
"/js/clients/payments/stripe-alipay.js": "/js/clients/payments/stripe-alipay.js?id=6dbe9316b98deea55421",
"/js/clients/payments/stripe-credit-card.js": "/js/clients/payments/stripe-credit-card.js?id=9418a9c5c137994c4bd8",
"/js/clients/payments/stripe-credit-card.js": "/js/clients/payments/stripe-credit-card.js?id=c37c3892d35c50d82521",
"/js/clients/payments/stripe-sofort.js": "/js/clients/payments/stripe-sofort.js?id=9b9fd56d655ad238f149",
"/js/clients/quotes/action-selectors.js": "/js/clients/quotes/action-selectors.js?id=1b8f9325aa6e8595e7fa",
"/js/clients/quotes/approve.js": "/js/clients/quotes/approve.js?id=85bcae0a646882e56b12",

View File

@ -1 +1 @@
{"app_name":"invoiceninja_flutter","version":"5.0.40","build_number":"40"}
{"app_name":"invoiceninja_flutter","version":"5.0.41","build_number":"41"}

View File

@ -95,7 +95,7 @@ class StripeCreditCard {
if (tokenBillingCheckbox) {
document.querySelector('input[name="store_card"]').value =
tokenBillingCheckbox.checked;
tokenBillingCheckbox.value;
}
document.getElementById('server-response').submit();

File diff suppressed because it is too large Load Diff

View File

@ -16,10 +16,11 @@
InvoiceNinja (contact@invoiceninja.com)
@endslot
@slot('footer')
@component('email.components.footer', ['url' => 'https://invoiceninja.com', 'url_text' => '&copy; InvoiceNinja'])
For any info, please visit InvoiceNinja.
@endcomponent
@endslot
@if(!$whitelabel)
@slot('footer')
@component('email.components.footer', ['url' => 'https://invoiceninja.com', 'url_text' => '&copy; InvoiceNinja'])
For any info, please visit InvoiceNinja.
@endcomponent
@endslot
@endif
@endcomponent

View File

@ -1,14 +1,23 @@
@component('email.template.master', ['design' => 'light', 'settings' => $settings])
@slot('header')
@include('email.components.header', ['logo' => $logo])
@endslot
@if(isset($greeting))
<p>{{ $greeting }}</p>
@endif
<p>{{ $title }}</p>
<p>{{ $message }}</p>
@if(isset($additional_info))
<p> {{ $additional_info }}</p>
@endif
@component('email.components.button', ['url' => $url])
@lang($button)
@endcomponent
@ -17,9 +26,11 @@
{{ $signature }}
@endslot
@slot('footer')
@component('email.components.footer', ['url' => 'https://invoiceninja.com', 'url_text' => '&copy; InvoiceNinja'])
For any info, please visit InvoiceNinja.
@endcomponent
@endslot
@if(isset($whitelabel) && !$whitelabel)
@slot('footer')
@component('email.components.footer', ['url' => 'https://invoiceninja.com', 'url_text' => '&copy; InvoiceNinja'])
For any info, please visit InvoiceNinja.
@endcomponent
@endslot
@endif
@endcomponent

View File

@ -6,51 +6,82 @@
<h1>Import completed</h1>
<p>Hello, here is the output of your recent import job.</p>
@if(isset($clients) && count($clients) >=1)
<h3>Clients Imported: {{ count($clients) }} </h3>
<p><b>If your logo imported correctly it will display below. If it didn't import, you'll need to reupload your logo</b></p>
<p><img src="{{ $company->present()->logo() }}"></p>
@if(isset($company) && count($company->clients) >=1)
<p><b>Clients Imported:</b> {{ count($company->clients) }} </p>
@endif
@if(isset($errors['clients']) && count($errors['clients']) >=1)
<h3>Client Errors</h3>
<ul>
@foreach($errors['clients'] as $error)
<li>{{ $error['client'] }} - {{ $error['error'] }}</li>
@endforeach
</ul>
@if(isset($company) && count($company->products) >=1)
<p><b>Products Imported:</b> {{ count($company->products) }} </p>
@endif
@if(isset($invoices) && count($invoices) >=1)
@if(isset($company) && count($company->invoices) >=1)
<p><b>Invoices Imported:</b> {{ count($company->invoices) }} </p>
<h3>Invoices Imported: {{ count($invoices) }} </h3>
<p>To test your PDF generation is working correctly, click <a href="{{$company->invoices->first()->invitations->first()->getLink() }}">here</a>. We've also attempted to attach the PDF to this email.
@endif
@if(isset($errors['invoices']) && count($errors['invoices']) >=1)
<h3>Invoices Errors</h3>
<ul>
@foreach($errors['invoices'] as $error)
<li>{{ $error['invoice'] }} - {{ $error['error'] }}</li>
@endforeach
</ul>
@if(isset($company) && count($company->payments) >=1)
<p><b>Payments Imported:</b> {{ count($company->payments) }} </p>
@endif
@if(isset($products) && count($products) >=1)
<h3>Products Imported: {{ count($products) }} </h3>
@if(isset($company) && count($company->recurring_invoices) >=1)
<p><b>Recurring Invoices Imported:</b> {{ count($company->recurring_invoices) }} </p>
@endif
@if(isset($errors['products']) && count($errors['products']) >=1)
<h3>Client Errors</h3>
<ul>
@foreach($errors['products'] as $error)
<li>{{ $error['product'] }} - {{ $error['error'] }}</li>
@endforeach
</ul>
@if(isset($company) && count($company->quotes) >=1)
<p><b>Quotes Imported:</b> {{ count($company->quotes) }} </p>
@endif
<a href="{{ url('/') }}" target="_blank" class="button">Visit portal</a>
@if(isset($company) && count($company->credits) >=1)
<p><b>Credits Imported:</b> {{ count($company->credits) }} </p>
@endif
<p>Thank you, <br/> Invoice Ninja.</p>
@endcomponent
@if(isset($company) && count($company->projects) >=1)
<p><b>Projects Imported:</b> {{ count($company->projects) }} </p>
@endif
@if(isset($company) && count($company->tasks) >=1)
<p><b>Tasks Imported:</b> {{ count($company->tasks) }} </p>
@endif
@if(isset($company) && count($company->vendors) >=1)
<p><b>Vendors Imported:</b> {{ count($company->vendors) }} </p>
@endif
@if(isset($company) && count($company->expenses) >=1)
<p><b>Expenses Imported:</b> {{ count($company->expenses) }} </p>
@endif
@if(isset($company) && count($company->company_gateways) >=1)
<p><b>Gateways Imported:</b> {{ count($company->company_gateways) }} </p>
@endif
@if(isset($company) && count($company->client_gateway_tokens) >=1)
<p><b>Client Gateway Tokens Imported:</b> {{ count($company->client_gateway_tokens) }} </p>
@endif
@if(isset($company) && count($company->tax_rates) >=1)
<p><b>Tax Rates Imported:</b> {{ count($company->tax_rates) }} </p>
@endif
@if(isset($company) && count($company->documents) >=1)
<p><b>Documents Imported:</b> {{ count($company->documents) }} </p>
@endif
<a href="{{ url('/') }}" target="_blank" class="button">{{ ctrans('texts.account_login')}}</a>
<p>{{ ctrans('texts.email_signature')}}<br/> {{ ctrans('texts.email_from') }}</p>
@if(!$whitelabel)
@slot('footer')
@component('email.components.footer', ['url' => 'https://invoiceninja.com', 'url_text' => '&copy; InvoiceNinja'])
For any info, please visit InvoiceNinja.
@endcomponent
@endslot
@endif
@endcomponent

View File

@ -56,7 +56,8 @@
{{ $invoice->formatDate($invoice->next_send_date, $invoice->client->date_format()) }}
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ $invoice->remaining_cycles }}
{{ $invoice->remaining_cycles == '-1' ? ctrans('texts.endless') : $invoice->remaining_cycles }}
@if($invoice->remaining_cycles == '-1') &#8734; @endif
</td>
<td class="px-6 py-4 whitespace-no-wrap text-sm leading-5 text-gray-500">
{{ \App\Utils\Number::formatMoney($invoice->amount, $invoice->client) }}

View File

@ -1,4 +1,10 @@
@if($gateway->company_gateway->token_billing !== 'always')
@php
$token_billing = $gateway instanceof \App\Models\CompanyGateway
? $gateway->token_billing !== 'always'
: $gateway->company_gateway->token_billing !== 'always';
@endphp
@if($token_billing)
<div class="sm:grid px-4 py-5 sm:grid-cols-3 sm:gap-4 sm:px-6" id="save-card--container">
<dt class="text-sm leading-5 font-medium text-gray-500">
{{ ctrans('texts.save_payment_method_details') }}

View File

@ -24,7 +24,7 @@
<div x-data="{ open: false }" @keydown.window.escape="open = false" @click.away="open = false" class="relative inline-block text-left" data-cy="payment-methods-dropdown">
<div>
<div class="rounded-md shadow-sm">
<button @click="open = !open" type="button" class="inline-flex justify-center w-full px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition duration-150 ease-in-out bg-white border border-gray-300 rounded-md hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-50 active:text-gray-800">
<button data-cy="pay-now-dropdown" @click="open = !open" type="button" class="inline-flex justify-center w-full px-4 py-2 text-sm font-medium leading-5 text-gray-700 transition duration-150 ease-in-out bg-white border border-gray-300 rounded-md hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-50 active:text-gray-800">
{{ ctrans('texts.pay_now') }}
<svg class="w-5 h-5 ml-2 -mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
@ -35,13 +35,13 @@
<div x-show="open" class="absolute right-0 w-56 mt-2 origin-top-right rounded-md shadow-lg">
<div class="bg-white rounded-md shadow-xs">
<div class="py-1">
@foreach($payment_methods as $payment_method)
@foreach($payment_methods as $index => $payment_method)
@if($payment_method['label'] == 'Custom')
<a href="#" @click="{ open = false }" data-company-gateway-id="{{ $payment_method['company_gateway_id'] }}" data-gateway-type-id="{{ $payment_method['gateway_type_id'] }}" class="block px-4 py-2 text-sm leading-5 text-gray-700 dropdown-gateway-button hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900" data-cy="payment-method">
<a href="#" @click="{ open = false }" data-cy="pay-with-custom" data-company-gateway-id="{{ $payment_method['company_gateway_id'] }}" data-gateway-type-id="{{ $payment_method['gateway_type_id'] }}" class="block px-4 py-2 text-sm leading-5 text-gray-700 dropdown-gateway-button hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900" data-cy="payment-method">
{{ \App\Models\CompanyGateway::find($payment_method['company_gateway_id'])->firstOrFail()->getConfigField('name') }}
</a>
@elseif($total > 0)
<a href="#" @click="{ open = false }" data-company-gateway-id="{{ $payment_method['company_gateway_id'] }}" data-gateway-type-id="{{ $payment_method['gateway_type_id'] }}" class="block px-4 py-2 text-sm leading-5 text-gray-700 dropdown-gateway-button hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900" data-cy="payment-method">
<a href="#" @click="{ open = false }" data-cy="pay-with-{{ $index }}" data-company-gateway-id="{{ $payment_method['company_gateway_id'] }}" data-gateway-type-id="{{ $payment_method['gateway_type_id'] }}" class="block px-4 py-2 text-sm leading-5 text-gray-700 dropdown-gateway-button hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900" data-cy="payment-method">
{{ $payment_method['label'] }}
</a>
@endif

View File

@ -128,7 +128,7 @@ class CompanyGatewayResolutionTest extends TestCase
$payment_methods = $this->client->service()->getPaymentMethods($amount);
$this->assertEquals(3, count($payment_methods));
$this->assertEquals(2, count($payment_methods));
}
public function testRemoveMethods()

View File

@ -51,7 +51,7 @@ class CompanyLedgerTest extends TestCase
$this->withoutExceptionHandling();
$this->artisan('db:seed');
$this->artisan('db:seed --force');
/* Warm up the cache !*/
$cached_tables = config('ninja.cached_tables');

View File

@ -131,7 +131,7 @@ trait MockAccountData
/* Warm up the cache !*/
$cached_tables = config('ninja.cached_tables');
$this->artisan('db:seed');
$this->artisan('db:seed --force');
foreach ($cached_tables as $name => $class) {

View File

@ -21,8 +21,8 @@ use Tests\TestCase;
*/
class AutoBillInvoiceTest extends TestCase
{
use MockAccountData;
use DatabaseTransactions;
use MockAccountData;
public function setUp() :void
{