Merge branch 'v5-develop' into #7909-SwissQR-InvoiceNr-ReferenceNr

This commit is contained in:
David Bomba 2022-11-15 13:52:52 +11:00 committed by GitHub
commit 319a52cdda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
360 changed files with 332364 additions and 321871 deletions

View File

@ -13,7 +13,7 @@ jobs:
strategy: strategy:
matrix: matrix:
operating-system: ['ubuntu-20.04', 'ubuntu-22.04'] operating-system: ['ubuntu-20.04', 'ubuntu-22.04']
php-versions: ['8.1'] php-versions: ['8.1.11']
phpunit-versions: ['latest'] phpunit-versions: ['latest']
env: env:

1
.gitignore vendored
View File

@ -4,6 +4,7 @@
/public/react /public/react
/storage/*.key /storage/*.key
/storage/debugbar /storage/debugbar
/storage/*
/vendor /vendor
/.idea /.idea
/.vscode /.vscode

View File

@ -3,7 +3,6 @@
</p> </p>
![v5-develop phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-develop) ![v5-develop phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-develop)
![v5-stable phpunit](https://github.com/invoiceninja/invoiceninja/workflows/phpunit/badge.svg?branch=v5-stable)
[![Codacy Badge](https://app.codacy.com/project/badge/Grade/d16c78aad8574466bf83232b513ef4fb)](https://www.codacy.com/gh/turbo124/invoiceninja/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=turbo124/invoiceninja&amp;utm_campaign=Badge_Grade) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/d16c78aad8574466bf83232b513ef4fb)](https://www.codacy.com/gh/turbo124/invoiceninja/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=turbo124/invoiceninja&amp;utm_campaign=Badge_Grade)
<a href="https://cla-assistant.io/invoiceninja/invoiceninja"><img src="https://cla-assistant.io/readme/badge/invoiceninja/invoiceninja" alt="CLA assistant" /></a> <a href="https://cla-assistant.io/invoiceninja/invoiceninja"><img src="https://cla-assistant.io/readme/badge/invoiceninja/invoiceninja" alt="CLA assistant" /></a>

View File

@ -1 +1 @@
5.5.36 5.5.39

View File

@ -13,7 +13,9 @@ namespace App\Console\Commands;
use App\Libraries\MultiDB; use App\Libraries\MultiDB;
use App\Models\Backup; use App\Models\Backup;
use App\Models\Company;
use App\Models\Design; use App\Models\Design;
use App\Models\Document;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use stdClass; use stdClass;
@ -25,14 +27,14 @@ class BackupUpdate extends Command
* *
* @var string * @var string
*/ */
protected $signature = 'ninja:backup-update'; protected $signature = 'ninja:backup-files {--disk=}';
/** /**
* The console command description. * The console command description.
* *
* @var string * @var string
*/ */
protected $description = 'Shift backups from DB to storage'; protected $description = 'Shift files between object storage locations';
/** /**
* Create a new command instance. * Create a new command instance.
@ -74,17 +76,52 @@ class BackupUpdate extends Command
{ {
set_time_limit(0); set_time_limit(0);
Backup::whereHas('activity')->whereRaw('html_backup IS NOT NULL')->cursor()->each(function ($backup) { //logos
if (strlen($backup->html_backup) > 1 && $backup->activity->invoice->exists()) { Company::cursor()
$client = $backup->activity->invoice->client; ->each(function ($company){
$backup->storeRemotely($backup->html_backup, $client);
} elseif (strlen($backup->html_backup) > 1 && $backup->activity->quote->exists()) { $company_logo = $company->present()->logo();
$client = $backup->activity->quote->client;
$backup->storeRemotely($backup->html_backup, $client); if($company_logo == 'https://invoicing.co/images/new_logo.png')
} elseif (strlen($backup->html_backup) > 1 && $backup->activity->credit->exists()) { return;
$client = $backup->activity->credit->client;
$backup->storeRemotely($backup->html_backup, $client); $logo = @file_get_contents($company_logo);
}
}); if($logo){
$path = str_replace("https://objects.invoicing.co/", "", $company->present()->logo());
$path = str_replace("https://v5-at-backup.us-southeast-1.linodeobjects.com/", "", $path);
Storage::disk($this->option('disk'))->put($path, $logo);
}
});
//documents
Document::cursor()
->each(function ($document){
$doc_bin = $document->getFile();
if($doc_bin)
Storage::disk($this->option('disk'))->put($document->url, $doc_bin);
});
//backups
Backup::cursor()
->each(function ($backup){
$backup_bin = Storage::disk('s3')->get($backup->filename);
if($backup_bin)
Storage::disk($this->option('disk'))->put($backup->filename, $backup_bin);
});
} }
} }

View File

@ -103,8 +103,9 @@ class MobileLocalization extends Command
$data = substr($data, $start, $end - $start - 5); $data = substr($data, $start, $end - $start - 5);
$data = str_replace("\n", '', $data); $data = str_replace("\n", '', $data);
$data = str_replace('"', "\'", $data); $data = str_replace("\'", "\#", $data);
$data = str_replace("'", '"', $data); $data = str_replace("'", '"', $data);
$data = str_replace("\#", "'", $data);
return json_decode('{'.rtrim($data, ',').'}'); return json_decode('{'.rtrim($data, ',').'}');
} }

View File

@ -98,7 +98,8 @@ class Kernel extends ConsoleKernel
$schedule->job(new AdjustEmailQuota)->dailyAt('23:30')->withoutOverlapping(); $schedule->job(new AdjustEmailQuota)->dailyAt('23:30')->withoutOverlapping();
$schedule->job(new SendFailedEmails)->daily()->withoutOverlapping(); //not used @deprecate
// $schedule->job(new SendFailedEmails)->daily()->withoutOverlapping();
$schedule->command('ninja:check-data --database=db-ninja-01')->daily('02:00')->withoutOverlapping(); $schedule->command('ninja:check-data --database=db-ninja-01')->daily('02:00')->withoutOverlapping();

View File

@ -0,0 +1,80 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataMapper\Analytics;
use Turbo124\Beacon\ExampleMetric\GenericMixedMetric;
class AccountSignup extends GenericMixedMetric
{
/**
* The type of Sample.
*
* Monotonically incrementing counter
*
* - counter
*
* @var string
*/
public $type = 'mixed_metric';
/**
* The name of the counter.
* @var string
*/
public $name = 'account.signup';
/**
* The datetime of the counter measurement.
*
* date("Y-m-d H:i:s")
*
* @var DateTime
*/
public $datetime;
/**
* The Class failure name
* set to 0.
*
* @var string
*/
public $string_metric5 = 'plan';
/**
* The exception string
* set to 0.
*
* @var string
*/
public $string_metric6 = 'term';
/**
* The counter
* set to 1.
*
* @var string
*/
public $int_metric1 = 1;
/**
* Company Key
* @var string
*/
public $string_metric7 = 'key';
public function __construct($string_metric5, $string_metric6, $string_metric7)
{
$this->string_metric5 = $string_metric5 ?: 'free';
$this->string_metric6 = $string_metric6 ?: 'year';
$this->string_metric7 = $string_metric7;
}
}

View File

@ -44,6 +44,8 @@ class InvoiceItem
public $line_total = 0; public $line_total = 0;
public $gross_line_total = 0; public $gross_line_total = 0;
public $tax_amount = 0;
public $date = ''; public $date = '';
@ -75,6 +77,7 @@ class InvoiceItem
'sort_id' => 'string', 'sort_id' => 'string',
'line_total' => 'float', 'line_total' => 'float',
'gross_line_total' => 'float', 'gross_line_total' => 'float',
'tax_amount' => 'float',
'date' => 'string', 'date' => 'string',
'custom_value1' => 'string', 'custom_value1' => 'string',
'custom_value2' => 'string', 'custom_value2' => 'string',

View File

@ -198,7 +198,7 @@ class Handler extends ExceptionHandler
// nlog($exception->validator->getMessageBag()); // nlog($exception->validator->getMessageBag());
return response()->json(['message' => 'The given data was invalid.', 'errors' => $exception->validator->getMessageBag()], 422); return response()->json(['message' => 'The given data was invalid.', 'errors' => $exception->validator->getMessageBag()], 422);
} elseif ($exception instanceof RelationNotFoundException && $request->expectsJson()) { } elseif ($exception instanceof RelationNotFoundException && $request->expectsJson()) {
return response()->json(['message' => $exception->getMessage()], 400); return response()->json(['message' => "Relation `{$exception->relation}` is not a valid include."], 400);
} elseif ($exception instanceof GenericPaymentDriverFailure && $request->expectsJson()) { } elseif ($exception instanceof GenericPaymentDriverFailure && $request->expectsJson()) {
return response()->json(['message' => $exception->getMessage()], 400); return response()->json(['message' => $exception->getMessage()], 400);
} elseif ($exception instanceof GenericPaymentDriverFailure) { } elseif ($exception instanceof GenericPaymentDriverFailure) {

View File

@ -88,6 +88,7 @@ class InvoiceItemExport extends BaseExport
private array $decorate_keys = [ private array $decorate_keys = [
'client', 'client',
'currency_id', 'currency_id',
'status'
]; ];
public function __construct(Company $company, array $input) public function __construct(Company $company, array $input)
@ -116,6 +117,7 @@ class InvoiceItemExport extends BaseExport
$this->csv->insertOne($this->buildHeader()); $this->csv->insertOne($this->buildHeader());
$query = Invoice::query() $query = Invoice::query()
->withTrashed()
->with('client')->where('company_id', $this->company->id) ->with('client')->where('company_id', $this->company->id)
->where('is_deleted',0); ->where('is_deleted',0);
@ -206,10 +208,10 @@ class InvoiceItemExport extends BaseExport
if(in_array('currency_id', $this->input['report_keys'])) if(in_array('currency_id', $this->input['report_keys']))
$entity['currency'] = $invoice->client->currency() ? $invoice->client->currency()->code : $invoice->company->currency()->code; $entity['currency'] = $invoice->client->currency() ? $invoice->client->currency()->code : $invoice->company->currency()->code;
if(in_array('client_id', $this->input['report_keys'])) // if(in_array('client_id', $this->input['report_keys']))
$entity['client'] = $invoice->client->present()->name(); $entity['client'] = $invoice->client->present()->name();
if(in_array('status_id', $this->input['report_keys'])) // if(in_array('status_id', $this->input['report_keys']))
$entity['status'] = $invoice->stringStatus($invoice->status_id); $entity['status'] = $invoice->stringStatus($invoice->status_id);
return $entity; return $entity;

View File

@ -89,7 +89,10 @@ class PaymentExport extends BaseExport
//insert the header //insert the header
$this->csv->insertOne($this->buildHeader()); $this->csv->insertOne($this->buildHeader());
$query = Payment::query()->where('company_id', $this->company->id)->where('is_deleted', 0); $query = Payment::query()
->withTrashed()
->where('company_id', $this->company->id)
->where('is_deleted', 0);
$query = $this->addDateRange($query); $query = $this->addDateRange($query);

View File

@ -82,7 +82,10 @@ class ProductExport extends BaseExport
//insert the header //insert the header
$this->csv->insertOne($this->buildHeader()); $this->csv->insertOne($this->buildHeader());
$query = Product::query()->where('company_id', $this->company->id)->where('is_deleted', 0); $query = Product::query()
->withTrashed()
->where('company_id', $this->company->id)
->where('is_deleted', 0);
$query = $this->addDateRange($query); $query = $this->addDateRange($query);

View File

@ -99,6 +99,7 @@ class QuoteExport extends BaseExport
$this->csv->insertOne($this->buildHeader()); $this->csv->insertOne($this->buildHeader());
$query = Quote::query() $query = Quote::query()
->withTrashed()
->with('client') ->with('client')
->where('company_id', $this->company->id) ->where('company_id', $this->company->id)
->where('is_deleted', 0); ->where('is_deleted', 0);

View File

@ -116,6 +116,7 @@ class QuoteItemExport extends BaseExport
$this->csv->insertOne($this->buildHeader()); $this->csv->insertOne($this->buildHeader());
$query = Quote::query() $query = Quote::query()
->withTrashed()
->with('client')->where('company_id', $this->company->id) ->with('client')->where('company_id', $this->company->id)
->where('is_deleted', 0); ->where('is_deleted', 0);

View File

@ -91,7 +91,10 @@ class TaskExport extends BaseExport
//insert the header //insert the header
$this->csv->insertOne($this->buildHeader()); $this->csv->insertOne($this->buildHeader());
$query = Task::query()->where('company_id', $this->company->id)->where('is_deleted', 0); $query = Task::query()
->withTrashed()
->where('company_id', $this->company->id)
->where('is_deleted', 0);
$query = $this->addDateRange($query); $query = $this->addDateRange($query);

View File

@ -0,0 +1,133 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Filters;
use App\Models\BankIntegration;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
/**
* BankIntegrationFilters.
*/
class BankIntegrationFilters extends QueryFilters
{
/**
* Filter by name.
*
* @param string $name
* @return Builder
*/
public function name(string $name = ''): Builder
{
if(strlen($name) >=1)
return $this->builder->where('bank_account_name', 'like', '%'.$name.'%');
return $this->builder;
}
/**
* Filter based on search text.
*
* @param string query filter
* @return Builder
* @deprecated
*/
public function filter(string $filter = '') : Builder
{
if (strlen($filter) == 0) {
return $this->builder;
}
return $this->builder->where(function ($query) use ($filter) {
$query->where('bank_integrations.bank_account_name', 'like', '%'.$filter.'%');
});
}
/**
* Filters the list based on the status
* archived, active, deleted.
*
* @param string filter
* @return Builder
*/
public function status(string $filter = '') : Builder
{
if (strlen($filter) == 0) {
return $this->builder;
}
$table = 'bank_integrations';
$filters = explode(',', $filter);
return $this->builder->where(function ($query) use ($filters, $table) {
$query->whereNull($table.'.id');
if (in_array(parent::STATUS_ACTIVE, $filters)) {
$query->orWhereNull($table.'.deleted_at');
}
if (in_array(parent::STATUS_ARCHIVED, $filters)) {
$query->orWhere(function ($query) use ($table) {
$query->whereNotNull($table.'.deleted_at');
if (! in_array($table, ['users'])) {
$query->where($table.'.is_deleted', '=', 0);
}
});
}
if (in_array(parent::STATUS_DELETED, $filters)) {
$query->orWhere($table.'.is_deleted', '=', 1);
}
});
}
/**
* Sorts the list based on $sort.
*
* @param string sort formatted as column|asc
* @return Builder
*/
public function sort(string $sort) : Builder
{
$sort_col = explode('|', $sort);
return $this->builder->orderBy($sort_col[0], $sort_col[1]);
}
/**
* Returns the base query.
*
* @param int company_id
* @param User $user
* @return Builder
* @deprecated
*/
public function baseQuery(int $company_id, User $user) : Builder
{
}
/**
* Filters the query by the users company ID.
*
* @return Illuminate\Database\Query\Builder
*/
public function entityFilter()
{
//return $this->builder->whereCompanyId(auth()->user()->company()->id);
return $this->builder->company();
}
}

View File

@ -0,0 +1,133 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Filters;
use App\Models\BankTransaction;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
/**
* BankTransactionFilters.
*/
class BankTransactionFilters extends QueryFilters
{
/**
* Filter by name.
*
* @param string $name
* @return Builder
*/
public function name(string $name = ''): Builder
{
if(strlen($name) >=1)
return $this->builder->where('bank_account_name', 'like', '%'.$name.'%');
return $this->builder;
}
/**
* Filter based on search text.
*
* @param string query filter
* @return Builder
* @deprecated
*/
public function filter(string $filter = '') : Builder
{
if (strlen($filter) == 0) {
return $this->builder;
}
return $this->builder->where(function ($query) use ($filter) {
$query->where('bank_transactions.description', 'like', '%'.$filter.'%');
});
}
/**
* Filters the list based on the status
* archived, active, deleted.
*
* @param string filter
* @return Builder
*/
public function status(string $filter = '') : Builder
{
if (strlen($filter) == 0) {
return $this->builder;
}
$table = 'bank_transactions';
$filters = explode(',', $filter);
return $this->builder->where(function ($query) use ($filters, $table) {
$query->whereNull($table.'.id');
if (in_array(parent::STATUS_ACTIVE, $filters)) {
$query->orWhereNull($table.'.deleted_at');
}
if (in_array(parent::STATUS_ARCHIVED, $filters)) {
$query->orWhere(function ($query) use ($table) {
$query->whereNotNull($table.'.deleted_at');
if (! in_array($table, ['users'])) {
$query->where($table.'.is_deleted', '=', 0);
}
});
}
if (in_array(parent::STATUS_DELETED, $filters)) {
$query->orWhere($table.'.is_deleted', '=', 1);
}
});
}
/**
* Sorts the list based on $sort.
*
* @param string sort formatted as column|asc
* @return Builder
*/
public function sort(string $sort) : Builder
{
$sort_col = explode('|', $sort);
return $this->builder->orderBy($sort_col[0], $sort_col[1]);
}
/**
* Returns the base query.
*
* @param int company_id
* @param User $user
* @return Builder
* @deprecated
*/
public function baseQuery(int $company_id, User $user) : Builder
{
}
/**
* Filters the query by the users company ID.
*
* @return Illuminate\Database\Query\Builder
*/
public function entityFilter()
{
//return $this->builder->whereCompanyId(auth()->user()->company()->id);
return $this->builder->company();
}
}

View File

@ -102,6 +102,9 @@ class ProductFilters extends QueryFilters
{ {
$sort_col = explode('|', $sort); $sort_col = explode('|', $sort);
if(!is_array($sort_col))
return $this->builder;
return $this->builder->orderBy($sort_col[0], $sort_col[1]); return $this->builder->orderBy($sort_col[0], $sort_col[1]);
} }

View File

@ -169,7 +169,7 @@ abstract class QueryFilters
public function clientFilter() public function clientFilter()
{ {
if (auth()->guard('contact')->user()) { if (auth()->guard('contact')->user()) {
return $this->builder->whereClientId(auth()->guard('contact')->user()->client->id); return $this->builder->where('client_id', auth()->guard('contact')->user()->client->id);
} }
} }
@ -179,6 +179,15 @@ abstract class QueryFilters
$created_at = date('Y-m-d H:i:s', $value); $created_at = date('Y-m-d H:i:s', $value);
if(is_string($created_at)){
$created_at = strtotime(str_replace("/","-",$created_at));
if(!$created_at)
return $this->builder;
}
return $this->builder->where('created_at', '>=', $created_at); return $this->builder->where('created_at', '>=', $created_at);
} }

View File

@ -33,10 +33,11 @@ class QuoteFilters extends QueryFilters
} }
return $this->builder->where(function ($query) use ($filter) { return $this->builder->where(function ($query) use ($filter) {
$query->where('quotes.custom_value1', 'like', '%'.$filter.'%') $query->where('quotes.number', 'like', '%'.$filter.'%')
->orWhere('quotes.custom_value2', 'like', '%'.$filter.'%') ->orwhere('quotes.custom_value1', 'like', '%'.$filter.'%')
->orWhere('quotes.custom_value3', 'like', '%'.$filter.'%') ->orWhere('quotes.custom_value2', 'like', '%'.$filter.'%')
->orWhere('quotes.custom_value4', 'like', '%'.$filter.'%'); ->orWhere('quotes.custom_value3', 'like', '%'.$filter.'%')
->orWhere('quotes.custom_value4', 'like', '%'.$filter.'%');
}); });
} }

View File

@ -89,7 +89,7 @@ class AccountTransformer implements AccountTransformerInterface
'account_type' => $account->CONTAINER, 'account_type' => $account->CONTAINER,
'account_name' => $account->accountName, 'account_name' => $account->accountName,
'account_status' => $account->accountStatus, 'account_status' => $account->accountStatus,
'account_number' => $account->accountNumber, 'account_number' => property_exists($account, 'accountNumber') ? '**** ' . substr($account?->accountNumber, -7) : '',
'provider_account_id' => $account->providerAccountId, 'provider_account_id' => $account->providerAccountId,
'provider_id' => $account->providerId, 'provider_id' => $account->providerId,
'provider_name' => $account->providerName, 'provider_name' => $account->providerName,

View File

@ -174,6 +174,20 @@ class Yodlee
} }
public function getAccount($account_id)
{
$token = $this->getAccessToken();
$response = Http::withHeaders($this->getHeaders(["Authorization" => "Bearer {$token}"]))->get($this->getEndpoint(). "/accounts/{$account_id}", []);
if($response->successful())
return true;
if($response->failed())
return false;
}
public function deleteAccount($account_id) public function deleteAccount($account_id)
{ {

View File

@ -0,0 +1,92 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Helpers\Epc;
use App\Models\Company;
use App\Models\Invoice;
use App\Utils\Ninja;
use BaconQrCode\Renderer\ImageRenderer;
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
use BaconQrCode\Writer;
/**
* EpcQrGenerator.
*/
class EpcQrGenerator
{
private array $sepa = [
'serviceTag' => 'BCD',
'version' => 2,
'characterSet' => 1,
'identification' => 'SCT',
'bic' => '',
'purpose' => '',
];
public function __construct(protected Company $company, protected Invoice $invoice, protected float $amount){}
public function getQrCode()
{
$renderer = new ImageRenderer(
new RendererStyle(200),
new SvgImageBackEnd()
);
$writer = new Writer($renderer);
$this->validateFields();
$qr = $writer->writeString($this->encodeMessage());
return "<svg viewBox='0 0 200 200' width='200' height='200' x='0' y='0' xmlns='http://www.w3.org/2000/svg'>
<rect x='0' y='0' width='100%'' height='100%' />{$qr}</svg>";
}
public function encodeMessage()
{
return rtrim(implode("\n", array(
$this->sepa['serviceTag'],
sprintf('%03d', $this->sepa['version']),
$this->sepa['characterSet'],
$this->sepa['identification'],
isset($this->company?->custom_fields?->company2) ? $this->company->settings->custom_value2 : '',
$this->company->present()->name(),
isset($this->company?->custom_fields?->company1) ? $this->company->settings->custom_value1 : '',
$this->formatMoney($this->amount),
$this->sepa['purpose'],
substr($this->invoice->number,0,34),
substr($this->invoice->public_notes,0,139),
''
)), "\n");
}
private function validateFields()
{
if(Ninja::isSelfHost() && isset($this->company?->custom_fields?->company2))
nlog('The BIC field is not present and _may_ be a required fields for EPC QR codes');
if(Ninja::isSelfHost() && isset($this->company?->custom_fields?->company1))
nlog('The IBAN field is required');
}
private function formatMoney($value) {
return sprintf('EUR%s', number_format($value, 2, '.', ''));
}
}

View File

@ -40,4 +40,7 @@ function nlog($output, $context = []): void
} else { } else {
\Illuminate\Support\Facades\Log::channel('invoiceninja')->info($output, $context); \Illuminate\Support\Facades\Log::channel('invoiceninja')->info($output, $context);
} }
$output = null;
$context = null;
} }

View File

@ -30,6 +30,8 @@ class InvoiceItemSum
private $gross_line_total; private $gross_line_total;
private $tax_amount;
private $currency; private $currency;
private $total_taxes; private $total_taxes;
@ -111,14 +113,10 @@ class InvoiceItemSum
$this->setLineTotal($this->getLineTotal() - $this->formatValue($this->item->discount, $this->currency->precision)); $this->setLineTotal($this->getLineTotal() - $this->formatValue($this->item->discount, $this->currency->precision));
} else { } else {
/*Test 16-08-2021*/
$discount = ($this->item->line_total * ($this->item->discount / 100)); $discount = ($this->item->line_total * ($this->item->discount / 100));
$this->setLineTotal($this->formatValue(($this->getLineTotal() - $discount), $this->currency->precision)); $this->setLineTotal($this->formatValue(($this->getLineTotal() - $discount), $this->currency->precision));
/*Test 16-08-2021*/
//replaces the following
// $this->setLineTotal($this->getLineTotal() - $this->formatValue(round($this->item->line_total * ($this->item->discount / 100), 2), $this->currency->precision));
} }
$this->item->is_amount_discount = $this->invoice->is_amount_discount; $this->item->is_amount_discount = $this->invoice->is_amount_discount;
@ -160,6 +158,8 @@ class InvoiceItemSum
$this->item->gross_line_total = $this->getLineTotal() + $item_tax; $this->item->gross_line_total = $this->getLineTotal() + $item_tax;
$this->item->tax_amount = $item_tax;
return $this; return $this;
} }

View File

@ -40,6 +40,8 @@ class InvoiceItemSumInclusive
private $tax_collection; private $tax_collection;
private $tax_amount;
public function __construct($invoice) public function __construct($invoice)
{ {
$this->tax_collection = collect([]); $this->tax_collection = collect([]);
@ -144,6 +146,8 @@ class InvoiceItemSumInclusive
$this->groupTax($this->item->tax_name3, $this->item->tax_rate3, $item_tax_rate3_total); $this->groupTax($this->item->tax_name3, $this->item->tax_rate3, $item_tax_rate3_total);
} }
$this->item->tax_amount = $this->formatValue($item_tax, $this->currency->precision);
$this->setTotalTaxes($this->formatValue($item_tax, $this->currency->precision)); $this->setTotalTaxes($this->formatValue($item_tax, $this->currency->precision));
return $this; return $this;

View File

@ -105,7 +105,7 @@ class SwissQrGenerator
// Add payment reference // Add payment reference
// This is what you will need to identify incoming payments. // This is what you will need to identify incoming payments.
if(stripos($this->invoice->number, "Live-") === 0) if(stripos($this->invoice->number, "Live") === 0)
{ {
// we're currently in preview status. Let's give a dummy reference for now // we're currently in preview status. Let's give a dummy reference for now
$invoice_number = "123456789"; $invoice_number = "123456789";
@ -140,6 +140,7 @@ class SwissQrGenerator
} }
$invoice_number = $calcInvoiceNumber; $invoice_number = $calcInvoiceNumber;
} }
if(strlen($this->company->present()->besr_id()) > 1) if(strlen($this->company->present()->besr_id()) > 1)

View File

@ -182,9 +182,7 @@ class ActivityController extends BaseController
} else { } else {
$html_backup = file_get_contents(Storage::disk(config('filesystems.default'))->path($backup->filename)); $html_backup = file_get_contents(Storage::disk(config('filesystems.default'))->path($backup->filename));
} }
} elseif ($backup && $backup->html_backup) { //db } else { //failed
$html_backup = $backup->html_backup;
} elseif (! $backup || ! $backup->html_backup) { //failed
return response()->json(['message'=> ctrans('texts.no_backup_exists'), 'errors' => new stdClass], 404); return response()->json(['message'=> ctrans('texts.no_backup_exists'), 'errors' => new stdClass], 404);
} }

View File

@ -33,6 +33,8 @@ class YodleeController extends BaseController
$company = $request->getCompany(); $company = $request->getCompany();
//ensure user is enterprise!!
if($company->account->bank_integration_account_id){ if($company->account->bank_integration_account_id){
$flow = 'edit'; $flow = 'edit';
@ -112,7 +114,192 @@ class YodleeController extends BaseController
}); });
}
/**
* Process Yodlee Refresh Webhook.
*
*
* @OA\Post(
* path="/api/v1/yodlee/refresh",
* operationId="yodleeRefreshWebhook",
* tags={"yodlee"},
* summary="Processing webhooks from Yodlee",
* description="Notifies the system when a data point can be refreshed",
* @OA\Parameter(ref="#/components/parameters/X-Api-Secret"),
* @OA\Parameter(ref="#/components/parameters/X-Api-Token"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Response(
* response=200,
* description="",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"),
* @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Credit"),
* ),
* @OA\Response(
* response=422,
* description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"),
*
* ),
* @OA\Response(
* response="default",
* description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"),
* ),
* )
*/
/*
{
"event":{
"info":"REFRESH.PROCESS_COMPLETED",
"loginName":"fri21",
"data":{
"providerAccount":[
{
"id":10995860,
"providerId":16441,
"isManual":false,
"createdDate":"2017-12-22T05:47:35Z",
"aggregationSource":"USER",
"status":"SUCCESS",
"requestId":"NSyMGo+R4dktywIu3hBIkc3PgWA=",
"dataset":[
{
"name":"BASIC_AGG_DATA",
"additionalStatus":"AVAILABLE_DATA_RETRIEVED",
"updateEligibility":"ALLOW_UPDATE",
"lastUpdated":"2017-12-22T05:48:16Z",
"lastUpdateAttempt":"2017-12-22T05:48:16Z"
}
]
}
]
}
}
}*/
public function refreshWebhook(Request $request)
{
//we should ignore this one
nlog("yodlee refresh");
nlog($request->all());
return response()->json(['message' => 'Success'], 200);
//
// return response()->json(['message' => 'Unauthorized'], 403);
}
/*
{
"event":{
"notificationId":"63c73475-4db5-49ef-8553-8303337ca7c3",
"info":"LATEST_BALANCE_UPDATES",
"loginName":"user1",
"data":{
"providerAccountId":658552,
"latestBalanceEvent":[
{
"accountId":12345,
"status":"SUCCESS"
},
{
"accountId":12346,
"status":"FAILED"
}
]
}
}
}
*/
public function balanceWebhook(Request $request)
{
nlog("yodlee refresh");
nlog($request->all());
return response()->json(['message' => 'Success'], 200);
//
// return response()->json(['message' => 'Unauthorized'], 403);
}
/*
{
"event":{
"data":[
{
"autoRefresh":{
"additionalStatus":"SCHEDULED",
"status":"ENABLED"
},
"accountIds":[
1112645899,
1112645898
],
"loginName":"YSL1555332811628",
"providerAccountId":11381459
}
],
"notificationTime":"2019-06-14T04:49:39Z",
"notificationId":"4e672150-156048777",
"info":"AUTO_REFRESH_UPDATES"
}
}
*/
public function refreshUpdatesWebhook(Request $request)
{
//notifies a user if there are problems with yodlee accessing the data
nlog("update refresh");
nlog($request->all());
return response()->json(['message' => 'Success'], 200);
//
// return response()->json(['message' => 'Unauthorized'], 403);
}
/*
"event": {
"notificationId": "64b7ed1a-1530523285",
"info": "DATA_UPDATES.USER_DATA",
"data": {
"userCount": 1,
"fromDate": "2017-11-10T10:18:44Z",
"toDate": "2017-11-10T11:18:43Z",
"userData": [{
"user": {
"loginName": "YSL1484052178554"
},
"links": [{
"methodType": "GET",
"rel": "getUserData",
"href": "dataExtracts/userData?fromDate=2017-11-10T10:18:44Z&toDate=2017-11-10T11:18:43Z&loginName=YSL1484052178554"
}]
}]
}
}
*/
public function dataUpdatesWebhook(Request $request)
{
//this is the main hook we use for notifications
nlog("data refresh");
nlog($request->all());
return response()->json(['message' => 'Success'], 200);
//
// return response()->json(['message' => 'Unauthorized'], 403);
} }
} }

View File

@ -12,6 +12,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Factory\BankIntegrationFactory; use App\Factory\BankIntegrationFactory;
use App\Filters\BankIntegrationFilters;
use App\Helpers\Bank\Yodlee\Yodlee; use App\Helpers\Bank\Yodlee\Yodlee;
use App\Http\Requests\BankIntegration\AdminBankIntegrationRequest; use App\Http\Requests\BankIntegration\AdminBankIntegrationRequest;
use App\Http\Requests\BankIntegration\CreateBankIntegrationRequest; use App\Http\Requests\BankIntegration\CreateBankIntegrationRequest;
@ -23,11 +24,11 @@ use App\Http\Requests\BankIntegration\UpdateBankIntegrationRequest;
use App\Jobs\Bank\ProcessBankTransactions; use App\Jobs\Bank\ProcessBankTransactions;
use App\Models\BankIntegration; use App\Models\BankIntegration;
use App\Repositories\BankIntegrationRepository; use App\Repositories\BankIntegrationRepository;
use App\Services\Bank\BankService; use App\Services\Bank\BankMatchingService;
use App\Transformers\BankIntegrationTransformer; use App\Transformers\BankIntegrationTransformer;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class BankIntegrationController extends BaseController class BankIntegrationController extends BaseController
{ {
@ -91,10 +92,10 @@ class BankIntegrationController extends BaseController
* @param Request $request * @param Request $request
* @return Response|mixed * @return Response|mixed
*/ */
public function index(Request $request) public function index(BankIntegrationFilters $filters)
{ {
$bank_integrations = BankIntegration::query()->company(); $bank_integrations = BankIntegration::filter($filters);
return $this->listResponse($bank_integrations); return $this->listResponse($bank_integrations);
@ -566,9 +567,22 @@ class BankIntegrationController extends BaseController
$bank_integration->currency = $account['account_currency']; $bank_integration->currency = $account['account_currency'];
$bank_integration->save(); $bank_integration->save();
} }
} }
$account = auth()->user()->account;
if(Cache::get("throttle_polling:{$account->key}"))
return response()->json(BankIntegration::query()->company(), 200);
$account->bank_integrations->each(function ($bank_integration) use ($account){
ProcessBankTransactions::dispatch($account->bank_integration_account_id, $bank_integration);
});
Cache::put("throttle_polling:{$account->key}", true, 300);
return response()->json(BankIntegration::query()->company(), 200); return response()->json(BankIntegration::query()->company(), 200);
} }

View File

@ -12,6 +12,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Factory\BankTransactionFactory; use App\Factory\BankTransactionFactory;
use App\Filters\BankTransactionFilters;
use App\Helpers\Bank\Yodlee\Yodlee; use App\Helpers\Bank\Yodlee\Yodlee;
use App\Http\Requests\BankTransaction\AdminBankTransactionRequest; use App\Http\Requests\BankTransaction\AdminBankTransactionRequest;
use App\Http\Requests\BankTransaction\CreateBankTransactionRequest; use App\Http\Requests\BankTransaction\CreateBankTransactionRequest;
@ -26,7 +27,7 @@ use App\Http\Requests\Import\PreImportRequest;
use App\Jobs\Bank\MatchBankTransactions; use App\Jobs\Bank\MatchBankTransactions;
use App\Models\BankTransaction; use App\Models\BankTransaction;
use App\Repositories\BankTransactionRepository; use App\Repositories\BankTransactionRepository;
use App\Services\Bank\BankService; use App\Services\Bank\BankMatchingService;
use App\Transformers\BankTransactionTransformer; use App\Transformers\BankTransactionTransformer;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -92,13 +93,13 @@ class BankTransactionController extends BaseController
* @OA\JsonContent(ref="#/components/schemas/Error"), * @OA\JsonContent(ref="#/components/schemas/Error"),
* ), * ),
* ) * )
* @param Request $request * @param BankTransactionFilters $filter
* @return Response|mixed * @return Response|mixed
*/ */
public function index(Request $request) public function index(BankTransactionFilters $filters)
{ {
$bank_transactions = BankTransaction::query()->company(); $bank_transactions = BankTransaction::filter($filters);
return $this->listResponse($bank_transactions); return $this->listResponse($bank_transactions);

View File

@ -12,6 +12,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Account; use App\Models\Account;
use App\Models\BankTransaction;
use App\Models\Company; use App\Models\Company;
use App\Models\User; use App\Models\User;
use App\Transformers\ArraySerializer; use App\Transformers\ArraySerializer;
@ -819,12 +820,15 @@ class BaseController extends Controller
// 10-01-2022 need to ensure we snake case properly here to ensure permissions work as expected // 10-01-2022 need to ensure we snake case properly here to ensure permissions work as expected
// 28-03-2022 this is definitely correct here, do not append _ to the view, it resolved correctly when snake cased // 28-03-2022 this is definitely correct here, do not append _ to the view, it resolved correctly when snake cased
if (auth()->user() && ! auth()->user()->hasPermission('view'.lcfirst(class_basename(Str::snake($this->entity_type))))) { if (auth()->user() && ! auth()->user()->hasPermission('view'.lcfirst(class_basename(Str::snake($this->entity_type))))) {
//06-10-2022 - some entities do not have assigned_user_id - this becomes an issue when we have a large company and low permission users //06-10-2022 - some entities do not have assigned_user_id - this becomes an issue when we have a large company and low permission users
if(lcfirst(class_basename(Str::snake($this->entity_type))) == 'user') if(lcfirst(class_basename(Str::snake($this->entity_type))) == 'user')
$query->where('id', auth()->user()->id); $query->where('id', auth()->user()->id);
elseif($this->entity_type == BankTransaction::class){ //table without assigned_user_id
$query->where('user_id', '=', auth()->user()->id);
}
elseif(in_array(lcfirst(class_basename(Str::snake($this->entity_type))),['design','group_setting','payment_term'])){ elseif(in_array(lcfirst(class_basename(Str::snake($this->entity_type))),['design','group_setting','payment_term'])){
//need to pass these back regardless //need to pass these back regardless
nlog($this->entity_type);
} }
else else
$query->where('user_id', '=', auth()->user()->id)->orWhere('assigned_user_id', auth()->user()->id); $query->where('user_id', '=', auth()->user()->id)->orWhere('assigned_user_id', auth()->user()->id);
@ -996,6 +1000,42 @@ class BaseController extends Controller
return redirect('/setup'); return redirect('/setup');
} }
public function reactCatch()
{
if ((bool) $this->checkAppSetup() !== false && $account = Account::first()) {
if (config('ninja.require_https') && ! request()->isSecure()) {
return redirect()->secure(request()->getRequestUri());
}
$data = [];
//pass report errors bool to front end
$data['report_errors'] = Ninja::isSelfHost() ? $account->report_errors : true;
//pass referral code to front end
$data['rc'] = request()->has('rc') ? request()->input('rc') : '';
$data['build'] = request()->has('build') ? request()->input('build') : '';
$data['login'] = request()->has('login') ? request()->input('login') : 'false';
$data['signup'] = request()->has('signup') ? request()->input('signup') : 'false';
$data['user_agent'] = request()->server('HTTP_USER_AGENT');
$data['path'] = $this->setBuild();
$this->buildCache();
if (Ninja::isSelfHost() && $account->set_react_as_default_ap) {
return view('react.index', $data);
} else {
abort('page not found', 404);
}
}
return redirect('/setup');
}
private function setBuild() private function setBuild()
{ {
$build = ''; $build = '';

View File

@ -56,8 +56,6 @@ class InvoiceController extends Controller
{ {
set_time_limit(0); set_time_limit(0);
// $invoice->service()->removeUnpaidGatewayFees()->save();
$invitation = $invoice->invitations()->where('client_contact_id', auth()->guard('contact')->user()->id)->first(); $invitation = $invoice->invitations()->where('client_contact_id', auth()->guard('contact')->user()->id)->first();
if ($invitation && auth()->guard('contact') && ! session()->get('is_silent') && ! $invitation->viewed_date) { if ($invitation && auth()->guard('contact') && ! session()->get('is_silent') && ! $invitation->viewed_date) {

View File

@ -110,6 +110,9 @@ class ConnectedAccountController extends BaseController
$email = $user->getMail() ?: $user->getUserPrincipalName(); $email = $user->getMail() ?: $user->getUserPrincipalName();
nlog("microsoft");
nlog($email);
if(auth()->user()->email != $email && MultiDB::checkUserEmailExists($email)) if(auth()->user()->email != $email && MultiDB::checkUserEmailExists($email))
return response()->json(['message' => ctrans('texts.email_already_register')], 400); return response()->json(['message' => ctrans('texts.email_already_register')], 400);

View File

@ -33,6 +33,7 @@ use App\Models\Client;
use App\Models\Credit; use App\Models\Credit;
use App\Models\Invoice; use App\Models\Invoice;
use App\Repositories\CreditRepository; use App\Repositories\CreditRepository;
use App\Services\PdfMaker\PdfMerge;
use App\Transformers\CreditTransformer; use App\Transformers\CreditTransformer;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\TempFile; use App\Utils\TempFile;
@ -534,6 +535,20 @@ class CreditController extends BaseController
return response()->json(['message' => ctrans('texts.sent_message')], 200); return response()->json(['message' => ctrans('texts.sent_message')], 200);
} }
if($action == 'bulk_print' && auth()->user()->can('view', $credits->first())){
$paths = $credits->map(function ($credit){
return $credit->service()->getCreditPdf($credit->invitations->first());
});
$merge = (new PdfMerge($paths->toArray()))->run();
return response()->streamDownload(function () use ($merge) {
echo ($merge);
}, 'print.pdf', ['Content-Type' => 'application/pdf']);
}
$credits->each(function ($credit, $key) use ($action) { $credits->each(function ($credit, $key) use ($action) {
if (auth()->user()->can('edit', $credit)) { if (auth()->user()->can('edit', $credit)) {
$this->performAction($credit, $action, true); $this->performAction($credit, $action, true);

View File

@ -126,7 +126,7 @@ class ImportController extends Controller
private function getEntityMap($entity_type) private function getEntityMap($entity_type)
{ {
return sprintf('App\\Import\\Definitions\%sMap', ucfirst($entity_type)); return sprintf('App\\Import\\Definitions\%sMap', ucfirst(Str::camel($entity_type)));
} }
private function getCsvData($csvfile) private function getCsvData($csvfile)

View File

@ -19,6 +19,7 @@ use App\Factory\CloneInvoiceToQuoteFactory;
use App\Factory\InvoiceFactory; use App\Factory\InvoiceFactory;
use App\Filters\InvoiceFilters; use App\Filters\InvoiceFilters;
use App\Http\Requests\Invoice\ActionInvoiceRequest; use App\Http\Requests\Invoice\ActionInvoiceRequest;
use App\Http\Requests\Invoice\BulkInvoiceRequest;
use App\Http\Requests\Invoice\CreateInvoiceRequest; use App\Http\Requests\Invoice\CreateInvoiceRequest;
use App\Http\Requests\Invoice\DestroyInvoiceRequest; use App\Http\Requests\Invoice\DestroyInvoiceRequest;
use App\Http\Requests\Invoice\EditInvoiceRequest; use App\Http\Requests\Invoice\EditInvoiceRequest;
@ -40,6 +41,7 @@ use App\Models\Invoice;
use App\Models\Quote; use App\Models\Quote;
use App\Models\TransactionEvent; use App\Models\TransactionEvent;
use App\Repositories\InvoiceRepository; use App\Repositories\InvoiceRepository;
use App\Services\PdfMaker\PdfMerge;
use App\Transformers\InvoiceTransformer; use App\Transformers\InvoiceTransformer;
use App\Transformers\QuoteTransformer; use App\Transformers\QuoteTransformer;
use App\Utils\Ninja; use App\Utils\Ninja;
@ -545,11 +547,11 @@ class InvoiceController extends BaseController
* ), * ),
* ) * )
*/ */
public function bulk() public function bulk(BulkInvoiceRequest $request)
{ {
$action = request()->input('action'); $action = $request->input('action');
$ids = request()->input('ids'); $ids = $request->input('ids');
if(Ninja::isHosted() && (stripos($action, 'email') !== false) && !auth()->user()->company()->account->account_sms_verified) if(Ninja::isHosted() && (stripos($action, 'email') !== false) && !auth()->user()->company()->account->account_sms_verified)
return response(['message' => 'Please verify your account to send emails.'], 400); return response(['message' => 'Please verify your account to send emails.'], 400);
@ -588,6 +590,20 @@ class InvoiceController extends BaseController
} }
if($action == 'bulk_print' && auth()->user()->can('view', $invoices->first())){
$paths = $invoices->map(function ($invoice){
return $invoice->service()->getInvoicePdf();
});
$merge = (new PdfMerge($paths->toArray()))->run();
return response()->streamDownload(function () use ($merge) {
echo ($merge);
}, 'print.pdf', ['Content-Type' => 'application/pdf']);
}
/* /*
* Send the other actions to the switch * Send the other actions to the switch
*/ */

View File

@ -26,7 +26,7 @@
* @OA\Property(property="tax_name3", type="string", example="", description="The tax name"), * @OA\Property(property="tax_name3", type="string", example="", description="The tax name"),
* @OA\Property(property="tax_rate3", type="number", format="float", example="10.00", description="The tax rate"), * @OA\Property(property="tax_rate3", type="number", format="float", example="10.00", description="The tax rate"),
* @OA\Property(property="total_taxes", type="number", format="float", example="10.00", description="The total taxes for the quote"), * @OA\Property(property="total_taxes", type="number", format="float", example="10.00", description="The total taxes for the quote"),
* @OA\Property(property="line_items", type="object", example="[{"product_key":"test", "unit_cost":10},{"product_key":"test", "unit_cost":10}]", description="An array of line items of the quote"), * @OA\Property(property="line_items", type="object", example="", description="An array of line items of the quote"),
* @OA\Property(property="amount", type="number", format="float", example="10.00", description="The total amount of the quote"), * @OA\Property(property="amount", type="number", format="float", example="10.00", description="The total amount of the quote"),
* @OA\Property(property="balance", type="number", format="float", example="10.00", description="The balance due of the quote"), * @OA\Property(property="balance", type="number", format="float", example="10.00", description="The balance due of the quote"),
* @OA\Property(property="paid_to_date", type="number", format="float", example="10.00", description="The amount that has been paid to date on the quote"), * @OA\Property(property="paid_to_date", type="number", format="float", example="10.00", description="The amount that has been paid to date on the quote"),

View File

@ -32,6 +32,7 @@ use App\Models\Client;
use App\Models\Expense; use App\Models\Expense;
use App\Models\PurchaseOrder; use App\Models\PurchaseOrder;
use App\Repositories\PurchaseOrderRepository; use App\Repositories\PurchaseOrderRepository;
use App\Services\PdfMaker\PdfMerge;
use App\Transformers\ExpenseTransformer; use App\Transformers\ExpenseTransformer;
use App\Transformers\PurchaseOrderTransformer; use App\Transformers\PurchaseOrderTransformer;
use App\Utils\Ninja; use App\Utils\Ninja;
@ -515,6 +516,20 @@ class PurchaseOrderController extends BaseController
return response()->json(['message' => ctrans('texts.sent_message')], 200); return response()->json(['message' => ctrans('texts.sent_message')], 200);
} }
if($action == 'bulk_print' && auth()->user()->can('view', $purchase_orders->first())){
$paths = $purchase_orders->map(function ($purchase_order){
return $purchase_order->service()->getPurchaseOrderPdf();
});
$merge = (new PdfMerge($paths->toArray()))->run();
return response()->streamDownload(function () use ($merge) {
echo ($merge);
}, 'print.pdf', ['Content-Type' => 'application/pdf']);
}
/* /*
* Send the other actions to the switch * Send the other actions to the switch
*/ */

View File

@ -35,6 +35,7 @@ use App\Models\Invoice;
use App\Models\Project; use App\Models\Project;
use App\Models\Quote; use App\Models\Quote;
use App\Repositories\QuoteRepository; use App\Repositories\QuoteRepository;
use App\Services\PdfMaker\PdfMerge;
use App\Transformers\InvoiceTransformer; use App\Transformers\InvoiceTransformer;
use App\Transformers\ProjectTransformer; use App\Transformers\ProjectTransformer;
use App\Transformers\QuoteTransformer; use App\Transformers\QuoteTransformer;
@ -561,6 +562,20 @@ class QuoteController extends BaseController
return $this->listResponse(Quote::withTrashed()->whereIn('id', $this->transformKeys($ids))->company()); return $this->listResponse(Quote::withTrashed()->whereIn('id', $this->transformKeys($ids))->company());
} }
if($action == 'bulk_print' && auth()->user()->can('view', $quotes->first())){
$paths = $quotes->map(function ($quote){
return $quote->service()->getQuotePdf();
});
$merge = (new PdfMerge($paths->toArray()))->run();
return response()->streamDownload(function () use ($merge) {
echo ($merge);
}, 'print.pdf', ['Content-Type' => 'application/pdf']);
}
if($action == 'convert_to_project') if($action == 'convert_to_project')
{ {

View File

@ -112,6 +112,8 @@ class SelfUpdateController extends BaseController
$zipFile->close(); $zipFile->close();
$zipFile = null;
nlog('Finished extracting files'); nlog('Finished extracting files');
unlink($file); unlink($file);

View File

@ -54,7 +54,7 @@ class StripeConnectController extends BaseController
if ($company_gateway) { if ($company_gateway) {
$config = $company_gateway->getConfig(); $config = $company_gateway->getConfig();
if (property_exists($config, 'account_id') && strlen($config->account_id) > 1) { if (property_exists($config, 'account_id') && strlen($config->account_id) > 5) {
return view('auth.connect.existing'); return view('auth.connect.existing');
} }
} }

View File

@ -97,6 +97,12 @@ class TwilioController extends BaseController
$account->account_sms_verified = true; $account->account_sms_verified = true;
$account->save(); $account->save();
//on confirmation we set the users phone number.
$user = auth()->user();
$user->phone = $account->account_sms_verification_number;
$user->verified_phone_number = true;
$user->save();
return response()->json(['message' => 'SMS verified'], 200); return response()->json(['message' => 'SMS verified'], 200);
} }
@ -117,7 +123,6 @@ class TwilioController extends BaseController
$twilio = new Client($sid, $token); $twilio = new Client($sid, $token);
try { try {
$verification = $twilio->verify $verification = $twilio->verify
->v2 ->v2
@ -158,9 +163,11 @@ class TwilioController extends BaseController
"code" => $request->code "code" => $request->code
]); ]);
if($verification_check->status == 'approved'){ if($verification_check->status == 'approved'){
if($request->query('validate_only') == 'true')
return response()->json(['message' => 'SMS verified'], 200);
$user->google_2fa_secret = ''; $user->google_2fa_secret = '';
$user->sms_verification_code = ''; $user->sms_verification_code = '';
$user->save(); $user->save();

View File

@ -26,7 +26,7 @@ class UpdateAccountRequest extends Request
*/ */
public function authorize() public function authorize()
{ {
return (auth()->user()->isAdmin() || auth()->user()->isOwner()) && (int) $this->account->id === auth()->user()->account_id; return (auth()->user()->isAdmin() || auth()->user()->isOwner()) && ($this->account->id == auth()->user()->account_id);
} }
/** /**

View File

@ -33,7 +33,8 @@ class StoreBankIntegrationRequest extends Request
{ {
$rules = [ $rules = [
'bank_account_name' => 'required|min:3' 'bank_account_name' => 'required|min:3',
'auto_sync' => 'sometimes|bool'
]; ];
return $rules; return $rules;

View File

@ -31,7 +31,9 @@ class UpdateBankIntegrationRequest extends Request
public function rules() public function rules()
{ {
/* Ensure we have a client name, and that all emails are unique*/ /* Ensure we have a client name, and that all emails are unique*/
$rules = []; $rules = [
'auto_sync' => 'sometimes|bool'
];
return $rules; return $rules;
} }

View File

@ -33,9 +33,9 @@ class MatchBankTransactionRequest extends Request
'transactions' => 'bail|array', 'transactions' => 'bail|array',
'transactions.*.id' => 'bail|required', 'transactions.*.id' => 'bail|required',
'transactions.*.invoice_ids' => 'nullable|string|sometimes', 'transactions.*.invoice_ids' => 'nullable|string|sometimes',
'transactions.*.ninja_category_id' => 'nullable|string|sometimes'
]; ];
$rules['transactions.*.ninja_category_id'] = 'bail|nullable|sometimes|exists:expense_categories,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
$rules['transactions.*.vendor_id'] = 'bail|sometimes|exists:vendors,id,company_id,'.auth()->user()->company()->id.',is_deleted,0'; $rules['transactions.*.vendor_id'] = 'bail|sometimes|exists:vendors,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
return $rules; return $rules;

View File

@ -34,6 +34,9 @@ class StoreBankTransactionRequest extends Request
$rules = []; $rules = [];
if(isset($this->bank_integration_id))
$rules['bank_integration_id'] = 'bail|required|exists:bank_integrations,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
return $rules; return $rules;
} }

View File

@ -33,7 +33,6 @@ class UpdateBankTransactionRequest extends Request
/* Ensure we have a client name, and that all emails are unique*/ /* Ensure we have a client name, and that all emails are unique*/
$rules = [ $rules = [
'date' => 'bail|required|date', 'date' => 'bail|required|date',
'description' => 'bail|sometimes|string',
'amount' => 'numeric|required', 'amount' => 'numeric|required',
]; ];
@ -46,6 +45,9 @@ class UpdateBankTransactionRequest extends Request
if(isset($this->expense_id)) if(isset($this->expense_id))
$rules['expense_id'] = 'bail|required|exists:expenses,id,company_id,'.auth()->user()->company()->id.',is_deleted,0'; $rules['expense_id'] = 'bail|required|exists:expenses,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
if(isset($this->bank_integration_id))
$rules['bank_integration_id'] = 'bail|required|exists:bank_integrations,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
return $rules; return $rules;
} }

View File

@ -59,7 +59,13 @@ class StoreCreditRequest extends Request
$rules['number'] = ['nullable', Rule::unique('credits')->where('company_id', auth()->user()->company()->id)]; $rules['number'] = ['nullable', Rule::unique('credits')->where('company_id', auth()->user()->company()->id)];
$rules['discount'] = 'sometimes|numeric'; $rules['discount'] = 'sometimes|numeric';
$rules['is_amount_discount'] = ['boolean']; $rules['is_amount_discount'] = ['boolean'];
$rules['tax_rate1'] = 'bail|sometimes|numeric';
$rules['tax_rate2'] = 'bail|sometimes|numeric';
$rules['tax_rate3'] = 'bail|sometimes|numeric';
$rules['tax_name1'] = 'bail|sometimes|string|nullable';
$rules['tax_name2'] = 'bail|sometimes|string|nullable';
$rules['tax_name3'] = 'bail|sometimes|string|nullable';
if ($this->invoice_id) { if ($this->invoice_id) {
$rules['invoice_id'] = new ValidInvoiceCreditRule(); $rules['invoice_id'] = new ValidInvoiceCreditRule();
} }

View File

@ -59,7 +59,13 @@ class UpdateCreditRequest extends Request
$rules['line_items'] = 'array'; $rules['line_items'] = 'array';
$rules['discount'] = 'sometimes|numeric'; $rules['discount'] = 'sometimes|numeric';
$rules['is_amount_discount'] = ['boolean']; $rules['is_amount_discount'] = ['boolean'];
$rules['tax_rate1'] = 'bail|sometimes|numeric';
$rules['tax_rate2'] = 'bail|sometimes|numeric';
$rules['tax_rate3'] = 'bail|sometimes|numeric';
$rules['tax_name1'] = 'bail|sometimes|string|nullable';
$rules['tax_name2'] = 'bail|sometimes|string|nullable';
$rules['tax_name3'] = 'bail|sometimes|string|nullable';
return $rules; return $rules;
} }

View File

@ -70,4 +70,4 @@ class ActionInvoiceRequest extends Request
'action' => $this->error_msg, 'action' => $this->error_msg,
]; ];
} }
} }

View File

@ -0,0 +1,32 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\Invoice;
use App\Http\Requests\Request;
class BulkInvoiceRequest extends Request
{
public function authorize() : bool
{
return true;
}
public function rules()
{
return [
'action' => 'required|string',
'ids' => 'required'
];
}
}

View File

@ -69,7 +69,13 @@ class StoreInvoiceRequest extends Request
$rules['line_items'] = 'array'; $rules['line_items'] = 'array';
$rules['discount'] = 'sometimes|numeric'; $rules['discount'] = 'sometimes|numeric';
$rules['tax_rate1'] = 'bail|sometimes|numeric';
$rules['tax_rate2'] = 'bail|sometimes|numeric';
$rules['tax_rate3'] = 'bail|sometimes|numeric';
$rules['tax_name1'] = 'bail|sometimes|string|nullable';
$rules['tax_name2'] = 'bail|sometimes|string|nullable';
$rules['tax_name3'] = 'bail|sometimes|string|nullable';
return $rules; return $rules;
} }

View File

@ -62,6 +62,12 @@ class UpdateInvoiceRequest extends Request
$rules['line_items'] = 'array'; $rules['line_items'] = 'array';
$rules['discount'] = 'sometimes|numeric'; $rules['discount'] = 'sometimes|numeric';
$rules['project_id'] = ['bail', 'sometimes', new ValidProjectForClient($this->all())]; $rules['project_id'] = ['bail', 'sometimes', new ValidProjectForClient($this->all())];
$rules['tax_rate1'] = 'bail|sometimes|numeric';
$rules['tax_rate2'] = 'bail|sometimes|numeric';
$rules['tax_rate3'] = 'bail|sometimes|numeric';
$rules['tax_name1'] = 'bail|sometimes|string|nullable';
$rules['tax_name2'] = 'bail|sometimes|string|nullable';
$rules['tax_name3'] = 'bail|sometimes|string|nullable';
return $rules; return $rules;
} }

View File

@ -58,7 +58,14 @@ class StoreRecurringInvoiceRequest extends Request
$rules['project_id'] = ['bail', 'sometimes', new ValidProjectForClient($this->all())]; $rules['project_id'] = ['bail', 'sometimes', new ValidProjectForClient($this->all())];
$rules['number'] = new UniqueRecurringInvoiceNumberRule($this->all()); $rules['number'] = new UniqueRecurringInvoiceNumberRule($this->all());
$rules['tax_rate1'] = 'bail|sometimes|numeric';
$rules['tax_rate2'] = 'bail|sometimes|numeric';
$rules['tax_rate3'] = 'bail|sometimes|numeric';
$rules['tax_name1'] = 'bail|sometimes|string|nullable';
$rules['tax_name2'] = 'bail|sometimes|string|nullable';
$rules['tax_name3'] = 'bail|sometimes|string|nullable';
return $rules; return $rules;
} }

View File

@ -54,7 +54,13 @@ class UpdateRecurringInvoiceRequest extends Request
} }
$rules['project_id'] = ['bail', 'sometimes', new ValidProjectForClient($this->all())]; $rules['project_id'] = ['bail', 'sometimes', new ValidProjectForClient($this->all())];
$rules['tax_rate1'] = 'bail|sometimes|numeric';
$rules['tax_rate2'] = 'bail|sometimes|numeric';
$rules['tax_rate3'] = 'bail|sometimes|numeric';
$rules['tax_name1'] = 'bail|sometimes|string|nullable';
$rules['tax_name2'] = 'bail|sometimes|string|nullable';
$rules['tax_name3'] = 'bail|sometimes|string|nullable';
return $rules; return $rules;
} }
@ -84,7 +90,7 @@ class UpdateRecurringInvoiceRequest extends Request
if (isset($input['invitations'])) { if (isset($input['invitations'])) {
foreach ($input['invitations'] as $key => $value) { foreach ($input['invitations'] as $key => $value) {
if (is_numeric($input['invitations'][$key]['id'])) { if (isset($input['invitations'][$key]['id']) && is_numeric($input['invitations'][$key]['id'])) {
unset($input['invitations'][$key]['id']); unset($input['invitations'][$key]['id']);
} }

View File

@ -16,6 +16,7 @@ use App\Factory\UserFactory;
use App\Http\Requests\Request; use App\Http\Requests\Request;
use App\Http\ValidationRules\Ninja\CanAddUserRule; use App\Http\ValidationRules\Ninja\CanAddUserRule;
use App\Http\ValidationRules\User\AttachableUser; use App\Http\ValidationRules\User\AttachableUser;
use App\Http\ValidationRules\User\HasValidPhoneNumber;
use App\Http\ValidationRules\ValidUserForCompany; use App\Http\ValidationRules\ValidUserForCompany;
use App\Libraries\MultiDB; use App\Libraries\MultiDB;
use App\Models\User; use App\Models\User;
@ -46,9 +47,13 @@ class StoreUserRequest extends Request
} else { } else {
$rules['email'] = ['email', new AttachableUser()]; $rules['email'] = ['email', new AttachableUser()];
} }
if (Ninja::isHosted()) { if (Ninja::isHosted()) {
$rules['id'] = new CanAddUserRule(); $rules['id'] = new CanAddUserRule();
if($this->phone && isset($this->phone))
$rules['phone'] = ['bail', 'string', 'sometimes', new HasValidPhoneNumber()];
} }
return $rules; return $rules;

View File

@ -13,9 +13,14 @@ namespace App\Http\Requests\User;
use App\Http\Requests\Request; use App\Http\Requests\Request;
use App\Http\ValidationRules\UniqueUserRule; use App\Http\ValidationRules\UniqueUserRule;
use App\Http\ValidationRules\User\HasValidPhoneNumber;
use App\Utils\Ninja;
class UpdateUserRequest extends Request class UpdateUserRequest extends Request
{ {
private bool $phone_has_changed = false;
/** /**
* Determine if the user is authorized to make this request. * Determine if the user is authorized to make this request.
* *
@ -38,6 +43,9 @@ class UpdateUserRequest extends Request
$rules['email'] = ['email', 'sometimes', new UniqueUserRule($this->user, $input['email'])]; $rules['email'] = ['email', 'sometimes', new UniqueUserRule($this->user, $input['email'])];
} }
if(Ninja::isHosted() && $this->phone_has_changed && $this->phone && isset($this->phone))
$rules['phone'] = ['sometimes', 'bail', 'string', new HasValidPhoneNumber()];
return $rules; return $rules;
} }
@ -57,6 +65,14 @@ class UpdateUserRequest extends Request
$input['last_name'] = strip_tags($input['last_name']); $input['last_name'] = strip_tags($input['last_name']);
} }
if(array_key_exists('phone', $input) && isset($input['phone']) && strlen($input['phone']) > 1 && ($this->user->phone != $input['phone'])){
$this->phone_has_changed = true;
}
if(array_key_exists('oauth_provider_id', $input) && $input['oauth_provider_id'] == '')
$input['oauth_user_id'] = '';
$this->replace($input); $this->replace($input);
} }
} }

View File

@ -55,7 +55,7 @@ class ValidRefundableRequest implements Rule
} }
$request_invoices = request()->has('invoices') ? $this->input['invoices'] : []; $request_invoices = request()->has('invoices') ? $this->input['invoices'] : [];
$request_credits = request()->has('credits') ? $this->input['credits'] : []; // $request_credits = request()->has('credits') ? $this->input['credits'] : [];
if ($payment->invoices()->exists()) { if ($payment->invoices()->exists()) {
foreach ($payment->invoices as $paymentable_invoice) { foreach ($payment->invoices as $paymentable_invoice) {

View File

@ -0,0 +1,83 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\ValidationRules\User;
use App\Models\CompanyUser;
use App\Models\User;
use Illuminate\Contracts\Validation\Rule;
/**
* Class HasValidPhoneNumber.
*/
class HasValidPhoneNumber implements Rule
{
public $message;
public function __construct()
{
}
public function message()
{
return [
'phone' => ctrans('texts.phone_validation_error'),
];
}
/**
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
$sid = config('ninja.twilio_account_sid');
$token = config('ninja.twilio_auth_token');
if(!$sid)
return true;
if(is_null($value))
return false;
$twilio = new \Twilio\Rest\Client($sid, $token);
$country = auth()->user()->account?->companies()?->first()?->country();
if(!$country || strlen(auth()->user()->phone) < 2)
return true;
$countryCode = $country->iso_3166_2;
try{
$phone_number = $twilio->lookups->v1->phoneNumbers($value)
->fetch(["countryCode" => $countryCode]);
$user = auth()->user();
request()->merge(['validated_phone' => $phone_number->phoneNumber ]);
$user->verified_phone_number = false;
$user->save();
return true;
}
catch(\Exception $e) {
return false;
}
}
}

View File

@ -16,16 +16,16 @@ class BankTransactionMap
public static function importable() public static function importable()
{ {
return [ return [
0 => 'bank.transaction_id', 0 => 'transaction.transaction_id',
1 => 'bank.amount', 1 => 'transaction.amount',
2 => 'bank.currency', 2 => 'transaction.currency',
3 => 'bank.account_type', 3 => 'transaction.account_type',
4 => 'bank.category_id', 4 => 'transaction.category_id',
5 => 'bank.category_type', 5 => 'transaction.category_type',
6 => 'bank.date', 6 => 'transaction.date',
7 => 'bank.bank_account_id', 7 => 'transaction.bank_account_id',
8 => 'bank.description', 8 => 'transaction.description',
9 => 'bank.base_type', 9 => 'transaction.base_type',
]; ];
} }

View File

@ -46,11 +46,14 @@ use App\Repositories\PaymentRepository;
use App\Repositories\ProductRepository; use App\Repositories\ProductRepository;
use App\Repositories\QuoteRepository; use App\Repositories\QuoteRepository;
use App\Repositories\VendorRepository; use App\Repositories\VendorRepository;
use App\Utils\Traits\MakesHash;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\ParameterBag;
class Csv extends BaseImport implements ImportInterface class Csv extends BaseImport implements ImportInterface
{ {
use MakesHash;
public array $entity_count = []; public array $entity_count = [];
public function import(string $entity) public function import(string $entity)
@ -77,24 +80,20 @@ class Csv extends BaseImport implements ImportInterface
$data = $this->getCsvData($entity_type); $data = $this->getCsvData($entity_type);
if (is_array($data)) { if (is_array($data))
{
$data = $this->preTransformCsv($data, $entity_type); $data = $this->preTransformCsv($data, $entity_type);
foreach($data as $key => $value)
if(array_key_exists('bank_integration_id', $this->request)){ {
$data[$key]['bank.bank_integration_id'] = $this->decodePrimaryKey($this->request['bank_integration_id']);
foreach($data as $key => $value)
{
$data['bank_integration_id'][$key] = $this->request['bank_integration_id'];
}
} }
} }
if (empty($data)) { if (empty($data)) {
$this->entity_count['bank_transactions'] = 0; $this->entity_count['bank_transactions'] = 0;
return; return;
} }
@ -102,6 +101,8 @@ class Csv extends BaseImport implements ImportInterface
$this->repository_name = BankTransactionRepository::class; $this->repository_name = BankTransactionRepository::class;
$this->factory_name = BankTransactionFactory::class; $this->factory_name = BankTransactionFactory::class;
$this->repository = app()->make($this->repository_name);
$this->transformer = new BankTransformer($this->company); $this->transformer = new BankTransformer($this->company);
$bank_transaction_count = $this->ingest($data, $entity_type); $bank_transaction_count = $this->ingest($data, $entity_type);
$this->entity_count['bank_transactions'] = $bank_transaction_count; $this->entity_count['bank_transactions'] = $bank_transaction_count;

View File

@ -44,6 +44,35 @@ class BaseTransformer
$this->company = $company; $this->company = $company;
} }
public function parseDate($date)
{
try{
$parsed_date = Carbon::parse($date);
return $parsed_date->format('Y-m-d');
}
catch(\Exception $e){
$parsed_date = date('Y-m-d', strtotime($date));
if($parsed_date == '1970-01-01')
return now()->format('Y-m-d');
return $parsed_date;
}
}
public function getNumber($data, $field)
{
return (isset($data->$field) && $data->$field) ? (int)$data->$field : 0;
}
public function getString($data, $field) public function getString($data, $field)
{ {
return isset($data[$field]) && $data[$field] ? trim($data[$field]) : ''; return isset($data[$field]) && $data[$field] ? trim($data[$field]) : '';

View File

@ -11,6 +11,7 @@
namespace App\Import\Transformer\Csv; namespace App\Import\Transformer\Csv;
use App\DataMapper\ClientSettings;
use App\Import\ImportException; use App\Import\ImportException;
use App\Import\Transformer\BaseTransformer; use App\Import\Transformer\BaseTransformer;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -31,7 +32,7 @@ class ClientTransformer extends BaseTransformer
throw new ImportException('Client already exists'); throw new ImportException('Client already exists');
} }
$settings = new \stdClass(); $settings = ClientSettings::defaults();
$settings->currency_id = (string) $this->getCurrencyByCode($data); $settings->currency_id = (string) $this->getCurrencyByCode($data);
return [ return [

View File

@ -31,7 +31,7 @@ class ExpenseTransformer extends BaseTransformer
return [ return [
'company_id' => $this->company->id, 'company_id' => $this->company->id,
'amount' => $this->getFloat($data, 'expense.amount'), 'amount' => abs($this->getFloat($data, 'expense.amount')),
'currency_id' => $this->getCurrencyByCode( 'currency_id' => $this->getCurrencyByCode(
$data, $data,
'expense.currency_id' 'expense.currency_id'

View File

@ -57,10 +57,10 @@ class InvoiceTransformer extends BaseTransformer
'discount' => $this->getFloat($invoice_data, 'invoice.discount'), 'discount' => $this->getFloat($invoice_data, 'invoice.discount'),
'po_number' => $this->getString($invoice_data, 'invoice.po_number'), 'po_number' => $this->getString($invoice_data, 'invoice.po_number'),
'date' => isset($invoice_data['invoice.date']) 'date' => isset($invoice_data['invoice.date'])
? date('Y-m-d', strtotime(str_replace("/","-",$invoice_data['invoice.date']))) ? $this->parseDate($invoice_data['invoice.date'])
: now()->format('Y-m-d'), : now()->format('Y-m-d'),
'due_date' => isset($invoice_data['invoice.due_date']) 'due_date' => isset($invoice_data['invoice.due_date'])
? date('Y-m-d', strtotime(str_replace("/","-",$invoice_data['invoice.due_date']))) ? $this->parseDate($invoice_data['invoice.due_date'])
: null, : null,
'terms' => $this->getString($invoice_data, 'invoice.terms'), 'terms' => $this->getString($invoice_data, 'invoice.terms'),
'public_notes' => $this->getString( 'public_notes' => $this->getString(
@ -94,7 +94,7 @@ class InvoiceTransformer extends BaseTransformer
'invoice.custom_value4' 'invoice.custom_value4'
), ),
'footer' => $this->getString($invoice_data, 'invoice.footer'), 'footer' => $this->getString($invoice_data, 'invoice.footer'),
'partial' => $this->getFloat($invoice_data, 'invoice.partial'), 'partial' => $this->getFloat($invoice_data, 'invoice.partial') > 0 ?: null,
'partial_due_date' => $this->getString( 'partial_due_date' => $this->getString(
$invoice_data, $invoice_data,
'invoice.partial_due_date' 'invoice.partial_due_date'
@ -140,10 +140,7 @@ class InvoiceTransformer extends BaseTransformer
$transformed['payments'] = [ $transformed['payments'] = [
[ [
'date' => isset($invoice_data['payment.date']) 'date' => isset($invoice_data['payment.date'])
? date( ? $this->parseDate($invoice_data['payment.date'])
'Y-m-d',
strtotime($invoice_data['payment.date'])
)
: date('y-m-d'), : date('y-m-d'),
'transaction_reference' => $this->getString( 'transaction_reference' => $this->getString(
$invoice_data, $invoice_data,
@ -159,10 +156,7 @@ class InvoiceTransformer extends BaseTransformer
$transformed['payments'] = [ $transformed['payments'] = [
[ [
'date' => isset($invoice_data['payment.date']) 'date' => isset($invoice_data['payment.date'])
? date( ? $this->parseDate($invoice_data['payment.date'])
'Y-m-d',
strtotime($invoice_data['payment.date'])
)
: date('y-m-d'), : date('y-m-d'),
'transaction_reference' => $this->getString( 'transaction_reference' => $this->getString(
$invoice_data, $invoice_data,
@ -182,10 +176,7 @@ class InvoiceTransformer extends BaseTransformer
$transformed['payments'] = [ $transformed['payments'] = [
[ [
'date' => isset($invoice_data['payment.date']) 'date' => isset($invoice_data['payment.date'])
? date( ? $this->parseDate($invoice_data['payment.date'])
'Y-m-d',
strtotime($invoice_data['payment.date'])
)
: date('y-m-d'), : date('y-m-d'),
'transaction_reference' => $this->getString( 'transaction_reference' => $this->getString(
$invoice_data, $invoice_data,

View File

@ -12,7 +12,7 @@
namespace App\Import\Transformers\Bank; namespace App\Import\Transformers\Bank;
use App\Import\ImportException; use App\Import\ImportException;
use App\Import\Transformers\BaseTransformer; use App\Import\Transformer\BaseTransformer;
use App\Models\BankTransaction; use App\Models\BankTransaction;
use App\Utils\Number; use App\Utils\Number;
@ -31,17 +31,17 @@ class BankTransformer extends BaseTransformer
$now = now(); $now = now();
$transformed = [ $transformed = [
// 'bank_integration_id' => $this->bank_integration->id, 'bank_integration_id' => $transaction['bank.bank_integration_id'],
'transaction_id' => $this->getNumber($transaction,'bank.transaction_id'), 'transaction_id' => $this->getNumber($transaction,'bank.transaction_id'),
'amount' => abs($this->getFloat($transaction, 'bank.amount')), 'amount' => abs($this->getFloat($transaction, 'bank.amount')),
'currency_id' => $this->getCurrencyByCode($transaction, 'bank.currency'), 'currency_id' => $this->getCurrencyByCode($transaction, 'bank.currency'),
'account_type' => strlen($this->getString($transaction, 'bank.account_type')) > 1 ? $this->getString($transaction, 'bank.account_type') : 'bank', 'account_type' => strlen($this->getString($transaction, 'bank.account_type')) > 1 ? $this->getString($transaction, 'bank.account_type') : 'bank',
'category_id' => $this->getNumber($transaction, 'bank.category_id') > 0 ? $this->getNumber($transaction, 'bank.category_id') : null, 'category_id' => $this->getNumber($transaction, 'bank.category_id') > 0 ? $this->getNumber($transaction, 'bank.category_id') : null,
'category_type' => $this->getString($transaction, 'category_type'), 'category_type' => $this->getString($transaction, 'bank.category_type'),
'date' => array_key_exists('date', $transaction) ? date('Y-m-d', strtotime(str_replace("/","-",$transaction['date']))) 'date' => array_key_exists('bank.date', $transaction) ? $this->parseDate($transaction['bank.date'])
: now()->format('Y-m-d'), : now()->format('Y-m-d'),
'bank_account_id' => array_key_exists('bank_account_id', $transaction) ? $transaction['bank_account_id'] : 0, 'bank_account_id' => array_key_exists('bank.bank_account_id', $transaction) ? $transaction['bank.bank_account_id'] : 0,
'description' => array_key_exists('description', $transaction)? $transaction['description'] : '', 'description' => array_key_exists('bank.description', $transaction) ? $transaction['bank.description'] : '',
'base_type' => $this->calculateType($transaction), 'base_type' => $this->calculateType($transaction),
'created_at' => $now, 'created_at' => $now,
'updated_at' => $now, 'updated_at' => $now,
@ -56,22 +56,22 @@ class BankTransformer extends BaseTransformer
private function calculateType($transaction) private function calculateType($transaction)
{ {
if(array_key_exists('base_type', $transaction) && $transaction['base_type'] == 'CREDIT') if(array_key_exists('bank.base_type', $transaction) && ($transaction['bank.base_type'] == 'CREDIT') || strtolower($transaction['bank.base_type']) == 'deposit')
return 'CREDIT'; return 'CREDIT';
if(array_key_exists('base_type', $transaction) && $transaction['base_type'] == 'DEBIT') if(array_key_exists('bank.base_type', $transaction) && ($transaction['bank.base_type'] == 'DEBIT') || strtolower($transaction['bank.bank_type']) == 'withdrawal')
return 'DEBIT'; return 'DEBIT';
if(array_key_exists('category_id',$transaction)) if(array_key_exists('bank.category_id', $transaction))
return 'DEBIT'; return 'DEBIT';
if(array_key_exists('category_type', $transaction) && $transaction['category_type'] == 'Income') if(array_key_exists('bank.category_type', $transaction) && $transaction['bank.category_type'] == 'Income')
return 'CREDIT'; return 'CREDIT';
if(array_key_exists('category_type', $transaction)) if(array_key_exists('bank.category_type', $transaction))
return 'DEBIT'; return 'DEBIT';
if(array_key_exists('amount', $transaction) && is_numeric($transaction['amount']) && $transaction['amount'] > 0) if(array_key_exists('bank.amount', $transaction) && is_numeric($transaction['bank.amount']) && $transaction['bank.amount'] > 0)
return 'CREDIT'; return 'CREDIT';
return 'DEBIT'; return 'DEBIT';

View File

@ -26,7 +26,7 @@ use App\Models\Currency;
use App\Models\ExpenseCategory; use App\Models\ExpenseCategory;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Services\Bank\BankService; use App\Services\Bank\BankMatchingService;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Traits\GeneratesCounter; use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
@ -59,6 +59,8 @@ class MatchBankTransactions implements ShouldQueue
private float $available_balance = 0; private float $available_balance = 0;
private float $applied_amount = 0;
private array $attachable_invoices = []; private array $attachable_invoices = [];
public $bts; public $bts;
@ -90,11 +92,14 @@ class MatchBankTransactions implements ShouldQueue
$this->company = Company::find($this->company_id); $this->company = Company::find($this->company_id);
$yodlee = new Yodlee($this->company->account->bank_integration_account_id); if($this->company->account->bank_integration_account_id)
$yodlee = new Yodlee($this->company->account->bank_integration_account_id);
else
$yodlee = false;
$bank_categories = Cache::get('bank_categories'); $bank_categories = Cache::get('bank_categories');
if(!$bank_categories){ if(!$bank_categories && $yodlee){
$_categories = $yodlee->getTransactionCategories(); $_categories = $yodlee->getTransactionCategories();
$this->categories = collect($_categories->transactionCategory); $this->categories = collect($_categories->transactionCategory);
Cache::forever('bank_categories', $this->categories); Cache::forever('bank_categories', $this->categories);
@ -157,7 +162,7 @@ class MatchBankTransactions implements ShouldQueue
$_invoices = Invoice::withTrashed()->find($this->getInvoices($input['invoice_ids'])); $_invoices = Invoice::withTrashed()->find($this->getInvoices($input['invoice_ids']));
$amount = $this->bt->amount; $amount = $this->bt->amount;
if($_invoices && $this->checkPayable($_invoices)){ if($_invoices && $this->checkPayable($_invoices)){
@ -209,29 +214,34 @@ class MatchBankTransactions implements ShouldQueue
$this->invoice = Invoice::withTrashed()->where('id', $invoice->id)->lockForUpdate()->first(); $this->invoice = Invoice::withTrashed()->where('id', $invoice->id)->lockForUpdate()->first();
if($invoices->count() == 1){ $_amount = false;
$_amount = $this->available_balance;
}
elseif($invoices->count() > 1 && floatval($this->invoice->balance) < floatval($this->available_balance) && $this->available_balance > 0)
{
$_amount = $this->invoice->balance;
$this->available_balance = $this->available_balance - $this->invoice->balance;
}
elseif($invoices->count() > 1 && floatval($this->invoice->balance) > floatval($this->available_balance) && $this->available_balance > 0)
{
$_amount = $this->available_balance;
$this->available_balance = 0;
}
$this->attachable_invoices[] = ['id' => $this->invoice->id, 'amount' => $_amount]; if(floatval($this->invoice->balance) < floatval($this->available_balance) && $this->available_balance > 0)
{
$_amount = $this->invoice->balance;
$this->applied_amount += $this->invoice->balance;
$this->available_balance = $this->available_balance - $this->invoice->balance;
}
elseif(floatval($this->invoice->balance) >= floatval($this->available_balance) && $this->available_balance > 0)
{
$_amount = $this->available_balance;
$this->applied_amount += $this->available_balance;
$this->available_balance = 0;
}
$this->invoice if($_amount)
->service() {
->setExchangeRate()
->updateBalance($_amount * -1) $this->attachable_invoices[] = ['id' => $this->invoice->id, 'amount' => $_amount];
->updatePaidToDate($_amount)
->setCalculatedStatus() $this->invoice
->save(); ->service()
->setExchangeRate()
->updateBalance($_amount * -1)
->updatePaidToDate($_amount)
->setCalculatedStatus()
->save();
}
}); });
@ -241,7 +251,7 @@ class MatchBankTransactions implements ShouldQueue
$payment = PaymentFactory::create($this->invoice->company_id, $this->invoice->user_id); $payment = PaymentFactory::create($this->invoice->company_id, $this->invoice->user_id);
$payment->amount = $amount; $payment->amount = $amount;
$payment->applied = $amount; $payment->applied = $this->applied_amount;
$payment->status_id = Payment::STATUS_COMPLETED; $payment->status_id = Payment::STATUS_COMPLETED;
$payment->client_id = $this->invoice->client_id; $payment->client_id = $this->invoice->client_id;
$payment->transaction_reference = $this->bt->description; $payment->transaction_reference = $this->bt->description;

View File

@ -16,7 +16,7 @@ use App\Libraries\MultiDB;
use App\Models\BankIntegration; use App\Models\BankIntegration;
use App\Models\BankTransaction; use App\Models\BankTransaction;
use App\Models\Company; use App\Models\Company;
use App\Services\Bank\BankService; use App\Services\Bank\BankMatchingService;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
@ -68,12 +68,18 @@ class ProcessBankTransactions implements ShouldQueue
do{ do{
$this->processTransactions(); try {
$this->processTransactions();
}
catch(\Exception $e) {
nlog("{$this->bank_integration_account_id} - exited abnormally => ". $e->getMessage());
return;
}
} }
while($this->stop_loop); while($this->stop_loop);
BankService::dispatch($this->company->id, $this->company->db); BankMatchingService::dispatch($this->company->id, $this->company->db);
} }
@ -83,6 +89,14 @@ class ProcessBankTransactions implements ShouldQueue
$yodlee = new Yodlee($this->bank_integration_account_id); $yodlee = new Yodlee($this->bank_integration_account_id);
if(!$yodlee->getAccount($this->bank_integration->bank_account_id))
{
$this->bank_integration->disabled_upstream = true;
$this->bank_integration->save();
$this->stop_loop = false;
return;
}
$data = [ $data = [
'top' => 500, 'top' => 500,
'fromDate' => $this->from_date, 'fromDate' => $this->from_date,
@ -102,7 +116,8 @@ class ProcessBankTransactions implements ShouldQueue
//if no transactions, update the from_date and move on //if no transactions, update the from_date and move on
if(count($transactions) == 0){ if(count($transactions) == 0){
$this->bank_integration->from_date = now(); $this->bank_integration->from_date = now()->subDays(2);
$this->bank_integration->disabled_upstream = false;
$this->bank_integration->save(); $this->bank_integration->save();
$this->stop_loop = false; $this->stop_loop = false;
return; return;
@ -144,8 +159,7 @@ class ProcessBankTransactions implements ShouldQueue
if($count < 500){ if($count < 500){
$this->stop_loop = false; $this->stop_loop = false;
$this->bank_integration->from_date = now()->subDays(2);
$this->bank_integration->from_date = now();
$this->bank_integration->save(); $this->bank_integration->save();
} }

View File

@ -1107,7 +1107,7 @@ class CompanyImport implements ShouldQueue
$storage_url = (object)$this->getObject('storage_url', true); $storage_url = (object)$this->getObject('storage_url', true);
if(!Storage::exists($new_document->url)){ if(!Storage::exists($new_document->url) && is_string($storage_url)){
$url = $storage_url . $new_document->url; $url = $storage_url . $new_document->url;

View File

@ -218,7 +218,15 @@ class CreateEntityPdf implements ShouldQueue
throw new FilePermissionsFailure($e->getMessage()); throw new FilePermissionsFailure($e->getMessage());
} }
} }
$this->invitation = null;
$this->entity = null;
$this->company = null;
$this->client = null;
$this->contact = null;
$maker = null;
$state = null;
return $file_path; return $file_path;
} }

View File

@ -200,6 +200,8 @@ class CreateRawPdf implements ShouldQueue
} }
if ($pdf) { if ($pdf) {
$maker =null;
$state = null;
return $pdf; return $pdf;
} }

View File

@ -128,6 +128,17 @@ class EmailEntity implements ShouldQueue
$nmo->entity = $this->entity; $nmo->entity = $this->entity;
(new NinjaMailerJob($nmo))->handle(); (new NinjaMailerJob($nmo))->handle();
$nmo = null;
$this->invitation = null;
$this->company = null;
$this->entity_string = null;
$this->entity = null;
$this->settings = null;
$this->reminder_template = null;
$this->html_engine = null;
$this->template_data = null;
$this->email_entity_builder = null;
} }
private function resolveEntityString() :string private function resolveEntityString() :string

View File

@ -23,6 +23,7 @@ use App\Libraries\MultiDB;
use App\Models\Client; use App\Models\Client;
use App\Models\Company; use App\Models\Company;
use App\Models\Vendor; use App\Models\Vendor;
use App\Utils\Ninja;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
@ -81,6 +82,7 @@ class CSVIngest implements ShouldQueue
$engine->finalizeImport(); $engine->finalizeImport();
$this->checkContacts(); $this->checkContacts();
} }
private function checkContacts() private function checkContacts()

View File

@ -115,6 +115,7 @@ class NinjaMailerJob implements ShouldQueue
//send email //send email
try { try {
nlog("trying to send to {$this->nmo->to_user->email} ". now()->toDateTimeString()); nlog("trying to send to {$this->nmo->to_user->email} ". now()->toDateTimeString());
nlog("Using mailer => ". $this->mailer); nlog("Using mailer => ". $this->mailer);
@ -128,7 +129,12 @@ class NinjaMailerJob implements ShouldQueue
LightLogs::create(new EmailSuccess($this->nmo->company->company_key)) LightLogs::create(new EmailSuccess($this->nmo->company->company_key))
->send(); ->send();
// nlog('Using ' . ((int) (memory_get_usage(true) / (1024 * 1024))) . 'MB ');
$this->nmo = null;
$this->company = null;
app('queue.worker')->shouldQuit = 1;
} catch (\Exception | \RuntimeException | \Google\Service\Exception $e) { } catch (\Exception | \RuntimeException | \Google\Service\Exception $e) {
nlog("error failed with {$e->getMessage()}"); nlog("error failed with {$e->getMessage()}");
@ -166,7 +172,15 @@ class NinjaMailerJob implements ShouldQueue
/* Don't send postmark failures to Sentry */ /* Don't send postmark failures to Sentry */
if(Ninja::isHosted() && (!$e instanceof ClientException)) if(Ninja::isHosted() && (!$e instanceof ClientException))
app('sentry')->captureException($e); app('sentry')->captureException($e);
$message = null;
$this->nmo = null;
$this->company = null;
} }
} }
/* Switch statement to handle failure notifications */ /* Switch statement to handle failure notifications */
@ -188,6 +202,7 @@ class NinjaMailerJob implements ShouldQueue
if ($this->nmo->to_user instanceof ClientContact) if ($this->nmo->to_user instanceof ClientContact)
$this->logMailError($message, $this->nmo->to_user->client); $this->logMailError($message, $this->nmo->to_user->client);
} }
private function setMailDriver() private function setMailDriver()
@ -213,8 +228,34 @@ class NinjaMailerJob implements ShouldQueue
break; break;
} }
if(Ninja::isSelfHost())
$this->setSelfHostMultiMailer();
} }
private function setSelfHostMultiMailer()
{
if (env($this->company->id . '_MAIL_HOST'))
{
config([
'mail.mailers.smtp' => [
'transport' => 'smtp',
'host' => env($this->company->id . '_MAIL_HOST'),
'port' => env($this->company->id . '_MAIL_PORT'),
'username' => env($this->company->id . '_MAIL_USERNAME'),
'password' => env($this->company->id . '_MAIL_PASSWORD'),
],
]);
}
}
private function setOfficeMailer() private function setOfficeMailer()
{ {
$sending_user = $this->nmo->settings->gmail_sending_user_id; $sending_user = $this->nmo->settings->gmail_sending_user_id;
@ -381,7 +422,7 @@ class NinjaMailerJob implements ShouldQueue
private function logMailError($errors, $recipient_object) private function logMailError($errors, $recipient_object)
{ {
SystemLogger::dispatch( SystemLogger::dispatchSync(
$errors, $errors,
SystemLog::CATEGORY_MAIL, SystemLog::CATEGORY_MAIL,
SystemLog::EVENT_MAIL_SEND, SystemLog::EVENT_MAIL_SEND,
@ -396,6 +437,9 @@ class NinjaMailerJob implements ShouldQueue
LightLogs::create($job_failure) LightLogs::create($job_failure)
->send(); ->send();
$job_failure = null;
} }
public function failed($exception = null) public function failed($exception = null)

View File

@ -64,16 +64,16 @@ class BankTransactionSync implements ShouldQueue
// $queue = Ninja::isHosted() ? 'bank' : 'default'; // $queue = Ninja::isHosted() ? 'bank' : 'default';
// if($account->isPaid()) if($account->isPaid() && $account->plan == 'enterprise')
// { {
$account->bank_integrations->each(function ($bank_integration) use ($account){ $account->bank_integrations()->where('auto_sync', true)->cursor()->each(function ($bank_integration) use ($account){
ProcessBankTransactions::dispatchSync($account->bank_integration_account_id, $bank_integration); ProcessBankTransactions::dispatchSync($account->bank_integration_account_id, $bank_integration);
}); });
// } }
}); });
} }

View File

@ -0,0 +1,84 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Jobs\User;
use App\DataMapper\CompanySettings;
use App\DataMapper\DefaultSettings;
use App\Events\User\UserWasCreated;
use App\Libraries\MultiDB;
use App\Models\User;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Http\Request;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class VerifyPhone implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesHash;
/**
* Create a new job instance.
*
* @param User $user
*/
public function __construct(private User $user){}
/**
* Execute the job.
*
* @return User|null
*/
public function handle() : void
{
MultiDB::checkUserEmailExists($this->user->email);
$sid = config('ninja.twilio_account_sid');
$token = config('ninja.twilio_auth_token');
if(!$sid)
return; // no twilio api credentials provided, bail.
$twilio = new \Twilio\Rest\Client($sid, $token);
$country = $this->user->account?->companies()?->first()?->country();
if(!$country || strlen($this->user->phone) < 2)
return;
$countryCode = $country->iso_3166_2;
try{
$phone_number = $twilio->lookups->v1->phoneNumbers($this->user->phone)
->fetch(["countryCode" => $countryCode]);
}
catch(\Exception $e) {
$this->user->verified_phone_number = false;
$this->user->save();
return;
}
if($phone_number && strlen($phone_number->phoneNumber) > 1)
{
$this->user->phone = $phone_number->phoneNumber;
$this->user->verified_phone_number = true;
$this->user->save();
}
}
}

View File

@ -271,7 +271,8 @@ class Import implements ShouldQueue
} }
/*After a migration first some basic jobs to ensure the system is up to date*/ /*After a migration first some basic jobs to ensure the system is up to date*/
VersionCheck::dispatch(); if(Ninja::isSelfHost())
VersionCheck::dispatch();
info('Completed🚀🚀🚀🚀🚀 at '.now()); info('Completed🚀🚀🚀🚀🚀 at '.now());
@ -575,7 +576,7 @@ class Import implements ShouldQueue
foreach ($data as $resource) { foreach ($data as $resource) {
$modified = $resource; $modified = $resource;
unset($modified['id']); unset($modified['id']);
unset($modified['password']); //cant import passwords. // unset($modified['password']); //cant import passwords.
unset($modified['confirmation_code']); //cant import passwords. unset($modified['confirmation_code']); //cant import passwords.
unset($modified['oauth_user_id']); unset($modified['oauth_user_id']);
unset($modified['oauth_provider_id']); unset($modified['oauth_provider_id']);
@ -587,6 +588,7 @@ class Import implements ShouldQueue
if($modified['deleted_at']) if($modified['deleted_at'])
$user->deleted_at = now(); $user->deleted_at = now();
$user->password = $modified['password'];
$user->save(); $user->save();
$user_agent = array_key_exists('token_name', $resource) ?: request()->server('HTTP_USER_AGENT'); $user_agent = array_key_exists('token_name', $resource) ?: request()->server('HTTP_USER_AGENT');

View File

@ -12,6 +12,7 @@
namespace App\Jobs\Util; namespace App\Jobs\Util;
use App\Models\Account; use App\Models\Account;
use App\Utils\Ninja;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
@ -38,6 +39,10 @@ class SchedulerCheck implements ShouldQueue
{ {
set_time_limit(0); set_time_limit(0);
if(Ninja::isHosted())
return;
if (config('ninja.app_version') != base_path('VERSION.txt')) { if (config('ninja.app_version') != base_path('VERSION.txt')) {
try { try {
Artisan::call('migrate', ['--force' => true]); Artisan::call('migrate', ['--force' => true]);

View File

@ -79,11 +79,22 @@ class SystemLogger implements ShouldQueue
if (! $this->log) { if (! $this->log) {
nlog('SystemLogger:: no log to store'); nlog('SystemLogger:: no log to store');
$this->category_id = null;
$this->event_id = null;
$this->type_id = null;
$this->client = null;
$this->company = null;
return; return;
} }
SystemLog::create($sl); SystemLog::create($sl);
$this->log = null;
$this->category_id = null;
$this->event_id = null;
$this->type_id = null;
$this->client = null;
$this->company = null;
} }
public function failed($e) public function failed($e)

View File

@ -213,6 +213,9 @@ class CreatePurchaseOrderPdf implements ShouldQueue
info($maker->getCompiledHTML()); info($maker->getCompiledHTML());
} }
$maker = null;
$state = null;
return $pdf; return $pdf;
} }

View File

@ -515,17 +515,10 @@ class MultiDB
{ {
/* This will set the database connection for the request */ /* This will set the database connection for the request */
config(['database.default' => $database]); config(['database.default' => $database]);
// for some reason this breaks everything _hard_
// DB::purge($database);
// DB::reconnect($database);
} }
public static function setDefaultDatabase() public static function setDefaultDatabase()
{ {
config(['database.default' => config('ninja.db.default')]); config(['database.default' => config('ninja.db.default')]);
// DB::purge(config('ninja.db.default'));
// DB::reconnect(config('ninja.db.default'));
} }
} }

View File

@ -251,17 +251,23 @@ class PaymentEmailEngine extends BaseEmailEngine
private function formatInvoiceField($field) private function formatInvoiceField($field)
{ {
$invoice = ''; $invoicex = '';
foreach ($this->payment->invoices as $invoice) { foreach ($this->payment->invoices as $invoice) {
$invoice_field = $invoice->{$field}; $invoice_field = $invoice->{$field};
$invoice .= ctrans('texts.invoice_number_short') . "{$invoice->number} {$invoice_field}"; if(in_array($field, ['amount', 'balance']))
$invoice_field = Number::formatMoney($invoice_field, $this->client);
if($field == 'due_date')
$invoice_field = $this->translateDate($invoice_field, $this->client->date_format(), $this->client->locale());
$invoicex .= ctrans('texts.invoice_number_short') . "{$invoice->number} {$invoice_field}";
} }
return $invoice; return $invoicex;
} }

View File

@ -36,15 +36,11 @@ class Backup extends BaseModel
$filename = now()->format('Y_m_d').'_'.md5(time()).'.html'; $filename = now()->format('Y_m_d').'_'.md5(time()).'.html';
$file_path = $path.$filename; $file_path = $path.$filename;
// Storage::disk(config('filesystems.default'))->makeDirectory($path, 0775);
Storage::disk(config('filesystems.default'))->put($file_path, $html); Storage::disk(config('filesystems.default'))->put($file_path, $html);
// if (Storage::disk(config('filesystems.default'))->exists($file_path)) {
$this->html_backup = '';
$this->filename = $file_path; $this->filename = $file_path;
$this->save(); $this->save();
// }
} }
public function deleteFile() public function deleteFile()

View File

@ -11,11 +11,13 @@
namespace App\Models; namespace App\Models;
use App\Models\Filterable;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
class BankIntegration extends BaseModel class BankIntegration extends BaseModel
{ {
use SoftDeletes; use SoftDeletes;
use Filterable;
protected $fillable = [ protected $fillable = [
'bank_account_name', 'bank_account_name',
@ -27,6 +29,7 @@ class BankIntegration extends BaseModel
'currency', 'currency',
'nickname', 'nickname',
'from_date', 'from_date',
'auto_sync',
]; ];
protected $dates = [ protected $dates = [

View File

@ -11,6 +11,9 @@
namespace App\Models; namespace App\Models;
use App\Models\Filterable;
use App\Models\Invoice;
use App\Services\Bank\BankService;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
@ -18,7 +21,8 @@ class BankTransaction extends BaseModel
{ {
use SoftDeletes; use SoftDeletes;
use MakesHash; use MakesHash;
use Filterable;
const STATUS_UNMATCHED = 1; const STATUS_UNMATCHED = 1;
const STATUS_MATCHED = 2; const STATUS_MATCHED = 2;
@ -95,4 +99,9 @@ class BankTransaction extends BaseModel
return $this->belongsTo(Account::class)->withTrashed(); return $this->belongsTo(Account::class)->withTrashed();
} }
public function service() :BankService
{
return new BankService($this);
}
} }

View File

@ -42,8 +42,10 @@ class CompanyPresenter extends EntityPresenter
return $settings->company_logo; return $settings->company_logo;
else if(strlen($settings->company_logo) >= 1) else if(strlen($settings->company_logo) >= 1)
return url('') . $settings->company_logo; return url('') . $settings->company_logo;
else else{
return asset('images/new_logo.png'); return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
//return asset('images/new_logo.png');
}
} }
@ -88,8 +90,10 @@ class CompanyPresenter extends EntityPresenter
return "data:image/png;base64, ". base64_encode(@file_get_contents($settings->company_logo, false, stream_context_create($context_options))); return "data:image/png;base64, ". base64_encode(@file_get_contents($settings->company_logo, false, stream_context_create($context_options)));
else if(strlen($settings->company_logo) >= 1) else if(strlen($settings->company_logo) >= 1)
return "data:image/png;base64, ". base64_encode(@file_get_contents(url('') . $settings->company_logo, false, stream_context_create($context_options))); return "data:image/png;base64, ". base64_encode(@file_get_contents(url('') . $settings->company_logo, false, stream_context_create($context_options)));
else else{
return "data:image/png;base64, ". base64_encode(@file_get_contents(asset('images/new_logo.png'), false, stream_context_create($context_options))); return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
//return "data:image/png;base64, ". base64_encode(@file_get_contents(asset('images/new_logo.png'), false, stream_context_create($context_options)));
}
} }

View File

@ -44,38 +44,6 @@ class QuoteInvitation extends BaseModel
return self::class; return self::class;
} }
// public function getSignatureDateAttribute($value)
// {
// if (!$value) {
// return (new Carbon($value))->format('Y-m-d');
// }
// return $value;
// }
// public function getSentDateAttribute($value)
// {
// if (!$value) {
// return (new Carbon($value))->format('Y-m-d');
// }
// return $value;
// }
// public function getViewedDateAttribute($value)
// {
// if (!$value) {
// return (new Carbon($value))->format('Y-m-d');
// }
// return $value;
// }
// public function getOpenedDateAttribute($value)
// {
// if (!$value) {
// return (new Carbon($value))->format('Y-m-d');
// }
// return $value;
// }
public function entityType() public function entityType()
{ {
return Quote::class; return Quote::class;

View File

@ -46,7 +46,7 @@ class InvoiceObserver
*/ */
public function updated(Invoice $invoice) public function updated(Invoice $invoice)
{ {
$subscriptions = Webhook::where('company_id', $invoice->company_id) $subscriptions = Webhook::where('company_id', $invoice->company_id)
->where('event_id', Webhook::EVENT_UPDATE_INVOICE) ->where('event_id', Webhook::EVENT_UPDATE_INVOICE)
->exists(); ->exists();

View File

@ -11,7 +11,9 @@
namespace App\Observers; namespace App\Observers;
use App\Jobs\User\VerifyPhone;
use App\Models\User; use App\Models\User;
use App\Utils\Ninja;
class UserObserver class UserObserver
{ {
@ -23,7 +25,9 @@ class UserObserver
*/ */
public function created(User $user) public function created(User $user)
{ {
if (Ninja::isHosted() && isset($user->phone)) {
VerifyPhone::dispatch($user);
}
} }
/** /**
@ -34,7 +38,9 @@ class UserObserver
*/ */
public function updated(User $user) public function updated(User $user)
{ {
// if (Ninja::isHosted() && $user->isDirty('phone')) {
// VerifyPhone::dispatch($user);
// }
} }
/** /**

View File

@ -130,13 +130,38 @@ class CreditCard implements MethodInterface
$http_status_code = $e->http_status_code; $http_status_code = $e->http_status_code;
$error_details = $e->error_details; $error_details = $e->error_details;
throw new PaymentFailed($e->getMessage()); if(is_array($error_details)) {
$error_details = end($e->error_details['error_codes']);
}
$human_exception = $error_details ? new \Exception($error_details, 400) : $e;
throw new PaymentFailed($human_exception);
} catch (CheckoutArgumentException $e) { } catch (CheckoutArgumentException $e) {
// Bad arguments // Bad arguments
throw new PaymentFailed($e->getMessage());
$error_details = $e->error_details;
if(is_array($error_details)) {
$error_details = end($e->error_details['error_codes']);
}
$human_exception = $error_details ? new \Exception($error_details, 400) : $e;
throw new PaymentFailed($human_exception);
} catch (CheckoutAuthorizationException $e) { } catch (CheckoutAuthorizationException $e) {
// Bad Invalid authorization // Bad Invalid authorization
throw new PaymentFailed($e->getMessage());
$error_details = $e->error_details;
if(is_array($error_details)) {
$error_details = end($e->error_details['error_codes']);
}
$human_exception = $error_details ? new \Exception($error_details, 400) : $e;
throw new PaymentFailed($human_exception);
} }
} }
@ -230,7 +255,6 @@ class CreditCard implements MethodInterface
} }
try { try {
// $response = $this->checkout->gateway->payments()->request($payment);
$response = $this->checkout->gateway->getPaymentsClient()->requestPayment($paymentRequest); $response = $this->checkout->gateway->getPaymentsClient()->requestPayment($paymentRequest);
@ -265,21 +289,71 @@ class CreditCard implements MethodInterface
$http_status_code = $e->http_status_code; $http_status_code = $e->http_status_code;
$error_details = $e->error_details; $error_details = $e->error_details;
if(is_array($error_details)) {
$error_details = end($e->error_details['error_codes']);
}
$this->checkout->unWindGatewayFees($this->checkout->payment_hash); $this->checkout->unWindGatewayFees($this->checkout->payment_hash);
return $this->checkout->processInternallyFailedPayment($this->checkout, $e); $human_exception = $error_details ? new \Exception($error_details, 400) : $e;
SystemLogger::dispatch(
$human_exception->getMessage(),
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_ERROR,
SystemLog::TYPE_CHECKOUT,
$this->checkout->client,
$this->checkout->client->company,
);
return $this->checkout->processInternallyFailedPayment($this->checkout, $human_exception);
} catch (CheckoutArgumentException $e) { } catch (CheckoutArgumentException $e) {
// Bad arguments // Bad arguments
$error_details = $e->error_details;
if(is_array($error_details)) {
$error_details = end($e->error_details['error_codes']);
}
$this->checkout->unWindGatewayFees($this->checkout->payment_hash); $this->checkout->unWindGatewayFees($this->checkout->payment_hash);
return $this->checkout->processInternallyFailedPayment($this->checkout, $e); $human_exception = $error_details ? new \Exception($error_details, 400) : $e;
SystemLogger::dispatch(
$human_exception->getMessage(),
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_ERROR,
SystemLog::TYPE_CHECKOUT,
$this->checkout->client,
$this->checkout->client->company,
);
return $this->checkout->processInternallyFailedPayment($this->checkout, $human_exception);
} catch (CheckoutAuthorizationException $e) { } catch (CheckoutAuthorizationException $e) {
// Bad Invalid authorization // Bad Invalid authorization
$error_details = $e->error_details;
if(is_array($error_details)) {
$error_details = end($e->error_details['error_codes']);
}
$this->checkout->unWindGatewayFees($this->checkout->payment_hash); $this->checkout->unWindGatewayFees($this->checkout->payment_hash);
return $this->checkout->processInternallyFailedPayment($this->checkout, $e); $human_exception = $error_details ? new \Exception($error_details, 400) : $e;
SystemLogger::dispatch(
$human_exception->getMessage(),
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_ERROR,
SystemLog::TYPE_CHECKOUT,
$this->checkout->client,
$this->checkout->client->company,
);
return $this->checkout->processInternallyFailedPayment($this->checkout, $human_exception);
} }
} }
} }

View File

@ -84,18 +84,22 @@ trait Utilities
public function processUnsuccessfulPayment($_payment, $throw_exception = true) public function processUnsuccessfulPayment($_payment, $throw_exception = true)
{ {
$error_message = ''; $error_message = '';
if (array_key_exists('response_summary', $_payment)) { if (is_array($_payment) && array_key_exists('actions', $_payment) && array_key_exists('response_summary', end($_payment['actions']))) {
$error_message = $_payment['response_summary']; $error_message = end($_payment['actions'])['response_summary'];
} elseif (array_key_exists('status', $_payment)) { } elseif (is_array($_payment) && array_key_exists('status', $_payment)) {
$error_message = $_payment['status']; $error_message = $_payment['status'];
} }
else {
$error_message = 'Error processing payment.';
}
$this->getParent()->sendFailureMail($error_message); $this->getParent()->sendFailureMail($error_message);
$message = [ $message = [
'server_response' => $_payment, 'server_response' => $_payment ?: 'Server did not return any response. Most likely failed before payment was created.',
'data' => $this->getParent()->payment_hash->data, 'data' => $this->getParent()->payment_hash->data,
]; ];
@ -109,7 +113,7 @@ trait Utilities
); );
if ($throw_exception) { if ($throw_exception) {
throw new PaymentFailed($_payment['status'].' '.$error_message, 500); throw new PaymentFailed($error_message, 500);
} }
} }

View File

@ -401,8 +401,13 @@ class CheckoutComPaymentDriver extends BaseDriver
$this->unWindGatewayFees($payment_hash); $this->unWindGatewayFees($payment_hash);
$message = $e->getMessage(); $message = $e->getMessage();
$error_details = '';
if(property_exists($e, 'error_details'))
$error_details = $e->error_details;
$data = [ $data = [
'status' => '', 'status' => $e->error_details,
'error_type' => '', 'error_type' => '',
'error_code' => $e->getCode(), 'error_code' => $e->getCode(),
'param' => '', 'param' => '',
@ -433,7 +438,7 @@ class CheckoutComPaymentDriver extends BaseDriver
$this->init(); $this->init();
$this->setPaymentHash($request->getPaymentHash()); $this->setPaymentHash($request->getPaymentHash());
//11-08-2022 check the user is autenticated //11-08-2022 check the user is authenticated
if (!Auth::guard('contact')->check()) { if (!Auth::guard('contact')->check()) {
$client = $request->getClient(); $client = $request->getClient();
auth()->guard('contact')->loginUsingId($client->contacts()->first()->id, true); auth()->guard('contact')->loginUsingId($client->contacts()->first()->id, true);
@ -450,6 +455,8 @@ class CheckoutComPaymentDriver extends BaseDriver
return $this->processUnsuccessfulPayment($payment); return $this->processUnsuccessfulPayment($payment);
} }
} catch (CheckoutApiException | Exception $e) { } catch (CheckoutApiException | Exception $e) {
nlog("checkout");
nlog($e->getMessage());
return $this->processInternallyFailedPayment($this, $e); return $this->processInternallyFailedPayment($this, $e);
} }
} }

View File

@ -89,18 +89,23 @@ class CreditCard
public function paymentResponse(PaymentResponseRequest $request) public function paymentResponse(PaymentResponseRequest $request)
{ {
$payment_hash = PaymentHash::whereRaw('BINARY `hash`= ?', [$request->input('payment_hash')])->firstOrFail(); $payment_hash = PaymentHash::where('hash', $request->input('payment_hash'))->firstOrFail();
$amount_with_fee = $payment_hash->data->total->amount_with_fee; $amount_with_fee = $payment_hash->data->total->amount_with_fee;
$invoice_totals = $payment_hash->data->total->invoice_totals; $invoice_totals = $payment_hash->data->total->invoice_totals;
$fee_total = 0; $fee_total = 0;
for ($i = ($invoice_totals * 100) ; $i < ($amount_with_fee * 100); $i++) { $fees_and_limits = $this->forte->company_gateway->getFeesAndLimits(GatewayType::CREDIT_CARD);
$calculated_fee = ( 3 * $i) / 100;
$calculated_amount_with_fee = round(($i + $calculated_fee) / 100,2); if(property_exists($fees_and_limits, 'fee_percent') && $fees_and_limits->fee_percent > 0)
if ($calculated_amount_with_fee == $amount_with_fee) { {
$fee_total = round($calculated_fee / 100,2); for ($i = ($invoice_totals * 100) ; $i < ($amount_with_fee * 100); $i++) {
$amount_with_fee = $calculated_amount_with_fee; $calculated_fee = ( 3 * $i) / 100;
break; $calculated_amount_with_fee = round(($i + $calculated_fee) / 100,2);
if ($calculated_amount_with_fee == $amount_with_fee) {
$fee_total = round($calculated_fee / 100,2);
$amount_with_fee = $calculated_amount_with_fee;
break;
}
} }
} }

View File

@ -60,6 +60,9 @@ class InstantBankPay implements MethodInterface
'amount' => (string) $data['amount_with_fee'] * 100, 'amount' => (string) $data['amount_with_fee'] * 100,
'currency' => $this->go_cardless->client->getCurrencyCode(), 'currency' => $this->go_cardless->client->getCurrencyCode(),
], ],
'metadata' => [
'payment_hash' => $this->go_cardless->payment_hash->hash,
],
], ],
]); ]);

View File

@ -16,6 +16,7 @@ use App\Http\Requests\Payments\PaymentWebhookRequest;
use App\Jobs\Util\SystemLogger; use App\Jobs\Util\SystemLogger;
use App\Models\ClientGatewayToken; use App\Models\ClientGatewayToken;
use App\Models\GatewayType; use App\Models\GatewayType;
use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Models\PaymentHash; use App\Models\PaymentHash;
use App\Models\PaymentType; use App\Models\PaymentType;
@ -273,11 +274,102 @@ class GoCardlessPaymentDriver extends BaseDriver
nlog('GoCardless completed'); nlog('GoCardless completed');
} }
} }
//billing_request fulfilled
//
//i need to build more context here, i need the client , the payment hash resolved and update the class properties.
//after i resolve the payment hash, ensure the invoice has not been marked as paid and the payment does not already exist.
//if it does exist, ensure it is completed and not pending.
if($event['action'] == 'fulfilled' && array_key_exists('billing_request', $event['links'])) {
$hash = PaymentHash::whereJsonContains('data->billing_request', $event['links']['billing_request'])->first();
if(!$hash){
nlog("GoCardless: couldn't find a hash, need to abort => Billing Request => " . $event['links']['billing_request']);
return response()->json([], 200);
}
$this->go_cardless->setPaymentHash($hash);
$billing_request = $this->go_cardless->gateway->billingRequests()->get(
$event['links']['billing_request']
);
$payment = $this->go_cardless->gateway->payments()->get(
$billing_request->payment_request->links->payment
);
if ($billing_request->status === 'fulfilled') {
$invoices = Invoice::whereIn('id', $this->transformKeys(array_column($hash->invoices(), 'invoice_id')))->withTrashed()->get();
$this->go_cardless->client = $invoices->first()->client;
$invoices->each(function ($invoice){
//if payments exist already, they just need to be confirmed.
if($invoice->payments()->exists){
$invoice->payments()->where('status_id', 1)->cursor()->each(function ($payment){
$payment->status_id = 4;
$payment->save();
});
}
});
// remove all paid invoices
$invoices->filter(function ($invoice){
return $invoice->isPayable();
});
//return early if nothing to do
if($invoices->count() == 0){
nlog("GoCardless: Could not harvest any invoices - probably all paid!!");
return response()->json([], 200);
}
$this->processSuccessfulPayment($payment);
}
}
} }
return response()->json([], 200); return response()->json([], 200);
} }
public function processSuccessfulPayment(\GoCardlessPro\Resources\Payment $payment, array $data = [])
{
$data = [
'payment_method' => $payment->links->mandate,
'payment_type' => PaymentType::INSTANT_BANK_PAY,
'amount' => $this->go_cardless->payment_hash->data->amount_with_fee,
'transaction_reference' => $payment->id,
'gateway_type_id' => GatewayType::INSTANT_BANK_PAY,
];
$payment = $this->go_cardless->createPayment($data, Payment::STATUS_COMPLETED);
$payment->status_id = Payment::STATUS_COMPLETED;
$payment->save();
SystemLogger::dispatch(
['response' => $payment, 'data' => $data],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_GOCARDLESS,
$this->go_cardless->client,
$this->go_cardless->client->company,
);
}
public function ensureMandateIsReady($token) public function ensureMandateIsReady($token)
{ {
try { try {

View File

@ -43,6 +43,11 @@ class PayPalExpressPaymentDriver extends BaseDriver
]; ];
} }
public function init()
{
return $this;
}
/** /**
* Initialize Omnipay PayPal_Express gateway. * Initialize Omnipay PayPal_Express gateway.
* *
@ -95,7 +100,7 @@ class PayPalExpressPaymentDriver extends BaseDriver
return $response->redirect(); return $response->redirect();
} }
$this->sendFailureMail($response->getMessage() ?: ''); // $this->sendFailureMail($response->getMessage() ?: '');
$message = [ $message = [
'server_response' => $response->getMessage(), 'server_response' => $response->getMessage(),

View File

@ -106,15 +106,6 @@ class SquarePaymentDriver extends BaseDriver
/** @var ApiResponse */ /** @var ApiResponse */
$response = $this->square->getRefundsApi()->refund($body); $response = $this->square->getRefundsApi()->refund($body);
// if ($response->isSuccess()) {
// return [
// 'transaction_reference' => $refund->action_id,
// 'transaction_response' => json_encode($response),
// 'success' => $checkout_payment->status == 'Refunded',
// 'description' => $checkout_payment->status,
// 'code' => $checkout_payment->http_code,
// ];
// }
} }
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash) public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)

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