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

This commit is contained in:
paulwer 2024-08-28 07:02:43 +02:00
commit 13dae4f524
674 changed files with 378255 additions and 336829 deletions

View File

@ -8,7 +8,7 @@ assignees: ''
---
<!-- Before posting please check our "Troubleshooting" category in the docs:
https://invoiceninja.github.io/docs/self-host-troubleshooting/ -->
https://invoiceninja.github.io/en/self-host-troubleshooting/ -->
## Setup
- Version: <!-- i.e. v4.5.25 / v5.0.30 -->

View File

@ -51,6 +51,8 @@ All Pro and Enterprise features from the hosted app are included in the open-sou
* [Docker File](https://hub.docker.com/r/invoiceninja/invoiceninja/)
* [Cloudron](https://www.cloudron.io/store/com.invoiceninja.cloudronapp2.html)
* [Softaculous](https://www.softaculous.com/apps/ecommerce/Invoice_Ninja)
* [Elestio](https://elest.io/open-source/invoiceninja)
* [YunoHost](https://apps.yunohost.org/app/invoiceninja5)
### Recommended Providers
* [Stripe](https://stripe.com/)

View File

@ -1 +1 @@
5.10.16
5.10.26

View File

@ -0,0 +1,50 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Casts;
use App\DataMapper\QuickbooksSettings;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
class QuickbooksSettingsCast implements CastsAttributes
{
public function get($model, string $key, $value, array $attributes)
{
$data = json_decode($value, true);
if(!is_array($data))
return null;
$qb = new QuickbooksSettings();
$qb->accessTokenKey = $data['accessTokenKey'];
$qb->refresh_token = $data['refresh_token'];
$qb->realmID = $data['realmID'];
$qb->accessTokenExpiresAt = $data['accessTokenExpiresAt'];
$qb->refreshTokenExpiresAt = $data['refreshTokenExpiresAt'];
$qb->settings = $data['settings'] ?? [];
return $qb;
}
public function set($model, string $key, $value, array $attributes)
{
return [
$key => json_encode([
'accessTokenKey' => $value->accessTokenKey,
'refresh_token' => $value->refresh_token,
'realmID' => $value->realmID,
'accessTokenExpiresAt' => $value->accessTokenExpiresAt,
'refreshTokenExpiresAt' => $value->refreshTokenExpiresAt,
'settings' => $value->settings,
])
];
}
}

View File

@ -15,7 +15,6 @@ use App\Jobs\Cron\AutoBillCron;
use App\Jobs\Cron\RecurringExpensesCron;
use App\Jobs\Cron\RecurringInvoicesCron;
use App\Jobs\Cron\SubscriptionCron;
use App\Jobs\Cron\UpdateCalculatedFields;
use App\Jobs\Invoice\InvoiceCheckLateWebhook;
use App\Jobs\Ninja\AdjustEmailQuota;
use App\Jobs\Ninja\BankTransactionSync;
@ -33,6 +32,7 @@ use App\Jobs\Util\SchedulerCheck;
use App\Jobs\Util\UpdateExchangeRates;
use App\Jobs\Util\VersionCheck;
use App\Models\Account;
use App\PaymentDrivers\Rotessa\Jobs\TransactionReport;
use App\Utils\Ninja;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@ -65,11 +65,11 @@ class Kernel extends ConsoleKernel
/* Checks for scheduled tasks */
$schedule->job(new TaskScheduler())->hourlyAt(10)->withoutOverlapping()->name('task-scheduler-job')->onOneServer();
/* Stale Invoice Cleanup*/
$schedule->job(new CleanStaleInvoiceOrder())->hourlyAt(30)->withoutOverlapping()->name('stale-invoice-job')->onOneServer();
/* Checks Rotessa Transactions */
$schedule->job(new TransactionReport())->dailyAt('01:48')->withoutOverlapping()->name('rotessa-transaction-report')->onOneServer();
/* Stale Invoice Cleanup*/
$schedule->job(new UpdateCalculatedFields())->hourlyAt(40)->withoutOverlapping()->name('update-calculated-fields-job')->onOneServer();
$schedule->job(new CleanStaleInvoiceOrder())->hourlyAt(30)->withoutOverlapping()->name('stale-invoice-job')->onOneServer();
/* Checks for large companies and marked them as is_large */
$schedule->job(new CompanySizeCheck())->dailyAt('23:20')->withoutOverlapping()->name('company-size-job')->onOneServer();

View File

@ -516,9 +516,10 @@ class CompanySettings extends BaseSettings
public $quote_late_fee_amount1 = 0;
public $quote_late_fee_percent1 = 0;
public string $payment_flow = 'default'; //smooth
public static $casts = [
'payment_flow' => 'string',
'enable_quote_reminder1' => 'bool',
'quote_num_days_reminder1' => 'int',
'quote_schedule_reminder1' => 'string',

View File

@ -0,0 +1,59 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataMapper;
use Illuminate\Contracts\Database\Eloquent\Castable;
use App\Casts\QuickbooksSettingsCast;
/**
* QuickbooksSettings.
*/
class QuickbooksSettings implements Castable
{
public string $accessTokenKey;
public string $refresh_token;
public string $realmID;
public int $accessTokenExpiresAt;
public int $refreshTokenExpiresAt;
/**
* entity client,invoice,quote,purchase_order,vendor,payment
* sync true/false
* update_record true/false
* direction push/pull/birectional
* */
public array $settings = [
'client' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
'vendor' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
'invoice' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
'quote' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
'purchase_order' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
'product' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
'payment' => ['sync' => true, 'update_record' => true, 'direction' => 'bidirectional'],
];
/**
* Get the name of the caster class to use when casting from / to this cast target.
*
* @param array<string, mixed> $arguments
*/
public static function castUsing(array $arguments): string
{
return QuickbooksSettingsCast::class;
}
}

View File

@ -17,6 +17,7 @@ use App\Models\Invoice;
use App\Models\Product;
use App\DataProviders\USStates;
use App\DataMapper\Tax\ZipTax\Response;
use App\Models\RecurringInvoice;
class BaseRule implements RuleInterface
{
@ -132,7 +133,7 @@ class BaseRule implements RuleInterface
public function shouldCalcTax(): bool
{
return $this->should_calc_tax;
return $this->should_calc_tax && $this->checkIfInvoiceLocked();
}
/**
* Initializes the tax rule for the entity.
@ -215,7 +216,7 @@ class BaseRule implements RuleInterface
$this->invoice->tax_data = $tax_data;
if(\DB::transactionLevel() == 0) {
if(\DB::transactionLevel() == 0 && isset($this->invoice->id)) {
try {
$this->invoice->saveQuietly();
@ -400,4 +401,40 @@ class BaseRule implements RuleInterface
return ! in_array($iso_3166_2, array_merge($this->eu_country_codes, array_keys($this->region_codes)));
}
private function checkIfInvoiceLocked(): bool
{
$lock_invoices = $this->client->getSetting('lock_invoices');
if($this->invoice instanceof RecurringInvoice) {
return true;
}
switch ($lock_invoices) {
case 'off':
return true;
case 'when_sent':
if ($this->invoice->status_id == Invoice::STATUS_SENT) {
return false;
}
return true;
case 'when_paid':
if ($this->invoice->status_id == Invoice::STATUS_PAID) {
return false;
}
return true;
//if now is greater than the end of month the invoice was dated - do not modify
case 'end_of_month':
if(\Carbon\Carbon::parse($this->invoice->date)->setTimezone($this->invoice->company->timezone()->name)->endOfMonth()->lte(now())) {
return false;
}
return true;
default:
return true;
}
}
}

View File

@ -43,6 +43,8 @@ class Rule extends BaseRule implements RuleInterface
public float $reduced_tax_rate = 0;
public string $tax_name1 = 'MwSt.';
private string $tax_name;
/**
* Initializes the rules and builds any required data.
*
@ -50,6 +52,7 @@ class Rule extends BaseRule implements RuleInterface
*/
public function init(): self
{
$this->tax_name = $this->tax_name1;
$this->calculateRates();
return $this;
@ -91,6 +94,7 @@ class Rule extends BaseRule implements RuleInterface
*/
public function reverseTax($item): self
{
$this->tax_name1 = $this->tax_name;
$this->tax_rate1 = 0;
return $this;
@ -103,6 +107,8 @@ class Rule extends BaseRule implements RuleInterface
*/
public function taxReduced($item): self
{
$this->tax_name1 = $this->tax_name;
$this->tax_rate1 = $this->reduced_tax_rate;
return $this;
@ -115,6 +121,8 @@ class Rule extends BaseRule implements RuleInterface
*/
public function zeroRated($item): self
{
$this->tax_name1 = $this->tax_name;
$this->tax_rate1 = 0;
return $this;
@ -142,6 +150,7 @@ class Rule extends BaseRule implements RuleInterface
public function taxDigital($item): self
{
$this->tax_name1 = $this->tax_name;
$this->tax_rate1 = $this->tax_rate;
return $this;
@ -155,6 +164,7 @@ class Rule extends BaseRule implements RuleInterface
public function taxService($item): self
{
$this->tax_name1 = $this->tax_name;
$this->tax_rate1 = $this->tax_rate;
return $this;
@ -168,6 +178,7 @@ class Rule extends BaseRule implements RuleInterface
public function taxShipping($item): self
{
$this->tax_name1 = $this->tax_name;
$this->tax_rate1 = $this->tax_rate;
return $this;
@ -181,6 +192,7 @@ class Rule extends BaseRule implements RuleInterface
public function taxPhysical($item): self
{
$this->tax_name1 = $this->tax_name;
$this->tax_rate1 = $this->tax_rate;
return $this;
@ -229,8 +241,7 @@ class Rule extends BaseRule implements RuleInterface
// nlog("tax exempt");
$this->tax_rate = 0;
$this->reduced_tax_rate = 0;
} elseif($this->client_subregion != $this->client->company->tax_data->seller_subregion && in_array($this->client_subregion, $this->eu_country_codes) && $this->client->vat_number && $this->eu_business_tax_exempt) {
// elseif($this->client_subregion != $this->client->company->tax_data->seller_subregion && in_array($this->client_subregion, $this->eu_country_codes) && $this->client->has_valid_vat_number && $this->eu_business_tax_exempt)
} elseif($this->client_subregion != $this->client->company->tax_data->seller_subregion && in_array($this->client_subregion, $this->eu_country_codes) && $this->client->vat_number && $this->client->has_valid_vat_number && $this->eu_business_tax_exempt) {
// nlog("euro zone and tax exempt");
$this->tax_rate = 0;
$this->reduced_tax_rate = 0;
@ -240,8 +251,8 @@ class Rule extends BaseRule implements RuleInterface
$this->reduced_tax_rate = 0;
} elseif(!in_array($this->client_subregion, $this->eu_country_codes)) {
$this->defaultForeign();
} elseif(in_array($this->client_subregion, $this->eu_country_codes) && !$this->client->vat_number) { //eu country / no valid vat
if(($this->client->company->tax_data->seller_subregion != $this->client_subregion) && $this->client->company->tax_data->regions->EU->has_sales_above_threshold) {
} elseif(in_array($this->client_subregion, $this->eu_country_codes) && ((strlen($this->client->vat_number ?? '') == 1) || !$this->client->has_valid_vat_number)) { //eu country / no valid vat
if($this->client->company->tax_data->seller_subregion != $this->client_subregion) {
// nlog("eu zone with sales above threshold");
$this->tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->tax_rate ?? 0;
$this->reduced_tax_rate = $this->client->company->tax_data->regions->EU->subregions->{$this->client->country->iso_3166_2}->reduced_tax_rate ?? 0;

View File

@ -48,8 +48,7 @@ class TaxModel
public function migrate(): self
{
if($this->version == 'alpha')
{
if($this->version == 'alpha') {
$this->regions->EU->subregions->PL = new \stdClass();
$this->regions->EU->subregions->PL->tax_rate = 23;
$this->regions->EU->subregions->PL->tax_name = 'VAT';

View File

@ -0,0 +1,68 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataProviders;
final class CAProvinces
{
/**
* The provinces and territories of Canada
*
* @var array
*/
protected static $provinces = [
'AB' => 'Alberta',
'BC' => 'British Columbia',
'MB' => 'Manitoba',
'NB' => 'New Brunswick',
'NL' => 'Newfoundland And Labrador',
'NS' => 'Nova Scotia',
'ON' => 'Ontario',
'PE' => 'Prince Edward Island',
'QC' => 'Quebec',
'SK' => 'Saskatchewan',
'NT' => 'Northwest Territories',
'NU' => 'Nunavut',
'YT' => 'Yukon'
];
/**
* Get the name of the province or territory for a given abbreviation.
*
* @param string $abbreviation
* @return string
*/
public static function getName($abbreviation)
{
return self::$provinces[$abbreviation];
}
/**
* Get all provinces and territories.
*
* @return array
*/
public static function get()
{
return self::$provinces;
}
/**
* Get the abbreviation for a given province or territory name.
*
* @param string $name
* @return string
*/
public static function getAbbreviation($name)
{
return array_search(ucwords($name), self::$provinces);
}
}

View File

@ -451,6 +451,7 @@ class BaseExport
'project' => 'task.project_id',
'billable' => 'task.billable',
'item_notes' => 'task.item_notes',
'time_log' => 'task.time_log',
];
protected array $forced_client_fields = [
@ -1258,7 +1259,7 @@ class BaseExport
$date_range = $this->input['date_range'];
if (array_key_exists('date_key', $this->input) && strlen($this->input['date_key']) > 1 && ($table_name && $this->columnExists($table_name, $this->input['date_key']))) {
if (array_key_exists('date_key', $this->input) && strlen($this->input['date_key'] ?? '') > 1 && ($table_name && $this->columnExists($table_name, $this->input['date_key']))) {
$this->date_key = $this->input['date_key'];
}

View File

@ -269,8 +269,7 @@ class ExpenseExport extends BaseExport
if($expense->uses_inclusive_taxes) {
$entity['expense.net_amount'] = round($expense->amount, $precision) - $total_tax_amount;
}
else {
} else {
$entity['expense.net_amount'] = round($expense->amount, $precision);
}

View File

@ -229,10 +229,6 @@ class InvoiceItemExport extends BaseExport
// $entity['currency'] = $invoice->client->currency() ? $invoice->client->currency()->code : $invoice->company->currency()->code;
// }
// if(array_key_exists('type', $entity)) {
// $entity['type'] = $invoice->typeIdString($entity['type']);
// }
// if(array_key_exists('tax_category', $entity)) {
// $entity['tax_category'] = $invoice->taxTypeString($entity['tax_category']);
// }

View File

@ -213,10 +213,6 @@ class PurchaseOrderItemExport extends BaseExport
// $entity['currency'] = $purchase_order->vendor->currency() ? $purchase_order->vendor->currency()->code : $purchase_order->company->currency()->code;
// }
// if(array_key_exists('type', $entity)) {
// $entity['type'] = $purchase_order->typeIdString($entity['type']);
// }
// if(array_key_exists('tax_category', $entity)) {
// $entity['tax_category'] = $purchase_order->taxTypeString($entity['tax_category']);
// }

View File

@ -29,7 +29,7 @@ class TaskExport extends BaseExport
{
private $entity_transformer;
public string $date_key = 'created_at';
public string $date_key = 'calculated_start_date';
private string $date_format = 'Y-m-d';
@ -156,7 +156,7 @@ class TaskExport extends BaseExport
$entity[$key] = $transformed_entity[$parts[1]];
} elseif (array_key_exists($key, $transformed_entity)) {
$entity[$key] = $transformed_entity[$key];
} elseif (in_array($key, ['task.start_date', 'task.end_date', 'task.duration', 'task.billable', 'task.item_notes'])) {
} elseif (in_array($key, ['task.start_date', 'task.end_date', 'task.duration', 'task.billable', 'task.item_notes', 'task.time_log'])) {
$entity[$key] = '';
} else {
$entity[$key] = $this->decorator->transform($key, $task);
@ -207,6 +207,9 @@ class TaskExport extends BaseExport
$seconds = $task->calcDuration();
$entity['task.duration'] = $seconds;
$entity['task.duration_words'] = $seconds > 86400 ? CarbonInterval::seconds($seconds)->locale($this->company->locale())->cascade()->forHumans() : now()->startOfDay()->addSeconds($seconds)->format('H:i:s');
$entity['task.time_log'] = (isset($item[1]) && $item[1] != 0) ? $item[1] - $item[0] : ctrans('texts.is_running');
}
if (in_array('task.billable', $this->input['report_keys']) || in_array('billable', $this->input['report_keys'])) {

View File

@ -98,7 +98,14 @@ class CreditFilters extends QueryFilters
->orWhere('last_name', 'like', '%'.$filter.'%')
->orWhere('email', 'like', '%'.$filter.'%');
})
->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']);
->orWhereRaw("
JSON_UNQUOTE(JSON_EXTRACT(
JSON_ARRAY(
JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')),
JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].product_key'))
), '$[*]')
) LIKE ?", ['%'.$filter.'%']);
// ->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']);
});
}

View File

@ -158,6 +158,19 @@ class ExpenseFilters extends QueryFilters
return $this->builder;
}
public function categories(string $categories = ''): Builder
{
$categories_exploded = explode(",", $categories);
if(empty($categories) || count(array_filter($categories_exploded)) == 0) {
return $this->builder;
}
$categories_keys = $this->transformKeys($categories_exploded);
return $this->builder->whereIn('category_id', $categories_keys);
}
public function number(string $number = ''): Builder
{
if (strlen($number) == 0) {
@ -205,6 +218,11 @@ class ExpenseFilters extends QueryFilters
->whereColumn('expense_categories.id', 'expenses.category_id'), $sort_col[1]);
}
if ($sort_col[0] == 'payment_date' && in_array($sort_col[1], ['asc', 'desc'])) {
return $this->builder
->orderByRaw('ISNULL(payment_date), payment_date '. $sort_col[1]);
}
if($sort_col[0] == 'number') {
return $this->builder->orderByRaw("REGEXP_REPLACE(number,'[^0-9]+','')+0 " . $dir);
}

View File

@ -125,7 +125,14 @@ class InvoiceFilters extends QueryFilters
->orWhere('last_name', 'like', '%'.$filter.'%')
->orWhere('email', 'like', '%'.$filter.'%');
})
->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']);
->orWhereRaw("
JSON_UNQUOTE(JSON_EXTRACT(
JSON_ARRAY(
JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')),
JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].product_key'))
), '$[*]')
) LIKE ?", ['%'.$filter.'%']);
// ->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']);
});
}

View File

@ -60,7 +60,7 @@ class ProjectFilters extends QueryFilters
{
$sort_col = explode('|', $sort);
if (!is_array($sort_col) || count($sort_col) != 2) {
if (!is_array($sort_col) || count($sort_col) != 2 || !in_array($sort_col[0], \Illuminate\Support\Facades\Schema::getColumnListing('projects'))) {
return $this->builder;
}

View File

@ -96,7 +96,14 @@ class PurchaseOrderFilters extends QueryFilters
->orWhere('custom_value4', 'like', '%'.$filter.'%')
->orWhereHas('vendor', function ($q) use ($filter) {
$q->where('name', 'like', '%'.$filter.'%');
});
})
->orWhereRaw("
JSON_UNQUOTE(JSON_EXTRACT(
JSON_ARRAY(
JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')),
JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].product_key'))
), '$[*]')
) LIKE ?", ['%'.$filter.'%']);
});
}

View File

@ -46,7 +46,14 @@ class QuoteFilters extends QueryFilters
->orWhere('last_name', 'like', '%'.$filter.'%')
->orWhere('email', 'like', '%'.$filter.'%');
})
->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']);
->orWhereRaw("
JSON_UNQUOTE(JSON_EXTRACT(
JSON_ARRAY(
JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')),
JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].product_key'))
), '$[*]')
) LIKE ?", ['%'.$filter.'%']);
// ->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']);
});
}

View File

@ -49,7 +49,14 @@ class RecurringInvoiceFilters extends QueryFilters
->orWhere('last_name', 'like', '%'.$filter.'%')
->orWhere('email', 'like', '%'.$filter.'%');
})
->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']);
->orWhereRaw("
JSON_UNQUOTE(JSON_EXTRACT(
JSON_ARRAY(
JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')),
JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].product_key'))
), '$[*]')
) LIKE ?", ['%'.$filter.'%']);
//->orWhereRaw("JSON_UNQUOTE(JSON_EXTRACT(line_items, '$[*].notes')) LIKE ?", ['%'.$filter.'%']);
});
}

View File

@ -85,6 +85,10 @@ class TaskFilters extends QueryFilters
$this->builder->whereNull('invoice_id');
}
if (in_array('is_running', $status_parameters)) {
$this->builder->where('is_running', true);
}
return $this->builder;
}

View File

@ -99,7 +99,7 @@ class TransactionTransformer implements BankRevenueInterface
} elseif (array_key_exists('internalTransactionId', $transaction)) {
$transactionId = $transaction["internalTransactionId"];
} else {
nlog(`Invalid Input for nordigen transaction transformer: ` . $transaction);
nlog('Invalid Input for nordigen transaction transformer: ' . $transaction);
throw new \Exception('invalid dataset: missing transactionId - Please report this error to the developer');
}

View File

@ -15,6 +15,7 @@ use App\Models\Quote;
use App\Utils\Number;
use App\Models\Client;
use App\Models\Credit;
use App\Models\Vendor;
use App\Models\Invoice;
use App\Models\PurchaseOrder;
use App\Models\RecurringQuote;
@ -121,7 +122,7 @@ class InvoiceItemSum
private $tax_collection;
private ?Client $client;
private Client | Vendor $client;
private bool $calc_tax = false;
@ -132,10 +133,10 @@ class InvoiceItemSum
$this->tax_collection = collect([]);
$this->invoice = $invoice;
$this->client = $invoice->client ?? $invoice->vendor;
if ($this->invoice->client) {
$this->currency = $this->invoice->client->currency();
$this->client = $this->invoice->client;
$this->shouldCalculateTax();
} else {
$this->currency = $this->invoice->vendor->currency();

View File

@ -15,6 +15,7 @@ use App\Models\Quote;
use App\Utils\Number;
use App\Models\Client;
use App\Models\Credit;
use App\Models\Vendor;
use App\Models\Invoice;
use App\Models\PurchaseOrder;
use App\Models\RecurringQuote;
@ -110,7 +111,7 @@ class InvoiceItemSumInclusive
private bool $calc_tax = false;
private ?Client $client;
private Client | Vendor $client;
private RuleInterface $rule;
@ -119,10 +120,10 @@ class InvoiceItemSumInclusive
$this->tax_collection = collect([]);
$this->invoice = $invoice;
$this->client = $invoice->client ?? $invoice->vendor;
if ($this->invoice->client) {
$this->currency = $this->invoice->client->currency();
$this->client = $this->invoice->client;
$this->shouldCalculateTax();
} else {
$this->currency = $this->invoice->vendor->currency();

View File

@ -11,12 +11,14 @@
namespace App\Helpers\Invoice;
use App\Models\Client;
use App\Models\Credit;
use App\Models\Invoice;
use App\Models\PurchaseOrder;
use App\Models\Quote;
use App\Models\RecurringInvoice;
use App\Models\RecurringQuote;
use App\Models\Vendor;
use App\Utils\Number;
use App\Utils\Traits\NumberFormatter;
use Illuminate\Support\Collection;
@ -50,6 +52,8 @@ class InvoiceSum
private $precision;
private Client | Vendor $client;
public InvoiceItemSum $invoice_items;
private $rappen_rounding = false;
@ -60,18 +64,15 @@ class InvoiceSum
*/
public function __construct($invoice)
{
$this->invoice = $invoice;
$this->client = $invoice->client ?? $invoice->vendor;
if ($this->invoice->client) {
$this->precision = $this->invoice->client->currency()->precision;
$this->rappen_rounding = $this->invoice->client->getSetting('enable_rappen_rounding');
} else {
$this->precision = $this->invoice->vendor->currency()->precision;
$this->rappen_rounding = $this->invoice->vendor->getSetting('enable_rappen_rounding');
}
$this->precision = $this->client->currency()->precision;
$this->rappen_rounding = $this->client->getSetting('enable_rappen_rounding');
$this->tax_map = new Collection();
}
public function build()
@ -131,7 +132,7 @@ class InvoiceSum
$tax += $this->getSurchargeTaxTotalForKey($this->invoice->tax_name1, $this->invoice->tax_rate1);
$this->total_taxes += $tax;
$this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate1), $this->invoice->client).'%', 'total' => $tax];
$this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate1), $this->client).'%', 'total' => $tax];
}
if (is_string($this->invoice->tax_name2) && strlen($this->invoice->tax_name2) >= 2) {
@ -139,7 +140,7 @@ class InvoiceSum
$tax += $this->getSurchargeTaxTotalForKey($this->invoice->tax_name2, $this->invoice->tax_rate2);
$this->total_taxes += $tax;
$this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate2), $this->invoice->client).'%', 'total' => $tax];
$this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate2), $this->client).'%', 'total' => $tax];
}
if (is_string($this->invoice->tax_name3) && strlen($this->invoice->tax_name3) >= 2) {
@ -147,7 +148,7 @@ class InvoiceSum
$tax += $this->getSurchargeTaxTotalForKey($this->invoice->tax_name3, $this->invoice->tax_rate3);
$this->total_taxes += $tax;
$this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate3), $this->invoice->client).'%', 'total' => $tax];
$this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate3), $this->client).'%', 'total' => $tax];
}
return $this;

View File

@ -13,7 +13,9 @@ namespace App\Helpers\Invoice;
use App\Models\Quote;
use App\Utils\Number;
use App\Models\Client;
use App\Models\Credit;
use App\Models\Vendor;
use App\Models\Invoice;
use App\Models\PurchaseOrder;
use App\Models\RecurringQuote;
@ -50,6 +52,8 @@ class InvoiceSumInclusive
private $rappen_rounding = false;
private Client | Vendor $client;
public InvoiceItemSumInclusive $invoice_items;
/**
* Constructs the object with Invoice and Settings object.
@ -59,14 +63,10 @@ class InvoiceSumInclusive
public function __construct($invoice)
{
$this->invoice = $invoice;
$this->client = $invoice->client ?? $invoice->vendor;
if ($this->invoice->client) {
$this->precision = $this->invoice->client->currency()->precision;
$this->rappen_rounding = $this->invoice->client->getSetting('enable_rappen_rounding');
} else {
$this->precision = $this->invoice->vendor->currency()->precision;
$this->rappen_rounding = $this->invoice->vendor->getSetting('enable_rappen_rounding');
}
$this->precision = $this->client->currency()->precision;
$this->rappen_rounding = $this->client->getSetting('enable_rappen_rounding');
$this->tax_map = new Collection();
}
@ -158,19 +158,19 @@ class InvoiceSumInclusive
$tax = $this->calcInclusiveLineTax($this->invoice->tax_rate1, $amount);
$this->total_taxes += $tax;
$this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate1), $this->invoice->client).'%', 'total' => $tax];
$this->total_tax_map[] = ['name' => $this->invoice->tax_name1.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate1), $this->client).'%', 'total' => $tax];
}
if (is_string($this->invoice->tax_name2) && strlen($this->invoice->tax_name2) > 1) {
$tax = $this->calcInclusiveLineTax($this->invoice->tax_rate2, $amount);
$this->total_taxes += $tax;
$this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate2), $this->invoice->client).'%', 'total' => $tax];
$this->total_tax_map[] = ['name' => $this->invoice->tax_name2.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate2), $this->client).'%', 'total' => $tax];
}
if (is_string($this->invoice->tax_name3) && strlen($this->invoice->tax_name3) > 1) {
$tax = $this->calcInclusiveLineTax($this->invoice->tax_rate3, $amount);
$this->total_taxes += $tax;
$this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate3), $this->invoice->client).'%', 'total' => $tax];
$this->total_tax_map[] = ['name' => $this->invoice->tax_name3.' '.Number::formatValueNoTrailingZeroes(floatval($this->invoice->tax_rate3), $this->client).'%', 'total' => $tax];
}
return $this;

View File

@ -22,5 +22,5 @@
*/
function ctrans(string $string, $replace = [], $locale = null): string
{
return trans($string, $replace, $locale);
return html_entity_decode(trans($string, $replace, $locale));
}

View File

@ -254,17 +254,20 @@ class ActivityController extends BaseController
$activity->client_id = $entity->client_id;
$activity->project_id = $entity->project_id;
$activity->vendor_id = $entity->vendor_id;
// no break
case Task::class:
$activity->task_id = $entity->id;
$activity->expense_id = $entity->id;
$activity->client_id = $entity->client_id;
$activity->project_id = $entity->project_id;
$activity->vendor_id = $entity->vendor_id;
// no break
case Payment::class:
$activity->payment_id = $entity->id;
$activity->expense_id = $entity->id;
$activity->client_id = $entity->client_id;
$activity->project_id = $entity->project_id;
// no break
default:
# code...
break;

View File

@ -41,8 +41,9 @@ class ContactLoginController extends Controller
$company = false;
$account = false;
if($request->query('intended'))
if($request->query('intended')) {
$request->session()->put('url.intended', $request->query('intended'));
}
if ($request->session()->has('company_key')) {
MultiDB::findAndSetDbByCompanyKey($request->session()->get('company_key'));
@ -142,8 +143,9 @@ class ContactLoginController extends Controller
$this->setRedirectPath();
if($intended)
if($intended) {
$this->redirectTo = $intended;
}
return $request->wantsJson()
? new JsonResponse([], 204)

View File

@ -20,7 +20,6 @@ use Illuminate\Http\Request;
*/
class BrevoController extends BaseController
{
public function __construct()
{
}

View File

@ -62,6 +62,7 @@ class InvoiceController extends Controller
$invitation = $invoice->invitations()->where('client_contact_id', auth()->guard('contact')->user()->id)->first();
// @phpstan-ignore-next-line
if ($invitation && auth()->guard('contact') && ! session()->get('is_silent') && ! $invitation->viewed_date) {
$invitation->markViewed();
@ -77,13 +78,17 @@ class InvoiceController extends Controller
'key' => $invitation ? $invitation->key : false,
'hash' => $hash,
'variables' => $variables,
'invoices' => [$invoice->hashed_id],
'db' => $invoice->company->db,
];
if ($request->query('mode') === 'fullscreen') {
return render('invoices.show-fullscreen', $data);
}
return $this->render('invoices.show', $data);
return auth()->guard('contact')->user()->client->getSetting('payment_flow') == 'default' ? $this->render('invoices.show', $data) : $this->render('invoices.show_smooth', $data);
// return $this->render('invoices.show_smooth', $data);
}
public function showBlob($hash)
@ -235,9 +240,12 @@ class InvoiceController extends Controller
'hashed_ids' => $invoices->pluck('hashed_id'),
'total' => $total,
'variables' => $variables,
'invitation' => $invitation,
'db' => $invitation->company->db,
];
return $this->render('invoices.payment', $data);
// return $this->render('invoices.payment', $data);
return auth()->guard('contact')->user()->client->getSetting('payment_flow') === 'default' ? $this->render('invoices.payment', $data) : $this->render('invoices.show_smooth_multi', $data);
}
/**

View File

@ -88,6 +88,8 @@ class NinjaPlanController extends Controller
{
$trial_started = "Trial Started @ ".now()->format('Y-m-d H:i:s');
auth()->guard('contact')->user()->fill($request->only(['first_name','last_name']))->save();
$client = auth()->guard('contact')->user()->client;
$client->private_notes = $trial_started;
$client->fill($request->all());

View File

@ -567,9 +567,9 @@ class CompanyGatewayController extends BaseController
{
//Throttle here
if (Cache::has("throttle_polling:import_customers:{$company_gateway->company->company_key}:{$company_gateway->hashed_id}")) {
return response()->json(['message' => 'Please wait whilst your previous attempts complete.'], 200);
}
// if (Cache::has("throttle_polling:import_customers:{$company_gateway->company->company_key}:{$company_gateway->hashed_id}")) {
// return response()->json(['message' => 'Please wait whilst your previous attempts complete.'], 200);
// }
dispatch(function () use ($company_gateway) {
MultiDB::setDb($company_gateway->company->db);

View File

@ -51,9 +51,9 @@ class EmailHistoryController extends BaseController
/** @var \App\Models\User $user */
$user = auth()->user();
$data = SystemLog::where('company_id', $user->company()->id)
->where('category_id', SystemLog::CATEGORY_MAIL)
->whereJsonContains('log->history->entity', $request->entity)
->whereJsonContains('log->history->entity_id', $this->encodePrimaryKey($request->entity_id))
->orderBy('id', 'DESC')
->cursor()

View File

@ -584,20 +584,14 @@ class ExpenseController extends BaseController
return $this->itemResponse($expense->fresh());
}
public function edocument(EDocumentRequest $request): string
public function edocument(EDocumentRequest $request)
{
if ($request->hasFile("documents"))
try {
$user = auth()->user();
return (new ImportEDocument($request->file("documents")->get(), $request->file("documents")->getClientOriginalName(), $request->file("documents")->getMimeType()))->handle();
} catch (\Exception $e) {
if ($e->getCode() == 409)
return $e->getMessage();
throw $e;
foreach ($request->file("documents") as $file) {
ImportEDocument::dispatch($file->get(), $file->getClientOriginalName(), $request->file("documents")->getMimeType(), $user->company());
}
return "No file found";
return response()->json(['message' => 'Processing....'], 200);
}
}

View File

@ -59,9 +59,9 @@ class ExportController extends BaseController
/** @var \App\Models\User $user */
$user = auth()->user();
$hash = Str::uuid();
$hash = Str::uuid()->toString();
$url = \Illuminate\Support\Facades\URL::temporarySignedRoute('protected_download', now()->addHour(), ['hash' => $hash]);
Cache::put($hash, $url, now()->addHour());
Cache::put($hash, $url, 3600);
CompanyExport::dispatch($user->getCompany(), $user, $hash);

View File

@ -0,0 +1,134 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Controllers;
use App\Http\Requests\Quickbooks\AuthorizedQuickbooksRequest;
use App\Libraries\MultiDB;
use Illuminate\Support\Facades\Cache;
use App\Http\Requests\Quickbooks\AuthQuickbooksRequest;
use App\Services\Quickbooks\QuickbooksService;
class ImportQuickbooksController extends BaseController
{
// private array $import_entities = [
// 'client' => 'Customer',
// 'invoice' => 'Invoice',
// 'product' => 'Item',
// 'payment' => 'Payment'
// ];
public function onAuthorized(AuthorizedQuickbooksRequest $request)
{
MultiDB::findAndSetDbByCompanyKey($request->getTokenContent()['company_key']);
$company = $request->getCompany();
$qb = new QuickbooksService($company);
$realm = $request->query('realmId');
$access_token_object = $qb->sdk()->accessTokenFromCode($request->query('code'), $realm);
$qb->sdk()->saveOAuthToken($access_token_object);
return redirect(config('ninja.react_url'));
}
/**
* Determine if the user is authorized to make this request.
*
*/
public function authorizeQuickbooks(AuthQuickbooksRequest $request, string $token)
{
MultiDB::findAndSetDbByCompanyKey($request->getTokenContent()['company_key']);
$company = $request->getCompany();
$qb = new QuickbooksService($company);
$authorizationUrl = $qb->sdk()->getAuthorizationUrl();
$state = $qb->sdk()->getState();
Cache::put($state, $token, 190);
return redirect()->to($authorizationUrl);
}
public function preimport(string $type, string $hash)
{
// // Check for authorization otherwise
// // Create a reference
// $data = [
// 'hash' => $hash,
// 'type' => $type
// ];
// $this->getData($data);
}
protected function getData($data)
{
// $entity = $this->import_entities[$data['type']];
// $cache_name = "{$data['hash']}-{$data['type']}";
// // TODO: Get or put cache or DB?
// if(! Cache::has($cache_name)) {
// $contents = call_user_func([$this->service, "fetch{$entity}s"]);
// if($contents->isEmpty()) {
// return;
// }
// Cache::put($cache_name, base64_encode($contents->toJson()), 600);
// }
}
/**
* @OA\Post(
* path="/api/v1/import_json",
* operationId="getImportJson",
* tags={"import"},
* summary="Import data from the system",
* description="Import data from the system",
* @OA\Parameter(ref="#/components/parameters/X-Requested-With"),
* @OA\Response(
* response=200,
* description="success",
* @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\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 import(Request $request)
{
// $hash = Str::random(32);
// foreach($request->input('import_types') as $type) {
// $this->preimport($type, $hash);
// }
// /** @var \App\Models\User $user */
// // $user = auth()->user() ?? Auth::loginUsingId(60);
// $data = ['import_types' => $request->input('import_types') ] + compact('hash');
// if (Ninja::isHosted()) {
// QuickbooksIngest::dispatch($data, $user->company());
// } else {
// QuickbooksIngest::dispatch($data, $user->company());
// }
// return response()->json(['message' => 'Processing'], 200);
}
}

View File

@ -22,7 +22,6 @@ use Illuminate\Support\Str;
class OneTimeTokenController extends BaseController
{
public function __construct()
{
parent::__construct();

View File

@ -24,7 +24,6 @@ use Illuminate\Http\Request;
*/
class PostMarkController extends BaseController
{
public function __construct()
{
}

View File

@ -297,6 +297,8 @@ class PreviewController extends BaseController
->setTemplate($design_object)
->mock();
} catch(SyntaxError $e) {
} catch(\Exception $e) {
return response()->json(['message' => 'invalid data access', 'errors' => ['design.design.body' => $e->getMessage()]], 422);
}
if (request()->query('html') == 'true') {

View File

@ -181,8 +181,9 @@ class SelfUpdateController extends BaseController
public function checkVersion()
{
if(Ninja::isHosted())
if(Ninja::isHosted()) {
return '5.10.SaaS';
}
return trim(file_get_contents(config('ninja.version_url')));
}

View File

@ -11,23 +11,25 @@
namespace App\Http\Controllers\VendorPortal;
use App\Events\Misc\InvitationWasViewed;
use App\Events\PurchaseOrder\PurchaseOrderWasAccepted;
use App\Events\PurchaseOrder\PurchaseOrderWasViewed;
use App\Utils\Ninja;
use App\Models\Webhook;
use Illuminate\View\View;
use App\Models\PurchaseOrder;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\MakesDates;
use App\Jobs\Entity\CreateRawPdf;
use App\Jobs\Util\WebhookHandler;
use App\Http\Controllers\Controller;
use App\Http\Requests\VendorPortal\PurchaseOrders\ProcessPurchaseOrdersInBulkRequest;
use App\Jobs\Invoice\InjectSignature;
use Illuminate\Support\Facades\Cache;
use Illuminate\Contracts\View\Factory;
use App\Models\PurchaseOrderInvitation;
use App\Events\Misc\InvitationWasViewed;
use App\Events\PurchaseOrder\PurchaseOrderWasViewed;
use App\Events\PurchaseOrder\PurchaseOrderWasAccepted;
use App\Http\Requests\VendorPortal\PurchaseOrders\ShowPurchaseOrderRequest;
use App\Http\Requests\VendorPortal\PurchaseOrders\ShowPurchaseOrdersRequest;
use App\Jobs\Entity\CreateRawPdf;
use App\Jobs\Invoice\InjectSignature;
use App\Models\PurchaseOrder;
use App\Models\PurchaseOrderInvitation;
use App\Utils\Ninja;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
use Illuminate\Contracts\View\Factory;
use Illuminate\Support\Facades\Cache;
use Illuminate\View\View;
use App\Http\Requests\VendorPortal\PurchaseOrders\ProcessPurchaseOrdersInBulkRequest;
class PurchaseOrderController extends Controller
{
@ -187,6 +189,9 @@ class PurchaseOrderController extends Controller
}
event(new PurchaseOrderWasAccepted($purchase_order, auth()->guard('vendor')->user(), $purchase_order->company, Ninja::eventVars()));
WebhookHandler::dispatch(Webhook::EVENT_ACCEPTED_PURCHASE_ORDER, $purchase_order, $purchase_order->company, 'vendor')->delay(0);
});
if ($purchase_count_query->count() == 1) {

View File

@ -14,7 +14,7 @@ namespace App\Http\Controllers\VendorPortal;
use App\Http\Controllers\Controller;
use App\Models\VendorContact;
use App\Utils\Traits\MakesHash;
use App\Utils\TranslationHelper;
use Illuminate\Http\Request;
class VendorContactController extends Controller
{
@ -58,14 +58,14 @@ class VendorContactController extends Controller
'settings' => $vendor_contact->vendor->company->settings,
'company' => $vendor_contact->vendor->company,
'sidebar' => $this->sidebarMenu(),
'countries' => TranslationHelper::getCountries(),
'countries' => app('countries'),
]);
}
public function update(VendorContact $vendor_contact)
public function update(Request $request, VendorContact $vendor_contact)
{
$vendor_contact->fill(request()->all());
$vendor_contact->vendor->fill(request()->all());
$vendor_contact->fill($request->all());
$vendor_contact->vendor->fill($request->all());
$vendor_contact->push();
return back()->withSuccess(ctrans('texts.profile_updated_successfully'));
@ -76,16 +76,10 @@ class VendorContactController extends Controller
$enabled_modules = auth()->guard('vendor')->user()->company->enabled_modules;
$data = [];
// TODO: Enable dashboard once it's completed.
// $this->settings->enable_client_portal_dashboard
// $data[] = [ 'title' => ctrans('texts.dashboard'), 'url' => 'client.dashboard', 'icon' => 'activity'];
if (self::MODULE_PURCHASE_ORDERS & $enabled_modules) {
$data[] = ['title' => ctrans('texts.purchase_orders'), 'url' => 'vendor.purchase_orders.index', 'icon' => 'file-text'];
}
// $data[] = ['title' => ctrans('texts.documents'), 'url' => 'client.documents.index', 'icon' => 'download'];
return $data;
}
}

View File

@ -79,7 +79,7 @@ class ContactRegister
// As a fallback for self-hosted, it will use default company in the system
// if key isn't provided in the url.
if (! $request->route()->parameter('company_key') && Ninja::isSelfHost()) {
$company = Account::query()->first()->default_company;
$company = Account::query()->first()->default_company ?? Account::query()->first()->companies->first();
if (! $company->client_can_register) {
abort(400, 'Registration disabled');

View File

@ -4,15 +4,15 @@ namespace App\Http\Middleware;
use Closure;
use Illuminate\Cache\RateLimiter;
use Illuminate\Contracts\Redis\Factory as Redis;
use Illuminate\Redis\Limiters\DurationLimiter;
use Illuminate\Routing\Middleware\ThrottleRequests;
class ThrottleRequestsWithPredis extends ThrottleRequests
class ThrottleRequestsWithPredis extends \Illuminate\Routing\Middleware\ThrottleRequests
{
/**
* The Redis factory implementation.
*
* @var \Illuminate\Redis\Connections\Connection
* @var \Illuminate\Contracts\Redis\Factory
*/
protected $redis;
@ -32,14 +32,14 @@ class ThrottleRequestsWithPredis extends ThrottleRequests
/**
* Create a new request throttler.
*
* @param \Illuminate\Cache\RateLimiter $limiter
* @return void
*/
public function __construct(RateLimiter $limiter)
/** @phpstan-ignore-next-line */
public function __construct(RateLimiter $limiter, Redis $redis)
{
parent::__construct($limiter);
/** @phpstan-ignore-next-line */
$this->redis = \Illuminate\Support\Facades\Redis::connection('sentinel-cache');
}
@ -56,7 +56,7 @@ class ThrottleRequestsWithPredis extends ThrottleRequests
protected function handleRequest($request, Closure $next, array $limits)
{
foreach ($limits as $limit) {
if ($this->tooManyAttempts($limit->key, $limit->maxAttempts, $limit->decayMinutes)) {
if ($this->tooManyAttempts($limit->key, $limit->maxAttempts, $limit->decaySeconds)) {
throw $this->buildException($request, $limit->key, $limit->maxAttempts, $limit->responseCallback);
}
}
@ -79,16 +79,16 @@ class ThrottleRequestsWithPredis extends ThrottleRequests
*
* @param string $key
* @param int $maxAttempts
* @param int $decayMinutes
* @param int $decaySeconds
* @return mixed
*/
protected function tooManyAttempts($key, $maxAttempts, $decayMinutes)
protected function tooManyAttempts($key, $maxAttempts, $decaySeconds)
{
$limiter = new DurationLimiter(
$this->redis,
$this->getRedisConnection(),
$key,
$maxAttempts,
$decayMinutes * 60
$decaySeconds
);
return tap(! $limiter->acquire(), function () use ($key, $limiter) {
@ -121,4 +121,13 @@ class ThrottleRequestsWithPredis extends ThrottleRequests
{
return $this->decaysAt[$key] - $this->currentTime();
}
/**
* Get the Redis connection that should be used for throttling.
*
*/
protected function getRedisConnection()
{
return $this->redis;
}
}

View File

@ -68,8 +68,9 @@ class StoreNoteRequest extends Request
public function getEntity()
{
if(!$this->entity)
if(!$this->entity) {
return false;
}
$class = "\\App\\Models\\".ucfirst(Str::camel(rtrim($this->entity, 's')));
return $class::withTrashed()->find(is_string($this->entity_id) ? $this->decodePrimaryKey($this->entity_id) : $this->entity_id);

View File

@ -48,7 +48,8 @@ class StoreBankTransactionRuleRequest extends Request
'rules.*.value' => 'bail|required|nullable',
'auto_convert' => 'bail|sometimes|bool',
'matches_on_all' => 'bail|sometimes|bool',
'applies_to' => 'bail|sometimes|string',
'applies_to' => 'bail|sometimes|string|in:CREDIT,DEBIT',
'on_credit_match' => 'bail|sometimes|in:create_payment,link_payment'
];
$rules['category_id'] = 'bail|sometimes|nullable|exists:expense_categories,id,company_id,'.$user->company()->id.',is_deleted,0';

View File

@ -66,8 +66,7 @@ class ShowCalculatedFieldRequest extends Request
$input['end_date'] = now()->format('Y-m-d');
}
if(isset($input['period']) && $input['period'] == 'previous')
{
if(isset($input['period']) && $input['period'] == 'previous') {
$dates = $this->calculatePreviousPeriodStartAndEndDates($input, $user->company());
$input['start_date'] = $dates[0];
$input['end_date'] = $dates[1];

View File

@ -25,9 +25,9 @@ class EDocumentRequest extends Request
$rules = [];
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->fileValidation();
$rules['documents.*'] = 'required|file|max:1000000|mimes:xml';
} elseif ($this->file('documents')) {
$rules['documents'] = $this->fileValidation();
$rules['documents'] = 'required|file|max:1000000|mimes:xml';
}
return $rules;
}

View File

@ -40,10 +40,11 @@ class BulkInvoiceRequest extends Request
/** @var \App\Models\User $user */
$user = auth()->user();
if(\Illuminate\Support\Facades\Cache::has($this->ip()."|".$this->input('action', 0)."|".json_encode($this->input('ids', ''))."|".$user->company()->company_key))
if(\Illuminate\Support\Facades\Cache::has($this->ip()."|".$this->input('action', 0)."|".$user->company()->company_key)) {
throw new DuplicatePaymentException('Duplicate request.', 429);
}
\Illuminate\Support\Facades\Cache::put(($this->ip()."|".$this->input('action', 0)."|".json_encode($this->input('ids', ''))."|".$user->company()->company_key), true, 1);
\Illuminate\Support\Facades\Cache::put(($this->ip()."|".$this->input('action', 0)."|".$user->company()->company_key), true, 1);
}

View File

@ -93,6 +93,12 @@ class StoreInvoiceRequest extends Request
/** @var \App\Models\User $user */
$user = auth()->user();
if(\Illuminate\Support\Facades\Cache::has($this->ip()."|INVOICE|".$this->input('client_id', '')."|".$user->company()->company_key)) {
usleep(200000);
}
\Illuminate\Support\Facades\Cache::put($this->ip()."|INVOICE|".$this->input('client_id', '')."|".$user->company()->company_key,1);
$input = $this->all();
$input = $this->decodePrimaryKeys($input);

View File

@ -80,8 +80,9 @@ class StorePaymentRequest extends Request
/** @var \App\Models\User $user */
$user = auth()->user();
if(\Illuminate\Support\Facades\Cache::has($this->ip()."|".$this->input('amount', 0)."|".$this->input('client_id', '')."|".$user->company()->company_key))
if(\Illuminate\Support\Facades\Cache::has($this->ip()."|".$this->input('amount', 0)."|".$this->input('client_id', '')."|".$user->company()->company_key)) {
throw new DuplicatePaymentException('Duplicate request.', 429);
}
\Illuminate\Support\Facades\Cache::put(($this->ip()."|".$this->input('amount', 0)."|".$this->input('client_id', '')."|".$user->company()->company_key), true, 1);

View File

@ -0,0 +1,69 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\Quickbooks;
use App\Models\Company;
use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Cache;
class AuthQuickbooksRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return is_array($this->getTokenContent());
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
//
];
}
/**
* Resolve one-time token instance.
*
* @return mixed
*/
public function getTokenContent()
{
if ($this->state) {
$this->token = $this->state;
}
$data = Cache::get($this->token);
return $data;
}
public function getContact(): ?User
{
return User::findOrFail($this->getTokenContent()['user_id']);
}
public function getCompany(): ?Company
{
return Company::query()->where('company_key', $this->getTokenContent()['company_key'])->firstOrFail();
}
}

View File

@ -0,0 +1,69 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\Quickbooks;
use App\Models\Company;
use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Cache;
class AuthorizedQuickbooksRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return is_array($this->getTokenContent());
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
'code' => 'required|string',
'state' => 'required|string',
'realmId' => 'required|string',
];
}
/**
* Resolve one-time token instance.
*
* @return mixed
*/
public function getTokenContent()
{
$token = Cache::get($this->state);
$data = Cache::get($token);
return $data;
}
public function getContact()
{
return User::findOrFail($this->getTokenContent()['user_id']);
}
public function getCompany()
{
return Company::where('company_key', $this->getTokenContent()['company_key'])->firstOrFail();
}
}

View File

@ -17,7 +17,6 @@ use Illuminate\Auth\Access\AuthorizationException;
class GenericReportRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*

View File

@ -133,7 +133,7 @@ class UpdateTaskRequest extends Request
}
if(!isset($input['time_log']) || empty($input['time_log']) || $input['time_log'] == '{}') {
if(!isset($input['time_log']) || empty($input['time_log']) || $input['time_log'] == '{}' || $input['time_log'] == '[""]') {
$input['time_log'] = json_encode([]);
}

View File

@ -15,7 +15,6 @@ use App\Http\Requests\Request;
class DisconnectUserMailerRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*

View File

@ -77,8 +77,7 @@ class UpdateUserRequest extends Request
unset($input['oauth_user_token']);
}
if(isset($input['password']) && is_string($input['password']))
{
if(isset($input['password']) && is_string($input['password'])) {
$input['password'] = trim($input['password']);
}

View File

@ -31,12 +31,8 @@ class TwigLint implements ValidationRule
try {
$twig->parse($twig->tokenize(new \Twig\Source(preg_replace('/<!--.*?-->/s', '', $value), '')));
} catch (\Twig\Error\SyntaxError $e) {
// echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
nlog($e->getMessage());
$fail($e->getMessage());
}
}
}

View File

@ -34,8 +34,7 @@ class ValidClientScheme implements ValidationRule, ValidatorAwareRule
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if(isset($value['Invoice']))
{
if(isset($value['Invoice'])) {
$r = new EInvoice();
$errors = $r->validateRequest($value['Invoice'], ClientLevel::class);

View File

@ -24,7 +24,6 @@ use Illuminate\Contracts\Validation\ValidatorAwareRule;
*/
class ValidCompanyScheme implements ValidationRule, ValidatorAwareRule
{
/**
* The validator instance.
*
@ -35,8 +34,7 @@ class ValidCompanyScheme implements ValidationRule, ValidatorAwareRule
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if(isset($value['Invoice']))
{
if(isset($value['Invoice'])) {
$r = new EInvoice();
$errors = $r->validateRequest($value['Invoice'], CompanyLevel::class);

View File

@ -0,0 +1,55 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\ViewComposers\Components\Rotessa;
use App\DataProviders\CAProvinces;
use App\DataProviders\USStates;
use Illuminate\View\Component;
use App\Models\ClientContact;
use Illuminate\Support\Arr;
use Illuminate\View\View;
// AmericanBankInfo Component
class AccountComponent extends Component
{
private $fields = [
'bank_account_type',
'routing_number',
'institution_number',
'transit_number',
'bank_name',
'country',
'account_number'
];
private $defaults = [
'bank_account_type' => null,
'routing_number' => null,
'institution_number' => null,
'transit_number' => null,
'bank_name' => null,
'account_number' => null,
'country' => 'US',
"authorization_type" => 'Online'
];
public function __construct(public array $account)
{
$this->attributes = $this->newAttributeBag(Arr::only($this->account, $this->fields));
}
public function render()
{
return render('gateways.rotessa.components.account', $this->attributes->getAttributes() + $this->defaults);
}
}

View File

@ -0,0 +1,58 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\ViewComposers\Components\Rotessa;
use App\DataProviders\CAProvinces;
use App\DataProviders\USStates;
use Illuminate\View\Component;
use App\Models\ClientContact;
use Illuminate\Support\Arr;
use Illuminate\View\View;
// Address Component
class AddressComponent extends Component
{
private $fields = [
'address_1',
'address_2',
'city',
'postal_code',
'province_code',
'country'
];
private $defaults = [
'country' => 'US'
];
public function __construct(public array $address)
{
if(strlen($this->address['state']) > 2) {
$this->address['state'] = $this->address['country'] == 'US' ? array_search($this->address['state'], USStates::$states) : CAProvinces::getAbbreviation($this->address['state']);
}
$this->attributes = $this->newAttributeBag(
Arr::only(
Arr::mapWithKeys($this->address, function ($item, $key) {
return in_array($key, ['address1','address2','state']) ? [ (['address1' => 'address_1','address2' => 'address_2','state' => 'province_code'])[$key] => $item ] : [ $key => $item ];
}),
$this->fields
)
);
}
public function render()
{
return render('gateways.rotessa.components.address', $this->attributes->getAttributes() + $this->defaults);
}
}

View File

@ -0,0 +1,57 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\ViewComposers\Components\Rotessa;
use App\DataProviders\CAProvinces;
use App\DataProviders\USStates;
use Illuminate\View\Component;
use App\Models\ClientContact;
use Illuminate\Support\Arr;
use Illuminate\View\View;
// Contact Component
class ContactComponent extends Component
{
public function __construct(ClientContact $contact)
{
$contact = collect($contact->client->contacts->firstWhere('is_primary', 1)->toArray())->merge([
'home_phone' => $contact->client->phone,
'custom_identifier' => $contact->client->client_hash,
'name' => $contact->client->name,
'id' => null,
])->all();
$this->attributes = $this->newAttributeBag(Arr::only($contact, $this->fields));
}
private $fields = [
'name',
'email',
'home_phone',
'phone',
'custom_identifier',
'customer_type' ,
'id'
];
private $defaults = [
'customer_type' => "Business",
'custom_identifier' => null,
'customer_id' => null
];
public function render()
{
return render('gateways.rotessa.components.contact', $this->attributes->getAttributes() + $this->defaults);
}
}

View File

@ -88,11 +88,11 @@ class PortalComposer
$data['sidebar'] = $this->sidebarMenu();
$data['header'] = [];
$data['footer'] = [];
$data['countries'] = TranslationHelper::getCountries();
$data['countries'] = app('countries');
$data['company'] = auth()->guard('contact')->user()->company;
$data['client'] = auth()->guard('contact')->user()->client;
$data['settings'] = $this->settings;
$data['currencies'] = TranslationHelper::getCurrencies();
$data['currencies'] = app('currencies');
$data['contact'] = auth()->guard('contact')->user();
$data['multiple_contacts'] = session()->get('multiple_contacts') ?: collect();
@ -136,11 +136,11 @@ class PortalComposer
$data[] = ['title' => ctrans('texts.statement'), 'url' => 'client.statement', 'icon' => 'activity'];
if (Ninja::isHosted() && auth()->guard('contact')->user()->company->id == config('ninja.ninja_default_company_id')) {
// if (Ninja::isHosted() && auth()->guard('contact')->user()->company->id == config('ninja.ninja_default_company_id')) {
$data[] = ['title' => ctrans('texts.plan'), 'url' => 'client.plan', 'icon' => 'credit-card'];
} else {
// } else {
$data[] = ['title' => ctrans('texts.subscriptions'), 'url' => 'client.subscriptions.index', 'icon' => 'calendar'];
}
// }
if (auth()->guard('contact')->user()->client->getSetting('client_initiated_payments')) {
$data[] = ['title' => ctrans('texts.pre_payment'), 'url' => 'client.pre_payments.index', 'icon' => 'dollar-sign'];

View File

@ -0,0 +1,24 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
use Illuminate\Support\Facades\View;
use App\DataProviders\CAProvinces;
use App\DataProviders\USStates;
View::composer(['*.rotessa.components.address','*.rotessa.components.banks.US.bank','*.rotessa.components.dropdowns.country.US'], function ($view) {
$states = USStates::get();
$view->with('states', $states);
});
View::composer(['*.rotessa.components.address','*.rotessa.components.banks.CA.bank','*.rotessa.components.dropdowns.country.CA'], function ($view) {
$provinces = CAProvinces::get();
$view->with('provinces', $provinces);
});

View File

@ -0,0 +1,253 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Import\Providers;
use App\Models\Invoice;
use App\Factory\ClientFactory;
use App\Factory\InvoiceFactory;
use App\Factory\PaymentFactory;
use App\Factory\ProductFactory;
use App\Import\ImportException;
use Illuminate\Support\Facades\Cache;
use App\Repositories\ClientRepository;
use App\Repositories\InvoiceRepository;
use App\Repositories\PaymentRepository;
use App\Repositories\ProductRepository;
use App\Http\Requests\Client\StoreClientRequest;
use App\Http\Requests\Invoice\StoreInvoiceRequest;
use App\Http\Requests\Payment\StorePaymentRequest;
use App\Http\Requests\Product\StoreProductRequest;
use App\Import\Transformer\Quickbooks\ClientTransformer;
use App\Import\Transformer\Quickbooks\InvoiceTransformer;
use App\Import\Transformer\Quickbooks\PaymentTransformer;
use App\Import\Transformer\Quickbooks\ProductTransformer;
class Quickbooks extends BaseImport
{
public array $entity_count = [];
public function import(string $entity)
{
if (
in_array($entity, [
'client',
'invoice',
'product',
'payment',
// 'vendor',
// 'expense',
])
) {
$this->{$entity}();
}
//collate any errors
// $this->finalizeImport();
}
public function client()
{
$entity_type = 'client';
$data = $this->getData($entity_type);
if (empty($data)) {
$this->entity_count['clients'] = 0;
return;
}
$this->request_name = StoreClientRequest::class;
$this->repository_name = ClientRepository::class;
$this->factory_name = ClientFactory::class;
$this->repository = app()->make($this->repository_name);
$this->repository->import_mode = true;
$this->transformer = new ClientTransformer($this->company);
$client_count = $this->ingest($data, $entity_type);
$this->entity_count['clients'] = $client_count;
}
public function product()
{
$entity_type = 'product';
$data = $this->getData($entity_type);
if (empty($data)) {
$this->entity_count['products'] = 0;
return;
}
$this->request_name = StoreProductRequest::class;
$this->repository_name = ProductRepository::class;
$this->factory_name = ProductFactory::class;
$this->repository = app()->make($this->repository_name);
$this->repository->import_mode = true;
$this->transformer = new ProductTransformer($this->company);
$count = $this->ingest($data, $entity_type);
$this->entity_count['products'] = $count;
}
public function getData($type)
{
// get the data from cache? file? or api ?
return json_decode(base64_decode(Cache::get("{$this->hash}-{$type}")), true);
}
public function payment()
{
$entity_type = 'payment';
$data = $this->getData($entity_type);
if (empty($data)) {
$this->entity_count['payments'] = 0;
return;
}
$this->request_name = StorePaymentRequest::class;
$this->repository_name = PaymentRepository::class;
$this->factory_name = PaymentFactory::class;
$this->repository = app()->make($this->repository_name);
$this->repository->import_mode = true;
$this->transformer = new PaymentTransformer($this->company);
$count = $this->ingest($data, $entity_type);
$this->entity_count['payments'] = $count;
}
public function invoice()
{
//make sure we update and create products
$initial_update_products_value = $this->company->update_products;
$this->company->update_products = true;
$this->company->save();
$entity_type = 'invoice';
$data = $this->getData($entity_type);
if (empty($data)) {
$this->entity_count['invoices'] = 0;
return;
}
$this->request_name = StoreInvoiceRequest::class;
$this->repository_name = InvoiceRepository::class;
$this->factory_name = InvoiceFactory::class;
$this->repository = app()->make($this->repository_name);
$this->repository->import_mode = true;
$this->transformer = new InvoiceTransformer($this->company);
$invoice_count = $this->ingestInvoices($data, '');
$this->entity_count['invoices'] = $invoice_count;
$this->company->update_products = $initial_update_products_value;
$this->company->save();
}
public function ingestInvoices($invoices, $invoice_number_key)
{
$count = 0;
$invoice_transformer = $this->transformer;
/** @var ClientRepository $client_repository */
$client_repository = app()->make(ClientRepository::class);
$client_repository->import_mode = true;
$invoice_repository = new InvoiceRepository();
$invoice_repository->import_mode = true;
foreach ($invoices as $raw_invoice) {
if(!is_array($raw_invoice)) {
continue;
}
try {
$invoice_data = $invoice_transformer->transform($raw_invoice);
$invoice_data['user_id'] = $this->company->owner()->id;
$invoice_data['line_items'] = (array) $invoice_data['line_items'];
$invoice_data['line_items'] = $this->cleanItems(
$invoice_data['line_items'] ?? []
);
if (
empty($invoice_data['client_id']) &&
! empty($invoice_data['client'])
) {
$client_data = $invoice_data['client'];
$client_data['user_id'] = $this->getUserIDForRecord(
$invoice_data
);
$client_repository->save(
$client_data,
$client = ClientFactory::create(
$this->company->id,
$client_data['user_id']
)
);
$invoice_data['client_id'] = $client->id;
unset($invoice_data['client']);
}
$validator = $this->request_name::runFormRequest($invoice_data);
if ($validator->fails()) {
$this->error_array['invoice'][] = [
'invoice' => $invoice_data,
'error' => $validator->errors()->all(),
];
} else {
if(!Invoice::where('number', $invoice_data['number'])->first()) {
$invoice = InvoiceFactory::create(
$this->company->id,
$this->company->owner()->id
);
$invoice->mergeFillable(['partial','amount','balance','line_items']);
if (! empty($invoice_data['status_id'])) {
$invoice->status_id = $invoice_data['status_id'];
}
$saveable_invoice_data = $invoice_data;
if(array_key_exists('payments', $saveable_invoice_data)) {
unset($saveable_invoice_data['payments']);
}
$invoice->fill($saveable_invoice_data);
$invoice->save();
$count++;
}
// $this->actionInvoiceStatus(
// $invoice,
// $invoice_data,
// $invoice_repository
// );
}
} catch (\Exception $ex) {
if (\DB::connection(config('database.default'))->transactionLevel() > 0) {
\DB::connection(config('database.default'))->rollBack();
}
if ($ex instanceof ImportException) {
$message = $ex->getMessage();
} else {
report($ex);
$message = 'Unknown error ';
nlog($ex->getMessage());
nlog($raw_invoice);
}
$this->error_array['invoice'][] = [
'invoice' => $raw_invoice,
'error' => $message,
];
}
}
return $count;
}
}

View File

@ -244,8 +244,7 @@ class Wave extends BaseImport implements ImportInterface
if (empty($expense_data['vendor_id'])) {
$vendor_data['user_id'] = $this->getUserIDForRecord($expense_data);
if(isset($raw_expense['Vendor Name']) || isset($raw_expense['Vendor']))
{
if(isset($raw_expense['Vendor Name']) || isset($raw_expense['Vendor'])) {
$vendor_repository->save(
['name' => isset($raw_expense['Vendor Name']) ? $raw_expense['Vendor Name'] : isset($raw_expense['Vendor'])],
$vendor = VendorFactory::create(

View File

@ -119,8 +119,9 @@ class BaseTransformer
{
$code = array_key_exists($key, $data) ? $data[$key] : false;
if(!$code)
if(!$code) {
return $this->company->settings->currency_id;
}
/** @var \Illuminate\Support\Collection<\App\Models\Currency> */
$currencies = app('currencies');

View File

@ -38,12 +38,13 @@ class ExpenseTransformer extends BaseTransformer
$tax_rate = $total_tax > 0 ? round(($total_tax / $amount) * 100, 3) : 0;
if(isset($data['Notes / Memo']) && strlen($data['Notes / Memo']) > 1)
if(isset($data['Notes / Memo']) && strlen($data['Notes / Memo']) > 1) {
$public_notes = $data['Notes / Memo'];
elseif (isset($data['Transaction Description']) && strlen($data['Transaction Description']) > 1)
} elseif (isset($data['Transaction Description']) && strlen($data['Transaction Description']) > 1) {
$public_notes = $data['Transaction Description'];
else
} else {
$public_notes = '';
}
$transformed = [

View File

@ -695,7 +695,7 @@ class CompanyExport implements ShouldQueue
$url = Cache::get($this->hash);
Cache::put($this->hash, $storage_path, now()->addHour());
Cache::put($this->hash, $storage_path, 3600);
App::forgetInstance('translator');
$t = app('translator');

View File

@ -114,8 +114,7 @@ class RecurringExpensesCron
$exchange_rate = new CurrencyApi();
$expense->exchange_rate = $exchange_rate->exchangeRate($expense->currency_id, (int)$expense->company->settings->currency_id, Carbon::parse($expense->date));
}
else {
} else {
$expense->exchange_rate = 1;
}

View File

@ -48,12 +48,12 @@ class RecurringInvoicesCron
Auth::logout();
if (! config('ninja.db.multi_db_enabled')) {
$recurring_invoices = RecurringInvoice::query()->where('recurring_invoices.status_id', RecurringInvoice::STATUS_ACTIVE)
->where('recurring_invoices.is_deleted', false)
->where('recurring_invoices.remaining_cycles', '!=', '0')
->whereNotNull('recurring_invoices.next_send_date')
->whereNull('recurring_invoices.deleted_at')
->where('recurring_invoices.next_send_date', '<=', now()->toDateTimeString())
$recurring_invoices = RecurringInvoice::query()->where('status_id', RecurringInvoice::STATUS_ACTIVE)
->where('is_deleted', false)
->where('remaining_cycles', '!=', '0')
->whereNotNull('next_send_date')
->whereNull('deleted_at')
->where('next_send_date', '<=', now()->toDateTimeString())
->whereHas('client', function ($query) {
$query->where('is_deleted', 0)
->where('deleted_at', null);
@ -87,27 +87,18 @@ class RecurringInvoicesCron
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$recurring_invoices = RecurringInvoice::query()->where('recurring_invoices.status_id', RecurringInvoice::STATUS_ACTIVE)
->where('recurring_invoices.is_deleted', false)
->where('recurring_invoices.remaining_cycles', '!=', '0')
->whereNull('recurring_invoices.deleted_at')
->whereNotNull('recurring_invoices.next_send_date')
->where('recurring_invoices.next_send_date', '<=', now()->toDateTimeString())
// ->whereHas('client', function ($query) {
// $query->where('is_deleted', 0)
// ->where('deleted_at', null);
// })
// ->whereHas('company', function ($query) {
// $query->where('is_disabled', 0);
// })
->leftJoin('clients', function ($join) {
$join->on('recurring_invoices.client_id', '=', 'clients.id')
->where('clients.is_deleted', 0)
->whereNull('clients.deleted_at');
$recurring_invoices = RecurringInvoice::query()->where('status_id', RecurringInvoice::STATUS_ACTIVE)
->where('is_deleted', false)
->where('remaining_cycles', '!=', '0')
->whereNull('deleted_at')
->whereNotNull('next_send_date')
->where('next_send_date', '<=', now()->toDateTimeString())
->whereHas('client', function ($query) {
$query->where('is_deleted', 0)
->where('deleted_at', null);
})
->leftJoin('companies', function ($join) {
$join->on('recurring_invoices.company_id', '=', 'companies.id')
->where('companies.is_disabled', 0);
->whereHas('company', function ($query) {
$query->where('is_disabled', 0);
})
->with('company')
->cursor();

View File

@ -1,105 +0,0 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Jobs\Cron;
use App\Libraries\MultiDB;
use App\Models\Project;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Support\Facades\Auth;
class UpdateCalculatedFields
{
use Dispatchable;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct()
{
}
/**
* Execute the job.
*
* @return void
*/
public function handle(): void
{
nlog("Updating calculated fields");
Auth::logout();
if (! config('ninja.db.multi_db_enabled')) {
Project::query()->with('tasks')->whereHas('tasks', function ($query) {
$query->where('updated_at', '>', now()->subHours(2));
})
->cursor()
->each(function ($project) {
$project->current_hours = $this->calculateDuration($project);
$project->save();
});
} else {
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
Project::query()->with('tasks')->whereHas('tasks', function ($query) {
$query->where('updated_at', '>', now()->subHours(2));
})
->cursor()
->each(function ($project) {
$project->current_hours = $this->calculateDuration($project);
$project->save();
});
//Clean password resets table
\DB::connection($db)->table('password_resets')->where('created_at', '<', now()->subHour())->delete();
}
}
}
private function calculateDuration($project): int
{
$duration = 0;
$project->tasks->each(function ($task) use (&$duration) {
if(is_iterable(json_decode($task->time_log))) {
foreach(json_decode($task->time_log) as $log) {
if(!is_array($log))
continue;
$start_time = $log[0];
$end_time = $log[1] == 0 ? time() : $log[1];
$duration += $end_time - $start_time;
}
}
});
return (int) round(($duration / 60 / 60), 0);
}
}

View File

@ -11,7 +11,6 @@
namespace App\Jobs\EDocument;
use App\Services\EDocument\Standards\RoEInvoice;
use App\Utils\Ninja;
use App\Models\Quote;
use App\Models\Credit;
@ -23,10 +22,12 @@ use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use App\Services\EDocument\Standards\Peppol;
use horstoeko\zugferd\ZugferdDocumentBuilder;
use App\Services\EDocument\Standards\FatturaPA;
use App\Services\EDocument\Standards\RoEInvoice;
use App\Services\EDocument\Standards\OrderXDocument;
use App\Services\EDocument\Standards\FacturaEInvoice;
use App\Services\EDocument\Standards\FatturaPA;
use App\Services\EDocument\Standards\ZugferdEDokument;
class CreateEDocument implements ShouldQueue
@ -68,6 +69,8 @@ class CreateEDocument implements ShouldQueue
if ($this->document instanceof Invoice) {
switch ($e_document_type) {
case "PEPPOL":
return (new Peppol($this->document))->toXml();
case "FACT1":
return (new RoEInvoice($this->document))->generateXml();
case "FatturaPA":

View File

@ -15,11 +15,14 @@ use App\Models\Expense;
use App\Services\EDocument\Imports\ParseEDocument;
use App\Utils\TempFile;
use Exception;
use App\Models\Company;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use App\Services\EDocument\Imports\ZugferdEDocument;
class ImportEDocument implements ShouldQueue
{
@ -28,9 +31,9 @@ class ImportEDocument implements ShouldQueue
use Queueable;
use SerializesModels;
public $deleteWhenMissingModels = true;
public $tries = 1;
public function __construct(private readonly string $file_content, private string $file_name, private string $file_mime_type)
public function __construct(private readonly string $file_content, private string $file_name, private string $file_mime_type, private Company $company)
{
}
@ -46,7 +49,21 @@ class ImportEDocument implements ShouldQueue
$file = TempFile::UploadedFileFromRaw($this->file_content, $this->file_name, $this->file_mime_type);
return (new ParseEDocument($file))->run();
return (new ParseEDocument($file, $this->company))->run();
}
public function middleware()
{
return [new WithoutOverlapping($this->company->company_key)];
}
public function failed($exception = null)
{
if ($exception) {
nlog("EXCEPTION:: ImportEDocument:: " . $exception->getMessage());
}
config(['queue.failed.driver' => null]);
}
}

View File

@ -0,0 +1,48 @@
<?php
namespace App\Jobs\Import;
use App\Libraries\MultiDB;
use Illuminate\Bus\Queueable;
use App\Import\Providers\Quickbooks;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class QuickbooksIngest implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
protected $engine;
protected $request;
protected $company;
/**
* Create a new job instance.
*/
public function __construct(array $request, $company)
{
$this->company = $company;
$this->request = $request;
}
/**
* Execute the job.
*/
public function handle(): void
{
MultiDB::setDb($this->company->db);
set_time_limit(0);
$engine = new Quickbooks(['import_type' => 'client', 'hash' => $this->request['hash'] ], $this->company);
foreach ($this->request['import_types'] as $entity) {
$engine->import($entity);
}
$engine->finalizeImport();
}
}

View File

@ -254,7 +254,7 @@ class NinjaMailerJob implements ShouldQueue
private function incrementEmailCounter(): void
{
if(in_array($this->mailer, ['default','mailgun','postmark'])) {
if(in_array($this->nmo->settings->email_sending_method, ['default','mailgun','postmark'])) {
Cache::increment("email_quota".$this->company->account->key);
}
@ -298,7 +298,7 @@ class NinjaMailerJob implements ShouldQueue
/** Force free/trials onto specific mail driver */
if($this->mailer == 'default' && $this->company->account->isNewHostedAccount()) {
if($this->nmo->settings->email_sending_method == 'default' && $this->company->account->isNewHostedAccount()) {
$this->mailer = 'mailgun';
$this->setHostedMailgunMailer();
return $this;

View File

@ -94,7 +94,9 @@ class ProcessMailgunWebhook implements ShouldQueue
}
MultiDB::findAndSetDbByCompanyKey($this->request['event-data']['tags'][0]);
$company = Company::query()->where('company_key', $this->request['event-data']['tags'][0])->first();
/** @var \App\Models\Company $company */
$company = Company::where('company_key', $this->request['event-data']['tags'][0])->first();
if ($company && $this->request['event-data']['event'] == 'complained' && config('ninja.notification.slack')) {
$company->notification(new EmailSpamNotification($company))->ninja();
@ -195,7 +197,7 @@ class ProcessMailgunWebhook implements ShouldQueue
'date' => \Carbon\Carbon::parse($this->request['event-data']['timestamp'])->format('Y-m-d H:i:s') ?? '',
];
if($sl) {
if($sl instanceof SystemLog) {
$data = $sl->log;
$data['history']['events'][] = $event;
$this->updateSystemLog($sl, $data);

View File

@ -79,7 +79,7 @@ class AdjustEmailQuota implements ShouldQueue
/** Use redis pipelines to execute bulk deletes efficiently */
$redis = Redis::connection('sentinel-cache');
$prefix = config('cache.prefix'). ":email_quota*";
$prefix = config('cache.prefix'). "email_quota*";
$keys = $redis->keys($prefix);
@ -92,7 +92,7 @@ class AdjustEmailQuota implements ShouldQueue
}
$keys = null;
$prefix = config('cache.prefix'). ":throttle_notified*";
$prefix = config('cache.prefix'). "throttle_notified*";
$keys = $redis->keys($prefix);

View File

@ -58,10 +58,6 @@ class EmailPayment implements ShouldQueue
*/
public function handle()
{
if ($this->company->is_disabled || (!$this->contact?->email ?? false)) {
nlog("company disabled - or - contact email not found");
return;
}
MultiDB::setDb($this->company->db);
@ -71,6 +67,11 @@ class EmailPayment implements ShouldQueue
$this->contact = $this->payment->client->contacts()->orderBy('is_primary', 'desc')->first();
}
if ($this->company->is_disabled || (!$this->contact?->email ?? false)) {
nlog("company disabled - or - contact email not found");
return;
}
$this->contact->load('client');
$email_builder = (new PaymentEmailEngine($this->payment, $this->contact))->build();

View File

@ -176,8 +176,8 @@ class SendRecurring implements ShouldQueue
private function createRecurringInvitations($invoice): Invoice
{
if ($this->recurring_invoice->invitations->count() == 0) {
$this->recurring_invoice->service()->createInvitations()->save();
$this->recurring_invoice = $this->recurring_invoice->fresh();
$this->recurring_invoice = $this->recurring_invoice->service()->createInvitations()->save();
// $this->recurring_invoice = $this->recurring_invoice->fresh();
}
$this->recurring_invoice->invitations->each(function ($recurring_invitation) use ($invoice) {

View File

@ -96,6 +96,8 @@ class CleanStaleInvoiceOrder implements ShouldQueue
$invoice->service()->removeUnpaidGatewayFees();
});
\DB::connection($db)->table('password_resets')->where('created_at', '<', now()->subHours(12))->delete();
}
}

View File

@ -48,8 +48,7 @@ class TaskAssigned implements ShouldQueue
$company_user = $this->task->assignedCompanyUser();
if(($company_user instanceof CompanyUser) && $this->findEntityAssignedNotification($company_user, 'task'))
{
if(($company_user instanceof CompanyUser) && $this->findEntityAssignedNotification($company_user, 'task')) {
$mo = new EmailObject();
$mo->subject = ctrans('texts.task_assigned_subject', ['task' => $this->task->number, 'date' => now()->setTimeZone($this->task->company->timezone()->name)->format($this->task->company->date_format()) ]);
$mo->body = ctrans('texts.task_assigned_body', ['task' => $this->task->number, 'description' => $this->task->description ?? '', 'client' => $this->task->client ? $this->task->client->present()->name() : ' ']);

View File

@ -61,10 +61,10 @@ class QuoteReminderJob implements ShouldQueue
nrlog("Sending quote reminders on ".now()->format('Y-m-d h:i:s'));
Quote::query()
->where('quotes.is_deleted', 0)
->whereIn('quotes.status_id', [Invoice::STATUS_SENT])
->whereNull('quotes.deleted_at')
->where('quotes.next_send_date', '<=', now()->toDateTimeString())
->where('is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT])
->whereNull('deleted_at')
->where('next_send_date', '<=', now()->toDateTimeString())
->whereHas('client', function ($query) {
$query->where('is_deleted', 0)
->where('deleted_at', null);
@ -88,10 +88,10 @@ class QuoteReminderJob implements ShouldQueue
nrlog("Sending quote reminders on db {$db} ".now()->format('Y-m-d h:i:s'));
Quote::query()
->where('quotes.is_deleted', 0)
->whereIn('quotes.status_id', [Invoice::STATUS_SENT])
->whereNull('quotes.deleted_at')
->where('quotes.next_send_date', '<=', now()->toDateTimeString())
->where('is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT])
->whereNull('deleted_at')
->where('next_send_date', '<=', now()->toDateTimeString())
->whereHas('client', function ($query) {
$query->where('is_deleted', 0)
->where('deleted_at', null);

View File

@ -59,7 +59,6 @@ class Register extends Component
public function register(array $data)
{
$service = new ClientRegisterService(
company: $this->subscription->company,
additional: $this->additional_fields,

View File

@ -349,20 +349,24 @@ class BillingPortalPurchase extends Component
}
if ((int)$this->price == 0) {
$this->steps['payment_required'] = false;
$this->steps['fetched_payment_methods'] = false;
$this->heading_text = ctrans('texts.payment_methods');
return $this;
} else {
// $this->steps['fetched_payment_methods'] = true;
}
$this->methods = $contact->client->service()->getPaymentMethods($this->price);
foreach($this->methods as $method){
$method_values = array_column($this->methods, 'is_paypal');
$is_paypal = in_array('1', $method_values);
if($method['is_paypal'] == '1' && !$this->steps['check_rff']){
if($is_paypal && !$this->steps['check_rff']) {
$this->rff();
break;
}
} elseif(!$this->steps['check_rff']) {
$this->steps['fetched_payment_methods'] = true;
}
$this->heading_text = ctrans('texts.payment_methods');

View File

@ -484,7 +484,7 @@ class BillingPortalPurchasev2 extends Component
$this->methods = [];
}
if ($this->contact && $this->float_amount_total >= 1) {
if ($this->contact && $this->float_amount_total >= 0) {
$this->methods = $this->contact->client->service()->getPaymentMethods($this->float_amount_total);
}
@ -515,8 +515,7 @@ class BillingPortalPurchasev2 extends Component
strlen($this->contact_email ?? '') == 0 ||
strlen($this->client_city ?? '') == 0 ||
strlen($this->client_postal_code ?? '') == 0
)
{
) {
$this->check_rff = true;
}

View File

@ -0,0 +1,287 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Livewire\Flow2;
use App\Libraries\MultiDB;
use App\Models\CompanyGateway;
use App\Models\Invoice;
use App\Utils\Number;
use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\WithSecureContext;
use Livewire\Attributes\Computed;
use Livewire\Attributes\On;
use Livewire\Component;
class InvoicePay extends Component
{
use MakesDates;
use MakesHash;
use WithSecureContext;
private $mappings = [
'client_name' => 'name',
'client_website' => 'website',
'client_phone' => 'phone',
'client_address_line_1' => 'address1',
'client_address_line_2' => 'address2',
'client_city' => 'city',
'client_state' => 'state',
'client_postal_code' => 'postal_code',
'client_country_id' => 'country_id',
'client_shipping_address_line_1' => 'shipping_address1',
'client_shipping_address_line_2' => 'shipping_address2',
'client_shipping_city' => 'shipping_city',
'client_shipping_state' => 'shipping_state',
'client_shipping_postal_code' => 'shipping_postal_code',
'client_shipping_country_id' => 'shipping_country_id',
'client_custom_value1' => 'custom_value1',
'client_custom_value2' => 'custom_value2',
'client_custom_value3' => 'custom_value3',
'client_custom_value4' => 'custom_value4',
'contact_first_name' => 'first_name',
'contact_last_name' => 'last_name',
'contact_email' => 'email',
// 'contact_phone' => 'phone',
];
public $client_address_array = [
'address1',
'address2',
'city',
'state',
'postal_code',
'country_id',
'shipping_address1',
'shipping_address2',
'shipping_city',
'shipping_state',
'shipping_postal_code',
'shipping_country_id',
];
public $invitation_id;
public $invoices;
public $variables;
public $db;
public $settings;
public $terms_accepted = false;
public $signature_accepted = false;
public $payment_method_accepted = false;
public $under_over_payment = false;
public $required_fields = false;
#[On('update.context')]
public function handleContext(string $property, $value): self
{
$this->setContext(property: $property, value: $value);
return $this;
}
#[On('terms-accepted')]
public function termsAccepted()
{
nlog("Terms accepted");
// $this->invite = \App\Models\InvoiceInvitation::withTrashed()->find($this->invitation_id)->withoutRelations();
$this->terms_accepted = true;
}
#[On('signature-captured')]
public function signatureCaptured($base64)
{
nlog("signature captured");
$this->signature_accepted = true;
$invite = \App\Models\InvoiceInvitation::withTrashed()->find($this->invitation_id);
$invite->signature_base64 = $base64;
$invite->signature_date = now()->addSeconds($invite->contact->client->timezone_offset());
$this->setContext('signature', $base64); // $this->context['signature'] = $base64;
$invite->save();
}
#[On('payable-amount')]
public function payableAmount($payable_amount)
{
// $this->setContext('payable_invoices.0.amount', Number::parseFloat($payable_amount)); // $this->context['payable_invoices'][0]['amount'] = Number::parseFloat($payable_amount); //TODO DB: check parseFloat()
$this->under_over_payment = false;
}
#[On('payment-method-selected')]
public function paymentMethodSelected($company_gateway_id, $gateway_type_id, $amount)
{
$this->setContext('company_gateway_id', $company_gateway_id);
$this->setContext('gateway_type_id', $gateway_type_id);
$this->setContext('amount', $amount);
$this->setContext('pre_payment', false);
$this->setContext('is_recurring', false);
$this->setContext('invitation_id', $this->invitation_id);
$this->payment_method_accepted = true;
$company_gateway = CompanyGateway::query()->find($company_gateway_id);
$this->checkRequiredFields($company_gateway);
}
#[On('required-fields')]
public function requiredFieldsFilled()
{
$this->required_fields = false;
}
private function checkRequiredFields(CompanyGateway $company_gateway)
{
$fields = $company_gateway->driver()->getClientRequiredFields();
$this->setContext('fields', $fields); // $this->context['fields'] = $fields;
if ($company_gateway->always_show_required_fields) {
return $this->required_fields = true;
}
$contact = $this->getContext()['contact'];
foreach ($fields as $index => $field) {
$_field = $this->mappings[$field['name']];
if (\Illuminate\Support\Str::startsWith($field['name'], 'client_')) {
if (
empty($contact->client->{$_field})
|| is_null($contact->client->{$_field})
) {
return $this->required_fields = true;
}
}
if (\Illuminate\Support\Str::startsWith($field['name'], 'contact_')) {
if (empty($contact->{$_field}) || is_null($contact->{$_field}) || str_contains($contact->{$_field}, '@example.com')) {
return $this->required_fields = true;
}
}
}
return $this->required_fields = false;
}
#[Computed()]
public function component(): string
{
if (!$this->terms_accepted) {
return Terms::class;
}
if (!$this->signature_accepted) {
return Signature::class;
}
if ($this->under_over_payment) {
return UnderOverPayment::class;
}
if (!$this->payment_method_accepted) {
return PaymentMethod::class;
}
if ($this->required_fields) {
return RequiredFields::class;
}
return ProcessPayment::class;
}
#[Computed()]
public function componentUniqueId(): string
{
return "purchase-" . md5(microtime());
}
public function mount()
{
$this->resetContext();
MultiDB::setDb($this->db);
// @phpstan-ignore-next-line
$invite = \App\Models\InvoiceInvitation::with('contact.client', 'company')->withTrashed()->find($this->invitation_id);
$client = $invite->contact->client;
$settings = $client->getMergedSettings();
$this->setContext('contact', $invite->contact); // $this->context['contact'] = $invite->contact;
$this->setContext('settings', $settings); // $this->context['settings'] = $settings;
$this->setContext('db', $this->db); // $this->context['db'] = $this->db;
nlog($this->invoices);
if(is_array($this->invoices)) {
$this->invoices = Invoice::find($this->transformKeys($this->invoices));
}
$invoices = $this->invoices->filter(function ($i) {
$i = $i->service()
->markSent()
->removeUnpaidGatewayFees()
->save();
return $i->isPayable();
});
//under-over / payment
//required fields
$this->terms_accepted = !$settings->show_accept_invoice_terms;
$this->signature_accepted = !$settings->require_invoice_signature;
$this->under_over_payment = $settings->client_portal_allow_over_payment || $settings->client_portal_allow_under_payment;
$this->required_fields = false;
$this->setContext('variables', $this->variables); // $this->context['variables'] = $this->variables;
$this->setContext('invoices', $invoices); // $this->context['invoices'] = $invoices;
$this->setContext('settings', $settings); // $this->context['settings'] = $settings;
$this->setContext('invitation', $invite); // $this->context['invitation'] = $invite;
$payable_invoices = $invoices->map(function ($i) {
/** @var \App\Models\Invoice $i */
return [
'invoice_id' => $i->hashed_id,
'amount' => $i->partial > 0 ? $i->partial : $i->balance,
'formatted_amount' => Number::formatValue($i->partial > 0 ? $i->partial : $i->balance, $i->client->currency()),
'number' => $i->number,
'date' => $i->translateDate($i->date, $i->client->date_format(), $i->client->locale())
];
})->toArray();
$this->setContext('payable_invoices', $payable_invoices);
}
public function render(): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
{
return render('flow2.invoice-pay');
}
}

View File

@ -0,0 +1,47 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Livewire\Flow2;
use App\Utils\Traits\WithSecureContext;
use Livewire\Attributes\On;
use Livewire\Component;
class InvoiceSummary extends Component
{
use WithSecureContext;
public $invoices;
public function mount()
{
//@TODO for a single invoice - show all details, for multi-invoices, only show the summaries
$this->invoices = $this->getContext()['invoices']; // $this->context['invitation']->invoice;
}
#[On(self::CONTEXT_UPDATE)]
public function onContextUpdate(): void
{
// refactor logic for updating the price for eg if it changes with under/over pay
$this->invoices = $this->getContext()['invoices'];
}
public function render(): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
{
return render('flow2.invoices-summary', [
'invoice' => $this->invoices,
'client' => $this->invoices->first()->client,
]);
}
}

View File

@ -0,0 +1,78 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Livewire\Flow2;
use App\Utils\Traits\WithSecureContext;
use Livewire\Component;
use App\Libraries\MultiDB;
class PaymentMethod extends Component
{
use WithSecureContext;
public $invoice;
public $variables;
public $methods = [];
public $isLoading = true;
public $amount = 0;
public function placeholder()
{
return <<<'HTML'
<div class="flex items-center justify-center min-h-screen">
<svg class="animate-spin h-10 w-10 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
HTML;
}
public function handleSelect(string $company_gateway_id, string $gateway_type_id, string $amount)
{
$this->isLoading = true;
$this->dispatch(
event: 'payment-method-selected',
company_gateway_id: $company_gateway_id,
gateway_type_id: $gateway_type_id,
amount: $amount,
);
}
public function mount()
{
$this->variables = $this->getContext()['variables'];
$this->amount = array_sum(array_column($this->getContext()['payable_invoices'], 'amount'));
MultiDB::setDb($this->getContext()['db']);
$this->methods = $this->getContext()['invitation']->contact->client->service()->getPaymentMethods($this->amount);
if (count($this->methods) == 1) {
$this->dispatch('singlePaymentMethodFound', company_gateway_id: $this->methods[0]['company_gateway_id'], gateway_type_id: $this->methods[0]['gateway_type_id'], amount: $this->amount);
} else {
$this->isLoading = false;
$this->dispatch('loadingCompleted');
}
}
public function render(): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
{
return render('flow2.payment-method', ['methods' => $this->methods]);
}
}

View File

@ -0,0 +1,86 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Livewire\Flow2;
use App\Exceptions\PaymentFailed;
use App\Utils\Traits\WithSecureContext;
use Livewire\Component;
use App\Libraries\MultiDB;
use App\Models\CompanyGateway;
use App\Models\InvoiceInvitation;
use App\Services\ClientPortal\LivewireInstantPayment;
class ProcessPayment extends Component
{
use WithSecureContext;
private ?string $payment_view;
private array $payment_data_payload = [];
public $isLoading = true;
public function mount()
{
MultiDB::setDb($this->getContext()['db']);
$invitation = InvoiceInvitation::find($this->getContext()['invitation_id']);
$data = [
'company_gateway_id' => $this->getContext()['company_gateway_id'],
'payment_method_id' => $this->getContext()['gateway_type_id'],
'payable_invoices' => $this->getContext()['payable_invoices'],
'signature' => isset($this->getContext()['signature']) ? $this->getContext()['signature'] : false,
'signature_ip' => isset($this->getContext()['signature_ip']) ? $this->getContext()['signature_ip'] : false,
'pre_payment' => false,
'frequency_id' => false,
'remaining_cycles' => false,
'is_recurring' => false,
// 'hash' => false,
];
$responder_data = (new LivewireInstantPayment($data))->run();
$company_gateway = CompanyGateway::find($this->getContext()['company_gateway_id']);
if (!$responder_data['success']) {
throw new PaymentFailed($responder_data['error'], 400);
}
$driver = $company_gateway
->driver($invitation->contact->client)
->setPaymentMethod($data['payment_method_id'])
->setPaymentHash($responder_data['payload']['ph']);
$this->payment_data_payload = $driver->processPaymentViewData($responder_data['payload']);
$this->payment_view = $driver->livewirePaymentView(
$this->payment_data_payload,
);
$this->isLoading = false;
}
public function render(): \Illuminate\Contracts\View\Factory|string|\Illuminate\View\View
{
if ($this->isLoading) {
return <<<'HTML'
<template></template>
HTML;
}
return render($this->payment_view, $this->payment_data_payload);
}
}

View File

@ -0,0 +1,136 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Livewire\Flow2;
use App\Libraries\MultiDB;
use App\Models\CompanyGateway;
use App\Services\Client\RFFService;
use App\Utils\Traits\WithSecureContext;
use Livewire\Component;
class RequiredFields extends Component
{
use WithSecureContext;
public ?CompanyGateway $company_gateway;
public ?string $client_name;
public ?string $contact_first_name;
public ?string $contact_last_name;
public ?string $contact_email;
public ?string $client_phone;
public ?string $client_address_line_1;
public ?string $client_city;
public ?string $client_state;
public ?int $client_country_id;
public ?string $client_postal_code;
public ?string $client_shipping_address_line_1;
public ?string $client_shipping_city;
public ?string $client_shipping_state;
public ?string $client_shipping_postal_code;
public ?int $client_shipping_country_id;
public ?string $client_custom_value1;
public ?string $client_custom_value2;
public ?string $client_custom_value3;
public ?string $client_custom_value4;
/** @var array<int, string> */
public array $fields = [];
public bool $is_loading = true;
public array $errors = [];
public function mount(): void
{
MultiDB::setDB(
$this->getContext()['db'],
);
$this->fields = $this->getContext()['fields'];
$this->company_gateway = CompanyGateway::withTrashed()
->with('company')
->find($this->getContext()['company_gateway_id']);
$contact = auth()->user();
$this->client_name = $contact->client->name;
$this->contact_first_name = $contact->first_name;
$this->contact_last_name = $contact->last_name;
$this->contact_email = $contact->email;
$this->client_phone = $contact->client->phone;
$this->client_address_line_1 = $contact->client->address1;
$this->client_city = $contact->client->city;
$this->client_state = $contact->client->state;
$this->client_country_id = $contact->client->country_id;
$this->client_postal_code = $contact->client->postal_code;
$this->client_shipping_address_line_1 = $contact->client->shipping_address1;
$this->client_shipping_city = $contact->client->shipping_city;
$this->client_shipping_state = $contact->client->shipping_state;
$this->client_shipping_postal_code = $contact->client->shipping_postal_code;
$this->client_shipping_country_id = $contact->client->shipping_country_id;
$this->client_custom_value1 = $contact->client->custom_value1;
$this->client_custom_value2 = $contact->client->custom_value2;
$this->client_custom_value3 = $contact->client->custom_value3;
$this->client_custom_value4 = $contact->client->custom_value4;
$rff = new RFFService(
fields: $this->getContext()['fields'],
database: $this->getContext()['db'],
company_gateway_id: $this->company_gateway->id,
);
/** @var \App\Models\ClientContact $contact */
$rff->check($contact);
if ($rff->unfilled_fields === 0) {
$this->dispatch('required-fields');
}
if ($rff->unfilled_fields > 0) {
$this->is_loading = false;
}
}
public function handleSubmit(array $data)
{
$this->errors = [];
$this->is_loading = true;
$rff = new RFFService(
fields: $this->fields,
database: $this->getContext()['db'],
company_gateway_id: $this->company_gateway->id,
);
$contact = auth()->user();
/** @var \App\Models\ClientContact $contact */
$errors = $rff->handleSubmit($data, $contact, return_errors: true, callback: function () {
$this->dispatch('required-fields');
});
if (is_array($errors) && count($errors)) {
$this->errors = $errors;
$this->is_loading = false;
}
}
public function render(): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
{
return render('flow2.required-fields', [
'contact' => $this->getContext()['contact'],
]);
}
}

View File

@ -0,0 +1,23 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Livewire\Flow2;
use Livewire\Component;
class Signature extends Component
{
public function render()
{
return render('components.livewire.signature');
}
}

View File

@ -0,0 +1,36 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Livewire\Flow2;
use App\Utils\Traits\WithSecureContext;
use Livewire\Component;
class Terms extends Component
{
use WithSecureContext;
public $invoice;
public $variables;
public function mount()
{
$this->invoice = $this->getContext()['invoices']->first();
$this->variables = $this->getContext()['variables'];
}
public function render()
{
return render('components.livewire.terms');
}
}

View File

@ -0,0 +1,76 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Livewire\Flow2;
use App\Utils\Number;
use App\Utils\Traits\WithSecureContext;
use Livewire\Component;
class UnderOverPayment extends Component
{
use WithSecureContext;
public $payableAmount;
public $currency;
public $invoice_amount;
public $errors = '';
public $payableInvoices = [];
public function mount()
{
$this->invoice_amount = array_sum(array_column($this->getContext()['payable_invoices'], 'amount'));
$this->currency = $this->getContext()['invitation']->contact->client->currency();
$this->payableInvoices = $this->getContext()['payable_invoices'];
}
public function checkValue(array $payableInvoices)
{
$this->errors = '';
$settings = $this->getContext()['settings'];
foreach($payableInvoices as $key => $invoice) {
$payableInvoices[$key]['amount'] = Number::parseFloat($invoice['formatted_amount']);
}
$input_amount = collect($payableInvoices)->sum('amount');
if($settings->client_portal_allow_under_payment && $settings->client_portal_under_payment_minimum != 0) {
if($input_amount <= $settings->client_portal_under_payment_minimum) {
// return error message under payment too low.
$this->errors = ctrans('texts.minimum_required_payment', ['amount' => $settings->client_portal_under_payment_minimum]);
$this->dispatch('errorMessageUpdate', errors: $this->errors);
}
}
if(!$settings->client_portal_allow_over_payment && ($input_amount > $this->invoice_amount)) {
$this->errors = ctrans('texts.over_payments_disabled');
$this->dispatch('errorMessageUpdate', errors: $this->errors);
}
if(!$this->errors) {
$this->setContext('payable_invoices', $payableInvoices);
$this->dispatch('payable-amount', payable_amount: $input_amount);
}
}
public function render(): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
{
return render('flow2.under-over-payments');
}
}

View File

@ -20,8 +20,6 @@ class PersonalAddress extends Component
public $country_id;
public $countries;
public $saved;
protected $rules = [
@ -33,7 +31,7 @@ class PersonalAddress extends Component
'country_id' => ['sometimes'],
];
public function mount($countries)
public function mount()
{
$this->fill([
'profile' => auth()->guard('contact')->user()->client,
@ -43,8 +41,6 @@ class PersonalAddress extends Component
'state' => auth()->guard('contact')->user()->client->state,
'postal_code' => auth()->guard('contact')->user()->client->postal_code,
'country_id' => auth()->guard('contact')->user()->client->country_id,
'countries' => $countries,
'saved' => ctrans('texts.save'),
]);
}

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