Merge branch 'v5-develop' of https://github.com/invoiceninja/invoiceninja into feature-inbound-email-expenses

This commit is contained in:
paulwer 2023-12-28 10:50:32 +01:00
commit 3fc35aefe7
205 changed files with 491376 additions and 455724 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

@ -101,9 +101,12 @@ class Kernel extends ConsoleKernel
/* Check ExpenseMainboxes */ /* Check ExpenseMainboxes */
$schedule->job(new ExpenseMailboxJob)->everyThirtyMinutes()->withoutOverlapping()->name('expense-mailboxes-job')->onOneServer(); $schedule->job(new ExpenseMailboxJob)->everyThirtyMinutes()->withoutOverlapping()->name('expense-mailboxes-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();
} }
@ -111,9 +114,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();

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);
} }
/** /**
@ -64,21 +66,44 @@ class UserFilters extends QueryFilters
*/ */
public function entityFilter() public function entityFilter()
{ {
return $this->builder->whereHas('company_users', function ($q) {
$q->where('company_id', '=', auth()->user()->company()->id); /** @var \App\Models\User $user */
$user = auth()->user();
return $this->builder->whereHas('company_users', function ($q) use ($user){
$q->where('company_id', '=', $user->company()->id);
}); });
} }
/**
* Hides owner users from the list.
*
* @return Builder
*/
public function hideOwnerUsers(): Builder
{
/** @var \App\Models\User $user */
$user = auth()->user();
return $this->builder->whereHas('company_users', function ($q) use ($user) {
$q->where('company_id', '=', $user->company()->id)->where('is_owner', false);
});
}
/** /**
* Filters users that have been removed from the * Filters users that have been removed from the
* company, but not deleted from the system. * company, but not deleted from the system.
* *
* @return void * @return Builder
*/ */
public function hideRemovedUsers() public function hideRemovedUsers(): Builder
{ {
return $this->builder->whereHas('company_users', function ($q) { /** @var \App\Models\User $user */
$q->where('company_id', '=', auth()->user()->company()->id)->whereNull('deleted_at'); $user = auth()->user();
return $this->builder->whereHas('company_users', function ($q) use ($user) {
$q->where('company_id', '=', $user->company()->id)->whereNull('deleted_at');
}); });
} }
@ -96,12 +121,21 @@ class UserFilters extends QueryFilters
return $this->builder; return $this->builder;
} }
/** @var \App\Models\User $user */
$user = auth()->user();
return $this->builder return $this->builder
->orWhere($this->with_property, $value) ->orWhere($this->with_property, $value)
->orderByRaw("{$this->with_property} = ? DESC", [$value]) ->orderByRaw("{$this->with_property} = ? DESC", [$value])
->where('account_id', auth()->user()->account_id); ->where('account_id', $user->account_id);
} }
/**
* Returns users with permissions to send emails via OAuth
*
* @param string $value
* @return Builder
*/
public function sending_users(string $value = ''): Builder public function sending_users(string $value = ''): Builder
{ {
if (strlen($value) == 0 || $value != 'true') { if (strlen($value) == 0 || $value != 'true') {

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,137 @@
<?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();
}
// 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, string $userLanguage)
{
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, $userLanguage);
}
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;
}
}
/**
* isAccountActive
*
* @param string $account_id
* @return bool
*/
public function isAccountActive(string $account_id): bool
{
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;
}
}
/**
* getTransactions
*
* @param string $accountId
* @param string $dateFrom
* @return array
*/
public function getTransactions(string $accountId, string $dateFrom = null): array
{
$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,
'category_type' => array_key_exists('additionalInformation', $transaction) ? $transaction["additionalInformation"] : '',
'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,308 @@
<?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();
/** @var array $context */
$context = $request->getTokenContent();
$company = $request->getCompany();
$lang = $company->locale();
$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->isEnterprisePaidClient())))
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, $lang);
} 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 ConfirmNordigenBankIntegrationRequest $request
*/
public function confirm(ConfirmNordigenBankIntegrationRequest $request)
{
$data = $request->all();
$company = $request->getCompany();
$account = $company->account;
$lang = $company->locale();
/** @var array $context */
$context = $request->getTokenContent();
if (!array_key_exists('lang', $data) && $context['lang'] != 'en')
return redirect()->route('nordigen.confirm', array_merge(["lang" => $context['lang']], $request->query()));
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",
]);
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->isEnterprisePaidClient())))
$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,30 @@ 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();
if (!$bank_account_id) { if ($bank_integration->integration_type == BankIntegration::INTEGRATION_TYPE_YODLEE)
$this->removeAccountYodlee($account, $bank_integration);
$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 +331,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

@ -61,7 +61,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()) {
@ -118,7 +118,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,70 @@
<?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 [
];
}
public function prepareForValidation()
{
$input = $this->all();
$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

@ -11,14 +11,19 @@
namespace App\Http\ValidationRules\Account; namespace App\Http\ValidationRules\Account;
use Illuminate\Contracts\Validation\Rule; use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
/** /**
* Class BlackListRule. * Class BlackListRule.
*/ */
class BlackListRule implements Rule class BlackListRule implements ValidationRule
{ {
/** Bad domains +/- dispoable email domains */
private array $blacklist = [ private array $blacklist = [
'secure-coinspot.com',
'casasotombo.com',
'otpku.com',
'ckptr.com', 'ckptr.com',
'pretreer.com', 'pretreer.com',
'candassociates.com', 'candassociates.com',
@ -57,6 +62,8 @@ class BlackListRule implements Rule
'10dk.email', '10dk.email',
'10mail.com', '10mail.com',
'10mail.org', '10mail.org',
'10mail.tk',
'10minmail.de',
'10minut.com.pl', '10minut.com.pl',
'10minut.xyz', '10minut.xyz',
'10minutemail.be', '10minutemail.be',
@ -75,6 +82,7 @@ class BlackListRule implements Rule
'10minutemailbox.com', '10minutemailbox.com',
'10minutemails.in', '10minutemails.in',
'10minutenemail.de', '10minutenemail.de',
'10minutenmail.xyz',
'10minutesmail.com', '10minutesmail.com',
'10minutesmail.fr', '10minutesmail.fr',
'10minutmail.pl', '10minutmail.pl',
@ -268,6 +276,7 @@ class BlackListRule implements Rule
'affinitywe.us', 'affinitywe.us',
'affluentwe.us', 'affluentwe.us',
'affordablewe.us', 'affordablewe.us',
'afia.pro',
'afrobacon.com', 'afrobacon.com',
'afterhourswe.us', 'afterhourswe.us',
'agedmail.com', 'agedmail.com',
@ -287,6 +296,8 @@ class BlackListRule implements Rule
'akapost.com', 'akapost.com',
'akerd.com', 'akerd.com',
'akgq701.com', 'akgq701.com',
'akmail.in',
'akugu.com',
'al-qaeda.us', 'al-qaeda.us',
'albionwe.us', 'albionwe.us',
'alchemywe.us', 'alchemywe.us',
@ -294,6 +305,7 @@ class BlackListRule implements Rule
'aliaswe.us', 'aliaswe.us',
'alienware13.com', 'alienware13.com',
'aligamel.com', 'aligamel.com',
'alina-schiesser.ch',
'alisongamel.com', 'alisongamel.com',
'alivance.com', 'alivance.com',
'alivewe.us', 'alivewe.us',
@ -376,6 +388,7 @@ class BlackListRule implements Rule
'antispam.de', 'antispam.de',
'antispam24.de', 'antispam24.de',
'antispammail.de', 'antispammail.de',
'any.pink',
'anyalias.com', 'anyalias.com',
'aoeuhtns.com', 'aoeuhtns.com',
'apfelkorps.de', 'apfelkorps.de',
@ -440,10 +453,12 @@ class BlackListRule implements Rule
'badoop.com', 'badoop.com',
'badpotato.tk', 'badpotato.tk',
'balaket.com', 'balaket.com',
'bangban.uk',
'banit.club', 'banit.club',
'banit.me', 'banit.me',
'bank-opros1.ru', 'bank-opros1.ru',
'bareed.ws', 'bareed.ws',
'barooko.com',
'barryogorman.com', 'barryogorman.com',
'bartdevos.be', 'bartdevos.be',
'basscode.org', 'basscode.org',
@ -451,11 +466,15 @@ class BlackListRule implements Rule
'bazaaboom.com', 'bazaaboom.com',
'bbbbyyzz.info', 'bbbbyyzz.info',
'bbhost.us', 'bbhost.us',
'bbitf.com',
'bbitj.com',
'bbitq.com',
'bcaoo.com', 'bcaoo.com',
'bcast.ws', 'bcast.ws',
'bcb.ro', 'bcb.ro',
'bccto.me', 'bccto.me',
'bdmuzic.pw', 'bdmuzic.pw',
'beaconmessenger.com',
'bearsarefuzzy.com', 'bearsarefuzzy.com',
'beddly.com', 'beddly.com',
'beefmilk.com', 'beefmilk.com',
@ -477,6 +496,7 @@ class BlackListRule implements Rule
'betr.co', 'betr.co',
'bgtmail.com', 'bgtmail.com',
'bgx.ro', 'bgx.ro',
'bheps.com',
'bidourlnks.com', 'bidourlnks.com',
'big1.us', 'big1.us',
'bigprofessor.so', 'bigprofessor.so',
@ -520,9 +540,11 @@ class BlackListRule implements Rule
'bouncr.com', 'bouncr.com',
'boxformail.in', 'boxformail.in',
'boximail.com', 'boximail.com',
'boxmail.lol',
'boxomail.live', 'boxomail.live',
'boxtemp.com.br', 'boxtemp.com.br',
'bptfp.net', 'bptfp.net',
'brand-app.biz',
'brandallday.net', 'brandallday.net',
'brasx.org', 'brasx.org',
'breakthru.com', 'breakthru.com',
@ -543,8 +565,10 @@ class BlackListRule implements Rule
'budaya-tionghoa.com', 'budaya-tionghoa.com',
'budayationghoa.com', 'budayationghoa.com',
'buffemail.com', 'buffemail.com',
'bugfoo.com',
'bugmenever.com', 'bugmenever.com',
'bugmenot.com', 'bugmenot.com',
'bukhariansiddur.com',
'bulrushpress.com', 'bulrushpress.com',
'bum.net', 'bum.net',
'bumpymail.com', 'bumpymail.com',
@ -582,6 +606,7 @@ class BlackListRule implements Rule
'caseedu.tk', 'caseedu.tk',
'cashflow35.com', 'cashflow35.com',
'casualdx.com', 'casualdx.com',
'catgroup.uk',
'cavi.mx', 'cavi.mx',
'cbair.com', 'cbair.com',
'cbes.net', 'cbes.net',
@ -605,6 +630,7 @@ class BlackListRule implements Rule
'cheaphub.net', 'cheaphub.net',
'cheatmail.de', 'cheatmail.de',
'chenbot.email', 'chenbot.email',
'chewydonut.com',
'chibakenma.ml', 'chibakenma.ml',
'chickenkiller.com', 'chickenkiller.com',
'chielo.com', 'chielo.com',
@ -621,6 +647,7 @@ class BlackListRule implements Rule
'chong-mail.org', 'chong-mail.org',
'chumpstakingdumps.com', 'chumpstakingdumps.com',
'cigar-auctions.com', 'cigar-auctions.com',
'civikli.com',
'civx.org', 'civx.org',
'ckaazaza.tk', 'ckaazaza.tk',
'ckiso.com', 'ckiso.com',
@ -637,6 +664,7 @@ class BlackListRule implements Rule
'clonemoi.tk', 'clonemoi.tk',
'cloud-mail.top', 'cloud-mail.top',
'cloudns.cx', 'cloudns.cx',
'clout.wiki',
'clrmail.com', 'clrmail.com',
'cmail.club', 'cmail.club',
'cmail.com', 'cmail.com',
@ -678,6 +706,7 @@ class BlackListRule implements Rule
'crazymailing.com', 'crazymailing.com',
'cream.pink', 'cream.pink',
'crepeau12.com', 'crepeau12.com',
'cringemonster.com',
'cross-law.ga', 'cross-law.ga',
'cross-law.gq', 'cross-law.gq',
'crossmailjet.com', 'crossmailjet.com',
@ -733,6 +762,7 @@ class BlackListRule implements Rule
'daymailonline.com', 'daymailonline.com',
'dayrep.com', 'dayrep.com',
'dbunker.com', 'dbunker.com',
'dcctb.com',
'dcemail.com', 'dcemail.com',
'ddcrew.com', 'ddcrew.com',
'de-a.org', 'de-a.org',
@ -769,6 +799,7 @@ class BlackListRule implements Rule
'dev-null.ga', 'dev-null.ga',
'dev-null.gq', 'dev-null.gq',
'dev-null.ml', 'dev-null.ml',
'developermail.com',
'devnullmail.com', 'devnullmail.com',
'deyom.com', 'deyom.com',
'dharmatel.net', 'dharmatel.net',
@ -800,6 +831,7 @@ class BlackListRule implements Rule
'discardmail.com', 'discardmail.com',
'discardmail.de', 'discardmail.de',
'discos4.com', 'discos4.com',
'dishcatfish.com',
'disign-concept.eu', 'disign-concept.eu',
'disign-revelation.com', 'disign-revelation.com',
'dispo.in', 'dispo.in',
@ -851,10 +883,12 @@ class BlackListRule implements Rule
'domforfb8.tk', 'domforfb8.tk',
'domforfb9.tk', 'domforfb9.tk',
'domozmail.com', 'domozmail.com',
'donebyngle.com',
'donemail.ru', 'donemail.ru',
'dongqing365.com', 'dongqing365.com',
'dontreg.com', 'dontreg.com',
'dontsendmespam.de', 'dontsendmespam.de',
'doojazz.com',
'doquier.tk', 'doquier.tk',
'dotman.de', 'dotman.de',
'dotmsg.com', 'dotmsg.com',
@ -869,13 +903,17 @@ class BlackListRule implements Rule
'dred.ru', 'dred.ru',
'drevo.si', 'drevo.si',
'drivetagdev.com', 'drivetagdev.com',
'drmail.in',
'droolingfanboy.de', 'droolingfanboy.de',
'dropcake.de', 'dropcake.de',
'dropjar.com', 'dropjar.com',
'droplar.com', 'droplar.com',
'dropmail.me', 'dropmail.me',
'dropsin.net', 'dropsin.net',
'drowblock.com',
'dsgvo.party',
'dsgvo.ru', 'dsgvo.ru',
'dshfjdafd.cloud',
'dsiay.com', 'dsiay.com',
'dspwebservices.com', 'dspwebservices.com',
'duam.net', 'duam.net',
@ -944,6 +982,7 @@ class BlackListRule implements Rule
'emailage.ml', 'emailage.ml',
'emailage.tk', 'emailage.tk',
'emailate.com', 'emailate.com',
'emailbin.net',
'emailcu.icu', 'emailcu.icu',
'emaildienst.de', 'emaildienst.de',
'emaildrop.io', 'emaildrop.io',
@ -1017,8 +1056,11 @@ class BlackListRule implements Rule
'eposta.buzz', 'eposta.buzz',
'eposta.work', 'eposta.work',
'eqiluxspam.ga', 'eqiluxspam.ga',
'ereplyzy.com',
'ericjohnson.ml', 'ericjohnson.ml',
'eripo.net',
'ero-tube.org', 'ero-tube.org',
'esadverse.com',
'esbano-ru.ru', 'esbano-ru.ru',
'esc.la', 'esc.la',
'escapehatchapp.com', 'escapehatchapp.com',
@ -1043,16 +1085,19 @@ class BlackListRule implements Rule
'evopo.com', 'evopo.com',
'evyush.com', 'evyush.com',
'exdonuts.com', 'exdonuts.com',
'exelica.com',
'existiert.net', 'existiert.net',
'exitstageleft.net', 'exitstageleft.net',
'explodemail.com', 'explodemail.com',
'express.net.ua', 'express.net.ua',
'extracurricularsociety.com',
'extremail.ru', 'extremail.ru',
'eyepaste.com', 'eyepaste.com',
'ez.lv', 'ez.lv',
'ezehe.com', 'ezehe.com',
'ezfill.com', 'ezfill.com',
'ezstest.com', 'ezstest.com',
'ezztt.com',
'f4k.es', 'f4k.es',
'f5.si', 'f5.si',
'facebook-email.cf', 'facebook-email.cf',
@ -1108,12 +1153,14 @@ class BlackListRule implements Rule
'fbma.tk', 'fbma.tk',
'fddns.ml', 'fddns.ml',
'fdfdsfds.com', 'fdfdsfds.com',
'femailtor.com',
'fer-gabon.org', 'fer-gabon.org',
'fermaxxi.ru', 'fermaxxi.ru',
'fettometern.com', 'fettometern.com',
'fexbox.org', 'fexbox.org',
'fexbox.ru', 'fexbox.ru',
'fexpost.com', 'fexpost.com',
'fextemp.com',
'ficken.de', 'ficken.de',
'fictionsite.com', 'fictionsite.com',
'fightallspam.com', 'fightallspam.com',
@ -1127,6 +1174,7 @@ class BlackListRule implements Rule
'filzmail.com', 'filzmail.com',
'findemail.info', 'findemail.info',
'findu.pl', 'findu.pl',
'finews.biz',
'fir.hk', 'fir.hk',
'firemailbox.club', 'firemailbox.club',
'fitnesrezink.ru', 'fitnesrezink.ru',
@ -1135,12 +1183,14 @@ class BlackListRule implements Rule
'fizmail.com', 'fizmail.com',
'fleckens.hu', 'fleckens.hu',
'flemail.ru', 'flemail.ru',
'fliegender.fish',
'flowu.com', 'flowu.com',
'flu.cc', 'flu.cc',
'fluidsoft.us', 'fluidsoft.us',
'flurred.com', 'flurred.com',
'fly-ts.de', 'fly-ts.de',
'flyinggeek.net', 'flyinggeek.net',
'flymail.tk',
'flyspam.com', 'flyspam.com',
'foobarbot.net', 'foobarbot.net',
'footard.com', 'footard.com',
@ -1158,6 +1208,7 @@ class BlackListRule implements Rule
'fosil.pro', 'fosil.pro',
'foxja.com', 'foxja.com',
'foxtrotter.info', 'foxtrotter.info',
'fr.cr',
'fr.nf', 'fr.nf',
'fr33mail.info', 'fr33mail.info',
'fragolina2.tk', 'fragolina2.tk',
@ -1204,11 +1255,15 @@ class BlackListRule implements Rule
'fuirio.com', 'fuirio.com',
'fukaru.com', 'fukaru.com',
'fukurou.ch', 'fukurou.ch',
'fullangle.org',
'fulvie.com', 'fulvie.com',
'fun64.com', 'fun64.com',
'funnycodesnippets.com', 'funnycodesnippets.com',
'funnymail.de', 'funnymail.de',
'furzauflunge.de', 'furzauflunge.de',
'futuramind.com',
'fuwa.be',
'fuwa.li',
'fuwamofu.com', 'fuwamofu.com',
'fuwari.be', 'fuwari.be',
'fux0ringduh.com', 'fux0ringduh.com',
@ -1291,6 +1346,7 @@ class BlackListRule implements Rule
'giveh2o.info', 'giveh2o.info',
'givememail.club', 'givememail.club',
'givmail.com', 'givmail.com',
'gixenmixen.com',
'glitch.sx', 'glitch.sx',
'globaltouron.com', 'globaltouron.com',
'glubex.com', 'glubex.com',
@ -1304,6 +1360,7 @@ class BlackListRule implements Rule
'gnctr-calgary.com', 'gnctr-calgary.com',
'go2usa.info', 'go2usa.info',
'go2vpn.net', 'go2vpn.net',
'goatmail.uk',
'goemailgo.com', 'goemailgo.com',
'golemico.com', 'golemico.com',
'gomail.in', 'gomail.in',
@ -1363,6 +1420,7 @@ class BlackListRule implements Rule
'guerrillamail.net', 'guerrillamail.net',
'guerrillamail.org', 'guerrillamail.org',
'guerrillamailblock.com', 'guerrillamailblock.com',
'gufum.com',
'gustr.com', 'gustr.com',
'gxemail.men', 'gxemail.men',
'gynzi.co.uk', 'gynzi.co.uk',
@ -1389,6 +1447,7 @@ class BlackListRule implements Rule
'haltospam.com', 'haltospam.com',
'hamham.uk', 'hamham.uk',
'hangxomcuatoilatotoro.ml', 'hangxomcuatoilatotoro.ml',
'happy2023year.com',
'happydomik.ru', 'happydomik.ru',
'harakirimail.com', 'harakirimail.com',
'haribu.com', 'haribu.com',
@ -1467,6 +1526,7 @@ class BlackListRule implements Rule
'huskion.net', 'huskion.net',
'hvastudiesucces.nl', 'hvastudiesucces.nl',
'hwsye.net', 'hwsye.net',
'hypenated-domain.com',
'i2pmail.org', 'i2pmail.org',
'i6.cloudns.cc', 'i6.cloudns.cc',
'iaoss.com', 'iaoss.com',
@ -1476,6 +1536,7 @@ class BlackListRule implements Rule
'ichigo.me', 'ichigo.me',
'icx.in', 'icx.in',
'icx.ro', 'icx.ro',
'icznn.com',
'idx4.com', 'idx4.com',
'idxue.com', 'idxue.com',
'ieatspam.eu', 'ieatspam.eu',
@ -1498,9 +1559,12 @@ class BlackListRule implements Rule
'imgof.com', 'imgof.com',
'imgv.de', 'imgv.de',
'immo-gerance.info', 'immo-gerance.info',
'imperialcnk.com',
'imstations.com', 'imstations.com',
'imul.info', 'imul.info',
'in-ulm.de', 'in-ulm.de',
'in2reach.com',
'inactivemachine.com',
'inbax.tk', 'inbax.tk',
'inbound.plus', 'inbound.plus',
'inbox.si', 'inbox.si',
@ -1530,6 +1594,7 @@ class BlackListRule implements Rule
'ineec.net', 'ineec.net',
'infocom.zp.ua', 'infocom.zp.ua',
'inggo.org', 'inggo.org',
'inkiny.com',
'inkomail.com', 'inkomail.com',
'inmynetwork.tk', 'inmynetwork.tk',
'inoutmail.de', 'inoutmail.de',
@ -1540,12 +1605,16 @@ class BlackListRule implements Rule
'insanumingeniumhomebrew.com', 'insanumingeniumhomebrew.com',
'insorg-mail.info', 'insorg-mail.info',
'instaddr.ch', 'instaddr.ch',
'instaddr.uk',
'instaddr.win',
'instance-email.com', 'instance-email.com',
'instant-mail.de', 'instant-mail.de',
'instantblingmail.info', 'instantblingmail.info',
'instantemailaddress.com', 'instantemailaddress.com',
'instantmail.fr', 'instantmail.fr',
'instmail.uk',
'internet-v-stavropole.ru', 'internet-v-stavropole.ru',
'internetkeno.com',
'internetoftags.com', 'internetoftags.com',
'interstats.org', 'interstats.org',
'intersteller.com', 'intersteller.com',
@ -1577,6 +1646,7 @@ class BlackListRule implements Rule
'italy-mail.com', 'italy-mail.com',
'itcompu.com', 'itcompu.com',
'itfast.net', 'itfast.net',
'itsjiff.com',
'itunesgiftcodegenerator.com', 'itunesgiftcodegenerator.com',
'iubridge.com', 'iubridge.com',
'iuemail.men', 'iuemail.men',
@ -1585,6 +1655,7 @@ class BlackListRule implements Rule
'ixx.io', 'ixx.io',
'j-p.us', 'j-p.us',
'jafps.com', 'jafps.com',
'jaga.email',
'jajxz.com', 'jajxz.com',
'janproz.com', 'janproz.com',
'jaqis.com', 'jaqis.com',
@ -1599,11 +1670,15 @@ class BlackListRule implements Rule
'jetable.net', 'jetable.net',
'jetable.org', 'jetable.org',
'jetable.pp.ua', 'jetable.pp.ua',
'ji5.de',
'ji6.de',
'ji7.de',
'jiooq.com', 'jiooq.com',
'jmail.ovh', 'jmail.ovh',
'jmail.ro', 'jmail.ro',
'jnxjn.com', 'jnxjn.com',
'jobbikszimpatizans.hu', 'jobbikszimpatizans.hu',
'jobbrett.com',
'jobposts.net', 'jobposts.net',
'jobs-to-be-done.net', 'jobs-to-be-done.net',
'joelpet.com', 'joelpet.com',
@ -1660,6 +1735,8 @@ class BlackListRule implements Rule
'killmail.com', 'killmail.com',
'killmail.net', 'killmail.net',
'kimsdisk.com', 'kimsdisk.com',
'kinda.email',
'kindamail.com',
'kingsq.ga', 'kingsq.ga',
'kino-100.ru', 'kino-100.ru',
'kiois.com', 'kiois.com',
@ -1668,16 +1745,20 @@ class BlackListRule implements Rule
'kitnastar.com', 'kitnastar.com',
'kjkszpjcompany.com', 'kjkszpjcompany.com',
'kkmail.be', 'kkmail.be',
'kkoup.com',
'kksm.be', 'kksm.be',
'klassmaster.com', 'klassmaster.com',
'klassmaster.net', 'klassmaster.net',
'klick-tipp.us', 'klick-tipp.us',
'klipschx12.com', 'klipschx12.com',
'kloap.com', 'kloap.com',
'klovenode.com',
'kludgemush.com', 'kludgemush.com',
'klzlk.com', 'klzlk.com',
'kmail.li', 'kmail.li',
'kmail.live',
'kmhow.com', 'kmhow.com',
'knickerbockerban.de',
'knol-power.nl', 'knol-power.nl',
'kobrandly.com', 'kobrandly.com',
'kommunity.biz', 'kommunity.biz',
@ -1711,6 +1792,7 @@ class BlackListRule implements Rule
'kwilco.net', 'kwilco.net',
'kyal.pl', 'kyal.pl',
'kyois.com', 'kyois.com',
'kzccv.com',
'l-c-a.us', 'l-c-a.us',
'l33r.eu', 'l33r.eu',
'l6factors.com', 'l6factors.com',
@ -1725,9 +1807,12 @@ class BlackListRule implements Rule
'lak.pp.ua', 'lak.pp.ua',
'lakelivingstonrealestate.com', 'lakelivingstonrealestate.com',
'lakqs.com', 'lakqs.com',
'lamasticots.com',
'lambsauce.de',
'landmail.co', 'landmail.co',
'laoeq.com', 'laoeq.com',
'larisia.com', 'larisia.com',
'larland.com',
'last-chance.pro', 'last-chance.pro',
'lastmail.co', 'lastmail.co',
'lastmail.com', 'lastmail.com',
@ -1757,6 +1842,7 @@ class BlackListRule implements Rule
'ligsb.com', 'ligsb.com',
'lillemap.net', 'lillemap.net',
'lilo.me', 'lilo.me',
'lilspam.com',
'lindenbaumjapan.com', 'lindenbaumjapan.com',
'link2mail.net', 'link2mail.net',
'linkedintuts2016.pw', 'linkedintuts2016.pw',
@ -1802,6 +1888,8 @@ class BlackListRule implements Rule
'lukop.dk', 'lukop.dk',
'luv2.us', 'luv2.us',
'lyfestylecreditsolutions.com', 'lyfestylecreditsolutions.com',
'lyft.live',
'lyricspad.net',
'lzoaq.com', 'lzoaq.com',
'm21.cc', 'm21.cc',
'm4ilweb.info', 'm4ilweb.info',
@ -1847,6 +1935,7 @@ class BlackListRule implements Rule
'mailapp.top', 'mailapp.top',
'mailback.com', 'mailback.com',
'mailbidon.com', 'mailbidon.com',
'mailbiscuit.com',
'mailbiz.biz', 'mailbiz.biz',
'mailblocks.com', 'mailblocks.com',
'mailbox.in.ua', 'mailbox.in.ua',
@ -1876,6 +1965,7 @@ class BlackListRule implements Rule
'mailed.ro', 'mailed.ro',
'maileimer.de', 'maileimer.de',
'maileme101.com', 'maileme101.com',
'mailers.edu.pl',
'mailexpire.com', 'mailexpire.com',
'mailf5.com', 'mailf5.com',
'mailfa.tk', 'mailfa.tk',
@ -1943,6 +2033,7 @@ class BlackListRule implements Rule
'mailonaut.com', 'mailonaut.com',
'mailorc.com', 'mailorc.com',
'mailorg.org', 'mailorg.org',
'mailosaur.net',
'mailox.fun', 'mailox.fun',
'mailpick.biz', 'mailpick.biz',
'mailpluss.com', 'mailpluss.com',
@ -2006,18 +2097,22 @@ class BlackListRule implements Rule
'mcache.net', 'mcache.net',
'mciek.com', 'mciek.com',
'mdhc.tk', 'mdhc.tk',
'mdz.email',
'meantinc.com', 'meantinc.com',
'mebelnu.info', 'mebelnu.info',
'mechanicalresumes.com', 'mechanicalresumes.com',
'medkabinet-uzi.ru', 'medkabinet-uzi.ru',
'meepsheep.eu', 'meepsheep.eu',
'meidecn.com',
'meinspamschutz.de', 'meinspamschutz.de',
'meltedbrownies.com', 'meltedbrownies.com',
'meltmail.com', 'meltmail.com',
'memsg.site', 'memsg.site',
'mentonit.net', 'mentonit.net',
'mepost.pw', 'mepost.pw',
'merepost.com',
'merry.pink', 'merry.pink',
'meruado.uk',
'messagebeamer.de', 'messagebeamer.de',
'messwiththebestdielikethe.rest', 'messwiththebestdielikethe.rest',
'metadownload.org', 'metadownload.org',
@ -2043,6 +2138,7 @@ class BlackListRule implements Rule
'migumail.com', 'migumail.com',
'mihep.com', 'mihep.com',
'mijnhva.nl', 'mijnhva.nl',
'minimail.gq',
'ministry-of-silly-walks.de', 'ministry-of-silly-walks.de',
'minsmail.com', 'minsmail.com',
'mintemail.com', 'mintemail.com',
@ -2054,6 +2150,7 @@ class BlackListRule implements Rule
'mjukglass.nu', 'mjukglass.nu',
'mkpfilm.com', 'mkpfilm.com',
'ml8.ca', 'ml8.ca',
'mliok.com',
'mm.my', 'mm.my',
'mm5.se', 'mm5.se',
'mnode.me', 'mnode.me',
@ -2109,6 +2206,7 @@ class BlackListRule implements Rule
'mucincanon.com', 'mucincanon.com',
'muehlacker.tk', 'muehlacker.tk',
'muell.icu', 'muell.icu',
'muell.io',
'muell.monster', 'muell.monster',
'muell.xyz', 'muell.xyz',
'muellemail.com', 'muellemail.com',
@ -2128,16 +2226,19 @@ class BlackListRule implements Rule
'mycleaninbox.net', 'mycleaninbox.net',
'mycorneroftheinter.net', 'mycorneroftheinter.net',
'myde.ml', 'myde.ml',
'mydefipet.live',
'mydemo.equipment', 'mydemo.equipment',
'myecho.es', 'myecho.es',
'myemailboxy.com', 'myemailboxy.com',
'mygeoweb.info', 'mygeoweb.info',
'myindohome.services', 'myindohome.services',
'myinfoinc.com',
'myinterserver.ml', 'myinterserver.ml',
'mykickassideas.com', 'mykickassideas.com',
'mymail-in.net', 'mymail-in.net',
'mymail90.com', 'mymail90.com',
'mymailoasis.com', 'mymailoasis.com',
'mymaily.lol',
'mynetstore.de', 'mynetstore.de',
'myopang.com', 'myopang.com',
'mypacks.net', 'mypacks.net',
@ -2171,10 +2272,12 @@ class BlackListRule implements Rule
'naslazhdai.ru', 'naslazhdai.ru',
'nationalgardeningclub.com', 'nationalgardeningclub.com',
'nawmin.info', 'nawmin.info',
'naymedia.com',
'nbzmr.com', 'nbzmr.com',
'negated.com', 'negated.com',
'neko2.net', 'neko2.net',
'nekochan.fr', 'nekochan.fr',
'nekosan.uk',
'neomailbox.com', 'neomailbox.com',
'neotlozhniy-zaim.ru', 'neotlozhniy-zaim.ru',
'nepwk.com', 'nepwk.com',
@ -2263,6 +2366,7 @@ class BlackListRule implements Rule
'nwytg.com', 'nwytg.com',
'nwytg.net', 'nwytg.net',
'ny7.me', 'ny7.me',
'nyasan.com',
'nypato.com', 'nypato.com',
'nyrmusic.com', 'nyrmusic.com',
'o2stk.org', 'o2stk.org',
@ -2280,6 +2384,7 @@ class BlackListRule implements Rule
'oepia.com', 'oepia.com',
'oerpub.org', 'oerpub.org',
'offshore-proxies.net', 'offshore-proxies.net',
'ofisher.net',
'ohaaa.de', 'ohaaa.de',
'ohi.tw', 'ohi.tw',
'oida.icu', 'oida.icu',
@ -2305,6 +2410,7 @@ class BlackListRule implements Rule
'onlatedotcom.info', 'onlatedotcom.info',
'online.ms', 'online.ms',
'onlineidea.info', 'onlineidea.info',
'onlyapp.net',
'onqin.com', 'onqin.com',
'ontyne.biz', 'ontyne.biz',
'oohioo.com', 'oohioo.com',
@ -2330,6 +2436,7 @@ class BlackListRule implements Rule
'ourklips.com', 'ourklips.com',
'ourpreviewdomain.com', 'ourpreviewdomain.com',
'outlawspam.com', 'outlawspam.com',
'outlook.edu.pl',
'outmail.win', 'outmail.win',
'ovomail.co', 'ovomail.co',
'ovpn.to', 'ovpn.to',
@ -2337,6 +2444,7 @@ class BlackListRule implements Rule
'owlpic.com', 'owlpic.com',
'ownsyou.de', 'ownsyou.de',
'oxopoha.com', 'oxopoha.com',
'ozatvn.com',
'ozyl.de', 'ozyl.de',
'p-banlis.ru', 'p-banlis.ru',
'p33.org', 'p33.org',
@ -2347,6 +2455,7 @@ class BlackListRule implements Rule
'pagamenti.tk', 'pagamenti.tk',
'paharpurmim.ga', 'paharpurmim.ga',
'pakadebu.ga', 'pakadebu.ga',
'pamaweb.com',
'pancakemail.com', 'pancakemail.com',
'papierkorb.me', 'papierkorb.me',
'paplease.com', 'paplease.com',
@ -2384,6 +2493,8 @@ class BlackListRule implements Rule
'pisls.com', 'pisls.com',
'pitaniezdorovie.ru', 'pitaniezdorovie.ru',
'pivo-bar.ru', 'pivo-bar.ru',
'pixiil.com',
'pizzajunk.com',
'pjjkp.com', 'pjjkp.com',
'placebomail10.com', 'placebomail10.com',
'pleasenoham.org', 'pleasenoham.org',
@ -2435,6 +2546,7 @@ class BlackListRule implements Rule
'prin.be', 'prin.be',
'privacy.net', 'privacy.net',
'privatdemail.net', 'privatdemail.net',
'privmail.edu.pl',
'privy-mail.com', 'privy-mail.com',
'privy-mail.de', 'privy-mail.de',
'privymail.de', 'privymail.de',
@ -2454,6 +2566,7 @@ class BlackListRule implements Rule
'prtz.eu', 'prtz.eu',
'psh.me', 'psh.me',
'psles.com', 'psles.com',
'psnator.com',
'psoxs.com', 'psoxs.com',
'puglieisi.com', 'puglieisi.com',
'puji.pro', 'puji.pro',
@ -2464,11 +2577,13 @@ class BlackListRule implements Rule
'put2.net', 'put2.net',
'puttanamaiala.tk', 'puttanamaiala.tk',
'putthisinyourspamdatabase.com', 'putthisinyourspamdatabase.com',
'pwpwa.com',
'pwrby.com', 'pwrby.com',
'qasti.com', 'qasti.com',
'qbfree.us', 'qbfree.us',
'qc.to', 'qc.to',
'qibl.at', 'qibl.at',
'qiott.com',
'qipmail.net', 'qipmail.net',
'qiq.us', 'qiq.us',
'qisdo.com', 'qisdo.com',
@ -2485,6 +2600,7 @@ class BlackListRule implements Rule
'quickinbox.com', 'quickinbox.com',
'quickmail.nl', 'quickmail.nl',
'quicksend.ch', 'quicksend.ch',
'quipas.com',
'ququb.com', 'ququb.com',
'qvy.me', 'qvy.me',
'qwickmail.com', 'qwickmail.com',
@ -2496,6 +2612,7 @@ class BlackListRule implements Rule
'raetp9.com', 'raetp9.com',
'rainbowly.ml', 'rainbowly.ml',
'raketenmann.de', 'raketenmann.de',
'ramenmail.de',
'rancidhome.net', 'rancidhome.net',
'randomail.io', 'randomail.io',
'randomail.net', 'randomail.net',
@ -2512,6 +2629,7 @@ class BlackListRule implements Rule
're-gister.com', 're-gister.com',
'reality-concept.club', 'reality-concept.club',
'reallymymail.com', 'reallymymail.com',
'realquickemail.com',
'realtyalerts.ca', 'realtyalerts.ca',
'rebates.stream', 'rebates.stream',
'receiveee.com', 'receiveee.com',
@ -2542,6 +2660,7 @@ class BlackListRule implements Rule
'rippb.com', 'rippb.com',
'risingsuntouch.com', 'risingsuntouch.com',
'riski.cf', 'riski.cf',
'risu.be',
'rklips.com', 'rklips.com',
'rkomo.com', 'rkomo.com',
'rm2rf.com', 'rm2rf.com',
@ -2578,6 +2697,7 @@ class BlackListRule implements Rule
's33db0x.com', 's33db0x.com',
'sabrestlouis.com', 'sabrestlouis.com',
'sackboii.com', 'sackboii.com',
'saeoil.com',
'safaat.cf', 'safaat.cf',
'safermail.info', 'safermail.info',
'safersignup.de', 'safersignup.de',
@ -2630,10 +2750,13 @@ class BlackListRule implements Rule
'sexforswingers.com', 'sexforswingers.com',
'sexical.com', 'sexical.com',
'sexyalwasmi.top', 'sexyalwasmi.top',
'sfolkar.com',
'shadap.org', 'shadap.org',
'shalar.net', 'shalar.net',
'sharedmailbox.org', 'sharedmailbox.org',
'sharkfaces.com',
'sharklasers.com', 'sharklasers.com',
'shchiba.uk',
'sheryli.com', 'sheryli.com',
'shhmail.com', 'shhmail.com',
'shhuut.org', 'shhuut.org',
@ -2662,6 +2785,7 @@ class BlackListRule implements Rule
'sify.com', 'sify.com',
'sika3.com', 'sika3.com',
'sikux.com', 'sikux.com',
'silenceofthespam.com',
'siliwangi.ga', 'siliwangi.ga',
'silvercoin.life', 'silvercoin.life',
'sim-simka.ru', 'sim-simka.ru',
@ -2681,6 +2805,7 @@ class BlackListRule implements Rule
'skrx.tk', 'skrx.tk',
'sky-inbox.com', 'sky-inbox.com',
'sky-ts.de', 'sky-ts.de',
'skygazerhub.com',
'skyrt.de', 'skyrt.de',
'slapsfromlastnight.com', 'slapsfromlastnight.com',
'slaskpost.se', 'slaskpost.se',
@ -2698,6 +2823,7 @@ class BlackListRule implements Rule
'smapfree24.eu', 'smapfree24.eu',
'smapfree24.info', 'smapfree24.info',
'smapfree24.org', 'smapfree24.org',
'smartnator.com',
'smarttalent.pw', 'smarttalent.pw',
'smashmail.de', 'smashmail.de',
'smellfear.com', 'smellfear.com',
@ -2705,12 +2831,14 @@ class BlackListRule implements Rule
'smellypotato.tk', 'smellypotato.tk',
'smtp99.com', 'smtp99.com',
'smwg.info', 'smwg.info',
'snakebutt.com',
'snakemail.com', 'snakemail.com',
'snapwet.com', 'snapwet.com',
'sneakmail.de', 'sneakmail.de',
'snece.com', 'snece.com',
'social-mailer.tk', 'social-mailer.tk',
'socialfurry.org', 'socialfurry.org',
'sociallymediocre.com',
'sofia.re', 'sofia.re',
'sofimail.com', 'sofimail.com',
'sofort-mail.de', 'sofort-mail.de',
@ -2731,6 +2859,7 @@ class BlackListRule implements Rule
'soodmail.com', 'soodmail.com',
'soodomail.com', 'soodomail.com',
'soodonims.com', 'soodonims.com',
'soombo.com',
'soon.it', 'soon.it',
'spacebazzar.ru', 'spacebazzar.ru',
'spam-be-gone.com', 'spam-be-gone.com',
@ -2763,6 +2892,7 @@ class BlackListRule implements Rule
'spamday.com', 'spamday.com',
'spamdecoy.net', 'spamdecoy.net',
'spamex.com', 'spamex.com',
'spamfellas.com',
'spamfighter.cf', 'spamfighter.cf',
'spamfighter.ga', 'spamfighter.ga',
'spamfighter.gq', 'spamfighter.gq',
@ -2791,6 +2921,7 @@ class BlackListRule implements Rule
'spamobox.com', 'spamobox.com',
'spamoff.de', 'spamoff.de',
'spamsalad.in', 'spamsalad.in',
'spamsandwich.com',
'spamslicer.com', 'spamslicer.com',
'spamsphere.com', 'spamsphere.com',
'spamspot.com', 'spamspot.com',
@ -2810,11 +2941,13 @@ class BlackListRule implements Rule
'spikio.com', 'spikio.com',
'spindl-e.com', 'spindl-e.com',
'spoofmail.de', 'spoofmail.de',
'sportrid.com',
'spr.io', 'spr.io',
'spritzzone.de', 'spritzzone.de',
'spruzme.com', 'spruzme.com',
'spybox.de', 'spybox.de',
'spymail.com', 'spymail.com',
'spymail.one',
'squizzy.de', 'squizzy.de',
'squizzy.net', 'squizzy.net',
'sroff.com', 'sroff.com',
@ -2852,10 +2985,12 @@ class BlackListRule implements Rule
'submic.com', 'submic.com',
'suburbanthug.com', 'suburbanthug.com',
'suckmyd.com', 'suckmyd.com',
'sudern.de',
'sueshaw.com', 'sueshaw.com',
'suexamplesb.com', 'suexamplesb.com',
'suioe.com', 'suioe.com',
'super-auswahl.de', 'super-auswahl.de',
'superblohey.com',
'supergreatmail.com', 'supergreatmail.com',
'supermailer.jp', 'supermailer.jp',
'superplatyna.com', 'superplatyna.com',
@ -2872,6 +3007,7 @@ class BlackListRule implements Rule
'sweetxxx.de', 'sweetxxx.de',
'swift-mail.net', 'swift-mail.net',
'swift10minutemail.com', 'swift10minutemail.com',
'syinxun.com',
'sylvannet.com', 'sylvannet.com',
'symphonyresume.com', 'symphonyresume.com',
'syosetu.gq', 'syosetu.gq',
@ -2891,6 +3027,8 @@ class BlackListRule implements Rule
'tastrg.com', 'tastrg.com',
'taukah.com', 'taukah.com',
'tb-on-line.net', 'tb-on-line.net',
'tcwlm.com',
'tcwlx.com',
'tdtda.com', 'tdtda.com',
'tech69.com', 'tech69.com',
'techblast.ch', 'techblast.ch',
@ -2901,6 +3039,7 @@ class BlackListRule implements Rule
'teewars.org', 'teewars.org',
'tefl.ro', 'tefl.ro',
'telecomix.pl', 'telecomix.pl',
'teleg.eu',
'teleworm.com', 'teleworm.com',
'teleworm.us', 'teleworm.us',
'tellos.xyz', 'tellos.xyz',
@ -2965,6 +3104,7 @@ class BlackListRule implements Rule
'thecloudindex.com', 'thecloudindex.com',
'thediamants.org', 'thediamants.org',
'thedirhq.info', 'thedirhq.info',
'theeyeoftruth.com',
'thejoker5.com', 'thejoker5.com',
'thelightningmail.net', 'thelightningmail.net',
'thelimestones.com', 'thelimestones.com',
@ -2974,6 +3114,7 @@ class BlackListRule implements Rule
'thereddoors.online', 'thereddoors.online',
'theroyalweb.club', 'theroyalweb.club',
'thescrappermovie.com', 'thescrappermovie.com',
'thespamfather.com',
'theteastory.info', 'theteastory.info',
'thex.ro', 'thex.ro',
'thichanthit.com', 'thichanthit.com',
@ -3010,9 +3151,13 @@ class BlackListRule implements Rule
'tkitc.de', 'tkitc.de',
'tlpn.org', 'tlpn.org',
'tmail.com', 'tmail.com',
'tmail.io',
'tmail.ws', 'tmail.ws',
'tmail3.com',
'tmail9.com',
'tmailinator.com', 'tmailinator.com',
'tmails.net', 'tmails.net',
'tmmbt.net',
'tmpbox.net', 'tmpbox.net',
'tmpemails.com', 'tmpemails.com',
'tmpeml.com', 'tmpeml.com',
@ -3020,6 +3165,7 @@ class BlackListRule implements Rule
'tmpjr.me', 'tmpjr.me',
'tmpmail.net', 'tmpmail.net',
'tmpmail.org', 'tmpmail.org',
'tmpx.sa.com',
'toddsbighug.com', 'toddsbighug.com',
'tofeat.com', 'tofeat.com',
'toiea.com', 'toiea.com',
@ -3049,6 +3195,7 @@ class BlackListRule implements Rule
'totoan.info', 'totoan.info',
'tourcc.com', 'tourcc.com',
'tp-qa-mail.com', 'tp-qa-mail.com',
'tpwlb.com',
'tqoai.com', 'tqoai.com',
'tqosi.com', 'tqosi.com',
'tradermail.info', 'tradermail.info',
@ -3097,6 +3244,7 @@ class BlackListRule implements Rule
'trialmail.de', 'trialmail.de',
'trickmail.net', 'trickmail.net',
'trillianpro.com', 'trillianpro.com',
'triots.com',
'trixtrux1.ru', 'trixtrux1.ru',
'trollproject.com', 'trollproject.com',
'tropicalbass.info', 'tropicalbass.info',
@ -3112,6 +3260,7 @@ class BlackListRule implements Rule
'turoid.com', 'turoid.com',
'turual.com', 'turual.com',
'turuma.com', 'turuma.com',
'tutuapp.bid',
'tvchd.com', 'tvchd.com',
'tverya.com', 'tverya.com',
'twinmail.de', 'twinmail.de',
@ -3150,6 +3299,7 @@ class BlackListRule implements Rule
'unit7lahaina.com', 'unit7lahaina.com',
'unmail.ru', 'unmail.ru',
'uooos.com', 'uooos.com',
'uorak.com',
'upliftnow.com', 'upliftnow.com',
'uplipht.com', 'uplipht.com',
'uploadnolimit.com', 'uploadnolimit.com',
@ -3268,6 +3418,7 @@ class BlackListRule implements Rule
'watchever.biz', 'watchever.biz',
'watchfull.net', 'watchfull.net',
'watchironman3onlinefreefullmovie.com', 'watchironman3onlinefreefullmovie.com',
'waterisgone.com',
'wazabi.club', 'wazabi.club',
'wbdev.tech', 'wbdev.tech',
'wbml.net', 'wbml.net',
@ -3307,6 +3458,7 @@ class BlackListRule implements Rule
'wegwrfmail.de', 'wegwrfmail.de',
'wegwrfmail.net', 'wegwrfmail.net',
'wegwrfmail.org', 'wegwrfmail.org',
'weizixu.com',
'wekawa.com', 'wekawa.com',
'welikecookies.com', 'welikecookies.com',
'wellsfargocomcardholders.com', 'wellsfargocomcardholders.com',
@ -3316,6 +3468,7 @@ class BlackListRule implements Rule
'wfgdfhj.tk', 'wfgdfhj.tk',
'wg0.com', 'wg0.com',
'wh4f.org', 'wh4f.org',
'whaaaaaaaaaat.com',
'whatiaas.com', 'whatiaas.com',
'whatifanalytics.com', 'whatifanalytics.com',
'whatpaas.com', 'whatpaas.com',
@ -3327,6 +3480,7 @@ class BlackListRule implements Rule
'wickmail.net', 'wickmail.net',
'widaryanto.info', 'widaryanto.info',
'widget.gg', 'widget.gg',
'wiemei.com',
'wierie.tk', 'wierie.tk',
'wifimaple.com', 'wifimaple.com',
'wifioak.com', 'wifioak.com',
@ -3355,6 +3509,7 @@ class BlackListRule implements Rule
'wudet.men', 'wudet.men',
'wuespdj.xyz', 'wuespdj.xyz',
'wupics.com', 'wupics.com',
'wuuvo.com',
'wuzup.net', 'wuzup.net',
'wuzupmail.net', 'wuzupmail.net',
'wwjmp.com', 'wwjmp.com',
@ -3466,6 +3621,7 @@ class BlackListRule implements Rule
'zebins.eu', 'zebins.eu',
'zehnminuten.de', 'zehnminuten.de',
'zehnminutenmail.de', 'zehnminutenmail.de',
'zemzar.net',
'zepp.dk', 'zepp.dk',
'zetmail.com', 'zetmail.com',
'zfymail.com', 'zfymail.com',
@ -3476,6 +3632,7 @@ class BlackListRule implements Rule
'zhorachu.com', 'zhorachu.com',
'zik.dj', 'zik.dj',
'zipcad.com', 'zipcad.com',
'zipcatfish.com',
'zipo1.gq', 'zipo1.gq',
'zippymail.info', 'zippymail.info',
'zipsendtest.com', 'zipsendtest.com',
@ -3497,27 +3654,13 @@ class BlackListRule implements Rule
'zzz.com', 'zzz.com',
]; ];
/** public function validate(string $attribute, mixed $value, Closure $fail): void
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value): bool
{ {
$parts = explode("@", $value); $parts = explode("@", $value);
if (is_array($parts)) { if (is_array($parts) && in_array($parts[1], $this->blacklist)) {
return ! in_array($parts[1], $this->blacklist); $fail('This domain is blacklisted, if you think this is in error, please email contact@invoiceninja.com');
} else {
return true;
} }
} }
/**
* @return string
*/
public function message(): string
{
return 'This domain is blacklisted, if you think this is in error, please email contact@invoiceninja.com';
}
} }

View File

@ -11,32 +11,26 @@
namespace App\Http\ValidationRules\Account; namespace App\Http\ValidationRules\Account;
use Illuminate\Contracts\Validation\Rule; use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
/** /**
* Class EmailBlackListRule. * Class EmailBlackListRule.
*/ */
class EmailBlackListRule implements Rule class EmailBlackListRule implements ValidationRule
{ {
public array $blacklist = [ public array $blacklist = [
'noddy@invoiceninja.com',
]; ];
/**
* @param string $attribute public function validate(string $attribute, mixed $value, Closure $fail): void
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{ {
return ! in_array($value, $this->blacklist);
if (in_array($value, $this->blacklist)) {
$fail('This email address is blacklisted, if you think this is in error, please email contact@invoiceninja.com');
}
} }
/**
* @return string
*/
public function message()
{
return 'This email address is blacklisted, if you think this is in error, please email contact@invoiceninja.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;
private bool $stop_loop = false;
/**
* 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()) {
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

@ -549,7 +549,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();
}
if ($account->isPaid() && $account->plan == 'enterprise') { } else {
$account->bank_integrations()->where('auto_sync', true)->cursor()->each(function ($bank_integration) use ($account) { $this->processYodlee();
(new ProcessBankTransactions($account->bank_integration_account_id, $bank_integration))->handle(); $this->processNordigen();
}
nlog("syncing transactions - done");
}
private function processYodlee()
{
if (Ninja::isHosted()) {
nlog("syncing transactions - yodlee");
Account::with('bank_integrations')->whereNotNull('bank_integration_account_id')->cursor()->each(function ($account) {
if ($account->isEnterprisePaidClient()) {
$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();
}); });
} }
});
}
}
private function processNordigen()
{
if (config("ninja.nordigen.secret_id") && config("ninja.nordigen.secret_key")) {
nlog("syncing transactions - nordigen");
Account::with('bank_integrations')->cursor()->each(function ($account) {
if ((Ninja::isSelfHost() || (Ninja::isHosted() && $account->isEnterprisePaidClient()))) {
$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,26 @@
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; use App\Notifications\Ninja\EmailBounceNotification;
class ProcessPostmarkWebhook implements ShouldQueue class ProcessPostmarkWebhook implements ShouldQueue
{ {
@ -82,9 +84,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;
} }
@ -97,6 +104,11 @@ class ProcessPostmarkWebhook implements ShouldQueue
case 'Delivery': case 'Delivery':
return $this->processDelivery(); return $this->processDelivery();
case 'Bounce': case 'Bounce':
if($this->request['Subject'] == ctrans('texts.confirmation_subject')) {
$company->notification(new EmailBounceNotification($this->request['Email']))->ninja();
}
return $this->processBounce(); return $this->processBounce();
case 'SpamComplaint': case 'SpamComplaint':
return $this->processSpamComplaint(); return $this->processSpamComplaint();
@ -257,8 +269,6 @@ class ProcessPostmarkWebhook implements ShouldQueue
(new SystemLogger($data, SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_BOUNCED, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company))->handle(); (new SystemLogger($data, SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_BOUNCED, SystemLog::TYPE_WEBHOOK_RESPONSE, $this->invitation->contact->client, $this->invitation->company))->handle();
// if(config('ninja.notification.slack'))
// $this->invitation->company->notification(new EmailBounceNotification($this->invitation->company->account))->ninja();
} }
// { // {
@ -305,14 +315,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");
} }
} }

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