mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-10 21:22:58 +01:00
commit
f8b99a1cea
2
.env.ci
2
.env.ci
@ -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
|
||||
|
2
.github/workflows/phpunit.yml
vendored
2
.github/workflows/phpunit.yml
vendored
@ -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:
|
||||
|
@ -1 +1 @@
|
||||
5.0.56
|
||||
5.1.0
|
@ -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')
|
||||
|
@ -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) {
|
||||
|
@ -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 ");
|
||||
|
||||
|
@ -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;
|
||||
// }
|
||||
// }
|
||||
|
||||
|
@ -33,6 +33,8 @@ class InvitationController extends Controller
|
||||
|
||||
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();
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
109
app/Jobs/Mail/AutoBillingFailureMailer.php
Normal file
109
app/Jobs/Mail/AutoBillingFailureMailer.php
Normal 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
111
app/Jobs/Mail/ClientPaymentFailureMailer.php
Normal file
111
app/Jobs/Mail/ClientPaymentFailureMailer.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -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;
|
||||
@ -84,13 +85,14 @@ class PaymentFailureMailer extends BaseMailerJob implements ShouldQueue
|
||||
$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
|
||||
|
@ -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()));
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
104
app/Mail/Admin/AutoBillingFailureObject.php
Normal file
104
app/Mail/Admin/AutoBillingFailureObject.php
Normal 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;
|
||||
}
|
||||
}
|
114
app/Mail/Admin/ClientPaymentFailureObject.php
Normal file
114
app/Mail/Admin/ClientPaymentFailureObject.php
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
return $this->from(config('mail.from.address'), config('mail.from.name'))
|
||||
->view('email.migration.completed', $data);
|
||||
$result = $this->from(config('mail.from.address'), config('mail.from.name'))
|
||||
->view('email.import.completed', $data);
|
||||
|
||||
if($this->company->invoices->count() >=1)
|
||||
$result->attach($this->company->invoices->first()->pdf_file_path());
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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(
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -79,86 +79,44 @@ 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' => '',
|
||||
];
|
||||
|
||||
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
|
||||
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;
|
||||
|
||||
$data = [
|
||||
'status' => '',
|
||||
'error_type' => '',
|
||||
'error_code' => '',
|
||||
'param' => '',
|
||||
'message' => 'Too many requests made to the API too quickly',
|
||||
];
|
||||
default:
|
||||
$data['message'] = $e->getMessage();
|
||||
break;
|
||||
}
|
||||
|
||||
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(),
|
||||
];
|
||||
$this->stripe->processInternallyFailedPayment($this->stripe, $e);
|
||||
|
||||
SystemLogger::dispatch($data, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_STRIPE, $this->stripe->client);
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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'));
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 [];
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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
909
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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',
|
||||
];
|
||||
|
@ -1,5 +1,8 @@
|
||||
{
|
||||
"video": false,
|
||||
"baseUrl": "http://localhost:8000/",
|
||||
"chromeWebSecurity": false
|
||||
"baseUrl": "https://localhost:8000/",
|
||||
"chromeWebSecurity": false,
|
||||
"env": {
|
||||
"runningEnvironment": "native"
|
||||
}
|
||||
}
|
||||
|
87
cypress/integration/client_portal/checkout_credit_card.spec.js
vendored
Normal file
87
cypress/integration/client_portal/checkout_credit_card.spec.js
vendored
Normal 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.');
|
||||
});
|
||||
});
|
@ -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
35
cypress/support/account.js
vendored
Normal 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
6
cypress/support/artisan.js
vendored
Normal 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}`);
|
||||
});
|
4
cypress/support/index.js
vendored
4
cypress/support/index.js
vendored
@ -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')
|
||||
|
@ -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.
|
||||
|
6
public/flutter_service_worker.js
vendored
6
public/flutter_service_worker.js
vendored
@ -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
241246
public/main.dart.js
vendored
File diff suppressed because one or more lines are too long
@ -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",
|
||||
|
@ -1 +1 @@
|
||||
{"app_name":"invoiceninja_flutter","version":"5.0.40","build_number":"40"}
|
||||
{"app_name":"invoiceninja_flutter","version":"5.0.41","build_number":"41"}
|
@ -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
@ -16,10 +16,11 @@
|
||||
InvoiceNinja (contact@invoiceninja.com)
|
||||
@endslot
|
||||
|
||||
@slot('footer')
|
||||
@component('email.components.footer', ['url' => 'https://invoiceninja.com', 'url_text' => '© InvoiceNinja'])
|
||||
For any info, please visit InvoiceNinja.
|
||||
@endcomponent
|
||||
@endslot
|
||||
|
||||
@if(!$whitelabel)
|
||||
@slot('footer')
|
||||
@component('email.components.footer', ['url' => 'https://invoiceninja.com', 'url_text' => '© InvoiceNinja'])
|
||||
For any info, please visit InvoiceNinja.
|
||||
@endcomponent
|
||||
@endslot
|
||||
@endif
|
||||
@endcomponent
|
@ -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' => '© 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' => '© InvoiceNinja'])
|
||||
For any info, please visit InvoiceNinja.
|
||||
@endcomponent
|
||||
@endslot
|
||||
@endif
|
||||
@endcomponent
|
||||
|
@ -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>
|
||||
@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' => '© InvoiceNinja'])
|
||||
For any info, please visit InvoiceNinja.
|
||||
@endcomponent
|
||||
@endslot
|
||||
@endif
|
||||
@endcomponent
|
@ -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') ∞ @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) }}
|
||||
|
@ -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') }}
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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');
|
||||
|
@ -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) {
|
||||
|
||||
|
@ -21,8 +21,8 @@ use Tests\TestCase;
|
||||
*/
|
||||
class AutoBillInvoiceTest extends TestCase
|
||||
{
|
||||
use MockAccountData;
|
||||
use DatabaseTransactions;
|
||||
use MockAccountData;
|
||||
|
||||
public function setUp() :void
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user