Merge branch 'v5-develop' of https://github.com/paulwer/invoiceninja into feature-brevo

This commit is contained in:
paulwer 2024-01-03 22:46:03 +01:00
commit 1a749c5d93
199 changed files with 489443 additions and 454446 deletions

View File

@ -68,4 +68,7 @@ MICROSOFT_REDIRECT_URI=
APPLE_CLIENT_ID= APPLE_CLIENT_ID=
APPLE_CLIENT_SECRET= APPLE_CLIENT_SECRET=
APPLE_REDIRECT_URI= APPLE_REDIRECT_URI=
NORDIGEN_SECRET_ID=
NORDIGEN_SECRET_KEY=

View File

@ -130,4 +130,4 @@ jobs:
DB_PORT: ${{ job.services.mysql.ports[3306] }} DB_PORT: ${{ job.services.mysql.ports[3306] }}
PHP_CS_FIXER_IGNORE_ENV: true PHP_CS_FIXER_IGNORE_ENV: true
CI_NODE_TOTAL: ${{ matrix.ci_node_total }} CI_NODE_TOTAL: ${{ matrix.ci_node_total }}
CI_NODE_INDEX: ${{ matrix.ci_node_index }} CI_NODE_INDEX: ${{ matrix.ci_node_index }}

View File

@ -36,7 +36,8 @@ We offer a $30 per year white-label license to remove the Invoice Ninja branding
### Desktop Apps ### Desktop Apps
* [macOS](https://apps.apple.com/app/id1503970375?platform=mac) * [macOS](https://apps.apple.com/app/id1503970375?platform=mac)
* [Windows](https://microsoft.com/en-us/p/invoice-ninja/9n3f2bbcfdr6) * [Windows](https://microsoft.com/en-us/p/invoice-ninja/9n3f2bbcfdr6)
* [Linux](https://snapcraft.io/invoiceninja) * [Linux - Snap](https://snapcraft.io/invoiceninja)
* [Linux - Flatpak](https://flathub.org/apps/com.invoiceninja.InvoiceNinja)
### Installation Options ### Installation Options
* [Docker File](https://hub.docker.com/r/invoiceninja/invoiceninja/) * [Docker File](https://hub.docker.com/r/invoiceninja/invoiceninja/)

View File

@ -1 +1 @@
5.7.59 5.7.63

View File

@ -314,7 +314,7 @@ class CheckData extends Command
$new_contact->client_id = $client->id; $new_contact->client_id = $client->id;
$new_contact->contact_key = Str::random(40); $new_contact->contact_key = Str::random(40);
$new_contact->is_primary = true; $new_contact->is_primary = true;
$new_contact->save(); $new_contact->saveQuietly();
} }
} }
} }
@ -362,7 +362,7 @@ class CheckData extends Command
$new_contact->vendor_id = $vendor->id; $new_contact->vendor_id = $vendor->id;
$new_contact->contact_key = Str::random(40); $new_contact->contact_key = Str::random(40);
$new_contact->is_primary = true; $new_contact->is_primary = true;
$new_contact->save(); $new_contact->saveQuietly();
} }
} }
} }
@ -392,7 +392,7 @@ class CheckData extends Command
$invitation->invoice_id = $invoice->id; $invitation->invoice_id = $invoice->id;
$invitation->client_contact_id = ClientContact::whereClientId($invoice->client_id)->first()->id; $invitation->client_contact_id = ClientContact::whereClientId($invoice->client_id)->first()->id;
$invitation->key = Str::random(config('ninja.key_length')); $invitation->key = Str::random(config('ninja.key_length'));
$invitation->save(); $invitation->saveQuietly();
} }
} }
} }
@ -583,7 +583,7 @@ class CheckData extends Command
if ($this->option('paid_to_date')) { if ($this->option('paid_to_date')) {
$this->logMessage("# {$client->id} " . $client->present()->name().' - '.$client->number." Fixing {$client->paid_to_date} to {$total_paid_to_date}"); $this->logMessage("# {$client->id} " . $client->present()->name().' - '.$client->number." Fixing {$client->paid_to_date} to {$total_paid_to_date}");
$client->paid_to_date = $total_paid_to_date; $client->paid_to_date = $total_paid_to_date;
$client->save(); $client->saveQuietly();
} }
} }
} }
@ -632,7 +632,7 @@ class CheckData extends Command
if ($this->option('client_balance')) { if ($this->option('client_balance')) {
$this->logMessage("# {$client_object->id} " . $client_object->present()->name().' - '.$client_object->number." Fixing {$client_object->balance} to " . $client['invoice_balance']); $this->logMessage("# {$client_object->id} " . $client_object->present()->name().' - '.$client_object->number." Fixing {$client_object->balance} to " . $client['invoice_balance']);
$client_object->balance = $client['invoice_balance']; $client_object->balance = $client['invoice_balance'];
$client_object->save(); $client_object->saveQuietly();
} }
$this->isValid = false; $this->isValid = false;
@ -678,7 +678,7 @@ class CheckData extends Command
$this->logMessage("# {$client->id} " . $client->present()->name().' - '.$client->number." Fixing {$client->balance} to 0"); $this->logMessage("# {$client->id} " . $client->present()->name().' - '.$client->number." Fixing {$client->balance} to 0");
$client->balance = $over_payment; $client->balance = $over_payment;
$client->save(); $client->saveQuietly();
} }
} }
}); });
@ -732,7 +732,7 @@ class CheckData extends Command
if ($this->option('client_balance')) { if ($this->option('client_balance')) {
$this->logMessage("# {$client->id} " . $client->present()->name().' - '.$client->number." Fixing {$client->balance} to {$invoice_balance}"); $this->logMessage("# {$client->id} " . $client->present()->name().' - '.$client->number." Fixing {$client->balance} to {$invoice_balance}");
$client->balance = $invoice_balance; $client->balance = $invoice_balance;
$client->save(); $client->saveQuietly();
} }
if ($ledger && (number_format($invoice_balance, 4) != number_format($ledger->balance, 4))) { if ($ledger && (number_format($invoice_balance, 4) != number_format($ledger->balance, 4))) {
@ -766,7 +766,7 @@ class CheckData extends Command
if ($this->option('ledger_balance')) { if ($this->option('ledger_balance')) {
$this->logMessage("# {$client->id} " . $client->present()->name().' - '.$client->number." Fixing {$client->balance} to {$invoice_balance}"); $this->logMessage("# {$client->id} " . $client->present()->name().' - '.$client->number." Fixing {$client->balance} to {$invoice_balance}");
$client->balance = $invoice_balance; $client->balance = $invoice_balance;
$client->save(); $client->saveQuietly();
$ledger->adjustment = $invoice_balance; $ledger->adjustment = $invoice_balance;
$ledger->balance = $invoice_balance; $ledger->balance = $invoice_balance;
@ -884,7 +884,7 @@ class CheckData extends Command
if ($this->option('fix') == 'true') { if ($this->option('fix') == 'true') {
Client::query()->whereNull('country_id')->cursor()->each(function ($client) { Client::query()->whereNull('country_id')->cursor()->each(function ($client) {
$client->country_id = $client->company->settings->country_id; $client->country_id = $client->company->settings->country_id;
$client->save(); $client->saveQuietly();
$this->logMessage("Fixing country for # {$client->id}"); $this->logMessage("Fixing country for # {$client->id}");
}); });
@ -896,7 +896,7 @@ class CheckData extends Command
if ($this->option('fix') == 'true') { if ($this->option('fix') == 'true') {
Vendor::query()->whereNull('currency_id')->orWhere('currency_id', '')->cursor()->each(function ($vendor) { Vendor::query()->whereNull('currency_id')->orWhere('currency_id', '')->cursor()->each(function ($vendor) {
$vendor->currency_id = $vendor->company->settings->currency_id; $vendor->currency_id = $vendor->company->settings->currency_id;
$vendor->save(); $vendor->saveQuietly();
$this->logMessage("Fixing vendor currency for # {$vendor->id}"); $this->logMessage("Fixing vendor currency for # {$vendor->id}");
}); });
@ -919,14 +919,14 @@ class CheckData extends Command
$invoice->balance = 0; $invoice->balance = 0;
$invoice->paid_to_date=$val; $invoice->paid_to_date=$val;
$invoice->save(); $invoice->saveQuietly();
$p = $invoice->payments->first(); $p = $invoice->payments->first();
if ($p && (int)$p->amount == 0) { if ($p && (int)$p->amount == 0) {
$p->amount = $val; $p->amount = $val;
$p->applied = $val; $p->applied = $val;
$p->save(); $p->saveQuietly();
$pivot = $p->paymentables->first(); $pivot = $p->paymentables->first();
$pivot->amount = $val; $pivot->amount = $val;

View File

@ -100,7 +100,7 @@ class TypeCheck extends Command
$entity_settings = $this->checkSettingType($client->settings); $entity_settings = $this->checkSettingType($client->settings);
$entity_settings->md5 = md5(time()); $entity_settings->md5 = md5(time());
$client->settings = $entity_settings; $client->settings = $entity_settings;
$client->save(); $client->saveQuietly();
} }
private function checkCompany($company) private function checkCompany($company)
@ -119,7 +119,7 @@ class TypeCheck extends Command
$entity_settings = $this->checkSettingType($client->settings); $entity_settings = $this->checkSettingType($client->settings);
$entity_settings->md5 = md5(time()); $entity_settings->md5 = md5(time());
$client->settings = $entity_settings; $client->settings = $entity_settings;
$client->save(); $client->saveQuietly();
}); });
Company::query()->cursor()->each(function ($company) { Company::query()->cursor()->each(function ($company) {

View File

@ -97,9 +97,12 @@ class Kernel extends ConsoleKernel
/* Fires webhooks for overdue Invoice */ /* Fires webhooks for overdue Invoice */
$schedule->job(new InvoiceCheckLateWebhook)->dailyAt('07:00')->withoutOverlapping()->name('invoice-overdue-job')->onOneServer(); $schedule->job(new InvoiceCheckLateWebhook)->dailyAt('07:00')->withoutOverlapping()->name('invoice-overdue-job')->onOneServer();
/* Pulls in bank transactions from third party services */
$schedule->job(new BankTransactionSync)->everyFourHours()->withoutOverlapping()->name('bank-trans-sync-job')->onOneServer();
if (Ninja::isSelfHost()) { if (Ninja::isSelfHost()) {
$schedule->call(function () { $schedule->call(function () {
Account::whereNotNull('id')->update(['is_scheduler_running' => true]); Account::query()->whereNotNull('id')->update(['is_scheduler_running' => true]);
})->everyFiveMinutes(); })->everyFiveMinutes();
} }
@ -107,9 +110,6 @@ class Kernel extends ConsoleKernel
if (Ninja::isHosted()) { if (Ninja::isHosted()) {
$schedule->job(new AdjustEmailQuota)->dailyAt('23:30')->withoutOverlapping(); $schedule->job(new AdjustEmailQuota)->dailyAt('23:30')->withoutOverlapping();
/* Pulls in bank transactions from third party services */
$schedule->job(new BankTransactionSync)->everyFourHours()->withoutOverlapping()->name('bank-trans-sync-job')->onOneServer();
/* Checks ACH verification status and updates state to authorize when verified */ /* Checks ACH verification status and updates state to authorize when verified */
$schedule->job(new CheckACHStatus)->everySixHours()->withoutOverlapping()->name('ach-status-job')->onOneServer(); $schedule->job(new CheckACHStatus)->everySixHours()->withoutOverlapping()->name('ach-status-job')->onOneServer();
@ -120,7 +120,7 @@ class Kernel extends ConsoleKernel
$schedule->command('ninja:s3-cleanup')->dailyAt('23:15')->withoutOverlapping()->name('s3-cleanup-job')->onOneServer(); $schedule->command('ninja:s3-cleanup')->dailyAt('23:15')->withoutOverlapping()->name('s3-cleanup-job')->onOneServer();
} }
if (config('queue.default') == 'database' && Ninja::isSelfHost() && config('ninja.internal_queue_enabled') && ! config('ninja.is_docker')) { if (config('queue.default') == 'database' && Ninja::isSelfHost() && config('ninja.internal_queue_enabled') && !config('ninja.is_docker')) {
$schedule->command('queue:work database --stop-when-empty --memory=256')->everyMinute()->withoutOverlapping(); $schedule->command('queue:work database --stop-when-empty --memory=256')->everyMinute()->withoutOverlapping();
$schedule->command('queue:restart')->everyFiveMinutes()->withoutOverlapping(); $schedule->command('queue:restart')->everyFiveMinutes()->withoutOverlapping();
@ -134,7 +134,7 @@ class Kernel extends ConsoleKernel
*/ */
protected function commands() protected function commands()
{ {
$this->load(__DIR__.'/Commands'); $this->load(__DIR__ . '/Commands');
require base_path('routes/console.php'); require base_path('routes/console.php');
} }

View File

@ -15,6 +15,23 @@ use Illuminate\Support\Facades\App;
class EmailTemplateDefaults class EmailTemplateDefaults
{ {
public array $templates = [
'email_template_invoice',
'email_template_quote',
'email_template_credit',
'email_template_payment',
'email_template_payment_partial',
'email_template_statement',
'email_template_reminder1',
'email_template_reminder2',
'email_template_reminder3',
'email_template_reminder_endless',
'email_template_custom1',
'email_template_custom2',
'email_template_custom3',
'email_template_purchase_order',
];
public static function getDefaultTemplate($template, $locale) public static function getDefaultTemplate($template, $locale)
{ {
App::setLocale($locale); App::setLocale($locale);

View File

@ -12,7 +12,8 @@
namespace App\DataProviders; namespace App\DataProviders;
class Domains { class Domains
{
private static array $verify_domains = [ private static array $verify_domains = [
'0-00.usa.cc', '0-00.usa.cc',

View File

@ -181,9 +181,9 @@ class ClientExport extends BaseExport
} }
} }
return $entity; // return $entity;
// return $this->decorateAdvancedFields($client, $entity); return $this->decorateAdvancedFields($client, $entity);
} }
public function processMetaData(array $row, $resource): array public function processMetaData(array $row, $resource): array
@ -221,21 +221,21 @@ class ClientExport extends BaseExport
$entity['client.assigned_user'] = $client->assigned_user ? $client->user->present()->name() : ''; $entity['client.assigned_user'] = $client->assigned_user ? $client->user->present()->name() : '';
} }
if (in_array('client.country_id', $this->input['report_keys'])) { // if (in_array('client.country_id', $this->input['report_keys'])) {
$entity['client.country_id'] = $client->country ? ctrans("texts.country_{$client->country->name}") : ''; // $entity['client.country_id'] = $client->country ? ctrans("texts.country_{$client->country->name}") : '';
} // }
if (in_array('client.shipping_country_id', $this->input['report_keys'])) { // if (in_array('client.shipping_country_id', $this->input['report_keys'])) {
$entity['client.shipping_country_id'] = $client->shipping_country ? ctrans("texts.country_{$client->shipping_country->name}") : ''; // $entity['client.shipping_country_id'] = $client->shipping_country ? ctrans("texts.country_{$client->shipping_country->name}") : '';
} // }
if (in_array('client.currency_id', $this->input['report_keys'])) { // if (in_array('client.currency_id', $this->input['report_keys'])) {
$entity['client.currency_id'] = $client->currency() ? $client->currency()->code : $client->company->currency()->code; // $entity['client.currency_id'] = $client->currency() ? $client->currency()->code : $client->company->currency()->code;
} // }
if (in_array('client.industry_id', $this->input['report_keys'])) { // if (in_array('client.industry_id', $this->input['report_keys'])) {
$entity['industry_id'] = $client->industry ? ctrans("texts.industry_{$client->industry->name}") : ''; // $entity['industry_id'] = $client->industry ? ctrans("texts.industry_{$client->industry->name}") : '';
} // }
if (in_array('client.classification', $this->input['report_keys']) && isset($client->classification)) { if (in_array('client.classification', $this->input['report_keys']) && isset($client->classification)) {
$entity['client.classification'] = ctrans("texts.{$client->classification}") ?? ''; $entity['client.classification'] = ctrans("texts.{$client->classification}") ?? '';

View File

@ -129,8 +129,8 @@ class ContactExport extends BaseExport
} }
} }
return $entity; // return $entity;
// return $this->decorateAdvancedFields($contact->client, $entity); return $this->decorateAdvancedFields($contact->client, $entity);
} }
private function decorateAdvancedFields(Client $client, array $entity) :array private function decorateAdvancedFields(Client $client, array $entity) :array
@ -151,6 +151,15 @@ class ContactExport extends BaseExport
$entity['industry_id'] = $client->industry ? ctrans("texts.industry_{$client->industry->name}") : ''; $entity['industry_id'] = $client->industry ? ctrans("texts.industry_{$client->industry->name}") : '';
} }
if (in_array('client.user_id', $this->input['report_keys'])) {
$entity['client.user_id'] = $client->user ? $client->user->present()->name() : '';
}
if (in_array('client.assigned_user_id', $this->input['report_keys'])) {
$entity['client.assigned_user_id'] = $client->assigned_user ? $client->assigned_user->present()->name() : '';
}
return $entity; return $entity;
} }
} }

View File

@ -161,29 +161,29 @@ class CreditExport extends BaseExport
private function decorateAdvancedFields(Credit $credit, array $entity) :array private function decorateAdvancedFields(Credit $credit, array $entity) :array
{ {
if (in_array('country_id', $this->input['report_keys'])) { // if (in_array('country_id', $this->input['report_keys'])) {
$entity['country'] = $credit->client->country ? ctrans("texts.country_{$credit->client->country->name}") : ''; // $entity['country'] = $credit->client->country ? ctrans("texts.country_{$credit->client->country->name}") : '';
} // }
if (in_array('currency_id', $this->input['report_keys'])) { // if (in_array('currency_id', $this->input['report_keys'])) {
$entity['currency_id'] = $credit->client->currency() ? $credit->client->currency()->code : $credit->company->currency()->code; // $entity['currency_id'] = $credit->client->currency() ? $credit->client->currency()->code : $credit->company->currency()->code;
} // }
if (in_array('invoice_id', $this->input['report_keys'])) { // if (in_array('invoice_id', $this->input['report_keys'])) {
$entity['invoice'] = $credit->invoice ? $credit->invoice->number : ''; // $entity['invoice'] = $credit->invoice ? $credit->invoice->number : '';
} // }
if (in_array('client_id', $this->input['report_keys'])) { // if (in_array('client_id', $this->input['report_keys'])) {
$entity['client'] = $credit->client->present()->name(); // $entity['client'] = $credit->client->present()->name();
} // }
if (in_array('status_id', $this->input['report_keys'])) { // if (in_array('status_id', $this->input['report_keys'])) {
$entity['status'] = $credit->stringStatus($credit->status_id); // $entity['status'] = $credit->stringStatus($credit->status_id);
} // }
if(in_array('credit.status', $this->input['report_keys'])) { // if(in_array('credit.status', $this->input['report_keys'])) {
$entity['credit.status'] = $credit->stringStatus($credit->status_id); // $entity['credit.status'] = $credit->stringStatus($credit->status_id);
} // }
if (in_array('credit.assigned_user_id', $this->input['report_keys'])) { if (in_array('credit.assigned_user_id', $this->input['report_keys'])) {
$entity['credit.assigned_user_id'] = $credit->assigned_user ? $credit->assigned_user->present()->name(): ''; $entity['credit.assigned_user_id'] = $credit->assigned_user ? $credit->assigned_user->present()->name(): '';

View File

@ -126,35 +126,35 @@ class ExpenseExport extends BaseExport
} }
return $entity; // return $entity;
// return $this->decorateAdvancedFields($expense, $entity); return $this->decorateAdvancedFields($expense, $entity);
} }
private function decorateAdvancedFields(Expense $expense, array $entity) :array private function decorateAdvancedFields(Expense $expense, array $entity) :array
{ {
if (in_array('expense.currency_id', $this->input['report_keys'])) { // if (in_array('expense.currency_id', $this->input['report_keys'])) {
$entity['expense.currency_id'] = $expense->currency ? $expense->currency->code : ''; // $entity['expense.currency_id'] = $expense->currency ? $expense->currency->code : '';
} // }
if (in_array('expense.client_id', $this->input['report_keys'])) { // if (in_array('expense.client_id', $this->input['report_keys'])) {
$entity['expense.client'] = $expense->client ? $expense->client->present()->name() : ''; // $entity['expense.client'] = $expense->client ? $expense->client->present()->name() : '';
} // }
if (in_array('expense.invoice_id', $this->input['report_keys'])) { // if (in_array('expense.invoice_id', $this->input['report_keys'])) {
$entity['expense.invoice_id'] = $expense->invoice ? $expense->invoice->number : ''; // $entity['expense.invoice_id'] = $expense->invoice ? $expense->invoice->number : '';
} // }
if (in_array('expense.category', $this->input['report_keys'])) { // if (in_array('expense.category', $this->input['report_keys'])) {
$entity['expense.category'] = $expense->category ? $expense->category->name : ''; // $entity['expense.category'] = $expense->category ? $expense->category->name : '';
} // }
if (in_array('expense.vendor_id', $this->input['report_keys'])) { // if (in_array('expense.vendor_id', $this->input['report_keys'])) {
$entity['expense.vendor'] = $expense->vendor ? $expense->vendor->name : ''; // $entity['expense.vendor'] = $expense->vendor ? $expense->vendor->name : '';
} // }
if (in_array('expense.payment_type_id', $this->input['report_keys'])) { // if (in_array('expense.payment_type_id', $this->input['report_keys'])) {
$entity['expense.payment_type_id'] = $expense->payment_type ? $expense->payment_type->name : ''; // $entity['expense.payment_type_id'] = $expense->payment_type ? $expense->payment_type->name : '';
} // }
if (in_array('expense.project_id', $this->input['report_keys'])) { if (in_array('expense.project_id', $this->input['report_keys'])) {
$entity['expense.project_id'] = $expense->project ? $expense->project->name : ''; $entity['expense.project_id'] = $expense->project ? $expense->project->name : '';

View File

@ -128,32 +128,32 @@ class InvoiceExport extends BaseExport
} }
return $entity; // return $entity;
// return $this->decorateAdvancedFields($invoice, $entity); return $this->decorateAdvancedFields($invoice, $entity);
} }
private function decorateAdvancedFields(Invoice $invoice, array $entity) :array private function decorateAdvancedFields(Invoice $invoice, array $entity) :array
{ {
if (in_array('invoice.country_id', $this->input['report_keys'])) { // if (in_array('invoice.country_id', $this->input['report_keys'])) {
$entity['invoice.country_id'] = $invoice->client->country ? ctrans("texts.country_{$invoice->client->country->name}") : ''; // $entity['invoice.country_id'] = $invoice->client->country ? ctrans("texts.country_{$invoice->client->country->name}") : '';
} // }
if (in_array('invoice.currency_id', $this->input['report_keys'])) { // if (in_array('invoice.currency_id', $this->input['report_keys'])) {
$entity['invoice.currency_id'] = $invoice->client->currency() ? $invoice->client->currency()->code : $invoice->company->currency()->code; // $entity['invoice.currency_id'] = $invoice->client->currency() ? $invoice->client->currency()->code : $invoice->company->currency()->code;
} // }
if (in_array('invoice.client_id', $this->input['report_keys'])) { // if (in_array('invoice.client_id', $this->input['report_keys'])) {
$entity['invoice.client_id'] = $invoice->client->present()->name(); // $entity['invoice.client_id'] = $invoice->client->present()->name();
} // }
if (in_array('invoice.status', $this->input['report_keys'])) { // if (in_array('invoice.status', $this->input['report_keys'])) {
$entity['invoice.status'] = $invoice->stringStatus($invoice->status_id); // $entity['invoice.status'] = $invoice->stringStatus($invoice->status_id);
} // }
if (in_array('invoice.recurring_id', $this->input['report_keys'])) { // if (in_array('invoice.recurring_id', $this->input['report_keys'])) {
$entity['invoice.recurring_id'] = $invoice->recurring_invoice->number ?? ''; // $entity['invoice.recurring_id'] = $invoice->recurring_invoice->number ?? '';
} // }
if (in_array('invoice.auto_bill_enabled', $this->input['report_keys'])) { if (in_array('invoice.auto_bill_enabled', $this->input['report_keys'])) {
$entity['invoice.auto_bill_enabled'] = $invoice->auto_bill_enabled ? ctrans('texts.yes') : ctrans('texts.no'); $entity['invoice.auto_bill_enabled'] = $invoice->auto_bill_enabled ? ctrans('texts.yes') : ctrans('texts.no');

View File

@ -196,43 +196,43 @@ class InvoiceItemExport extends BaseExport
// $entity[$key] = $this->resolveKey($key, $invoice, $this->invoice_transformer); // $entity[$key] = $this->resolveKey($key, $invoice, $this->invoice_transformer);
} }
} }
return $entity; // return $entity;
// return $this->decorateAdvancedFields($invoice, $entity); return $this->decorateAdvancedFields($invoice, $entity);
} }
private function decorateAdvancedFields(Invoice $invoice, array $entity) :array private function decorateAdvancedFields(Invoice $invoice, array $entity) :array
{ {
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(array_key_exists('type', $entity)) { // if(array_key_exists('type', $entity)) {
$entity['type'] = $invoice->typeIdString($entity['type']); // $entity['type'] = $invoice->typeIdString($entity['type']);
} // }
if(array_key_exists('tax_category', $entity)) { // if(array_key_exists('tax_category', $entity)) {
$entity['tax_category'] = $invoice->taxTypeString($entity['tax_category']); // $entity['tax_category'] = $invoice->taxTypeString($entity['tax_category']);
} // }
if (in_array('invoice.country_id', $this->input['report_keys'])) { // if (in_array('invoice.country_id', $this->input['report_keys'])) {
$entity['invoice.country_id'] = $invoice->client->country ? ctrans("texts.country_{$invoice->client->country->name}") : ''; // $entity['invoice.country_id'] = $invoice->client->country ? ctrans("texts.country_{$invoice->client->country->name}") : '';
} // }
if (in_array('invoice.currency_id', $this->input['report_keys'])) { // if (in_array('invoice.currency_id', $this->input['report_keys'])) {
$entity['invoice.currency_id'] = $invoice->client->currency() ? $invoice->client->currency()->code : $invoice->company->currency()->code; // $entity['invoice.currency_id'] = $invoice->client->currency() ? $invoice->client->currency()->code : $invoice->company->currency()->code;
} // }
if (in_array('invoice.client_id', $this->input['report_keys'])) { // if (in_array('invoice.client_id', $this->input['report_keys'])) {
$entity['invoice.client_id'] = $invoice->client->present()->name(); // $entity['invoice.client_id'] = $invoice->client->present()->name();
} // }
if (in_array('invoice.status', $this->input['report_keys'])) { // if (in_array('invoice.status', $this->input['report_keys'])) {
$entity['invoice.status'] = $invoice->stringStatus($invoice->status_id); // $entity['invoice.status'] = $invoice->stringStatus($invoice->status_id);
} // }
if (in_array('invoice.recurring_id', $this->input['report_keys'])) { // if (in_array('invoice.recurring_id', $this->input['report_keys'])) {
$entity['invoice.recurring_id'] = $invoice->recurring_invoice->number ?? ''; // $entity['invoice.recurring_id'] = $invoice->recurring_invoice->number ?? '';
} // }
if (in_array('invoice.assigned_user_id', $this->input['report_keys'])) { if (in_array('invoice.assigned_user_id', $this->input['report_keys'])) {
$entity['invoice.assigned_user_id'] = $invoice->assigned_user ? $invoice->assigned_user->present()->name(): ''; $entity['invoice.assigned_user_id'] = $invoice->assigned_user ? $invoice->assigned_user->present()->name(): '';

View File

@ -125,55 +125,55 @@ class PaymentExport extends BaseExport
} }
return $entity; // return $entity;
// return $this->decorateAdvancedFields($payment, $entity); return $this->decorateAdvancedFields($payment, $entity);
} }
private function decorateAdvancedFields(Payment $payment, array $entity) :array private function decorateAdvancedFields(Payment $payment, array $entity) :array
{ {
if (in_array('status_id', $this->input['report_keys'])) { // if (in_array('status_id', $this->input['report_keys'])) {
$entity['status'] = $payment->stringStatus($payment->status_id); // $entity['status'] = $payment->stringStatus($payment->status_id);
} // }
if (in_array('vendor_id', $this->input['report_keys'])) { // if (in_array('vendor_id', $this->input['report_keys'])) {
$entity['vendor'] = $payment->vendor()->exists() ? $payment->vendor->name : ''; // $entity['vendor'] = $payment->vendor()->exists() ? $payment->vendor->name : '';
} // }
if (in_array('project_id', $this->input['report_keys'])) { // if (in_array('project_id', $this->input['report_keys'])) {
$entity['project'] = $payment->project()->exists() ? $payment->project->name : ''; // $entity['project'] = $payment->project()->exists() ? $payment->project->name : '';
} // }
if (in_array('currency_id', $this->input['report_keys'])) { // if (in_array('currency_id', $this->input['report_keys'])) {
$entity['currency'] = $payment->currency()->exists() ? $payment->currency->code : ''; // $entity['currency'] = $payment->currency()->exists() ? $payment->currency->code : '';
} // }
if (in_array('payment.currency', $this->input['report_keys'])) { // if (in_array('payment.currency', $this->input['report_keys'])) {
$entity['payment.currency'] = $payment->currency()->exists() ? $payment->currency->code : ''; // $entity['payment.currency'] = $payment->currency()->exists() ? $payment->currency->code : '';
} // }
if (in_array('exchange_currency_id', $this->input['report_keys'])) { // if (in_array('exchange_currency_id', $this->input['report_keys'])) {
$entity['exchange_currency'] = $payment->exchange_currency()->exists() ? $payment->exchange_currency->code : ''; // $entity['exchange_currency'] = $payment->exchange_currency()->exists() ? $payment->exchange_currency->code : '';
} // }
if (in_array('client_id', $this->input['report_keys'])) { // if (in_array('client_id', $this->input['report_keys'])) {
$entity['client'] = $payment->client->present()->name(); // $entity['client'] = $payment->client->present()->name();
} // }
if (in_array('type_id', $this->input['report_keys'])) { // if (in_array('type_id', $this->input['report_keys'])) {
$entity['type'] = $payment->translatedType(); // $entity['type'] = $payment->translatedType();
} // }
if (in_array('payment.method', $this->input['report_keys'])) { // if (in_array('payment.method', $this->input['report_keys'])) {
$entity['payment.method'] = $payment->translatedType(); // $entity['payment.method'] = $payment->translatedType();
} // }
if (in_array('payment.status', $this->input['report_keys'])) { // if (in_array('payment.status', $this->input['report_keys'])) {
$entity['payment.status'] = $payment->stringStatus($payment->status_id); // $entity['payment.status'] = $payment->stringStatus($payment->status_id);
} // }
if (in_array('gateway_type_id', $this->input['report_keys'])) { // if (in_array('gateway_type_id', $this->input['report_keys'])) {
$entity['gateway'] = $payment->gateway_type ? $payment->gateway_type->name : 'Unknown Type'; // $entity['gateway'] = $payment->gateway_type ? $payment->gateway_type->name : 'Unknown Type';
} // }
if (in_array('payment.assigned_user_id', $this->input['report_keys'])) { if (in_array('payment.assigned_user_id', $this->input['report_keys'])) {
$entity['payment.assigned_user_id'] = $payment->assigned_user ? $payment->assigned_user->present()->name() : ''; $entity['payment.assigned_user_id'] = $payment->assigned_user ? $payment->assigned_user->present()->name() : '';

View File

@ -173,8 +173,8 @@ class PurchaseOrderExport extends BaseExport
} }
return $entity; // return $entity;
// return $this->decorateAdvancedFields($purchase_order, $entity); return $this->decorateAdvancedFields($purchase_order, $entity);
} }
private function decorateAdvancedFields(PurchaseOrder $purchase_order, array $entity) :array private function decorateAdvancedFields(PurchaseOrder $purchase_order, array $entity) :array
@ -195,6 +195,15 @@ class PurchaseOrderExport extends BaseExport
$entity['purchase_order.status'] = $purchase_order->stringStatus($purchase_order->status_id); $entity['purchase_order.status'] = $purchase_order->stringStatus($purchase_order->status_id);
} }
if (in_array('purchase_order.user_id', $this->input['report_keys'])) {
$entity['purchase_order.user_id'] = $purchase_order->user ? $purchase_order->user->present()->name() : '';
}
if (in_array('purchase_order.assigned_user_id', $this->input['report_keys'])) {
$entity['purchase_order.assigned_user_id'] = $purchase_order->assigned_user ? $purchase_order->assigned_user->present()->name() : '';
}
return $entity; return $entity;
} }
} }

View File

@ -206,6 +206,16 @@ class PurchaseOrderItemExport extends BaseExport
$entity['status'] = $purchase_order->stringStatus($purchase_order->status_id); $entity['status'] = $purchase_order->stringStatus($purchase_order->status_id);
} }
if (in_array('purchase_order.user_id', $this->input['report_keys'])) {
$entity['purchase_order.user_id'] = $purchase_order->user ? $purchase_order->user->present()->name() : '';
}
if (in_array('purchase_order.assigned_user_id', $this->input['report_keys'])) {
$entity['purchase_order.assigned_user_id'] = $purchase_order->assigned_user ? $purchase_order->assigned_user->present()->name() : '';
}
return $entity; return $entity;
} }

View File

@ -133,8 +133,8 @@ class QuoteExport extends BaseExport
} }
} }
return $entity; // return $entity;
// return $this->decorateAdvancedFields($quote, $entity); return $this->decorateAdvancedFields($quote, $entity);
} }
private function decorateAdvancedFields(Quote $quote, array $entity) :array private function decorateAdvancedFields(Quote $quote, array $entity) :array

View File

@ -189,22 +189,22 @@ class QuoteItemExport extends BaseExport
} }
} }
return $entity; // return $entity;
// return $this->decorateAdvancedFields($quote, $entity); return $this->decorateAdvancedFields($quote, $entity);
} }
private function decorateAdvancedFields(Quote $quote, array $entity) :array private function decorateAdvancedFields(Quote $quote, array $entity) :array
{ {
if (in_array('currency_id', $this->input['report_keys'])) { // if (in_array('currency_id', $this->input['report_keys'])) {
$entity['currency'] = $quote->client->currency() ? $quote->client->currency()->code : $quote->company->currency()->code; // $entity['currency'] = $quote->client->currency() ? $quote->client->currency()->code : $quote->company->currency()->code;
} // }
if (in_array('client_id', $this->input['report_keys'])) { // if (in_array('client_id', $this->input['report_keys'])) {
$entity['client'] = $quote->client->present()->name(); // $entity['client'] = $quote->client->present()->name();
} // }
if (in_array('status_id', $this->input['report_keys'])) { // if (in_array('status_id', $this->input['report_keys'])) {
$entity['status'] = $quote->stringStatus($quote->status_id); // $entity['status'] = $quote->stringStatus($quote->status_id);
} // }
if (in_array('quote.assigned_user_id', $this->input['report_keys'])) { if (in_array('quote.assigned_user_id', $this->input['report_keys'])) {
$entity['quote.assigned_user_id'] = $quote->assigned_user ? $quote->assigned_user->present()->name(): ''; $entity['quote.assigned_user_id'] = $quote->assigned_user ? $quote->assigned_user->present()->name(): '';

View File

@ -109,8 +109,6 @@ class RecurringInvoiceExport extends BaseExport
private function buildRow(RecurringInvoice $invoice) :array private function buildRow(RecurringInvoice $invoice) :array
{ {
$transformed_invoice = $this->invoice_transformer->transform($invoice); $transformed_invoice = $this->invoice_transformer->transform($invoice);
$transformed_invoice['frequency_id'] = $invoice->frequencyForKey($invoice->frequency_id); //need to inject this here because it is also a valid key
// nlog($transformed_invoice);
$entity = []; $entity = [];
@ -131,36 +129,36 @@ class RecurringInvoiceExport extends BaseExport
} }
} }
// nlog($entity);
return $entity; // return $entity;
// return $this->decorateAdvancedFields($invoice, $entity); return $this->decorateAdvancedFields($invoice, $entity);
} }
private function decorateAdvancedFields(RecurringInvoice $invoice, array $entity) :array private function decorateAdvancedFields(RecurringInvoice $invoice, array $entity) :array
{ {
if (in_array('country_id', $this->input['report_keys'])) { // if (in_array('country_id', $this->input['report_keys'])) {
$entity['country'] = $invoice->client->country ? ctrans("texts.country_{$invoice->client->country->name}") : ''; // $entity['country'] = $invoice->client->country ? ctrans("texts.country_{$invoice->client->country->name}") : '';
} // }
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('recurring_invoice.status', $this->input['report_keys'])) { // if (in_array('recurring_invoice.status', $this->input['report_keys'])) {
$entity['recurring_invoice.status'] = $invoice->stringStatus($invoice->status_id); // $entity['recurring_invoice.status'] = $invoice->stringStatus($invoice->status_id);
} // }
if (in_array('project_id', $this->input['report_keys'])) { // if (in_array('project_id', $this->input['report_keys'])) {
$entity['project'] = $invoice->project ? $invoice->project->name : ''; // $entity['project'] = $invoice->project ? $invoice->project->name : '';
} // }
if (in_array('vendor_id', $this->input['report_keys'])) { // if (in_array('vendor_id', $this->input['report_keys'])) {
$entity['vendor'] = $invoice->vendor ? $invoice->vendor->name : ''; // $entity['vendor'] = $invoice->vendor ? $invoice->vendor->name : '';
} // }
if (in_array('recurring_invoice.frequency_id', $this->input['report_keys']) || in_array('frequency_id', $this->input['report_keys'])) { if (in_array('recurring_invoice.frequency_id', $this->input['report_keys']) || in_array('frequency_id', $this->input['report_keys'])) {
$entity['recurring_invoice.frequency_id'] = $invoice->frequencyForKey($invoice->frequency_id); $entity['recurring_invoice.frequency_id'] = $invoice->frequencyForKey($invoice->frequency_id);

View File

@ -197,7 +197,7 @@ class TaskExport extends BaseExport
$entity['task.duration'] = $task->calcDuration(); $entity['task.duration'] = $task->calcDuration();
} }
// $entity = $this->decorateAdvancedFields($task, $entity); $entity = $this->decorateAdvancedFields($task, $entity);
$this->storage_array[] = $entity; $this->storage_array[] = $entity;
@ -218,6 +218,15 @@ class TaskExport extends BaseExport
$entity['task.project_id'] = $task->project()->exists() ? $task->project->name : ''; $entity['task.project_id'] = $task->project()->exists() ? $task->project->name : '';
} }
if (in_array('task.user_id', $this->input['report_keys'])) {
$entity['task.user_id'] = $task->user ? $task->user->present()->name() : '';
}
if (in_array('task.assigned_user_id', $this->input['report_keys'])) {
$entity['task.assigned_user_id'] = $task->assigned_user ? $task->assigned_user->present()->name() : '';
}
return $entity; return $entity;
} }
} }

View File

@ -126,15 +126,14 @@ class VendorExport extends BaseExport
} elseif (is_array($parts) && $parts[0] == 'vendor_contact' && isset($transformed_contact[$parts[1]])) { } elseif (is_array($parts) && $parts[0] == 'vendor_contact' && isset($transformed_contact[$parts[1]])) {
$entity[$key] = $transformed_contact[$parts[1]]; $entity[$key] = $transformed_contact[$parts[1]];
} else { } else {
// nlog($key);
$entity[$key] = $this->decorator->transform($key, $vendor); $entity[$key] = $this->decorator->transform($key, $vendor);
// $entity[$key] = $this->resolveKey($key, $vendor, $this->vendor_transformer);
} }
} }
return $entity; // return $entity;
// return $this->decorateAdvancedFields($vendor, $entity); return $this->decorateAdvancedFields($vendor, $entity);
} }
private function decorateAdvancedFields(Vendor $vendor, array $entity) :array private function decorateAdvancedFields(Vendor $vendor, array $entity) :array
@ -151,6 +150,15 @@ class VendorExport extends BaseExport
$entity['vendor.classification'] = ctrans("texts.{$vendor->classification}") ?? ''; $entity['vendor.classification'] = ctrans("texts.{$vendor->classification}") ?? '';
} }
if (in_array('vendor.user_id', $this->input['report_keys'])) {
$entity['vendor.user_id'] = $vendor->user ? $vendor->user->present()->name() : '';
}
if (in_array('vendor.assigned_user_id', $this->input['report_keys'])) {
$entity['vendor.assigned_user_id'] = $vendor->assigned_user ? $vendor->assigned_user->present()->name() : '';
}
// $entity['status'] = $this->calculateStatus($vendor); // $entity['status'] = $this->calculateStatus($vendor);
return $entity; return $entity;

View File

@ -97,7 +97,9 @@ class BankIntegrationFilters extends QueryFilters
return $this->builder; return $this->builder;
} }
return $this->builder->orderBy($sort_col[0], $sort_col[1]); $dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
return $this->builder->orderBy($sort_col[0], $dir);
} }
/** /**

View File

@ -128,13 +128,15 @@ class BankTransactionFilters extends QueryFilters
if (!is_array($sort_col) || count($sort_col) != 2) { if (!is_array($sort_col) || count($sort_col) != 2) {
return $this->builder; return $this->builder;
} }
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
if ($sort_col[0] == 'deposit') { if ($sort_col[0] == 'deposit') {
return $this->builder->where('base_type', 'CREDIT')->orderBy('amount', $sort_col[1]); return $this->builder->where('base_type', 'CREDIT')->orderBy('amount', $dir);
} }
if ($sort_col[0] == 'withdrawal') { if ($sort_col[0] == 'withdrawal') {
return $this->builder->where('base_type', 'DEBIT')->orderBy('amount', $sort_col[1]); return $this->builder->where('base_type', 'DEBIT')->orderBy('amount', $dir);
} }
if ($sort_col[0] == 'status') { if ($sort_col[0] == 'status') {
@ -145,7 +147,7 @@ class BankTransactionFilters extends QueryFilters
return $this->builder; return $this->builder;
} }
return $this->builder->orderBy($sort_col[0], $sort_col[1]); return $this->builder->orderBy($sort_col[0], $dir);
} }
/** /**

View File

@ -65,7 +65,9 @@ class BankTransactionRuleFilters extends QueryFilters
return $this->builder; return $this->builder;
} }
return $this->builder->orderBy($sort_col[0], $sort_col[1]); $dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
return $this->builder->orderBy($sort_col[0], $dir);
} }
/** /**

View File

@ -161,8 +161,10 @@ class ClientFilters extends QueryFilters
if ($sort_col[0] == 'display_name') { if ($sort_col[0] == 'display_name') {
$sort_col[0] = 'name'; $sort_col[0] = 'name';
} }
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
return $this->builder->orderBy($sort_col[0], $sort_col[1]); return $this->builder->orderBy($sort_col[0], $dir);
} }
/** /**

View File

@ -49,8 +49,10 @@ class CompanyGatewayFilters extends QueryFilters
if (!is_array($sort_col) || count($sort_col) != 2) { if (!is_array($sort_col) || count($sort_col) != 2) {
return $this->builder; return $this->builder;
} }
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
return $this->builder->orderBy($sort_col[0], $sort_col[1]); return $this->builder->orderBy($sort_col[0], $dir);
} }
/** /**

View File

@ -139,12 +139,14 @@ class CreditFilters extends QueryFilters
return $this->builder; return $this->builder;
} }
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
if ($sort_col[0] == 'client_id') { if ($sort_col[0] == 'client_id') {
return $this->builder->orderBy(\App\Models\Client::select('name') return $this->builder->orderBy(\App\Models\Client::select('name')
->whereColumn('clients.id', 'credits.client_id'), $sort_col[1]); ->whereColumn('clients.id', 'credits.client_id'), $dir);
} }
return $this->builder->orderBy($sort_col[0], $sort_col[1]); return $this->builder->orderBy($sort_col[0], $dir);
} }
/** /**

View File

@ -51,7 +51,9 @@ class DesignFilters extends QueryFilters
return $this->builder; return $this->builder;
} }
return $this->builder->orderBy($sort_col[0], $sort_col[1]); $dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
return $this->builder->orderBy($sort_col[0], $dir);
} }
public function entities(string $entities = ''): Builder public function entities(string $entities = ''): Builder

View File

@ -12,6 +12,7 @@
namespace App\Filters; namespace App\Filters;
use App\Models\Company; use App\Models\Company;
use App\Filters\QueryFilters;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
/** /**
@ -63,7 +64,9 @@ class DocumentFilters extends QueryFilters
return $this->builder; return $this->builder;
} }
return $this->builder->orderBy($sort_col[0], $sort_col[1]); $dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
return $this->builder->orderBy($sort_col[0], $dir);
} }

View File

@ -47,7 +47,7 @@ class ExpenseCategoryFilters extends QueryFilters
if (!is_array($sort_col) || count($sort_col) != 2) { if (!is_array($sort_col) || count($sort_col) != 2) {
return $this->builder; return $this->builder;
} }
if (is_array($sort_col) && in_array($sort_col[1], ['asc', 'desc']) && in_array($sort_col[0], ['name'])) { if (is_array($sort_col) && in_array($sort_col[1], ['asc', 'desc']) && in_array($sort_col[0], ['name'])) {
return $this->builder->orderBy($sort_col[0], $sort_col[1]); return $this->builder->orderBy($sort_col[0], $sort_col[1]);
} }

View File

@ -64,8 +64,10 @@ class GroupSettingFilters extends QueryFilters
if (!is_array($sort_col) || count($sort_col) != 2) { if (!is_array($sort_col) || count($sort_col) != 2) {
return $this->builder; return $this->builder;
} }
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
return $this->builder->orderBy($sort_col[0], $sort_col[1]); return $this->builder->orderBy($sort_col[0], $dir);
} }
/** /**

View File

@ -146,16 +146,26 @@ class InvoiceFilters extends QueryFilters
*/ */
public function upcoming(): Builder public function upcoming(): Builder
{ {
return $this->builder->whereIn('status_id', [Invoice::STATUS_PARTIAL, Invoice::STATUS_SENT])
->whereNull('due_date') return $this->builder->where(function ($query) {
$query->whereIn('status_id', [Invoice::STATUS_PARTIAL, Invoice::STATUS_SENT])
->where('is_deleted', 0)
->where('balance', '>', 0)
->where(function ($query) {
$query->whereNull('due_date')
->orWhere(function ($q) { ->orWhere(function ($q) {
$q->where('due_date', '>=', now()->startOfDay()->subSecond())->where('partial', 0); $q->where('due_date', '>=', now()->startOfDay()->subSecond())->where('partial', 0);
}) })
->orWhere(function ($q) { ->orWhere(function ($q) {
$q->where('partial_due_date', '>=', now()->startOfDay()->subSecond())->where('partial', '>', 0); $q->where('partial_due_date', '>=', now()->startOfDay()->subSecond())->where('partial', '>', 0);
}) });
->orderByRaw('ISNULL(due_date), due_date '. 'desc')
->orderByRaw('ISNULL(partial_due_date), partial_due_date '. 'desc'); })
->orderByRaw('ISNULL(due_date), due_date ' . 'desc')
->orderByRaw('ISNULL(partial_due_date), partial_due_date ' . 'desc');
});
} }
/** /**
@ -165,13 +175,18 @@ class InvoiceFilters extends QueryFilters
*/ */
public function overdue(): Builder public function overdue(): Builder
{ {
return $this->builder->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]) return $this->builder->where(function ($query) {
$query->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->where('is_deleted', 0) ->where('is_deleted', 0)
->where('balance', '>', 0)
->where(function ($query) { ->where(function ($query) {
$query->where('due_date', '<', now()) $query->where('due_date', '<', now())
->orWhere('partial_due_date', '<', now()); ->orWhere('partial_due_date', '<', now());
}) })
->orderBy('due_date', 'ASC'); ->orderBy('due_date', 'ASC');
});
} }
/** /**
@ -271,14 +286,16 @@ class InvoiceFilters extends QueryFilters
return $this->builder; return $this->builder;
} }
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
if ($sort_col[0] == 'client_id') { if ($sort_col[0] == 'client_id') {
return $this->builder->orderBy(\App\Models\Client::select('name') return $this->builder->orderBy(\App\Models\Client::select('name')
->whereColumn('clients.id', 'invoices.client_id'), $sort_col[1]); ->whereColumn('clients.id', 'invoices.client_id'), $dir);
} }
return $this->builder->orderBy($sort_col[0], $sort_col[1]); return $this->builder->orderBy($sort_col[0], $dir);
} }
/** /**

View File

@ -69,9 +69,11 @@ class ProjectFilters extends QueryFilters
return $this->builder; return $this->builder;
} }
if (is_array($sort_col)) { if (is_array($sort_col) && in_array($sort_col[1], ['asc','desc'])) {
return $this->builder->orderBy($sort_col[0], $sort_col[1]); return $this->builder->orderBy($sort_col[0], $sort_col[1]);
} }
return $this->builder;
} }
/** /**

View File

@ -123,12 +123,14 @@ class PurchaseOrderFilters extends QueryFilters
return $this->builder; return $this->builder;
} }
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
if ($sort_col[0] == 'vendor_id') { if ($sort_col[0] == 'vendor_id') {
return $this->builder->orderBy(\App\Models\Vendor::select('name') return $this->builder->orderBy(\App\Models\Vendor::select('name')
->whereColumn('vendors.id', 'purchase_orders.vendor_id'), $sort_col[1]); ->whereColumn('vendors.id', 'purchase_orders.vendor_id'), $dir);
} }
return $this->builder->orderBy($sort_col[0], $sort_col[1]); return $this->builder->orderBy($sort_col[0], $dir);
} }
/** /**

View File

@ -146,10 +146,12 @@ class QuoteFilters extends QueryFilters
return $this->builder; return $this->builder;
} }
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
if($sort_col[0] == 'client_id') { if($sort_col[0] == 'client_id') {
return $this->builder->orderBy(\App\Models\Client::select('name') return $this->builder->orderBy(\App\Models\Client::select('name')
->whereColumn('clients.id', 'quotes.client_id'), $sort_col[1]); ->whereColumn('clients.id', 'quotes.client_id'), $dir);
} }
@ -157,7 +159,7 @@ class QuoteFilters extends QueryFilters
$sort_col[0] = 'due_date'; $sort_col[0] = 'due_date';
} }
return $this->builder->orderBy($sort_col[0], $sort_col[1]); return $this->builder->orderBy($sort_col[0], $dir);
} }
/** /**

View File

@ -63,7 +63,9 @@ class RecurringExpenseFilters extends QueryFilters
return $this->builder; return $this->builder;
} }
return $this->builder->orderBy($sort_col[0], $sort_col[1]); $dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
return $this->builder->orderBy($sort_col[0], $dir);
} }
/** /**

View File

@ -114,20 +114,25 @@ class RecurringInvoiceFilters extends QueryFilters
*/ */
public function sort(string $sort = ''): Builder public function sort(string $sort = ''): Builder
{ {
$sort_col = explode('|', $sort);
$sort_col = explode('|', $sort);
if (!is_array($sort_col) || count($sort_col) != 2) { if (!is_array($sort_col) || count($sort_col) != 2) {
return $this->builder; return $this->builder;
} }
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
if ($sort_col[0] == 'client_id') { if ($sort_col[0] == 'client_id') {
return $this->builder->orderBy(\App\Models\Client::select('name') return $this->builder->orderBy(\App\Models\Client::select('name')
->whereColumn('clients.id', 'recurring_invoices.client_id'), $sort_col[1]); ->whereColumn('clients.id', 'recurring_invoices.client_id'), $dir);
} }
if($sort_col[0] == 'number'){
return $this->builder->orderByRaw("ABS(number) {$dir}");
}
return $this->builder->orderBy($sort_col[0], $sort_col[1]); return $this->builder->orderBy($sort_col[0], $dir);
} }
/** /**

View File

@ -62,7 +62,9 @@ class RecurringQuoteFilters extends QueryFilters
return $this->builder; return $this->builder;
} }
return $this->builder->orderBy($sort_col[0], $sort_col[1]); $dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
return $this->builder->orderBy($sort_col[0], $dir);
} }
/** /**

View File

@ -50,7 +50,9 @@ class SchedulerFilters extends QueryFilters
return $this->builder; return $this->builder;
} }
return $this->builder->orderBy($sort_col[0], $sort_col[1]); $dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
return $this->builder->orderBy($sort_col[0], $dir);
} }
/** /**

View File

@ -50,7 +50,9 @@ class SubscriptionFilters extends QueryFilters
return $this->builder; return $this->builder;
} }
return $this->builder->orderBy($sort_col[0], $sort_col[1]); $dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
return $this->builder->orderBy($sort_col[0], $dir);
} }
/** /**

View File

@ -75,7 +75,9 @@ class SystemLogFilters extends QueryFilters
return $this->builder; return $this->builder;
} }
return $this->builder->orderBy($sort_col[0], $sort_col[1]); $dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
return $this->builder->orderBy($sort_col[0], $dir);
} }
/** /**

View File

@ -131,17 +131,19 @@ class TaskFilters extends QueryFilters
return $this->builder; return $this->builder;
} }
$dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
if ($sort_col[0] == 'client_id') { if ($sort_col[0] == 'client_id') {
return $this->builder->orderBy(\App\Models\Client::select('name') return $this->builder->orderBy(\App\Models\Client::select('name')
->whereColumn('clients.id', 'tasks.client_id'), $sort_col[1]); ->whereColumn('clients.id', 'tasks.client_id'), $dir);
} }
if ($sort_col[0] == 'user_id') { if ($sort_col[0] == 'user_id') {
return $this->builder->orderBy(\App\Models\User::select('first_name') return $this->builder->orderBy(\App\Models\User::select('first_name')
->whereColumn('users.id', 'tasks.user_id'), $sort_col[1]); ->whereColumn('users.id', 'tasks.user_id'), $dir);
} }
return $this->builder->orderBy($sort_col[0], $sort_col[1]); return $this->builder->orderBy($sort_col[0], $dir);
} }
public function task_status(string $value = ''): Builder public function task_status(string $value = ''): Builder

View File

@ -50,7 +50,9 @@ class TaskStatusFilters extends QueryFilters
return $this->builder; return $this->builder;
} }
return $this->builder->orderBy($sort_col[0], $sort_col[1]); $dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
return $this->builder->orderBy($sort_col[0], $dir);
} }
/** /**

View File

@ -50,7 +50,9 @@ class TaxRateFilters extends QueryFilters
return $this->builder; return $this->builder;
} }
return $this->builder->orderBy($sort_col[0], $sort_col[1]); $dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
return $this->builder->orderBy($sort_col[0], $dir);
} }
/** /**

View File

@ -56,7 +56,9 @@ class TokenFilters extends QueryFilters
return $this->builder; return $this->builder;
} }
return $this->builder->orderBy($sort_col[0], $sort_col[1]); $dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
return $this->builder->orderBy($sort_col[0], $dir);
} }
/** /**

View File

@ -54,7 +54,9 @@ class UserFilters extends QueryFilters
return $this->builder; return $this->builder;
} }
return $this->builder->orderBy($sort_col[0], $sort_col[1]); $dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
return $this->builder->orderBy($sort_col[0], $dir);
} }
/** /**

View File

@ -69,7 +69,9 @@ class VendorFilters extends QueryFilters
return $this->builder; return $this->builder;
} }
return $this->builder->orderBy($sort_col[0], $sort_col[1]); $dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
return $this->builder->orderBy($sort_col[0], $dir);
} }
/** /**

View File

@ -50,7 +50,9 @@ class WebhookFilters extends QueryFilters
return $this->builder; return $this->builder;
} }
return $this->builder->orderBy($sort_col[0], $sort_col[1]); $dir = ($sort_col[1] == 'asc') ? 'asc' : 'desc';
return $this->builder->orderBy($sort_col[0], $dir);
} }
/** /**

View File

@ -0,0 +1,126 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*
* Documentation of Api-Usage: https://developer.gocardless.com/bank-account-data/overview
*
* Institutions: Are Banks or Payment-Providers, which manages bankaccounts.
*
* Accounts: Accounts are existing bank_accounts at a specific institution.
*
* Requisitions: Are registered/active user-flows to authenticate one or many accounts. After completition, the accoundId could be used to fetch data for this account. After the access expires, the user could create a new requisition to connect accounts again.
*/
namespace App\Helpers\Bank\Nordigen;
use App\Helpers\Bank\Nordigen\Transformer\AccountTransformer;
use App\Helpers\Bank\Nordigen\Transformer\TransactionTransformer;
class Nordigen
{
public bool $test_mode; // https://developer.gocardless.com/bank-account-data/sandbox
public string $sandbox_institutionId = "SANDBOXFINANCE_SFIN0000";
protected \Nordigen\NordigenPHP\API\NordigenClient $client;
public function __construct()
{
$this->test_mode = config('ninja.nordigen.test_mode');
if (!(config('ninja.nordigen.secret_id') && config('ninja.nordigen.secret_key')))
throw new \Exception('missing nordigen credentials');
$this->client = new \Nordigen\NordigenPHP\API\NordigenClient(config('ninja.nordigen.secret_id'), config('ninja.nordigen.secret_key'));
$this->client->createAccessToken(); // access_token is valid 24h -> so we dont have to implement a refresh-cycle
}
// metadata-section for frontend
public function getInstitutions()
{
if ($this->test_mode)
return [$this->client->institution->getInstitution($this->sandbox_institutionId)];
return $this->client->institution->getInstitutions();
}
// requisition-section
public function createRequisition(string $redirect, string $initutionId, string $reference)
{
if ($this->test_mode && $initutionId != $this->sandbox_institutionId)
throw new \Exception('invalid institutionId while in test-mode');
return $this->client->requisition->createRequisition($redirect, $initutionId, null, $reference);
}
public function getRequisition(string $requisitionId)
{
try {
return $this->client->requisition->getRequisition($requisitionId);
} catch (\Exception $e) {
if (strpos($e->getMessage(), "Invalid Requisition ID") !== false)
return false;
throw $e;
}
}
// TODO: return null on not found
public function getAccount(string $account_id)
{
try {
$out = new \stdClass();
$out->data = $this->client->account($account_id)->getAccountDetails()["account"];
$out->metadata = $this->client->account($account_id)->getAccountMetaData();
$out->balances = $this->client->account($account_id)->getAccountBalances()["balances"];
$out->institution = $this->client->institution->getInstitution($out->metadata["institution_id"]);
$it = new AccountTransformer();
return $it->transform($out);
} catch (\Exception $e) {
if (strpos($e->getMessage(), "Invalid Account ID") !== false)
return false;
throw $e;
}
}
public function isAccountActive(string $account_id)
{
try {
$account = $this->client->account($account_id)->getAccountMetaData();
if ($account["status"] != "READY") {
nlog('nordigen account was not in status ready. accountId: ' . $account_id . ' status: ' . $account["status"]);
return false;
}
return true;
} catch (\Exception $e) {
if (strpos($e->getMessage(), "Invalid Account ID") !== false)
return false;
throw $e;
}
}
/**
* this method returns booked transactions from the bank_account, pending transactions are not part of the result
* @todo @turbo124 should we include pending transactions within the integration-process and mark them with a specific category?!
*/
public function getTransactions(string $accountId, string $dateFrom = null)
{
$transactionResponse = $this->client->account($accountId)->getAccountTransactions($dateFrom);
$it = new TransactionTransformer();
return $it->transform($transactionResponse);
}
}

View File

@ -0,0 +1,121 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Helpers\Bank\Nordigen\Transformer;
use App\Helpers\Bank\AccountTransformerInterface;
/**
[0] => stdClass Object
(
[data] => stdClass Object
(
[resourceId] => XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
[iban] => DE0286055592XXXXXXXXXX
[currency] => EUR
[ownerName] => Max Mustermann
[product] => GiroKomfort
[bic] => WELADE8LXXX
[usage] => PRIV
)
[metadata] => stdClass Object
(
[id] => XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
[created] => 2022-12-05T18:41:53.986028Z
[last_accessed] => 2023-10-29T08:35:34.003611Z
[iban] => DE0286055592XXXXXXXXXX
[institution_id] => STADT_KREISSPARKASSE_LEIPZIG_WELADE8LXXX
[status] => READY
[owner_name] => Max Mustermann
)
[balances] => [
{
[balanceAmount]: {
[amount] => 9825.64
[currency] => EUR
},
[balanceType] => closingBooked
[referenceDate] => 2023-12-01
},
{
[balanceAmount[: {
[amount] => 10325.64
[currency] => EUR
},
[balanceType] => interimAvailable
[creditLimitIncluded]: true,
[referenceDate] => 2023-12-01
}
]
[institution] => stdClass Object
(
[id] => STADT_KREISSPARKASSE_LEIPZIG_WELADE8LXXX
[name] => Stadt- und Kreissparkasse Leipzig
[bic] => WELADE8LXXX
[transaction_total_days] => 360
[countries] => [
"DE"
],
[logo] => https://storage.googleapis.com/gc-prd-institution_icons-production/DE/PNG/sparkasse.png
[supported_payments] => {
[single-payment] => [
"SCT",
"ISCT"
]
},
[supported_features] => [
"card_accounts",
"payments",
"pending_transactions"
],
[identification_codes] => []
)
)
*/
class AccountTransformer implements AccountTransformerInterface
{
public function transform($nordigen_account)
{
if (!property_exists($nordigen_account, 'data') || !property_exists($nordigen_account, 'metadata') || !property_exists($nordigen_account, 'balances') || !property_exists($nordigen_account, 'institution'))
throw new \Exception('invalid dataset');
$used_balance = $nordigen_account->balances[0];
// prefer entry with closingBooked
foreach ($nordigen_account->balances as $entry) {
if ($entry["balanceType"] === 'closingBooked') { // available: closingBooked, interimAvailable
$used_balance = $entry;
break;
}
}
return [
'id' => $nordigen_account->metadata["id"],
'account_type' => "bank",
'account_name' => $nordigen_account->data["iban"],
'account_status' => $nordigen_account->metadata["status"],
'account_number' => '**** ' . substr($nordigen_account->data["iban"], -7),
'provider_account_id' => $nordigen_account->metadata["id"],
'provider_id' => $nordigen_account->institution["id"],
'provider_name' => $nordigen_account->institution["name"],
'nickname' => $nordigen_account->data["ownerName"] ? $nordigen_account->data["ownerName"] : '',
'current_balance' => (int) $used_balance ? $used_balance["balanceAmount"]["amount"] : 0,
'account_currency' => $used_balance ? $used_balance["balanceAmount"]["currency"] : '',
];
}
}

View File

@ -0,0 +1,149 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Helpers\Bank\Nordigen\Transformer;
use App\Helpers\Bank\BankRevenueInterface;
use App\Models\BankIntegration;
use App\Utils\Traits\AppSetup;
use Illuminate\Support\Facades\Cache;
use Log;
/**
{
"transactions": {
"booked": [
{
"transactionId": "string",
"debtorName": "string",
"debtorAccount": {
"iban": "string"
},
"transactionAmount": {
"currency": "string",
"amount": "328.18"
},
"bankTransactionCode": "string",
"bookingDate": "date",
"valueDate": "date",
"remittanceInformationUnstructured": "string"
},
{
"transactionId": "string",
"transactionAmount": {
"currency": "string",
"amount": "947.26"
},
"bankTransactionCode": "string",
"bookingDate": "date",
"valueDate": "date",
"remittanceInformationUnstructured": "string"
}
],
"pending": [
{
"transactionAmount": {
"currency": "string",
"amount": "99.20"
},
"valueDate": "date",
"remittanceInformationUnstructured": "string"
}
]
}
}
*/
class TransactionTransformer implements BankRevenueInterface
{
use AppSetup;
public function transform($transactionResponse)
{
$data = [];
if (!array_key_exists('transactions', $transactionResponse) || !array_key_exists('booked', $transactionResponse["transactions"]))
throw new \Exception('invalid dataset');
foreach ($transactionResponse["transactions"]["booked"] as $transaction) {
$data[] = $this->transformTransaction($transaction);
}
return $data;
}
public function transformTransaction($transaction)
{
if (!array_key_exists('transactionId', $transaction) || !array_key_exists('transactionAmount', $transaction))
throw new \Exception('invalid dataset');
// description could be in varios places
$description = '';
if (array_key_exists('remittanceInformationStructured', $transaction))
$description = $transaction["remittanceInformationStructured"];
else if (array_key_exists('remittanceInformationStructuredArray', $transaction))
$description = implode('\n', $transaction["remittanceInformationStructuredArray"]);
else if (array_key_exists('remittanceInformationUnstructured', $transaction))
$description = $transaction["remittanceInformationUnstructured"];
else if (array_key_exists('remittanceInformationUnstructuredArray', $transaction))
$description = implode('\n', $transaction["remittanceInformationUnstructuredArray"]);
else
Log::warning("Missing description for the following transaction: " . json_encode($transaction));
// participant
$participant = array_key_exists('debtorAccount', $transaction) && array_key_exists('iban', $transaction["debtorAccount"]) ?
$transaction['debtorAccount']['iban'] :
(array_key_exists('creditorAccount', $transaction) && array_key_exists('iban', $transaction["creditorAccount"]) ?
$transaction['creditorAccount']['iban'] : null);
$participant_name = array_key_exists('debtorName', $transaction) ?
$transaction['debtorName'] :
(array_key_exists('creditorName', $transaction) ?
$transaction['creditorName'] : null);
return [
'transaction_id' => $transaction["transactionId"],
'amount' => abs((int) $transaction["transactionAmount"]["amount"]),
'currency_id' => $this->convertCurrency($transaction["transactionAmount"]["currency"]),
'category_id' => null, // nordigen has no categories
'category_type' => array_key_exists('additionalInformation', $transaction) ? $transaction["additionalInformation"] : null, // TODO: institution specific keys like: GUTSCHRIFT, ABSCHLUSS, MONATSABSCHLUSS etc
'date' => $transaction["bookingDate"],
'description' => $description,
'participant' => $participant,
'participant_name' => $participant_name,
'base_type' => (int) $transaction["transactionAmount"]["amount"] <= 0 ? 'DEBIT' : 'CREDIT',
];
}
private function convertCurrency(string $code)
{
$currencies = Cache::get('currencies');
if (!$currencies) {
$this->buildCache(true);
}
$currency = $currencies->filter(function ($item) use ($code) {
return $item->code == $code;
})->first();
if ($currency)
return $currency->id;
return 1;
}
}

View File

@ -0,0 +1,41 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Helpers\Encrypt;
class Secure
{
public static function encrypt(string $hash): ?string
{
$data = null;
$public_key = openssl_pkey_get_public(config('ninja.encryption.public_key'));
if (openssl_public_encrypt($hash, $encrypted, $public_key)) {
$data = base64_encode($encrypted);
}
return $data;
}
public static function decrypt(string $hash): ?string
{
$data = null;
$private_key = openssl_pkey_get_private(config('ninja.encryption.private_key'));
if (openssl_private_decrypt(base64_decode($hash), $decrypted, $private_key)) {
$data = $decrypted;
}
return $data;
}
}

View File

@ -11,17 +11,18 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Requests\Account\CreateAccountRequest;
use App\Http\Requests\Account\UpdateAccountRequest;
use App\Jobs\Account\CreateAccount;
use App\Libraries\MultiDB;
use App\Models\Account; use App\Models\Account;
use App\Libraries\MultiDB;
use App\Utils\TruthSource;
use App\Models\CompanyUser; use App\Models\CompanyUser;
use Illuminate\Http\Response;
use App\Helpers\Encrypt\Secure;
use App\Jobs\Account\CreateAccount;
use App\Transformers\AccountTransformer; use App\Transformers\AccountTransformer;
use App\Transformers\CompanyUserTransformer; use App\Transformers\CompanyUserTransformer;
use App\Utils\TruthSource;
use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Http\Response; use App\Http\Requests\Account\CreateAccountRequest;
use App\Http\Requests\Account\UpdateAccountRequest;
class AccountController extends BaseController class AccountController extends BaseController
{ {
@ -65,6 +66,33 @@ class AccountController extends BaseController
*/ */
public function store(CreateAccountRequest $request) public function store(CreateAccountRequest $request)
{ {
if($request->has('cf-turnstile-response') && config('ninja.cloudflare.turnstile.secret')) {
$r = \Illuminate\Support\Facades\Http::post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [
'secret' => config('ninja.cloudflare.turnstile.secret'),
'response' => $request->input('cf-turnstile-response'),
'remoteip' => $request->getClientIp(),
]);
if($r->successful()){
if($r->json()['success'] === true) {
// Captcha passed
} else {
return response()->json(['message' => 'Captcha Failed'], 400);
}
}
}
if($request->has('hash') && config('ninja.cloudflare.turnstile.secret')) { //@todo once all platforms are implemented, we disable access to the rest of this route without a success response.
if(Secure::decrypt($request->input('hash')) !== $request->input('email')) {
return response()->json(['message' => 'Invalid Signup Payload'], 400);
}
}
$account = (new CreateAccount($request->all(), $request->getClientIp()))->handle(); $account = (new CreateAccount($request->all(), $request->getClientIp()))->handle();
if (! ($account instanceof Account)) { if (! ($account instanceof Account)) {
return $account; return $account;

View File

@ -0,0 +1,304 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers\Bank;
use App\Helpers\Bank\Nordigen\Nordigen;
use App\Http\Controllers\BaseController;
use App\Http\Requests\Nordigen\ConfirmNordigenBankIntegrationRequest;
use App\Http\Requests\Nordigen\ConnectNordigenBankIntegrationRequest;
use App\Jobs\Bank\ProcessBankTransactionsNordigen;
use App\Models\BankIntegration;
use App\Utils\Ninja;
use Cache;
use Illuminate\Http\Request;
use Nordigen\NordigenPHP\Exceptions\NordigenExceptions\NordigenException;
class NordigenController extends BaseController
{
/**
* VIEW: Connect Nordigen Bank Integration
* @param ConnectNordigenBankIntegrationRequest $request
*/
public function connect(ConnectNordigenBankIntegrationRequest $request)
{
$data = $request->all();
$context = $request->getTokenContent();
$lang = $data['lang'] ?? 'en';
$context["lang"] = $lang;
if (!$context)
return view('bank.nordigen.handler', [
'lang' => $lang,
'failed_reason' => "token-invalid",
"redirectUrl" => config("ninja.app_url") . "?action=nordigen_connect&status=failed&reason=token-invalid",
]);
$context["redirect"] = $data["redirect"];
if ($context["context"] != "nordigen" || array_key_exists("requisitionId", $context))
return view('bank.nordigen.handler', [
'lang' => $lang,
'failed_reason' => "token-invalid",
"redirectUrl" => ($context["redirect"]) . "?action=nordigen_connect&status=failed&reason=token-invalid",
]);
$company = $request->getCompany();
$account = $company->account;
if (!(config('ninja.nordigen.secret_id') && config('ninja.nordigen.secret_key')))
return view('bank.nordigen.handler', [
'lang' => $lang,
'company' => $company,
'account' => $company->account,
'failed_reason' => "account-config-invalid",
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=account-config-invalid",
]);
if (!(Ninja::isSelfHost() || (Ninja::isHosted() && $account->isPaid() && $account->plan == 'enterprise')))
return view('bank.nordigen.handler', [
'lang' => $lang,
'company' => $company,
'account' => $company->account,
'failed_reason' => "not-available",
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=not-available",
]);
$nordigen = new Nordigen();
// show bank_selection_screen, when institution_id is not present
if (!array_key_exists("institution_id", $data))
return view('bank.nordigen.handler', [
'lang' => $lang,
'company' => $company,
'account' => $company->account,
'institutions' => $nordigen->getInstitutions(),
'redirectUrl' => $context["redirect"] . "?action=nordigen_connect&status=user-aborted"
]);
// redirect to requisition flow
try {
$requisition = $nordigen->createRequisition(config('ninja.app_url') . '/nordigen/confirm', $data['institution_id'], $request->token);
} catch (NordigenException $e) { // TODO: property_exists returns null in these cases... => why => therefore we just get unknown error everytime $responseBody is typeof GuzzleHttp\Psr7\Stream
$responseBody = (string) $e->getResponse()->getBody();
if (str_contains($responseBody, '"institution_id"')) // provided institution_id was wrong
return view('bank.nordigen.handler', [
'lang' => $lang,
'company' => $company,
'account' => $company->account,
'failed_reason' => "institution-invalid",
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=institution-invalid",
]);
else if (str_contains($responseBody, '"reference"')) // this error can occur, when a reference was used double or is invalid => therefor we suggest the frontend to use another token
return view('bank.nordigen.handler', [
'lang' => $lang,
'company' => $company,
'account' => $company->account,
'failed_reason' => "token-invalid",
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=token-invalid",
]);
else {
nlog("Unknown Error from nordigen: " . $e);
nlog($responseBody);
return view('bank.nordigen.handler', [
'lang' => $lang,
'company' => $company,
'account' => $company->account,
'failed_reason' => "unknown",
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=unknown",
]);
}
}
// save cache
$context["requisitionId"] = $requisition["id"];
Cache::put($request->token, $context, 3600);
return response()->redirectTo($requisition["link"]);
}
/**
* VIEW: Confirm Nordigen Bank Integration (redirect after nordigen flow)
* @param ConnectNordigenBankIntegrationRequest $request
*/
public function confirm(ConfirmNordigenBankIntegrationRequest $request)
{
$data = $request->all();
$context = $request->getTokenContent();
if (!array_key_exists('lang', $data) && $context['lang'] != 'en')
return redirect()->route('nordigen.confirm', array_merge(["lang" => $context['lang']], $request->query())); // redirect is required in order for the bank-ui to display everything properly
$lang = $data['lang'] ?? 'en';
if (!$context || $context["context"] != "nordigen" || !array_key_exists("requisitionId", $context))
return view('bank.nordigen.handler', [
'lang' => $lang,
'failed_reason' => "ref-invalid",
"redirectUrl" => ($context && array_key_exists("redirect", $context) ? $context["redirect"] : config('ninja.app_url')) . "?action=nordigen_connect&status=failed&reason=ref-invalid",
]);
$company = $request->getCompany();
$account = $company->account;
if (!(config('ninja.nordigen.secret_id') && config('ninja.nordigen.secret_key')))
return view('bank.nordigen.handler', [
'lang' => $lang,
'company' => $company,
'account' => $company->account,
'failed_reason' => "account-config-invalid",
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=account-config-invalid",
]);
if (!(Ninja::isSelfHost() || (Ninja::isHosted() && $account->isPaid() && $account->plan == 'enterprise')))
return view('bank.nordigen.handler', [
'lang' => $lang,
'company' => $company,
'account' => $company->account,
'failed_reason' => "not-available",
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=not-available",
]);
// fetch requisition
$nordigen = new Nordigen();
$requisition = $nordigen->getRequisition($context["requisitionId"]);
// check validity of requisition
if (!$requisition)
return view('bank.nordigen.handler', [
'lang' => $lang,
'company' => $company,
'account' => $company->account,
'failed_reason' => "requisition-not-found",
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=requisition-not-found",
]);
if ($requisition["status"] != "LN")
return view('bank.nordigen.handler', [
'lang' => $lang,
'company' => $company,
'account' => $company->account,
'failed_reason' => "requisition-invalid-status",
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=requisition-invalid-status&status=" . $requisition["status"],
]);
if (sizeof($requisition["accounts"]) == 0)
return view('bank.nordigen.handler', [
'lang' => $lang,
'company' => $company,
'account' => $company->account,
'failed_reason' => "requisition-no-accounts",
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=requisition-no-accounts",
]);
// connect new accounts
$bank_integration_ids = [];
foreach ($requisition["accounts"] as $nordigenAccountId) {
$nordigen_account = $nordigen->getAccount($nordigenAccountId);
$existing_bank_integration = BankIntegration::withTrashed()->where('nordigen_account_id', $nordigen_account['id'])->where('company_id', $company->id)->first();
if (!$existing_bank_integration) {
$bank_integration = new BankIntegration();
$bank_integration->integration_type = BankIntegration::INTEGRATION_TYPE_NORDIGEN;
$bank_integration->company_id = $company->id;
$bank_integration->account_id = $company->account_id;
$bank_integration->user_id = $company->owner()->id;
$bank_integration->nordigen_account_id = $nordigen_account['id'];
$bank_integration->bank_account_type = $nordigen_account['account_type'];
$bank_integration->bank_account_name = $nordigen_account['account_name'];
$bank_integration->bank_account_status = $nordigen_account['account_status'];
$bank_integration->bank_account_number = $nordigen_account['account_number'];
$bank_integration->nordigen_institution_id = $nordigen_account['provider_id'];
$bank_integration->provider_name = $nordigen_account['provider_name'];
$bank_integration->nickname = $nordigen_account['nickname'];
$bank_integration->balance = $nordigen_account['current_balance'];
$bank_integration->currency = $nordigen_account['account_currency'];
$bank_integration->disabled_upstream = false;
$bank_integration->auto_sync = true;
$bank_integration->from_date = now()->subDays(90); // default max-fetch interval of nordigen is 90 days
$bank_integration->save();
array_push($bank_integration_ids, $bank_integration->id);
} else {
// resetting metadata for account status
$existing_bank_integration->balance = $account['current_balance'];
$existing_bank_integration->bank_account_status = $account['account_status'];
$existing_bank_integration->disabled_upstream = false;
$existing_bank_integration->auto_sync = true;
$existing_bank_integration->from_date = now()->subDays(90); // default max-fetch interval of nordigen is 90 days
$existing_bank_integration->deleted_at = null;
$existing_bank_integration->save();
array_push($bank_integration_ids, $existing_bank_integration->id);
}
}
// perform update in background
$company->account->bank_integrations->where("integration_type", BankIntegration::INTEGRATION_TYPE_NORDIGEN)->where('auto_sync', true)->each(function ($bank_integration) {
ProcessBankTransactionsNordigen::dispatch($bank_integration);
});
// prevent rerun of this method with same ref
Cache::delete($data["ref"]);
// Successfull Response => Redirect
return response()->redirectTo($context["redirect"] . "?action=nordigen_connect&status=success&bank_integrations=" . implode(',', $bank_integration_ids));
}
/**
* Process Nordigen Institutions GETTER.
*
*
* @OA\Post(
* path="/api/v1/nordigen/institutions",
* operationId="nordigenRefreshWebhook",
* tags={"nordigen"},
* summary="Getting available institutions from nordigen",
* description="Used to determine the available institutions for sending and creating a new connect-link",
* @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"),
* ),
* )
*/
public function institutions(Request $request)
{
if (!(config('ninja.nordigen.secret_id') && config('ninja.nordigen.secret_key')))
return response()->json(['message' => 'Not yet authenticated with Nordigen Bank Integration service'], 400);
$nordigen = new Nordigen();
return response()->json($nordigen->getInstitutions());
}
}

View File

@ -16,7 +16,7 @@ use App\Helpers\Bank\Yodlee\Yodlee;
use App\Http\Controllers\BaseController; use App\Http\Controllers\BaseController;
use App\Http\Requests\Yodlee\YodleeAdminRequest; use App\Http\Requests\Yodlee\YodleeAdminRequest;
use App\Http\Requests\Yodlee\YodleeAuthRequest; use App\Http\Requests\Yodlee\YodleeAuthRequest;
use App\Jobs\Bank\ProcessBankTransactions; use App\Jobs\Bank\ProcessBankTransactionsYodlee;
use App\Models\BankIntegration; use App\Models\BankIntegration;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -44,7 +44,7 @@ class YodleeController extends BaseController
$company->push(); $company->push();
} }
$yodlee = new Yodlee($token); $yodlee = new Yodlee($token);
if ($request->has('window_closed') && $request->input("window_closed") == "true") { if ($request->has('window_closed') && $request->input("window_closed") == "true") {
@ -90,6 +90,7 @@ class YodleeController extends BaseController
$bank_integration->balance = $account['current_balance']; $bank_integration->balance = $account['current_balance'];
$bank_integration->currency = $account['account_currency']; $bank_integration->currency = $account['account_currency'];
$bank_integration->from_date = now()->subYear(); $bank_integration->from_date = now()->subYear();
$bank_integration->auto_sync = true; $bank_integration->auto_sync = true;
$bank_integration->save(); $bank_integration->save();
@ -97,47 +98,45 @@ class YodleeController extends BaseController
} }
$company->account->bank_integrations->each(function ($bank_integration) use ($company) { $company->account->bank_integrations->where("integration_type", BankIntegration::INTEGRATION_TYPE_YODLEE)->where('auto_sync', true)->each(function ($bank_integration) use ($company) { // TODO: filter to yodlee only
ProcessBankTransactions::dispatch($company->account->bank_integration_account_id, $bank_integration); ProcessBankTransactionsYodlee::dispatch($company->account->id, $bank_integration);
}); });
} }
/** /**
* Process Yodlee Refresh Webhook. * Process Yodlee Refresh Webhook.
* *
* *
* @OA\Post( * @OA\Post(
* path="/api/v1/yodlee/refresh", * path="/api/v1/yodlee/refresh",
* operationId="yodleeRefreshWebhook", * operationId="yodleeRefreshWebhook",
* tags={"yodlee"}, * tags={"yodlee"},
* summary="Processing webhooks from Yodlee", * summary="Processing webhooks from Yodlee",
* description="Notifies the system when a data point can be refreshed", * description="Notifies the system when a data point can be refreshed",
* @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"), * @OA\Parameter(ref="#/components/parameters/X-API-TOKEN"),
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"), * @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Parameter(ref="#/components/parameters/include"), * @OA\Parameter(ref="#/components/parameters/include"),
* @OA\Response( * @OA\Response(
* response=200, * response=200,
* description="", * description="",
* @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), * @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-Remaining", ref="#/components/headers/X-RateLimit-Remaining"),
* @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"),
* @OA\JsonContent(ref="#/components/schemas/Credit"), * @OA\JsonContent(ref="#/components/schemas/Credit"),
* ), * ),
* @OA\Response( * @OA\Response(
* response=422, * response=422,
* description="Validation error", * description="Validation error",
* @OA\JsonContent(ref="#/components/schemas/ValidationError"), * @OA\JsonContent(ref="#/components/schemas/ValidationError"),
* *
* ), * ),
* @OA\Response( * @OA\Response(
* response="default", * response="default",
* description="Unexpected Error", * description="Unexpected Error",
* @OA\JsonContent(ref="#/components/schemas/Error"), * @OA\JsonContent(ref="#/components/schemas/Error"),
* ), * ),
* ) * )
*/ */
/* /*
{ {
"event":{ "event":{
@ -174,12 +173,12 @@ class YodleeController extends BaseController
// nlog($request->all()); // nlog($request->all());
return response()->json(['message' => 'Success'], 200); return response()->json(['message' => 'Success'], 200);
// //
// return response()->json(['message' => 'Unauthorized'], 403); // return response()->json(['message' => 'Unauthorized'], 403);
} }
/* /*
{ {
"event":{ "event":{
@ -208,12 +207,12 @@ class YodleeController extends BaseController
nlog($request->all()); nlog($request->all());
return response()->json(['message' => 'Success'], 200); return response()->json(['message' => 'Success'], 200);
// //
// return response()->json(['message' => 'Unauthorized'], 403); // return response()->json(['message' => 'Unauthorized'], 403);
} }
/* /*
{ {
"event":{ "event":{
@ -244,7 +243,7 @@ class YodleeController extends BaseController
// nlog($request->all()); // nlog($request->all());
return response()->json(['message' => 'Success'], 200); return response()->json(['message' => 'Success'], 200);
// //
// return response()->json(['message' => 'Unauthorized'], 403); // return response()->json(['message' => 'Unauthorized'], 403);
@ -278,7 +277,7 @@ class YodleeController extends BaseController
return response()->json(['message' => 'Success'], 200); return response()->json(['message' => 'Success'], 200);
// //
// return response()->json(['message' => 'Unauthorized'], 403); // return response()->json(['message' => 'Unauthorized'], 403);
@ -290,19 +289,19 @@ class YodleeController extends BaseController
$user = auth()->user(); $user = auth()->user();
$bank_integration = BankIntegration::query() $bank_integration = BankIntegration::query()
->withTrashed() ->withTrashed()
->where('company_id', $user->company()->id) ->where('company_id', $user->company()->id)
->where('account_id', $account_number) ->where('account_id', $account_number)
->exists(); ->exists();
if(!$bank_integration) { if (!$bank_integration) {
return response()->json(['message' => 'Account does not exist.'], 400); return response()->json(['message' => 'Account does not exist.'], 400);
} }
$yodlee = new Yodlee($user->account->bank_integration_account_id); $yodlee = new Yodlee($user->account->bank_integration_account_id);
$summary = $yodlee->getAccountSummary($account_number); $summary = $yodlee->getAccountSummary($account_number);
$transformed_summary = AccountSummary::from($summary[0]); $transformed_summary = AccountSummary::from($summary[0]);
return response()->json($transformed_summary, 200); return response()->json($transformed_summary, 200);

View File

@ -14,6 +14,7 @@ namespace App\Http\Controllers;
use App\Factory\BankIntegrationFactory; use App\Factory\BankIntegrationFactory;
use App\Filters\BankIntegrationFilters; use App\Filters\BankIntegrationFilters;
use App\Helpers\Bank\Yodlee\Yodlee; use App\Helpers\Bank\Yodlee\Yodlee;
use App\Helpers\Bank\Nordigen\Nordigen;
use App\Http\Requests\BankIntegration\AdminBankIntegrationRequest; use App\Http\Requests\BankIntegration\AdminBankIntegrationRequest;
use App\Http\Requests\BankIntegration\BulkBankIntegrationRequest; use App\Http\Requests\BankIntegration\BulkBankIntegrationRequest;
use App\Http\Requests\BankIntegration\CreateBankIntegrationRequest; use App\Http\Requests\BankIntegration\CreateBankIntegrationRequest;
@ -22,10 +23,14 @@ use App\Http\Requests\BankIntegration\EditBankIntegrationRequest;
use App\Http\Requests\BankIntegration\ShowBankIntegrationRequest; use App\Http\Requests\BankIntegration\ShowBankIntegrationRequest;
use App\Http\Requests\BankIntegration\StoreBankIntegrationRequest; use App\Http\Requests\BankIntegration\StoreBankIntegrationRequest;
use App\Http\Requests\BankIntegration\UpdateBankIntegrationRequest; use App\Http\Requests\BankIntegration\UpdateBankIntegrationRequest;
use App\Jobs\Bank\ProcessBankTransactions; use App\Jobs\Bank\ProcessBankTransactionsYodlee;
use App\Jobs\Bank\ProcessBankTransactionsNordigen;
use App\Models\Account;
use App\Models\BankIntegration; use App\Models\BankIntegration;
use App\Models\User;
use App\Repositories\BankIntegrationRepository; use App\Repositories\BankIntegrationRepository;
use App\Transformers\BankIntegrationTransformer; use App\Transformers\BankIntegrationTransformer;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response; use Illuminate\Http\Response;
@ -168,13 +173,13 @@ class BankIntegrationController extends BaseController
$action = request()->input('action'); $action = request()->input('action');
$ids = request()->input('ids'); $ids = request()->input('ids');
BankIntegration::withTrashed()->whereIn('id', $this->transformKeys($ids)) BankIntegration::withTrashed()->whereIn('id', $this->transformKeys($ids))
->company() ->company()
->cursor() ->cursor()
->each(function ($bank_integration, $key) use ($action) { ->each(function ($bank_integration, $key) use ($action) {
$this->bank_integration_repo->{$action}($bank_integration); $this->bank_integration_repo->{$action}($bank_integration);
}); });
/* Need to understand which permission are required for the given bulk action ie. view / edit */ /* Need to understand which permission are required for the given bulk action ie. view / edit */
@ -189,27 +194,45 @@ class BankIntegrationController extends BaseController
*/ */
public function refreshAccounts(AdminBankIntegrationRequest $request) public function refreshAccounts(AdminBankIntegrationRequest $request)
{ {
// As yodlee is the first integration we don't need to perform switches yet, however
// if we add additional providers we can reuse this class
/** @var \App\Models\User $user */ /** @var \App\Models\User $user */
$user = auth()->user(); $user = auth()->user();
$user_account = $user->account; $user_account = $user->account;
$bank_account_id = $user_account->bank_integration_account_id; $this->refreshAccountsYodlee($user);
if (!$bank_account_id) { $this->refreshAccountsNordigen($user);
return response()->json(['message' => 'Not yet authenticated with Bank Integration service'], 400);
}
$yodlee = new Yodlee($bank_account_id); if (Cache::get("throttle_polling:{$user_account->key}"))
return response()->json(BankIntegration::query()->company(), 200);
// Processing transactions for each bank account
if (Ninja::isHosted() && $user->account->bank_integration_account_id)
$user_account->bank_integrations->where("integration_type", BankIntegration::INTEGRATION_TYPE_YODLEE)->each(function ($bank_integration) use ($user_account) {
ProcessBankTransactionsYodlee::dispatch($user_account->id, $bank_integration);
});
if (config('ninja.nordigen.secret_id') && config('ninja.nordigen.secret_key') && (Ninja::isSelfHost() || (Ninja::isHosted() && $user_account->isPaid() && $user_account->plan == 'enterprise')))
$user_account->bank_integrations->where("integration_type", BankIntegration::INTEGRATION_TYPE_NORDIGEN)->each(function ($bank_integration) {
ProcessBankTransactionsNordigen::dispatch($bank_integration);
});
Cache::put("throttle_polling:{$user_account->key}", true, 300);
return response()->json(BankIntegration::query()->company(), 200);
}
private function refreshAccountsYodlee(User $user)
{
if (!Ninja::isHosted() || !$user->account->bank_integration_account_id)
return;
$yodlee = new Yodlee($user->account->bank_integration_account_id);
$accounts = $yodlee->getAccounts(); $accounts = $yodlee->getAccounts();
foreach ($accounts as $account) { foreach ($accounts as $account) {
if ($bi = BankIntegration::withTrashed()->where('bank_account_id', $account['id'])->where('company_id', $user->company()->id)->first()) { if ($bi = BankIntegration::withTrashed()->where("integration_type", BankIntegration::INTEGRATION_TYPE_YODLEE)->where('bank_account_id', $account['id'])->where('company_id', $user->company()->id)->first()) {
$bi->balance = $account['current_balance']; $bi->balance = $account['current_balance'];
$bi->currency = $account['account_currency']; $bi->currency = $account['account_currency'];
$bi->save(); $bi->save();
@ -229,22 +252,35 @@ class BankIntegrationController extends BaseController
$bank_integration->balance = $account['current_balance']; $bank_integration->balance = $account['current_balance'];
$bank_integration->currency = $account['account_currency']; $bank_integration->currency = $account['account_currency'];
$bank_integration->auto_sync = true; $bank_integration->auto_sync = true;
$bank_integration->save(); $bank_integration->save();
} }
} }
}
if (Cache::get("throttle_polling:{$user_account->key}")) {
return response()->json(BankIntegration::query()->company(), 200);
}
$user_account->bank_integrations->each(function ($bank_integration) use ($user_account) { private function refreshAccountsNordigen(User $user)
ProcessBankTransactions::dispatch($user_account->bank_integration_account_id, $bank_integration); {
if (!(config('ninja.nordigen.secret_id') && config('ninja.nordigen.secret_key')))
return;
$nordigen = new Nordigen();
BankIntegration::where("integration_type", BankIntegration::INTEGRATION_TYPE_NORDIGEN)->whereNotNull('nordigen_account_id')->each(function (BankIntegration $bank_integration) use ($nordigen) {
$account = $nordigen->getAccount($bank_integration->nordigen_account_id);
if (!$account) {
$bank_integration->disabled_upstream = true;
$bank_integration->save();
return;
}
$bank_integration->disabled_upstream = false;
$bank_integration->bank_account_status = $account['account_status'];
$bank_integration->balance = $account['current_balance'];
$bank_integration->currency = $account['account_currency'];
$bank_integration->save();
}); });
Cache::put("throttle_polling:{$user_account->key}", true, 300);
return response()->json(BankIntegration::query()->company(), 200);
} }
/** /**
@ -262,23 +298,27 @@ class BankIntegrationController extends BaseController
$account = $user->account; $account = $user->account;
$bank_account_id = $account->bank_integration_account_id; $bank_integration = BankIntegration::withTrashed()->where('bank_account_id', $acc_id)->orWhere('nordigen_account_id', $acc_id)->company()->firstOrFail(); // @turbo124 please check
if (!$bank_account_id) { if ($bank_integration->integration_type == BankIntegration::INTEGRATION_TYPE_YODLEE)
$this->removeAccountYodlee($account, $bank_integration);
// we dont remove Accounts from nordigen, because they could be used within other companies
$this->bank_integration_repo->delete($bank_integration);
return $this->itemResponse($bank_integration->fresh());
}
private function removeAccountYodlee(Account $account, BankIntegration $bank_integration)
{
if (!$account->bank_integration_account_id) {
return response()->json(['message' => 'Not yet authenticated with Bank Integration service'], 400); return response()->json(['message' => 'Not yet authenticated with Bank Integration service'], 400);
} }
$bi = BankIntegration::withTrashed()->where('bank_account_id', $acc_id)->company()->firstOrFail(); $yodlee = new Yodlee($account->bank_integration_account_id);
$yodlee->deleteAccount($bank_integration->bank_account_id);
$yodlee = new Yodlee($bank_account_id);
$res = $yodlee->deleteAccount($acc_id);
$this->bank_integration_repo->delete($bi);
return $this->itemResponse($bi->fresh());
} }
/** /**
* Return the remote list of accounts stored on the third party provider * Return the remote list of accounts stored on the third party provider
* and update our local cache. * and update our local cache.
@ -288,12 +328,20 @@ class BankIntegrationController extends BaseController
*/ */
public function getTransactions(AdminBankIntegrationRequest $request) public function getTransactions(AdminBankIntegrationRequest $request)
{ {
/** @var \App\Models\User $user */ /** @var \App\Models\Account $account */
$user = auth()->user(); $account = auth()->user()->account;
$user->account->bank_integrations->each(function ($bank_integration) use ($user) { if (Ninja::isHosted() && $account->isPaid() && $account->plan == 'enterprise') {
(new ProcessBankTransactions($user->account->bank_integration_account_id, $bank_integration))->handle(); $account->bank_integrations()->where('integration_type', BankIntegration::INTEGRATION_TYPE_YODLEE)->where('auto_sync', true)->cursor()->each(function ($bank_integration) use ($account) {
}); (new ProcessBankTransactionsYodlee($account->id, $bank_integration))->handle();
});
}
if (config("ninja.nordigen.secret_id") && config("ninja.nordigen.secret_key") && (Ninja::isSelfHost() || (Ninja::isHosted() && $account->isPaid() && $account->plan == 'enterprise'))) {
$account->bank_integrations()->where('integration_type', BankIntegration::INTEGRATION_TYPE_NORDIGEN)->where('auto_sync', true)->cursor()->each(function ($bank_integration) {
(new ProcessBankTransactionsNordigen($bank_integration))->handle();
});
}
return response()->json(['message' => 'Fetching transactions....'], 200); return response()->json(['message' => 'Fetching transactions....'], 200);
} }

View File

@ -167,8 +167,8 @@ class PaymentMethodController extends Controller
if (request()->query('method') == GatewayType::BACS) { if (request()->query('method') == GatewayType::BACS) {
return $client_contact->client->getBACSGateway(); return $client_contact->client->getBACSGateway();
} }
if (in_array(request()->query('method'), [GatewayType::BANK_TRANSFER, GatewayType::DIRECT_DEBIT, GatewayType::SEPA])) { if (in_array(request()->query('method'), [GatewayType::BANK_TRANSFER, GatewayType::DIRECT_DEBIT, GatewayType::SEPA, GatewayType::ACSS])) {
return $client_contact->client->getBankTransferGateway(); return $client_contact->client->getBankTransferGateway();
} }

View File

@ -11,36 +11,37 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Events\Credit\CreditWasCreated; use App\Utils\Ninja;
use App\Events\Credit\CreditWasUpdated; use App\Models\Client;
use App\Factory\CloneCreditFactory; use App\Models\Credit;
use App\Models\Account;
use App\Models\Invoice;
use App\Models\Webhook;
use Illuminate\Http\Response;
use App\Factory\CreditFactory; use App\Factory\CreditFactory;
use App\Filters\CreditFilters; use App\Filters\CreditFilters;
use App\Http\Requests\Credit\ActionCreditRequest; use App\Jobs\Credit\ZipCredits;
use App\Utils\Traits\MakesHash;
use App\Jobs\Entity\EmailEntity;
use App\Factory\CloneCreditFactory;
use App\Services\PdfMaker\PdfMerge;
use Illuminate\Support\Facades\App;
use App\Utils\Traits\SavesDocuments;
use App\Repositories\CreditRepository;
use App\Events\Credit\CreditWasCreated;
use App\Events\Credit\CreditWasUpdated;
use App\Transformers\CreditTransformer;
use Illuminate\Support\Facades\Storage;
use App\Services\Template\TemplateAction;
use App\Http\Requests\Credit\BulkCreditRequest; use App\Http\Requests\Credit\BulkCreditRequest;
use App\Http\Requests\Credit\CreateCreditRequest;
use App\Http\Requests\Credit\DestroyCreditRequest;
use App\Http\Requests\Credit\EditCreditRequest; use App\Http\Requests\Credit\EditCreditRequest;
use App\Http\Requests\Credit\ShowCreditRequest; use App\Http\Requests\Credit\ShowCreditRequest;
use App\Http\Requests\Credit\StoreCreditRequest; use App\Http\Requests\Credit\StoreCreditRequest;
use App\Http\Requests\Credit\ActionCreditRequest;
use App\Http\Requests\Credit\CreateCreditRequest;
use App\Http\Requests\Credit\UpdateCreditRequest; use App\Http\Requests\Credit\UpdateCreditRequest;
use App\Http\Requests\Credit\UploadCreditRequest; use App\Http\Requests\Credit\UploadCreditRequest;
use App\Jobs\Credit\ZipCredits; use App\Http\Requests\Credit\DestroyCreditRequest;
use App\Jobs\Entity\EmailEntity;
use App\Models\Account;
use App\Models\Client;
use App\Models\Credit;
use App\Models\Invoice;
use App\Repositories\CreditRepository;
use App\Services\PdfMaker\PdfMerge;
use App\Services\Template\TemplateAction;
use App\Transformers\CreditTransformer;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\SavesDocuments;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Storage;
/** /**
* Class CreditController. * Class CreditController.
@ -153,6 +154,7 @@ class CreditController extends BaseController
$user = auth()->user(); $user = auth()->user();
$credit = CreditFactory::create($user->company()->id, $user->id); $credit = CreditFactory::create($user->company()->id, $user->id);
$credit->date = now()->addSeconds($user->company()->utc_offset())->format('Y-m-d');
return $this->itemResponse($credit); return $this->itemResponse($credit);
} }
@ -638,23 +640,14 @@ class CreditController extends BaseController
} }
break; break;
case 'email': case 'email':
$credit->invitations->load('contact.client.country', 'credit.client.country', 'credit.company')->each(function ($invitation) use ($credit) {
EmailEntity::dispatch($invitation, $credit->company, 'credit');
});
if (! $bulk) {
return response()->json(['message'=>'email sent'], 200);
}
break;
case 'send_email': case 'send_email':
$credit->invitations->load('contact.client.country', 'credit.client.country', 'credit.company')->each(function ($invitation) use ($credit) { $credit->invitations->load('contact.client.country', 'credit.client.country', 'credit.company')->each(function ($invitation) use ($credit) {
EmailEntity::dispatch($invitation, $credit->company, 'credit'); EmailEntity::dispatch($invitation, $credit->company, 'credit');
}); });
$credit->sendEvent(Webhook::EVENT_SENT_CREDIT, "client");
if (! $bulk) { if (! $bulk) {
return response()->json(['message'=>'email sent'], 200); return response()->json(['message'=>'email sent'], 200);
} }

View File

@ -11,25 +11,26 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Events\Credit\CreditWasEmailed; use App\Utils\Ninja;
use App\Events\Quote\QuoteWasEmailed; use App\Models\Quote;
use App\Http\Requests\Email\SendEmailRequest;
use App\Jobs\PurchaseOrder\PurchaseOrderEmail;
use App\Models\Credit; use App\Models\Credit;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Webhook;
use App\Models\PurchaseOrder; use App\Models\PurchaseOrder;
use App\Models\Quote;
use App\Models\RecurringInvoice;
use App\Services\Email\Email; use App\Services\Email\Email;
use App\Utils\Traits\MakesHash;
use App\Models\RecurringInvoice;
use App\Services\Email\EmailObject; use App\Services\Email\EmailObject;
use App\Events\Quote\QuoteWasEmailed;
use App\Transformers\QuoteTransformer;
use Illuminate\Mail\Mailables\Address;
use App\Events\Credit\CreditWasEmailed;
use App\Transformers\CreditTransformer; use App\Transformers\CreditTransformer;
use App\Transformers\InvoiceTransformer; use App\Transformers\InvoiceTransformer;
use App\Http\Requests\Email\SendEmailRequest;
use App\Jobs\PurchaseOrder\PurchaseOrderEmail;
use App\Transformers\PurchaseOrderTransformer; use App\Transformers\PurchaseOrderTransformer;
use App\Transformers\QuoteTransformer;
use App\Transformers\RecurringInvoiceTransformer; use App\Transformers\RecurringInvoiceTransformer;
use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use Illuminate\Mail\Mailables\Address;
class EmailController extends BaseController class EmailController extends BaseController
{ {
@ -100,6 +101,7 @@ class EmailController extends BaseController
if ($entity_obj->invitations->count() >= 1) { if ($entity_obj->invitations->count() >= 1) {
$entity_obj->entityEmailEvent($entity_obj->invitations->first(), 'invoice', $template); $entity_obj->entityEmailEvent($entity_obj->invitations->first(), 'invoice', $template);
$entity_obj->sendEvent(Webhook::EVENT_SENT_INVOICE, "client");
} }
} }
@ -109,6 +111,8 @@ class EmailController extends BaseController
if ($entity_obj->invitations->count() >= 1) { if ($entity_obj->invitations->count() >= 1) {
event(new QuoteWasEmailed($entity_obj->invitations->first(), $entity_obj->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), 'quote')); event(new QuoteWasEmailed($entity_obj->invitations->first(), $entity_obj->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), 'quote'));
$entity_obj->sendEvent(Webhook::EVENT_SENT_QUOTE, "client");
} }
} }
@ -118,6 +122,7 @@ class EmailController extends BaseController
if ($entity_obj->invitations->count() >= 1) { if ($entity_obj->invitations->count() >= 1) {
event(new CreditWasEmailed($entity_obj->invitations->first(), $entity_obj->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), 'credit')); event(new CreditWasEmailed($entity_obj->invitations->first(), $entity_obj->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), 'credit'));
$entity_obj->sendEvent(Webhook::EVENT_SENT_CREDIT, "client");
} }
} }
@ -143,7 +148,8 @@ class EmailController extends BaseController
$data['template'] = $template; $data['template'] = $template;
PurchaseOrderEmail::dispatch($entity_obj, $entity_obj->company, $data); PurchaseOrderEmail::dispatch($entity_obj, $entity_obj->company, $data);
$entity_obj->sendEvent(Webhook::EVENT_SENT_PURCHASE_ORDER, "vendor");
return $this->itemResponse($entity_obj); return $this->itemResponse($entity_obj);
} }

View File

@ -166,6 +166,7 @@ class InvoiceController extends BaseController
/** @var \App\Models\User $user */ /** @var \App\Models\User $user */
$user = auth()->user(); $user = auth()->user();
$invoice = InvoiceFactory::create($user->company()->id, $user->id); $invoice = InvoiceFactory::create($user->company()->id, $user->id);
$invoice->date = now()->addSeconds($user->company()->utc_offset())->format('Y-m-d');
return $this->itemResponse($invoice); return $this->itemResponse($invoice);
} }
@ -538,8 +539,6 @@ class InvoiceController extends BaseController
return (new \App\Jobs\Entity\CreateRawPdf($invoice->invitations->first()))->handle(); return (new \App\Jobs\Entity\CreateRawPdf($invoice->invitations->first()))->handle();
}); });
return response()->streamDownload(function () use ($paths) { return response()->streamDownload(function () use ($paths) {
echo $merge = (new PdfMerge($paths->toArray()))->run(); echo $merge = (new PdfMerge($paths->toArray()))->run();
}, 'print.pdf', ['Content-Type' => 'application/pdf']); }, 'print.pdf', ['Content-Type' => 'application/pdf']);

View File

@ -155,6 +155,7 @@ class PaymentController extends BaseController
$user = auth()->user(); $user = auth()->user();
$payment = PaymentFactory::create($user->company()->id, $user->id); $payment = PaymentFactory::create($user->company()->id, $user->id);
$payment->date = now()->addSeconds($user->company()->utc_offset())->format('Y-m-d');
return $this->itemResponse($payment); return $this->itemResponse($payment);
} }

View File

@ -144,6 +144,7 @@ class PurchaseOrderController extends BaseController
$user = auth()->user(); $user = auth()->user();
$purchase_order = PurchaseOrderFactory::create($user->company()->id, $user->id); $purchase_order = PurchaseOrderFactory::create($user->company()->id, $user->id);
$purchase_order->date = now()->addSeconds($user->company()->utc_offset())->format('Y-m-d');
return $this->itemResponse($purchase_order); return $this->itemResponse($purchase_order);
} }

View File

@ -168,6 +168,7 @@ class QuoteController extends BaseController
$user = auth()->user(); $user = auth()->user();
$quote = QuoteFactory::create($user->company()->id, $user->id); $quote = QuoteFactory::create($user->company()->id, $user->id);
$quote->date = now()->addSeconds($user->company()->utc_offset())->format('Y-m-d');
return $this->itemResponse($quote); return $this->itemResponse($quote);
} }

View File

@ -30,6 +30,12 @@ class SubdomainController extends BaseController
return response()->json(['message' => ctrans('texts.subdomain_is_not_available')], 401); return response()->json(['message' => ctrans('texts.subdomain_is_not_available')], 401);
} }
if (!preg_match('/^[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?$/', request()->input('subdomain'))) {
return response()->json(['message' => ctrans('texts.subdomain_is_not_available')], 401);
}
return response()->json(['message' => 'Domain available'], 200); return response()->json(['message' => 'Domain available'], 200);
} }
} }

View File

@ -21,6 +21,12 @@ use Twilio\Rest\Client;
class TwilioController extends BaseController class TwilioController extends BaseController
{ {
private array $invalid_codes = [
'+21',
'+17152567760',
];
public function __construct() public function __construct()
{ {
parent::__construct(); parent::__construct();
@ -36,8 +42,16 @@ class TwilioController extends BaseController
/** @var \App\Models\User $user */ /** @var \App\Models\User $user */
$user = auth()->user(); $user = auth()->user();
if(!$user->email_verified_at) {
return response()->json(['message' => 'Please verify your email address before verifying your phone number'], 400);
}
$account = $user->company()->account; $account = $user->company()->account;
if(!$this->checkPhoneValidity($request->phone)) {
return response()->json(['message' => 'This phone number is not supported'], 400);
}
if (MultiDB::hasPhoneNumber($request->phone)) { if (MultiDB::hasPhoneNumber($request->phone)) {
return response()->json(['message' => 'This phone number has already been verified with another account'], 400); return response()->json(['message' => 'This phone number has already been verified with another account'], 400);
} }
@ -65,6 +79,19 @@ class TwilioController extends BaseController
return response()->json(['message' => 'Code sent.'], 200); return response()->json(['message' => 'Code sent.'], 200);
} }
private function checkPhoneValidity($phone)
{
foreach($this->invalid_codes as $code){
if(stripos($phone, $code) !== false) {
return false;
}
return true;
}
}
/** /**
* Show the form for creating a new resource. * Show the form for creating a new resource.
* *
@ -117,12 +144,24 @@ class TwilioController extends BaseController
*/ */
public function generate2faResetCode(Generate2faRequest $request) public function generate2faResetCode(Generate2faRequest $request)
{ {
nlog($request->all());
nlog($request->headers());
$user = User::where('email', $request->email)->first(); $user = User::where('email', $request->email)->first();
if (!$user) { if (!$user) {
return response()->json(['message' => 'Unable to retrieve user.'], 400); return response()->json(['message' => 'Unable to retrieve user.'], 400);
} }
if(!$user->email_verified_at) {
return response()->json(['message' => 'Please verify your email address before verifying your phone number'], 400);
}
if(!$user->first_name || !$user->last_name) {
return response()->json(['message' => 'Please update your first and/or last name in the User Details before verifying your number.'], 400);
}
if (!$user->phone || $user->phone == '') { if (!$user->phone || $user->phone == '') {
return response()->json(['message' => 'User found, but no valid phone number on file, please contact support.'], 400); return response()->json(['message' => 'User found, but no valid phone number on file, please contact support.'], 400);
} }

View File

@ -196,7 +196,7 @@ class UserController extends BaseController
*/ */
public function destroy(DestroyUserRequest $request, User $user) public function destroy(DestroyUserRequest $request, User $user)
{ {
if ($user->isOwner()) { if ($user->hasOwnerFlag()) {
return response()->json(['message', 'Cannot detach owner.'], 401); return response()->json(['message', 'Cannot detach owner.'], 401);
} }

View File

@ -55,6 +55,8 @@ class PdfSlot extends Component
public $is_quote = false; public $is_quote = false;
private $entity_calc;
public function mount() public function mount()
{ {
MultiDB::setDb($this->db); MultiDB::setDb($this->db);
@ -123,6 +125,7 @@ class PdfSlot extends Component
{ {
$this->entity_type = $this->resolveEntityType(); $this->entity_type = $this->resolveEntityType();
$this->entity_calc = $this->entity->calc();
$this->settings = $this->entity->client ? $this->entity->client->getMergedSettings() : $this->entity->company->settings; $this->settings = $this->entity->client ? $this->entity->client->getMergedSettings() : $this->entity->company->settings;
@ -149,6 +152,8 @@ class PdfSlot extends Component
'services' => $this->getServices(), 'services' => $this->getServices(),
'amount' => Number::formatMoney($this->entity->amount, $this->entity->client ?: $this->entity->vendor), 'amount' => Number::formatMoney($this->entity->amount, $this->entity->client ?: $this->entity->vendor),
'balance' => Number::formatMoney($this->entity->balance, $this->entity->client ?: $this->entity->vendor), 'balance' => Number::formatMoney($this->entity->balance, $this->entity->client ?: $this->entity->vendor),
'discount' => $this->entity_calc->getTotalDiscount() > 0 ? Number::formatMoney($this->entity_calc->getTotalDiscount(), $this->entity->client ?: $this->entity->vendor) : false,
'taxes' => $this->entity_calc->getTotalTaxes() > 0 ? Number::formatMoney($this->entity_calc->getTotalTaxes(), $this->entity->client ?: $this->entity->vendor) : false,
'company_details' => $this->getCompanyDetails(), 'company_details' => $this->getCompanyDetails(),
'company_address' => $this->getCompanyAddress(), 'company_address' => $this->getCompanyAddress(),
'entity_details' => $this->getEntityDetails(), 'entity_details' => $this->getEntityDetails(),
@ -198,20 +203,20 @@ class PdfSlot extends Component
if($this->entity_type == 'invoice' || $this->entity_type == 'recurring_invoice') { if($this->entity_type == 'invoice' || $this->entity_type == 'recurring_invoice') {
foreach($this->settings->pdf_variables->invoice_details as $variable) { foreach($this->settings->pdf_variables->invoice_details as $variable) {
$entity_details .= "<div class='flex px-5 block'><p class= w-36 block'>{$variable}_label</p><p class='pl-5 w-36 block entity-field'>{$variable}</p></div>"; $entity_details .= "<div class='flex px-5 block'><p class= w-36 block'>{$variable}_label</p><p class='ml-5 w-36 block entity-field'>{$variable}</p></div>";
} }
} elseif($this->entity_type == 'quote') { } elseif($this->entity_type == 'quote') {
foreach($this->settings->pdf_variables->quote_details ?? [] as $variable) { foreach($this->settings->pdf_variables->quote_details ?? [] as $variable) {
$entity_details .= "<div class='flex px-5 block'><p class= w-36 block'>{$variable}_label</p><p class='pl-5 w-36 block entity-field'>{$variable}</p></div>"; $entity_details .= "<div class='flex px-5 block'><p class= w-36 block'>{$variable}_label</p><p class='ml-5 w-36 block entity-field'>{$variable}</p></div>";
} }
} elseif($this->entity_type == 'credit') { } elseif($this->entity_type == 'credit') {
foreach($this->settings->pdf_variables->credit_details ?? [] as $variable) { foreach($this->settings->pdf_variables->credit_details ?? [] as $variable) {
$entity_details .= "<div class='flex px-5 block'><p class= w-36 block'>{$variable}_label</p><p class='pl-5 w-36 block entity-field'>{$variable}</p></div>"; $entity_details .= "<div class='flex px-5 block'><p class= w-36 block'>{$variable}_label</p><p class='ml-5 w-36 block entity-field'>{$variable}</p></div>";
} }
} elseif($this->entity_type == 'purchase_order') { } elseif($this->entity_type == 'purchase_order') {
foreach($this->settings->pdf_variables->purchase_order_details ?? [] as $variable) { foreach($this->settings->pdf_variables->purchase_order_details ?? [] as $variable) {
$entity_details .= "<div class='flex px-5 block'><p class= w-36 block'>{$variable}_label</p><p class='pl-5 w-36 block entity-field'>{$variable}</p></div>"; $entity_details .= "<div class='flex px-5 block'><p class= w-36 block'>{$variable}_label</p><p class='ml-5 w-36 block entity-field'>{$variable}</p></div>";
} }
} }
@ -305,6 +310,7 @@ class PdfSlot extends Component
return 'purchase_order'; return 'purchase_order';
} }
return ''; return '';
} }
} }

View File

@ -22,7 +22,10 @@ class BulkBankIntegrationRequest extends Request
*/ */
public function authorize() : bool public function authorize() : bool
{ {
return auth()->user()->isAdmin(); /** @var \App\Models\User $user */
$user = auth()->user();
return $user->isAdmin();
} }
public function rules() public function rules()

View File

@ -60,7 +60,7 @@ class UpdateCompanyRequest extends Request
// $rules['client_registration_fields'] = 'array'; // $rules['client_registration_fields'] = 'array';
if (isset($input['portal_mode']) && ($input['portal_mode'] == 'domain' || $input['portal_mode'] == 'iframe')) { if (isset($input['portal_mode']) && ($input['portal_mode'] == 'domain' || $input['portal_mode'] == 'iframe')) {
$rules['portal_domain'] = 'sometimes|url'; $rules['portal_domain'] = 'bail|nullable|sometimes|url';
} }
if (Ninja::isHosted()) { if (Ninja::isHosted()) {
@ -115,7 +115,7 @@ class UpdateCompanyRequest extends Request
} }
if (isset($settings['email_style_custom'])) { if (isset($settings['email_style_custom'])) {
$settings['email_style_custom'] = str_replace(['{{','}}'], ['',''], $settings['email_style_custom']); $settings['email_style_custom'] = str_replace(['{!!','!!}','{{','}}','@if(','@endif','@isset','@unless','@auth','@empty','@guest','@env','@section','@switch', '@foreach', '@while', '@include', '@each', '@once', '@push', '@use', '@forelse', '@verbatim', '<?php', '@php', '@for'], '', $settings['email_style_custom']);
} }
if (! $account->isFreeHostedClient()) { if (! $account->isFreeHostedClient()) {

View File

@ -60,7 +60,7 @@ class UpdateCreditRequest extends Request
$rules['file'] = $this->file_validation; $rules['file'] = $this->file_validation;
} }
$rules['number'] = ['bail', 'sometimes', Rule::unique('credits')->where('company_id', $user->company()->id)->ignore($this->credit->id)]; $rules['number'] = ['bail', 'sometimes', 'nullable', Rule::unique('credits')->where('company_id', $user->company()->id)->ignore($this->credit->id)];
$rules['client_id'] = ['bail', 'sometimes',Rule::in([$this->credit->client_id])]; $rules['client_id'] = ['bail', 'sometimes',Rule::in([$this->credit->client_id])];

View File

@ -11,17 +11,37 @@
namespace App\Http\Requests\Email; namespace App\Http\Requests\Email;
use App\Http\Requests\Request;
use App\Utils\Ninja; use App\Utils\Ninja;
use App\Utils\Traits\MakesHash;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use App\Http\Requests\Request;
use App\Utils\Traits\MakesHash;
use Illuminate\Validation\Rule;
use Illuminate\Auth\Access\AuthorizationException;
class SendEmailRequest extends Request class SendEmailRequest extends Request
{ {
use MakesHash; use MakesHash;
private string $entity_plural = '';
private string $error_message = ''; private string $error_message = '';
public array $templates = [
'email_template_invoice',
'email_template_quote',
'email_template_credit',
'email_template_payment',
'email_template_payment_partial',
'email_template_statement',
'email_template_reminder1',
'email_template_reminder2',
'email_template_reminder3',
'email_template_reminder_endless',
'email_template_custom1',
'email_template_custom2',
'email_template_custom3',
'email_template_purchase_order',
];
/** /**
* Determine if the user is authorized to make this request. * Determine if the user is authorized to make this request.
* *
@ -39,14 +59,16 @@ class SendEmailRequest extends Request
*/ */
public function rules() public function rules()
{ {
/** @var \App\Models\User $user */
$user = auth()->user();
return [ return [
'template' => 'bail|required', 'template' => 'bail|required|in:'.implode(',', $this->templates),
'entity' => 'bail|required', 'entity' => 'bail|required|in:App\Models\Invoice,App\Models\Quote,App\Models\Credit,App\Models\RecurringInvoice,App\Models\PurchaseOrder,App\Models\Payment',
'entity_id' => 'bail|required', 'entity_id' => ['bail', 'required', Rule::exists($this->entity_plural, 'id')->where('company_id', $user->company()->id)],
'cc_email.*' => 'bail|sometimes|email', 'cc_email.*' => 'bail|sometimes|email',
]; ];
} }
public function prepareForValidation() public function prepareForValidation()
@ -70,6 +92,8 @@ class SendEmailRequest extends Request
$input['entity_id'] = $this->decodePrimaryKey($input['entity_id']); $input['entity_id'] = $this->decodePrimaryKey($input['entity_id']);
} }
$this->entity_plural = Str::plural($input['entity']) ?? '';
if (isset($input['entity'])) { if (isset($input['entity'])) {
$input['entity'] = "App\Models\\".ucfirst(Str::camel($input['entity'])); $input['entity'] = "App\Models\\".ucfirst(Str::camel($input['entity']));
} }

View File

@ -37,7 +37,17 @@ class ImportRequest extends Request
'column_map' => 'required_with:hash|array', 'column_map' => 'required_with:hash|array',
'skip_header' => 'required_with:hash|boolean', 'skip_header' => 'required_with:hash|boolean',
'files.*' => 'file|mimes:csv,txt', 'files.*' => 'file|mimes:csv,txt',
'bank_integration_id' => 'bail|required_if:column_map,bank_transaction|min:2' 'bank_integration_id' => 'bail|required_with:column_map.bank_transaction|min:2'
]; ];
} }
public function prepareForValidation()
{
$input = $this->all();
if(!isset($input['column_map']['bank_transaction']) && array_key_exists('bank_integration_id',$input))
unset($input['bank_integration_id']);
$this->replace($input);
}
} }

View File

@ -59,9 +59,8 @@ class UpdateInvoiceRequest extends Request
$rules['id'] = new LockedInvoiceRule($this->invoice); $rules['id'] = new LockedInvoiceRule($this->invoice);
$rules['number'] = ['bail', 'sometimes', Rule::unique('invoices')->where('company_id', $user->company()->id)->ignore($this->invoice->id)]; $rules['number'] = ['bail', 'sometimes', 'nullable', Rule::unique('invoices')->where('company_id', $user->company()->id)->ignore($this->invoice->id)];
$rules['is_amount_discount'] = ['boolean']; $rules['is_amount_discount'] = ['boolean'];
$rules['client_id'] = ['bail', 'sometimes', Rule::in([$this->invoice->client_id])]; $rules['client_id'] = ['bail', 'sometimes', Rule::in([$this->invoice->client_id])];
$rules['line_items'] = 'array'; $rules['line_items'] = 'array';

View File

@ -0,0 +1,58 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\Nordigen;
use App\Http\Requests\Request;
use App\Libraries\MultiDB;
use App\Models\Company;
use Cache;
class ConfirmNordigenBankIntegrationRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'ref' => 'required|string', // nordigen redirects only with the ref-property
'lang' => 'string',
];
}
public function getTokenContent()
{
$input = $this->all();
$data = Cache::get($input['ref']);
return $data;
}
public function getCompany()
{
MultiDB::findAndSetDbByCompanyKey($this->getTokenContent()['company_key']);
return Company::where('company_key', $this->getTokenContent()['company_key'])->firstOrFail();
}
}

View File

@ -0,0 +1,75 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\Nordigen;
use App\Http\Requests\Request;
use App\Libraries\MultiDB;
use App\Models\Company;
use Cache;
class ConnectNordigenBankIntegrationRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'lang' => 'string',
'institution_id' => 'string',
'redirect' => 'string',
];
}
// @turbo124 @todo please check for validity, when request from frontend
public function prepareForValidation()
{
$input = $this->all();
if (!array_key_exists('redirect', $input)) {
$context = $this->getTokenContent();
$input["redirect"] = isset($context["is_react"]) && $context['is_react'] ? config('ninja.react_url') . "/#/settings/bank_accounts" : config('ninja.app_url');
$this->replace($input);
}
}
public function getTokenContent()
{
if ($this->state) {
$this->token = $this->state;
}
$data = Cache::get($this->token);
return $data;
}
public function getCompany()
{
MultiDB::findAndSetDbByCompanyKey($this->getTokenContent()['company_key']);
return Company::where('company_key', $this->getTokenContent()['company_key'])->firstOrFail();
}
}

View File

@ -23,7 +23,10 @@ class StoreProductRequest extends Request
*/ */
public function authorize() : bool public function authorize() : bool
{ {
return auth()->user()->can('create', Product::class); /** @var \App\Models\User $user */
$user = auth()->user();
return $user->can('create', Product::class);
} }
public function rules() public function rules()
@ -54,7 +57,7 @@ class StoreProductRequest extends Request
{ {
$input = $this->all(); $input = $this->all();
if (! isset($input['quantity']) || $input['quantity'] < 1) { if (! isset($input['quantity'])) {
$input['quantity'] = 1; $input['quantity'] = 1;
} }

View File

@ -56,7 +56,7 @@ class UpdateProductRequest extends Request
{ {
$input = $this->all(); $input = $this->all();
if (! isset($input['quantity']) || $input['quantity'] < 1) { if (! isset($input['quantity'])) {
$input['quantity'] = 1; $input['quantity'] = 1;
} }

View File

@ -48,7 +48,7 @@ class UpdatePurchaseOrderRequest extends Request
$rules = []; $rules = [];
$rules['number'] = ['bail', 'sometimes', Rule::unique('purchase_orders')->where('company_id', $user->company()->id)->ignore($this->purchase_order->id)]; $rules['number'] = ['bail', 'sometimes', 'nullable', Rule::unique('purchase_orders')->where('company_id', $user->company()->id)->ignore($this->purchase_order->id)];
$rules['vendor_id'] = ['bail', 'sometimes', Rule::in([$this->purchase_order->vendor_id])]; $rules['vendor_id'] = ['bail', 'sometimes', Rule::in([$this->purchase_order->vendor_id])];
$rules['line_items'] = 'array'; $rules['line_items'] = 'array';

View File

@ -55,7 +55,7 @@ class UpdateQuoteRequest extends Request
} }
$rules['number'] = ['bail', 'sometimes', Rule::unique('quotes')->where('company_id', $user->company()->id)->ignore($this->quote->id)]; $rules['number'] = ['bail', 'sometimes', 'nullable', Rule::unique('quotes')->where('company_id', $user->company()->id)->ignore($this->quote->id)];
$rules['client_id'] = ['bail', 'sometimes', Rule::in([$this->quote->client_id])]; $rules['client_id'] = ['bail', 'sometimes', Rule::in([$this->quote->client_id])];
@ -73,6 +73,8 @@ class UpdateQuoteRequest extends Request
$input = $this->decodePrimaryKeys($input); $input = $this->decodePrimaryKeys($input);
$input['id'] = $this->quote->id;
if (isset($input['line_items'])) { if (isset($input['line_items'])) {
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
} }
@ -85,7 +87,6 @@ class UpdateQuoteRequest extends Request
$input['exchange_rate'] = 1; $input['exchange_rate'] = 1;
} }
$input['id'] = $this->quote->id;
$this->replace($input); $this->replace($input);
} }

View File

@ -42,14 +42,14 @@ class StoreSchedulerRequest extends Request
'template' => 'bail|required|string', 'template' => 'bail|required|string',
'parameters' => 'bail|array', 'parameters' => 'bail|array',
'parameters.clients' => ['bail','sometimes', 'array', new ValidClientIds()], 'parameters.clients' => ['bail','sometimes', 'array', new ValidClientIds()],
'parameters.date_range' => 'bail|sometimes|string|in:last7_days,last30_days,last365_days,this_month,last_month,this_quarter,last_quarter,this_year,last_year,all_time,custom', 'parameters.date_range' => 'bail|sometimes|string|in:last7_days,last30_days,last365_days,this_month,last_month,this_quarter,last_quarter,this_year,last_year,all_time,custom,all',
'parameters.start_date' => ['bail', 'sometimes', 'date:Y-m-d', 'required_if:parameters.date_rate,custom'], 'parameters.start_date' => ['bail', 'sometimes', 'date:Y-m-d', 'required_if:parameters.date_rate,custom'],
'parameters.end_date' => ['bail', 'sometimes', 'date:Y-m-d', 'required_if:parameters.date_rate,custom', 'after_or_equal:parameters.start_date'], 'parameters.end_date' => ['bail', 'sometimes', 'date:Y-m-d', 'required_if:parameters.date_rate,custom', 'after_or_equal:parameters.start_date'],
'parameters.entity' => ['bail', 'sometimes', 'string', 'in:invoice,credit,quote,purchase_order'], 'parameters.entity' => ['bail', 'sometimes', 'string', 'in:invoice,credit,quote,purchase_order'],
'parameters.entity_id' => ['bail', 'sometimes', 'string'], 'parameters.entity_id' => ['bail', 'sometimes', 'string'],
'parameters.report_name' => ['bail','sometimes', 'string', 'required_if:template,email_report','in:ar_detailed,ar_summary,client_balance,tax_summary,profitloss,client_sales,user_sales,product_sales,clients,client_contacts,credits,documents,expenses,invoices,invoice_items,quotes,quote_items,recurring_invoices,payments,products,tasks'], 'parameters.report_name' => ['bail','sometimes', 'string', 'required_if:template,email_report','in:ar_detailed,ar_summary,client_balance,tax_summary,profitloss,client_sales,user_sales,product_sales,activity,client,contact,client_contact,credit,document,expense,invoice,invoice_item,quote,quote_item,recurring_invoice,payment,product,task'],
'parameters.date_key' => ['bail','sometimes', 'string'], 'parameters.date_key' => ['bail','sometimes', 'string'],
'parameters.status' => ['bail','sometimes', 'string', 'in:all,draft,paid,unpaid,overdue'], 'parameters.status' => ['bail','sometimes', 'string'],
]; ];
return $rules; return $rules;
@ -67,6 +67,18 @@ class StoreSchedulerRequest extends Request
$input['frequency_id'] = 0; $input['frequency_id'] = 0;
} }
if(isset($input['parameters']) && !isset($input['parameters']['clients'])) {
$input['parameters']['clients'] = [];
}
if(isset($input['parameters']['status'])) {
$input['parameters']['status'] = collect(explode(",", $input['parameters']['status']))
->filter(function ($status) {
return in_array($status, ['all','draft','paid','unpaid','overdue']);
})->implode(",") ?? '';
}
$this->replace($input); $this->replace($input);
} }
} }

View File

@ -39,13 +39,14 @@ class UpdateSchedulerRequest extends Request
'template' => 'bail|required|string', 'template' => 'bail|required|string',
'parameters' => 'bail|array', 'parameters' => 'bail|array',
'parameters.clients' => ['bail','sometimes', 'array', new ValidClientIds()], 'parameters.clients' => ['bail','sometimes', 'array', new ValidClientIds()],
'parameters.date_range' => 'bail|sometimes|string|in:last7_days,last30_days,last365_days,this_month,last_month,this_quarter,last_quarter,this_year,last_year,all_time,custom', 'parameters.date_range' => 'bail|sometimes|string|in:last7_days,last30_days,last365_days,this_month,last_month,this_quarter,last_quarter,this_year,last_year,all_time,custom,all',
'parameters.start_date' => ['bail', 'sometimes', 'date:Y-m-d', 'required_if:parameters.date_rate,custom'], 'parameters.start_date' => ['bail', 'sometimes', 'date:Y-m-d', 'required_if:parameters.date_rate,custom'],
'parameters.end_date' => ['bail', 'sometimes', 'date:Y-m-d', 'required_if:parameters.date_rate,custom', 'after_or_equal:parameters.start_date'], 'parameters.end_date' => ['bail', 'sometimes', 'date:Y-m-d', 'required_if:parameters.date_rate,custom', 'after_or_equal:parameters.start_date'],
'parameters.entity' => ['bail', 'sometimes', 'string', 'in:invoice,credit,quote,purchase_order'], 'parameters.entity' => ['bail', 'sometimes', 'string', 'in:invoice,credit,quote,purchase_order'],
'parameters.entity_id' => ['bail', 'sometimes', 'string'], 'parameters.entity_id' => ['bail', 'sometimes', 'string'],
'parameters.report_name' => ['bail','sometimes', 'string', 'required_if:template,email_report', 'in:ar_detailed,ar_summary,client_balance,tax_summary,profitloss,client_sales,user_sales,product_sales,clients,client_contacts,credits,documents,expenses,invoices,invoice_items,quotes,quote_items,recurring_invoices,payments,products,tasks'], 'parameters.report_name' => ['bail','sometimes', 'string', 'required_if:template,email_report','in:ar_detailed,ar_summary,client_balance,tax_summary,profitloss,client_sales,user_sales,product_sales,activity,client,contact,client_contact,credit,document,expense,invoice,invoice_item,quote,quote_item,recurring_invoice,payment,product,task'],
'parameters.date_key' => ['bail','sometimes', 'string'], 'parameters.date_key' => ['bail','sometimes', 'string'],
'parameters.status' => ['bail','sometimes', 'string'],
]; ];
return $rules; return $rules;
@ -63,8 +64,21 @@ class UpdateSchedulerRequest extends Request
$input['frequency_id'] = 0; $input['frequency_id'] = 0;
} }
if(isset($input['parameters']) && !isset($input['parameters']['clients'])) {
$input['parameters']['clients'] = [];
}
if(isset($input['parameters']['status'])) {
$input['parameters']['status'] = collect(explode(",", $input['parameters']['status']))
->filter(function ($status) {
return in_array($status, ['all','draft','paid','unpaid','overdue']);
})->implode(",") ?? '';
}
$this->replace($input); $this->replace($input);
} }
} }

View File

@ -3387,9 +3387,6 @@ class BlackListRule implements Rule
'xrap.de', 'xrap.de',
'xrho.com', 'xrho.com',
'xvx.us', 'xvx.us',
'xwaretech.com',
'xwaretech.info',
'xwaretech.net',
'xww.ro', 'xww.ro',
'xxhamsterxx.ga', 'xxhamsterxx.ga',
'xxi2.com', 'xxi2.com',

View File

@ -58,7 +58,7 @@ class MatchBankTransactions implements ShouldQueue
private $categories; private $categories;
private float $available_balance = 0; private float $available_balance = 0;
private float $applied_amount = 0; private float $applied_amount = 0;
private array $attachable_invoices = []; private array $attachable_invoices = [];
@ -94,7 +94,6 @@ class MatchBankTransactions implements ShouldQueue
} }
$bank_categories = Cache::get('bank_categories'); $bank_categories = Cache::get('bank_categories');
if (!$bank_categories && $yodlee) { if (!$bank_categories && $yodlee) {
$_categories = $yodlee->getTransactionCategories(); $_categories = $yodlee->getTransactionCategories();
$this->categories = collect($_categories->transactionCategory); $this->categories = collect($_categories->transactionCategory);
@ -135,11 +134,11 @@ class MatchBankTransactions implements ShouldQueue
return $collection->toArray(); return $collection->toArray();
} }
private function checkPayable($invoices) :bool private function checkPayable($invoices): bool
{ {
foreach ($invoices as $invoice) { foreach ($invoices as $invoice) {
$invoice->service()->markSent(); $invoice->service()->markSent();
if (!$invoice->isPayable()) { if (!$invoice->isPayable()) {
return false; return false;
} }
@ -158,12 +157,12 @@ class MatchBankTransactions implements ShouldQueue
$_expenses = explode(",", $input['expense_id']); $_expenses = explode(",", $input['expense_id']);
foreach($_expenses as $_expense) { foreach ($_expenses as $_expense) {
$expense = Expense::withTrashed() $expense = Expense::withTrashed()
->where('id', $this->decodePrimaryKey($_expense)) ->where('id', $this->decodePrimaryKey($_expense))
->where('company_id', $this->bt->company_id) ->where('company_id', $this->bt->company_id)
->first(); ->first();
if ($expense && !$expense->transaction_id) { if ($expense && !$expense->transaction_id) {
$expense->transaction_id = $this->bt->id; $expense->transaction_id = $this->bt->id;
@ -178,7 +177,7 @@ class MatchBankTransactions implements ShouldQueue
$this->bts->push($this->bt->id); $this->bts->push($this->bt->id);
} }
} }
return $this; return $this;
} }
@ -202,7 +201,7 @@ class MatchBankTransactions implements ShouldQueue
} }
$payment = Payment::withTrashed()->find($input['payment_id']); $payment = Payment::withTrashed()->find($input['payment_id']);
if ($payment && !$payment->transaction_id) { if ($payment && !$payment->transaction_id) {
$payment->transaction_id = $this->bt->id; $payment->transaction_id = $this->bt->id;
$payment->saveQuietly(); $payment->saveQuietly();
@ -218,7 +217,7 @@ class MatchBankTransactions implements ShouldQueue
return $this; return $this;
} }
private function matchInvoicePayment($input) :self private function matchInvoicePayment($input): self
{ {
$this->bt = BankTransaction::withTrashed()->find($input['id']); $this->bt = BankTransaction::withTrashed()->find($input['id']);
@ -227,10 +226,10 @@ class MatchBankTransactions implements ShouldQueue
} }
$_invoices = Invoice::query() $_invoices = Invoice::query()
->withTrashed() ->withTrashed()
->where('company_id', $this->bt->company_id) ->where('company_id', $this->bt->company_id)
->whereIn('id', $this->getInvoices($input['invoice_ids'])); ->whereIn('id', $this->getInvoices($input['invoice_ids']));
$amount = $this->bt->amount; $amount = $this->bt->amount;
if ($_invoices && $this->checkPayable($_invoices)) { if ($_invoices && $this->checkPayable($_invoices)) {
@ -242,7 +241,7 @@ class MatchBankTransactions implements ShouldQueue
return $this; return $this;
} }
private function matchExpense($input) :self private function matchExpense($input): self
{ {
//if there is a category id, pull it from Yodlee and insert - or just reuse!! //if there is a category id, pull it from Yodlee and insert - or just reuse!!
$this->bt = BankTransaction::query()->withTrashed()->find($input['id']); $this->bt = BankTransaction::query()->withTrashed()->find($input['id']);
@ -274,7 +273,7 @@ class MatchBankTransactions implements ShouldQueue
if (array_key_exists('vendor_id', $input)) { if (array_key_exists('vendor_id', $input)) {
$this->bt->vendor_id = $input['vendor_id']; $this->bt->vendor_id = $input['vendor_id'];
} }
$this->bt->status_id = BankTransaction::STATUS_CONVERTED; $this->bt->status_id = BankTransaction::STATUS_CONVERTED;
$this->bt->save(); $this->bt->save();
@ -283,7 +282,7 @@ class MatchBankTransactions implements ShouldQueue
return $this; return $this;
} }
private function createPayment($invoices, float $amount) :void private function createPayment($invoices, float $amount): void
{ {
$this->available_balance = $amount; $this->available_balance = $amount;
@ -320,7 +319,7 @@ class MatchBankTransactions implements ShouldQueue
if (!$this->invoice) { if (!$this->invoice) {
return; return;
} }
/* Create Payment */ /* Create Payment */
$payment = PaymentFactory::create($this->invoice->company_id, $this->invoice->user_id); $payment = PaymentFactory::create($this->invoice->company_id, $this->invoice->user_id);
@ -333,7 +332,7 @@ class MatchBankTransactions implements ShouldQueue
$payment->currency_id = $this->bt->currency_id; $payment->currency_id = $this->bt->currency_id;
$payment->is_manual = false; $payment->is_manual = false;
$payment->date = $this->bt->date ? Carbon::parse($this->bt->date) : now(); $payment->date = $this->bt->date ? Carbon::parse($this->bt->date) : now();
/* Bank Transfer! */ /* Bank Transfer! */
$payment_type_id = 1; $payment_type_id = 1;
@ -341,7 +340,7 @@ class MatchBankTransactions implements ShouldQueue
$payment->saveQuietly(); $payment->saveQuietly();
$payment->service()->applyNumber()->save(); $payment->service()->applyNumber()->save();
if ($payment->client->getSetting('send_email_on_mark_paid')) { if ($payment->client->getSetting('send_email_on_mark_paid')) {
$payment->service()->sendEmail(); $payment->service()->sendEmail();
} }
@ -360,24 +359,24 @@ class MatchBankTransactions implements ShouldQueue
$this->invoice->next_send_date = null; $this->invoice->next_send_date = null;
$this->invoice $this->invoice
->service() ->service()
->applyNumber() ->applyNumber()
->deletePdf() ->deletePdf()
->save(); ->save();
$payment->ledger() $payment->ledger()
->updatePaymentBalance($amount * -1); ->updatePaymentBalance($amount * -1);
$this->invoice $this->invoice
->client ->client
->service() ->service()
->updateBalanceAndPaidToDate($this->applied_amount*-1, $amount) ->updateBalanceAndPaidToDate($this->applied_amount * -1, $amount)
->save(); ->save();
$this->invoice = $this->invoice $this->invoice = $this->invoice
->service() ->service()
->workFlow() ->workFlow()
->save(); ->save();
/* Update Invoice balance */ /* Update Invoice balance */
event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
@ -389,13 +388,13 @@ class MatchBankTransactions implements ShouldQueue
$this->bt->save(); $this->bt->save();
} }
private function resolveCategory($input) :?int private function resolveCategory($input): ?int
{ {
if (array_key_exists('ninja_category_id', $input) && (int)$input['ninja_category_id'] > 1) { if (array_key_exists('ninja_category_id', $input) && (int) $input['ninja_category_id'] > 1) {
$this->bt->ninja_category_id = $input['ninja_category_id']; $this->bt->ninja_category_id = $input['ninja_category_id'];
$this->bt->save(); $this->bt->save();
return (int)$input['ninja_category_id']; return (int) $input['ninja_category_id'];
} }
$category = $this->categories->firstWhere('highLevelCategoryId', $this->bt->category_id); $category = $this->categories->firstWhere('highLevelCategoryId', $this->bt->category_id);
@ -414,7 +413,7 @@ class MatchBankTransactions implements ShouldQueue
return $ec->id; return $ec->id;
} }
return null; return null;
} }

View File

@ -0,0 +1,177 @@
<?php
/**
* Credit Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Credit Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Jobs\Bank;
use App\Helpers\Bank\Nordigen\Nordigen;
use App\Libraries\MultiDB;
use App\Models\BankIntegration;
use App\Models\BankTransaction;
use App\Models\Company;
use App\Notifications\Ninja\GenericNinjaAdminNotification;
use App\Services\Bank\BankMatchingService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessBankTransactionsNordigen implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private BankIntegration $bank_integration;
private ?string $from_date;
public Company $company;
public Nordigen $nordigen;
public $nordigen_account;
/**
* Create a new job instance.
*/
public function __construct(BankIntegration $bank_integration)
{
$this->bank_integration = $bank_integration;
$this->from_date = $bank_integration->from_date ?: now()->subDays(90);
$this->company = $this->bank_integration->company;
}
/**
* Execute the job.
*
*
* @return void
*/
public function handle()
{
if ($this->bank_integration->integration_type != BankIntegration::INTEGRATION_TYPE_NORDIGEN)
throw new \Exception("Invalid BankIntegration Type");
if (!(config('ninja.nordigen.secret_id') && config('ninja.nordigen.secret_key')))
throw new \Exception("Missing credentials for bank_integration service nordigen");
$this->nordigen = new Nordigen();
set_time_limit(0);
nlog("Nordigen: Processing transactions for account: {$this->bank_integration->account->key}");
// UPDATE ACCOUNT
try {
$this->updateAccount();
} catch (\Exception $e) {
nlog("Nordigen: {$this->bank_integration->nordigen_account_id} - exited abnormally => " . $e->getMessage());
$content = [
"Processing transactions for account: {$this->bank_integration->nordigen_account_id} failed",
"Exception Details => ",
$e->getMessage(),
];
$this->bank_integration->company->notification(new GenericNinjaAdminNotification($content))->ninja();
throw $e;
}
if (!$this->nordigen_account)
return;
// UPDATE TRANSACTIONS
try {
$this->processTransactions();
} catch (\Exception $e) {
nlog("Nordigen: {$this->bank_integration->nordigen_account_id} - exited abnormally => " . $e->getMessage());
$content = [
"Processing transactions for account: {$this->bank_integration->nordigen_account_id} failed",
"Exception Details => ",
$e->getMessage(),
];
$this->bank_integration->company->notification(new GenericNinjaAdminNotification($content))->ninja();
throw $e;
}
// Perform Matching
BankMatchingService::dispatch($this->company->id, $this->company->db);
}
private function updateAccount()
{
if (!$this->nordigen->isAccountActive($this->bank_integration->nordigen_account_id)) {
$this->bank_integration->disabled_upstream = true;
$this->bank_integration->save();
$this->stop_loop = false;
nlog("Nordigen: account inactive: " . $this->bank_integration->nordigen_account_id);
// @turbo124 @todo send email for expired account
return;
}
$this->nordigen_account = $this->nordigen->getAccount($this->bank_integration->nordigen_account_id);
$this->bank_integration->disabled_upstream = false;
$this->bank_integration->bank_account_status = $this->nordigen_account['account_status'];
$this->bank_integration->balance = $this->nordigen_account['current_balance'];
$this->bank_integration->save();
}
private function processTransactions()
{
//Get transaction count object
$transactions = $this->nordigen->getTransactions($this->bank_integration->nordigen_account_id, $this->from_date);
//if no transactions, update the from_date and move on
if (count($transactions) == 0) {
$this->bank_integration->from_date = now()->subDays(5);
$this->bank_integration->disabled_upstream = false;
$this->bank_integration->save();
return;
}
//Harvest the company
MultiDB::setDb($this->company->db);
/*Get the user */
$user_id = $this->company->owner()->id;
/* Unguard the model to perform batch inserts */
BankTransaction::unguard();
$now = now();
foreach ($transactions as $transaction) {
if (BankTransaction::where('transaction_id', $transaction['transaction_id'])->where('company_id', $this->company->id)->where('bank_integration_id', $this->bank_integration->id)->withTrashed()->exists())
continue;
//this should be much faster to insert than using ::create()
\DB::table('bank_transactions')->insert(
array_merge($transaction, [
'company_id' => $this->company->id,
'user_id' => $user_id,
'bank_integration_id' => $this->bank_integration->id,
'created_at' => $now,
'updated_at' => $now,
])
);
}
$this->bank_integration->from_date = now()->subDays(5);
$this->bank_integration->save();
}
}

View File

@ -14,6 +14,7 @@ namespace App\Jobs\Bank;
use App\Helpers\Bank\Yodlee\Transformer\AccountTransformer; use App\Helpers\Bank\Yodlee\Transformer\AccountTransformer;
use App\Helpers\Bank\Yodlee\Yodlee; use App\Helpers\Bank\Yodlee\Yodlee;
use App\Libraries\MultiDB; use App\Libraries\MultiDB;
use App\Models\Account;
use App\Models\BankIntegration; use App\Models\BankIntegration;
use App\Models\BankTransaction; use App\Models\BankTransaction;
use App\Models\Company; use App\Models\Company;
@ -26,7 +27,7 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
class ProcessBankTransactions implements ShouldQueue class ProcessBankTransactionsYodlee implements ShouldQueue
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
@ -61,21 +62,24 @@ class ProcessBankTransactions implements ShouldQueue
*/ */
public function handle() public function handle()
{ {
if ($this->bank_integration->integration_type != BankIntegration::INTEGRATION_TYPE_YODLEE)
throw new \Exception("Invalid BankIntegration Type");
set_time_limit(0); set_time_limit(0);
//Loop through everything until we are up to date //Loop through everything until we are up to date
$this->from_date = $this->from_date ?: '2021-01-01'; $this->from_date = $this->from_date ?: '2021-01-01';
nlog("Processing transactions for account: {$this->bank_integration->account->key}"); nlog("Yodlee: Processing transactions for account: {$this->bank_integration->account->key}");
do { do {
try { try {
$this->processTransactions(); $this->processTransactions();
} catch(\Exception $e) { } catch (\Exception $e) {
nlog("{$this->bank_integration_account_id} - exited abnormally => ". $e->getMessage()); nlog("Yodlee: {$this->bank_integration->bank_account_id} - exited abnormally => " . $e->getMessage());
$content = [ $content = [
"Processing transactions for account: {$this->bank_integration->account->key} failed", "Processing transactions for account: {$this->bank_integration->bank_account_id} failed",
"Exception Details => ", "Exception Details => ",
$e->getMessage(), $e->getMessage(),
]; ];
@ -103,21 +107,21 @@ class ProcessBankTransactions implements ShouldQueue
try { try {
$account_summary = $yodlee->getAccountSummary($this->bank_integration->bank_account_id); $account_summary = $yodlee->getAccountSummary($this->bank_integration->bank_account_id);
if($account_summary) { if ($account_summary) {
$at = new AccountTransformer(); $at = new AccountTransformer();
$account = $at->transform($account_summary); $account = $at->transform($account_summary);
if($account[0]['current_balance']) { if ($account[0]['current_balance']) {
$this->bank_integration->balance = $account[0]['current_balance']; $this->bank_integration->balance = $account[0]['current_balance'];
$this->bank_integration->currency = $account[0]['account_currency']; $this->bank_integration->currency = $account[0]['account_currency'];
$this->bank_integration->bank_account_status = $account[0]['account_status']; $this->bank_integration->bank_account_status = $account[0]['account_status'];
$this->bank_integration->save(); $this->bank_integration->save();
} }
} }
} catch(\Exception $e) { } catch (\Exception $e) {
nlog("YODLEE: unable to update account summary for {$this->bank_integration->bank_account_id} => ". $e->getMessage()); nlog("YODLEE: unable to update account summary for {$this->bank_integration->bank_account_id} => " . $e->getMessage());
} }
$data = [ $data = [
@ -151,14 +155,14 @@ class ProcessBankTransactions implements ShouldQueue
/*Get the user */ /*Get the user */
$user_id = $this->company->owner()->id; $user_id = $this->company->owner()->id;
/* Unguard the model to perform batch inserts */ /* Unguard the model to perform batch inserts */
BankTransaction::unguard(); BankTransaction::unguard();
$now = now(); $now = now();
foreach ($transactions as $transaction) { foreach ($transactions as $transaction) {
if (BankTransaction::query()->where('transaction_id', $transaction['transaction_id'])->where('company_id', $this->company->id)->withTrashed()->exists()) { if (BankTransaction::query()->where('transaction_id', $transaction['transaction_id'])->where('company_id', $this->company->id)->where('bank_integration_id', $this->bank_integration->id)->withTrashed()->exists()) { // @turbo124 was not scoped to bank_integration_id => from my pov this should be present, because when an account was historized (is_deleted) a transaction can occur multiple (in the archived bank_integration and in the new one
continue; continue;
} }
@ -189,7 +193,7 @@ class ProcessBankTransactions implements ShouldQueue
{ {
return [new WithoutOverlapping($this->bank_integration_account_id)]; return [new WithoutOverlapping($this->bank_integration_account_id)];
} }
public function backoff() public function backoff()
{ {
return [rand(10, 15), rand(30, 40), rand(60, 79), rand(160, 200), rand(3000, 5000)]; return [rand(10, 15), rand(30, 40), rand(60, 79), rand(160, 200), rand(3000, 5000)];

View File

@ -224,7 +224,7 @@ class CompanyExport implements ShouldQueue
$this->export_data['invoices'] = $this->company->invoices()->orderBy('number', 'DESC')->cursor()->map(function ($invoice) { $this->export_data['invoices'] = $this->company->invoices()->orderBy('number', 'DESC')->cursor()->map(function ($invoice) {
$invoice = $this->transformBasicEntities($invoice); $invoice = $this->transformBasicEntities($invoice);
$invoice = $this->transformArrayOfKeys($invoice, ['recurring_id','client_id', 'vendor_id', 'project_id', 'design_id', 'subscription_id','project_id']); $invoice = $this->transformArrayOfKeys($invoice, ['recurring_id','client_id', 'vendor_id', 'project_id', 'design_id', 'subscription_id']);
$invoice->tax_data = ''; $invoice->tax_data = '';
return $invoice->makeVisible(['id', return $invoice->makeVisible(['id',
@ -331,7 +331,8 @@ class CompanyExport implements ShouldQueue
$task = $this->transformBasicEntities($task); $task = $this->transformBasicEntities($task);
$task = $this->transformArrayOfKeys($task, ['client_id', 'invoice_id', 'project_id', 'status_id']); $task = $this->transformArrayOfKeys($task, ['client_id', 'invoice_id', 'project_id', 'status_id']);
return $task->makeVisible(['id']); return $task->makeHidden(['hash','meta'])->makeVisible(['id']);
// return $task->makeHidden(['hash','meta'])->makeVisible(['id']); //@release v5.7.63
})->all(); })->all();
$this->export_data['task_statuses'] = $this->company->task_statuses->map(function ($status) { $this->export_data['task_statuses'] = $this->company->task_statuses->map(function ($status) {
@ -387,19 +388,19 @@ class CompanyExport implements ShouldQueue
})->all(); })->all();
$this->export_data['bank_integrations'] = $this->company->bank_integrations()->orderBy('id', 'ASC')->cursor()->map(function ($bank_integration) { $this->export_data['bank_integrations'] = $this->company->bank_integrations()->withTrashed()->orderBy('id', 'ASC')->cursor()->map(function ($bank_integration) {
$bank_integration = $this->transformArrayOfKeys($bank_integration, ['account_id','company_id', 'user_id']); $bank_integration = $this->transformArrayOfKeys($bank_integration, ['account_id','company_id', 'user_id']);
return $bank_integration->makeVisible(['id','user_id','company_id','account_id']); return $bank_integration->makeVisible(['id','user_id','company_id','account_id','hashed_id']);
})->all(); })->all();
$this->export_data['bank_transactions'] = $this->company->bank_transactions()->orderBy('id', 'ASC')->cursor()->map(function ($bank_transaction) { $this->export_data['bank_transactions'] = $this->company->bank_transactions()->withTrashed()->orderBy('id', 'ASC')->cursor()->map(function ($bank_transaction) {
$bank_transaction = $this->transformArrayOfKeys($bank_transaction, ['company_id', 'user_id','bank_integration_id','expense_id','category_id','ninja_category_id','vendor_id']); $bank_transaction = $this->transformArrayOfKeys($bank_transaction, ['company_id', 'user_id','bank_integration_id','expense_id','ninja_category_id','vendor_id']);
return $bank_transaction->makeVisible(['id','user_id','company_id']); return $bank_transaction->makeVisible(['id','user_id','company_id']);
})->all(); })->all();
$this->export_data['schedulers'] = $this->company->schedulers()->orderBy('id', 'ASC')->cursor()->map(function ($scheduler) { $this->export_data['schedulers'] = $this->company->schedulers()->withTrashed()->orderBy('id', 'ASC')->cursor()->map(function ($scheduler) {
$scheduler = $this->transformArrayOfKeys($scheduler, ['company_id', 'user_id']); $scheduler = $this->transformArrayOfKeys($scheduler, ['company_id', 'user_id']);
return $scheduler->makeVisible(['id','user_id','company_id']); return $scheduler->makeVisible(['id','user_id','company_id']);

View File

@ -63,14 +63,12 @@ use App\Utils\Ninja;
use App\Utils\TempFile; use App\Utils\TempFile;
use App\Utils\Traits\GeneratesCounter; use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use function GuzzleHttp\json_encode;
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;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use JsonMachine\JsonDecoder\ExtJsonDecoder; use JsonMachine\JsonDecoder\ExtJsonDecoder;
@ -142,7 +140,6 @@ class CompanyImport implements ShouldQueue
'recurring_expenses', 'recurring_expenses',
'expenses', 'expenses',
'tasks', 'tasks',
'payments',
'company_ledger', 'company_ledger',
'designs', 'designs',
'documents', 'documents',
@ -569,7 +566,7 @@ class CompanyImport implements ShouldQueue
['expenses' => 'expense_id'], ['expenses' => 'expense_id'],
['vendors' => 'vendor_id'], ['vendors' => 'vendor_id'],
['expense_categories' => 'ninja_category_id'], ['expense_categories' => 'ninja_category_id'],
['expense_categories' => 'category_id'], // ['expense_categories' => 'category_id'],
['bank_integrations' => 'bank_integration_id'] ['bank_integrations' => 'bank_integration_id']
], ],
'bank_transactions', 'bank_transactions',
@ -1143,7 +1140,34 @@ class CompanyImport implements ShouldQueue
continue; continue;
} }
$storage_url = (object)$this->getObject('storage_url', true);
if (!Storage::exists($document->url) && is_string($storage_url)) {
$url = $storage_url . $document->url;
$file = @file_get_contents($url);
if ($file) {
try {
Storage::disk(config('filesystems.default'))->put($document->url, $file);
} catch(\Exception $e) {
nlog($e->getMessage());
nlog("I could not upload {$document->url}");
}
}
else
continue;
}
else
continue;
$new_document = new Document(); $new_document = new Document();
$new_document->disk = config('filesystems.default');
$new_document->user_id = $this->transformId('users', $document->user_id); $new_document->user_id = $this->transformId('users', $document->user_id);
$new_document->assigned_user_id = $this->transformId('users', $document->assigned_user_id); $new_document->assigned_user_id = $this->transformId('users', $document->assigned_user_id);
$new_document->company_id = $this->company->id; $new_document->company_id = $this->company->id;
@ -1169,26 +1193,6 @@ class CompanyImport implements ShouldQueue
$new_document->save(['timestamps' => false]); $new_document->save(['timestamps' => false]);
$storage_url = (object)$this->getObject('storage_url', true);
if (!Storage::exists($new_document->url) && is_string($storage_url)) {
$url = $storage_url . $new_document->url;
$file = @file_get_contents($url);
if ($file) {
try {
Storage::disk(config('filesystems.default'))->put($new_document->url, $file);
$new_document->disk = config('filesystems.default');
$new_document->save();
} catch(\Exception $e) {
nlog($e->getMessage());
nlog("I could not upload {$new_document->url}");
$new_document->forceDelete();
}
}
}
} }
return $this; return $this;
@ -1727,7 +1731,9 @@ class CompanyImport implements ShouldQueue
*/ */
private function transformId(string $resource, ?string $old): ?int private function transformId(string $resource, ?string $old): ?int
{ {
if (empty($old)) {
// WjnegYbwZ1 == 0 return null;
if (empty($old) || $old == 'WjnegYbwZ1') {
return null; return null;
} }
@ -1736,6 +1742,7 @@ class CompanyImport implements ShouldQueue
} }
if (! array_key_exists($resource, $this->ids)) { if (! array_key_exists($resource, $this->ids)) {
$this->sendImportMail("The Import failed due to missing data in the import file. Resource {$resource} not available."); $this->sendImportMail("The Import failed due to missing data in the import file. Resource {$resource} not available.");
throw new \Exception("Resource {$resource} not available."); throw new \Exception("Resource {$resource} not available.");
@ -1744,16 +1751,12 @@ class CompanyImport implements ShouldQueue
if (! array_key_exists("{$old}", $this->ids[$resource])) { if (! array_key_exists("{$old}", $this->ids[$resource])) {
// nlog($this->ids[$resource]); // nlog($this->ids[$resource]);
nlog("searching for {$old} in {$resource}"); nlog("searching for {$old} in {$resource}");
nlog("If we are missing a user - default to the company owner");
if ($resource == 'users') { if ($resource == 'users') {
return $this->company_owner->id; return $this->company_owner->id;
} }
$this->sendImportMail("The Import failed due to missing data in the import file. Resource {$resource} not available."); $this->sendImportMail("The Import failed due to missing data in the import file. Key {$old} not found in {$resource}.");
nlog($this->ids[$resource]);
throw new \Exception("Missing {$resource} key: {$old}"); throw new \Exception("Missing {$resource} key: {$old}");
} }

View File

@ -65,7 +65,7 @@ class CreateCompany
$company->settings = $settings; $company->settings = $settings;
$company->db = config('database.default'); $company->db = config('database.default');
$company->enabled_modules = config('ninja.enabled_modules'); $company->enabled_modules = config('ninja.enabled_modules');
$company->subdomain = isset($this->request['subdomain']) ? $this->request['subdomain'] : ''; $company->subdomain = isset($this->request['subdomain']) ? $this->request['subdomain'] : MultiDB::randomSubdomainGenerator();
$company->custom_fields = new \stdClass; $company->custom_fields = new \stdClass;
$company->default_password_timeout = 1800000; $company->default_password_timeout = 1800000;
$company->client_registration_fields = ClientRegistrationFields::generate(); $company->client_registration_fields = ClientRegistrationFields::generate();

View File

@ -11,14 +11,15 @@
namespace App\Jobs\Cron; namespace App\Jobs\Cron;
use App\Jobs\Entity\EmailEntity;
use App\Libraries\MultiDB;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Webhook;
use App\Libraries\MultiDB;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use App\Jobs\Entity\EmailEntity;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class AutoBill implements ShouldQueue class AutoBill implements ShouldQueue
{ {
@ -77,6 +78,8 @@ class AutoBill implements ShouldQueue
} }
}); });
$invoice->sendEvent(Webhook::EVENT_SENT_INVOICE, "client");
} }
} }

View File

@ -11,13 +11,14 @@
namespace App\Jobs\Invoice; namespace App\Jobs\Invoice;
use App\Jobs\Entity\EmailEntity;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Webhook;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use App\Jobs\Entity\EmailEntity;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class BulkInvoiceJob implements ShouldQueue class BulkInvoiceJob implements ShouldQueue
{ {
@ -50,6 +51,8 @@ class BulkInvoiceJob implements ShouldQueue
if ($this->invoice->invitations->count() >= 1) { if ($this->invoice->invitations->count() >= 1) {
$this->invoice->entityEmailEvent($this->invoice->invitations->first(), 'invoice', $this->reminder_template); $this->invoice->entityEmailEvent($this->invoice->invitations->first(), 'invoice', $this->reminder_template);
$this->invoice->sendEvent(Webhook::EVENT_SENT_INVOICE, "client");
} }
} }
} }

View File

@ -583,7 +583,7 @@ class NinjaMailerJob implements ShouldQueue
/* On the hosted platform if the user has not verified their account we fail here - but still check what they are trying to send! */ /* On the hosted platform if the user has not verified their account we fail here - but still check what they are trying to send! */
if (Ninja::isHosted() && $this->company->account && !$this->company->account->account_sms_verified) { if (Ninja::isHosted() && $this->company->account && !$this->company->account->account_sms_verified) {
if (class_exists(\Modules\Admin\Jobs\Account\EmailQuality::class)) { if (class_exists(\Modules\Admin\Jobs\Account\EmailQuality::class)) {
return (new \Modules\Admin\Jobs\Account\EmailQuality($this->nmo, $this->company))->run(); (new \Modules\Admin\Jobs\Account\EmailQuality($this->nmo, $this->company))->run();
} }
return true; return true;

View File

@ -105,7 +105,7 @@ class PaymentFailedMailer implements ShouldQueue
}); });
//add client payment failures here. //add client payment failures here.
//
if ($this->client->contacts()->whereNotNull('email')->exists() && $this->payment_hash) { if ($this->client->contacts()->whereNotNull('email')->exists() && $this->payment_hash) {
$contact = $this->client->contacts()->whereNotNull('email')->first(); $contact = $this->client->contacts()->whereNotNull('email')->first();

View File

@ -11,9 +11,11 @@
namespace App\Jobs\Ninja; namespace App\Jobs\Ninja;
use App\Jobs\Bank\ProcessBankTransactions; use App\Jobs\Bank\ProcessBankTransactionsYodlee;
use App\Jobs\Bank\ProcessBankTransactionsNordigen;
use App\Libraries\MultiDB; use App\Libraries\MultiDB;
use App\Models\Account; use App\Models\Account;
use App\Models\BankIntegration;
use App\Utils\Ninja; use App\Utils\Ninja;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
@ -43,20 +45,52 @@ class BankTransactionSync implements ShouldQueue
*/ */
public function handle() public function handle()
{ {
//multiDB environment, need to if (config('ninja.db.multi_db_enabled')) {
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
nlog("syncing transactions"); foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$a = Account::with('bank_integrations')->whereNotNull('bank_integration_account_id')->cursor()->each(function ($account) { $this->processYodlee();
// $queue = Ninja::isHosted() ? 'bank' : 'default'; $this->processNordigen();
}
} else {
$this->processYodlee();
$this->processNordigen();
}
nlog("syncing transactions - done");
}
private function processYodlee()
{
if (Ninja::isHosted()) { // @turbo124 @todo I migrated the schedule for the job within the kernel to execute on all platforms and use the same expression here to determine if yodlee can run or not. Please chek/verify
nlog("syncing transactions - yodlee");
Account::with('bank_integrations')->whereNotNull('bank_integration_account_id')->cursor()->each(function ($account) {
if ($account->isPaid() && $account->plan == 'enterprise') { if ($account->isPaid() && $account->plan == 'enterprise') {
$account->bank_integrations()->where('auto_sync', true)->cursor()->each(function ($bank_integration) use ($account) { $account->bank_integrations()->where('integration_type', BankIntegration::INTEGRATION_TYPE_YODLEE)->where('auto_sync', true)->cursor()->each(function ($bank_integration) use ($account) {
(new ProcessBankTransactions($account->bank_integration_account_id, $bank_integration))->handle(); (new ProcessBankTransactionsYodlee($account->id, $bank_integration))->handle();
}); });
} }
});
}
}
private function processNordigen()
{
if (config("ninja.nordigen.secret_id") && config("ninja.nordigen.secret_key")) { // @turbo124 check condition, when to execute this should be placed here (isSelfHosted || isPro/isEnterprise)
nlog("syncing transactions - nordigen");
Account::with('bank_integrations')->cursor()->each(function ($account) {
if ((Ninja::isSelfHost() || (Ninja::isHosted() && $account->isPaid() && $account->plan == 'enterprise'))) {
$account->bank_integrations()->where('integration_type', BankIntegration::INTEGRATION_TYPE_NORDIGEN)->where('auto_sync', true)->cursor()->each(function ($bank_integration) {
(new ProcessBankTransactionsNordigen($bank_integration))->handle();
});
}
}); });
} }
} }

View File

@ -53,11 +53,19 @@ class CompanySizeCheck implements ShouldQueue
nlog("updating all client credit balances"); nlog("updating all client credit balances");
Client::where('updated_at', '>', now()->subDay()) Client::query()
->where('updated_at', '>', now()->subDay())
->cursor() ->cursor()
->each(function ($client) { ->each(function ($client) {
$client->credit_balance = $client->service()->getCreditBalance();
$client->save(); $old_credit_balance = $client->credit_balance;
$new_credit_balance = $client->service()->getCreditBalance();
if(floatval($old_credit_balance) !== floatval($new_credit_balance)){
$client->credit_balance = $client->service()->getCreditBalance();
$client->saveQuietly();
}
}); });
/* Ensures lower permissioned users return the correct dataset and refresh responses */ /* Ensures lower permissioned users return the correct dataset and refresh responses */
@ -87,11 +95,20 @@ class CompanySizeCheck implements ShouldQueue
nlog("updating all client credit balances"); nlog("updating all client credit balances");
Client::where('updated_at', '>', now()->subDay()) Client::query()->where('updated_at', '>', now()->subDay())
->cursor() ->cursor()
->each(function ($client) { ->each(function ($client) {
$client->credit_balance = $client->service()->getCreditBalance();
$client->save();
$old_credit_balance = $client->credit_balance;
$new_credit_balance = $client->service()->getCreditBalance();
if(floatval($old_credit_balance) !== floatval($new_credit_balance)) {
$client->credit_balance = $client->service()->getCreditBalance();
$client->saveQuietly();
}
}); });
Account::where('plan', 'enterprise') Account::where('plan', 'enterprise')

View File

@ -215,6 +215,7 @@ class SendReminders implements ShouldQueue
EmailEntity::dispatch($invitation, $invitation->company, $template)->delay(10); EmailEntity::dispatch($invitation, $invitation->company, $template)->delay(10);
event(new InvoiceWasEmailed($invoice->invitations->first(), $invoice->company, Ninja::eventVars(), $template)); event(new InvoiceWasEmailed($invoice->invitations->first(), $invoice->company, Ninja::eventVars(), $template));
$invoice->sendEvent(Webhook::EVENT_REMIND_INVOICE, "client");
} }
}); });

View File

@ -11,24 +11,25 @@
namespace App\Jobs\PostMark; namespace App\Jobs\PostMark;
use App\DataMapper\Analytics\Mail\EmailBounce; use App\Models\Company;
use App\DataMapper\Analytics\Mail\EmailSpam; use App\Models\SystemLog;
use App\Jobs\Util\SystemLogger;
use App\Libraries\MultiDB; use App\Libraries\MultiDB;
use Postmark\PostmarkClient;
use Illuminate\Bus\Queueable;
use App\Jobs\Util\SystemLogger;
use App\Models\QuoteInvitation;
use App\Models\CreditInvitation; use App\Models\CreditInvitation;
use App\Models\InvoiceInvitation; use App\Models\InvoiceInvitation;
use Illuminate\Queue\SerializesModels;
use Turbo124\Beacon\Facades\LightLogs;
use App\Models\PurchaseOrderInvitation; use App\Models\PurchaseOrderInvitation;
use App\Models\QuoteInvitation; use Illuminate\Queue\InteractsWithQueue;
use App\Models\RecurringInvoiceInvitation; use App\Models\RecurringInvoiceInvitation;
use App\Models\SystemLog;
use App\Notifications\Ninja\EmailSpamNotification;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use App\DataMapper\Analytics\Mail\EmailSpam;
use Illuminate\Queue\SerializesModels; use App\DataMapper\Analytics\Mail\EmailBounce;
use Postmark\PostmarkClient; use App\Notifications\Ninja\EmailSpamNotification;
use Turbo124\Beacon\Facades\LightLogs;
class ProcessPostmarkWebhook implements ShouldQueue class ProcessPostmarkWebhook implements ShouldQueue
{ {
@ -82,9 +83,14 @@ class ProcessPostmarkWebhook implements ShouldQueue
public function handle() public function handle()
{ {
MultiDB::findAndSetDbByCompanyKey($this->request['Tag']); MultiDB::findAndSetDbByCompanyKey($this->request['Tag']);
$company = Company::where('company_key', $this->request['Tag'])->first();
$this->invitation = $this->discoverInvitation($this->request['MessageID']); $this->invitation = $this->discoverInvitation($this->request['MessageID']);
if ($company && $this->request['RecordType'] == 'SpamComplaint' && config('ninja.notification.slack')) {
$company->notification(new EmailSpamNotification($company))->ninja();
}
if (!$this->invitation) { if (!$this->invitation) {
return; return;
} }
@ -305,14 +311,8 @@ class ProcessPostmarkWebhook implements ShouldQueue
if($sl) { if($sl) {
$this->updateSystemLog($sl, $data); $this->updateSystemLog($sl, $data);
return;
} }
(new SystemLogger($data, SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_SPAM_COMPLAINT, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company))->handle();
if (config('ninja.notification.slack')) {
$this->invitation->company->notification(new EmailSpamNotification($this->invitation->company->account))->ninja();
}
} }
private function discoverInvitation($message_id) private function discoverInvitation($message_id)

View File

@ -11,24 +11,25 @@
namespace App\Jobs\RecurringInvoice; namespace App\Jobs\RecurringInvoice;
use App\DataMapper\Analytics\SendRecurringFailure;
use App\Events\Invoice\InvoiceWasCreated;
use App\Factory\InvoiceInvitationFactory;
use App\Factory\RecurringInvoiceToInvoiceFactory;
use App\Jobs\Cron\AutoBill;
use App\Jobs\Entity\EmailEntity;
use App\Models\Invoice;
use App\Models\RecurringInvoice;
use App\Utils\Ninja;
use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\MakesHash;
use Carbon\Carbon; use Carbon\Carbon;
use App\Utils\Ninja;
use App\Models\Invoice;
use App\Models\Webhook;
use App\Jobs\Cron\AutoBill;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use App\Utils\Traits\MakesHash;
use Illuminate\Foundation\Bus\Dispatchable; use App\Jobs\Entity\EmailEntity;
use Illuminate\Queue\InteractsWithQueue; use App\Models\RecurringInvoice;
use App\Utils\Traits\GeneratesCounter;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Turbo124\Beacon\Facades\LightLogs; use Turbo124\Beacon\Facades\LightLogs;
use Illuminate\Queue\InteractsWithQueue;
use App\Events\Invoice\InvoiceWasCreated;
use App\Factory\InvoiceInvitationFactory;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use App\Factory\RecurringInvoiceToInvoiceFactory;
use App\DataMapper\Analytics\SendRecurringFailure;
class SendRecurring implements ShouldQueue class SendRecurring implements ShouldQueue
{ {
@ -117,6 +118,7 @@ class SendRecurring implements ShouldQueue
//04-08-2023 edge case to support where online payment notifications are not enabled //04-08-2023 edge case to support where online payment notifications are not enabled
if(!$invoice->client->getSetting('client_online_payment_notification')) { if(!$invoice->client->getSetting('client_online_payment_notification')) {
$this->sendRecurringEmails($invoice); $this->sendRecurringEmails($invoice);
$invoice->sendEvent(Webhook::EVENT_SENT_INVOICE, "client");
} }
} elseif ($invoice->auto_bill_enabled && $invoice->client->getSetting('auto_bill_date') == 'on_due_date' && $invoice->client->getSetting('auto_email_invoice') && ($invoice->due_date && Carbon::parse($invoice->due_date)->startOfDay()->lte(now()->startOfDay()))) { } elseif ($invoice->auto_bill_enabled && $invoice->client->getSetting('auto_bill_date') == 'on_due_date' && $invoice->client->getSetting('auto_email_invoice') && ($invoice->due_date && Carbon::parse($invoice->due_date)->startOfDay()->lte(now()->startOfDay()))) {
nlog("attempting to autobill {$invoice->number}"); nlog("attempting to autobill {$invoice->number}");
@ -125,10 +127,12 @@ class SendRecurring implements ShouldQueue
//04-08-2023 edge case to support where online payment notifications are not enabled //04-08-2023 edge case to support where online payment notifications are not enabled
if(!$invoice->client->getSetting('client_online_payment_notification')) { if(!$invoice->client->getSetting('client_online_payment_notification')) {
$this->sendRecurringEmails($invoice); $this->sendRecurringEmails($invoice);
$invoice->sendEvent(Webhook::EVENT_SENT_INVOICE, "client");
} }
} elseif ($invoice->client->getSetting('auto_email_invoice')) { } elseif ($invoice->client->getSetting('auto_email_invoice')) {
$this->sendRecurringEmails($invoice); $this->sendRecurringEmails($invoice);
$invoice->sendEvent(Webhook::EVENT_SENT_INVOICE, "client");
} }
} }

View File

@ -11,22 +11,23 @@
namespace App\Jobs\Util; namespace App\Jobs\Util;
use App\Utils\Ninja;
use App\Models\Invoice;
use App\Models\Webhook;
use App\Libraries\MultiDB;
use Illuminate\Bus\Queueable;
use Illuminate\Support\Carbon;
use App\DataMapper\InvoiceItem; use App\DataMapper\InvoiceItem;
use App\Factory\InvoiceFactory; use App\Factory\InvoiceFactory;
use App\Jobs\Entity\EmailEntity; use App\Jobs\Entity\EmailEntity;
use App\Libraries\MultiDB;
use App\Models\Invoice;
use App\Utils\Ninja;
use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesDates;
use Illuminate\Support\Facades\App;
use App\Utils\Traits\MakesReminders; use App\Utils\Traits\MakesReminders;
use Illuminate\Bus\Queueable; use Illuminate\Support\Facades\Auth;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Auth;
class ReminderJob implements ShouldQueue class ReminderJob implements ShouldQueue
{ {
@ -150,6 +151,7 @@ class ReminderJob implements ShouldQueue
EmailEntity::dispatch($invitation, $invitation->company, $reminder_template); EmailEntity::dispatch($invitation, $invitation->company, $reminder_template);
nlog("Firing reminder email for invoice {$invoice->number} - {$reminder_template}"); nlog("Firing reminder email for invoice {$invoice->number} - {$reminder_template}");
$invoice->entityEmailEvent($invitation, $reminder_template); $invoice->entityEmailEvent($invitation, $reminder_template);
$invoice->sendEvent(Webhook::EVENT_REMIND_INVOICE, "client");
} }
}); });
} }
@ -220,6 +222,7 @@ class ReminderJob implements ShouldQueue
EmailEntity::dispatch($invitation, $invitation->company, $reminder_template); EmailEntity::dispatch($invitation, $invitation->company, $reminder_template);
nlog("Firing reminder email for invoice {$invoice->number} - {$reminder_template}"); nlog("Firing reminder email for invoice {$invoice->number} - {$reminder_template}");
$invoice->entityEmailEvent($invitation, $reminder_template); $invoice->entityEmailEvent($invitation, $reminder_template);
$invoice->sendEvent(Webhook::EVENT_REMIND_INVOICE, "client");
} }
}); });
} }
@ -307,8 +310,6 @@ class ReminderJob implements ShouldQueue
/**Refresh Invoice values*/ /**Refresh Invoice values*/
$invoice = $invoice->calc()->getInvoice(); $invoice = $invoice->calc()->getInvoice();
// nlog('adjusting client balance and invoice balance by #'.$invoice->number.' '.($invoice->balance - $temp_invoice_balance));
// $invoice->client->service()->updateBalance($invoice->balance - $temp_invoice_balance);
$invoice->ledger()->updateInvoiceBalance($invoice->balance - $temp_invoice_balance, "Late Fee Adjustment for invoice {$invoice->number}"); $invoice->ledger()->updateInvoiceBalance($invoice->balance - $temp_invoice_balance, "Late Fee Adjustment for invoice {$invoice->number}");
$invoice->client->service()->calculateBalance(); $invoice->client->service()->calculateBalance();

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