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

This commit is contained in:
paulwer 2024-06-22 14:20:04 +02:00
commit b13e54f49b
273 changed files with 302561 additions and 299001 deletions

View File

@ -24,3 +24,4 @@ PHANTOMJS_PDF_GENERATION=false
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
PDF_GENERATOR=hosted_ninja

View File

@ -1 +1 @@
5.9.6
5.9.9

View File

@ -176,6 +176,7 @@ class BackupUpdate extends Command
try {
$doc_bin = $document->getFile();
} catch(\Exception $e) {
nlog("Exception:: BackupUpdate::" . $e->getMessage());
nlog($e->getMessage());
}
@ -184,8 +185,6 @@ class BackupUpdate extends Command
$document->disk = $this->option('disk');
$document->saveQuietly();
nlog("Documents - Moving {$document->url} to {$this->option('disk')}");
}
});
@ -199,8 +198,6 @@ class BackupUpdate extends Command
if ($backup_bin) {
Storage::disk($this->option('disk'))->put($backup->filename, $backup_bin);
nlog("Backups - Moving {$backup->filename} to {$this->option('disk')}");
}
});
}

View File

@ -121,28 +121,6 @@ class CreateAccount extends Command
(new CreateCompanyTaskStatuses($company, $user))->handle();
(new VersionCheck())->handle();
$this->warmCache();
}
private function warmCache()
{
/* Warm up the cache !*/
$cached_tables = config('ninja.cached_tables');
foreach ($cached_tables as $name => $class) {
if ($name == 'payment_terms') {
$orderBy = 'num_days';
} elseif ($name == 'fonts') {
$orderBy = 'sort_order';
} elseif (in_array($name, ['currencies', 'industries', 'languages', 'countries', 'banks'])) {
$orderBy = 'name';
} else {
$orderBy = 'id';
}
$tableData = $class::orderBy($orderBy)->get();
if ($tableData->count()) {
Cache::forever($name, $tableData);
}
}
}
}

View File

@ -97,10 +97,6 @@ class CreateSingleAccount extends Command
$this->count = 5;
$this->gateway = $this->argument('gateway');
$this->info('Warming up cache');
$this->warmCache();
$this->createSmallAccount();
@ -774,32 +770,6 @@ class CreateSingleAccount extends Command
return $line_items;
}
private function warmCache()
{
/* Warm up the cache !*/
$cached_tables = config('ninja.cached_tables');
foreach ($cached_tables as $name => $class) {
// check that the table exists in case the migration is pending
if (! Schema::hasTable((new $class())->getTable())) {
continue;
}
if ($name == 'payment_terms') {
$orderBy = 'num_days';
} elseif ($name == 'fonts') {
$orderBy = 'sort_order';
} elseif (in_array($name, ['currencies', 'industries', 'languages', 'countries', 'banks'])) {
$orderBy = 'name';
} else {
$orderBy = 'id';
}
$tableData = $class::orderBy($orderBy)->get();
if ($tableData->count()) {
Cache::forever($name, $tableData);
}
}
}
private function createGateways($company, $user)
{
if (config('ninja.testvars.stripe') && ($this->gateway == 'all' || $this->gateway == 'stripe')) {

View File

@ -86,8 +86,6 @@ class CreateTestData extends Command
$this->info('Warming up cache');
$this->warmCache();
$this->createSmallAccount();
$this->createMediumAccount();
$this->createLargeAccount();
@ -673,31 +671,4 @@ class CreateTestData extends Command
return $line_items;
}
private function warmCache()
{
/* Warm up the cache !*/
$cached_tables = config('ninja.cached_tables');
foreach ($cached_tables as $name => $class) {
if (! Cache::has($name)) {
// check that the table exists in case the migration is pending
if (! Schema::hasTable((new $class())->getTable())) {
continue;
}
if ($name == 'payment_terms') {
$orderBy = 'num_days';
} elseif ($name == 'fonts') {
$orderBy = 'sort_order';
} elseif (in_array($name, ['currencies', 'industries', 'languages', 'countries', 'banks'])) {
$orderBy = 'name';
} else {
$orderBy = 'id';
}
$tableData = $class::orderBy($orderBy)->get();
if ($tableData->count()) {
Cache::forever($name, $tableData);
}
}
}
}
}

View File

@ -84,15 +84,13 @@ class DemoMode extends Command
$this->invoice_repo = new InvoiceRepository();
$cached_tables = config('ninja.cached_tables');
$this->info('Migrating');
Artisan::call('migrate:fresh --force');
$this->info('Seeding');
Artisan::call('db:seed --force');
$this->buildCache(true);
Artisan::call('db:seed --force');
Artisan::call('cache:clear');
$this->info('Seeding Random Data');
$this->createSmallAccount();
@ -623,31 +621,4 @@ class DemoMode extends Command
return $line_items;
}
private function warmCache()
{
/* Warm up the cache !*/
$cached_tables = config('ninja.cached_tables');
foreach ($cached_tables as $name => $class) {
if (! Cache::has($name)) {
// check that the table exists in case the migration is pending
if (! Schema::hasTable((new $class())->getTable())) {
continue;
}
if ($name == 'payment_terms') {
$orderBy = 'num_days';
} elseif ($name == 'fonts') {
$orderBy = 'sort_order';
} elseif (in_array($name, ['currencies', 'industries', 'languages', 'countries', 'banks'])) {
$orderBy = 'name';
} else {
$orderBy = 'id';
}
$tableData = $class::orderBy($orderBy)->get();
if ($tableData->count()) {
Cache::forever($name, $tableData);
}
}
}
}
}

View File

@ -62,7 +62,6 @@ class HostedMigrations extends Command
*/
public function handle()
{
$this->buildCache();
if (! MultiDB::userFindAndSetDb($this->option('email'))) {
$this->info('Could not find a user with that email address');

View File

@ -75,8 +75,6 @@ class ImportMigrations extends Command
{
$this->faker = Factory::create();
$this->buildCache();
$path = $this->option('path') ?? public_path('storage/migrations/import');
$directory = new DirectoryIterator($path);

View File

@ -86,8 +86,7 @@ class PostUpdate extends Command
info('queue restarted');
$this->buildCache(true);
Artisan::call('cache:clear');
VersionCheck::dispatch();
info('Sent for version check');

View File

@ -27,6 +27,7 @@ use App\Jobs\Ninja\TaskScheduler;
use App\Jobs\Quote\QuoteCheckExpired;
use App\Jobs\Subscription\CleanStaleInvoiceOrder;
use App\Jobs\Util\DiskCleanup;
use App\Jobs\Util\QuoteReminderJob;
use App\Jobs\Util\ReminderJob;
use App\Jobs\Util\SchedulerCheck;
use App\Jobs\Util\UpdateExchangeRates;
@ -55,6 +56,9 @@ class Kernel extends ConsoleKernel
/* Send reminders */
$schedule->job(new ReminderJob())->hourly()->withoutOverlapping()->name('reminder-job')->onOneServer();
/* Send quote reminders */
$schedule->job(new QuoteReminderJob())->hourly()->withoutOverlapping()->name('quote-reminder-job')->onOneServer();
/* Sends recurring invoices*/
$schedule->job(new RecurringInvoicesCron())->hourly()->withoutOverlapping()->name('recurring-invoice-job')->onOneServer();

View File

@ -507,7 +507,25 @@ class CompanySettings extends BaseSettings
public int $task_round_to_nearest = 1;
/** quote reminders */
public $email_quote_template_reminder1 = '';
public $email_quote_subject_reminder1 = '';
public $enable_quote_reminder1 = false;
public $quote_num_days_reminder1 = 0;
public $quote_schedule_reminder1 = ''; //before_valid_until_date,after_valid_until_date,after_quote_date
public $quote_late_fee_amount1 = 0;
public $quote_late_fee_percent1 = 0;
public static $casts = [
'enable_quote_reminder1' => 'bool',
'quote_num_days_reminder1' => 'int',
'quote_schedule_reminder1' => 'string',
'quote_late_fee_amount1' => 'float',
'quote_late_fee_percent1' => 'float',
'email_quote_template_reminder1' => 'string',
'email_quote_subject_reminder1' => 'string',
'task_round_up' => 'bool',
'task_round_to_nearest' => 'int',
'e_quote_type' => 'string',
@ -962,6 +980,7 @@ class CompanySettings extends BaseSettings
'$invoice.due_date',
'$invoice.total',
'$invoice.balance_due',
'$invoice.project',
],
'quote_details' => [
'$quote.number',
@ -969,6 +988,7 @@ class CompanySettings extends BaseSettings
'$quote.date',
'$quote.valid_until',
'$quote.total',
'$quote.project',
],
'credit_details' => [
'$credit.number',

View File

@ -115,12 +115,32 @@ class EmailTemplateDefaults
case 'email_vendor_notification_body':
return self::emailVendorNotificationBody();
case 'email_quote_template_reminder1':
return self::emailQuoteReminder1Body();
case 'email_quote_subject_reminder1':
return self::emailQuoteReminder1Subject();
default:
return self::emailInvoiceTemplate();
}
}
public static function emailQuoteReminder1Subject()
{
return ctrans('texts.quote_reminder_subject', ['quote' => '$number', 'company' => '$company.name']);
}
public static function emailQuoteReminder1Body()
{
$invoice_message = '<p>$client<br><br>'.self::transformText('quote_reminder_message').'</p><div class="center">$view_button</div>';
return $invoice_message;
}
public static function emailVendorNotificationSubject()
{
return self::transformText('vendor_notification_subject');

View File

@ -220,6 +220,7 @@ class BaseRule implements RuleInterface
try {
$this->invoice->saveQuietly();
} catch(\Exception $e) {
nlog("Exception:: BaseRule::" . $e->getMessage());
}
}
@ -261,7 +262,7 @@ class BaseRule implements RuleInterface
return $this->client->state;
}
return USStates::getState(strlen($this->client->postal_code) > 1 ? $this->client->postal_code : $this->client->shipping_postal_code);
return USStates::getState(strlen($this->client->postal_code ?? '') > 1 ? $this->client->postal_code : $this->client->shipping_postal_code);
} catch (\Exception $e) {
return 'CA';

View File

@ -34006,7 +34006,7 @@ class USStates
'WA', 'WA', 'WA', 'WA', 'WA', 'WA', 'WA', 'AK', 'AK', 'AK', 'AK', 'AK'
];
$prefix = substr($zip, 0, 3);
$prefix = substr(($zip ?? ''), 0, 3);
$index = intval($prefix);
/* converts prefix to integer */
return $zip_by_state[$index] == "--" ? false : $zip_by_state[$index];

View File

@ -0,0 +1,28 @@
<?php
/**
* Quote Ninja (https://Quoteninja.com).
*
* @link https://github.com/Quoteninja/Quoteninja source repository
*
* @copyright Copyright (c) 2024. Quote Ninja LLC (https://Quoteninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Events\Quote;
use App\Models\Company;
use App\Models\QuoteInvitation;
use Illuminate\Queue\SerializesModels;
/**
* Class QuoteReminderWasEmailed.
*/
class QuoteReminderWasEmailed
{
use SerializesModels;
public function __construct(public QuoteInvitation $invitation, public Company $company, public array $event_vars, public string $template)
{
}
}

View File

@ -0,0 +1,44 @@
<?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\Exceptions;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class DuplicatePaymentException extends Exception
{
/**
* Report the exception.
*
* @return void
*/
public function report()
{
//
}
/**
* Render the exception into an HTTP response.
*
* @param Request $request
* @return JsonResponse
*/
public function render($request)
{
return response()->json([
'message' => 'Duplicate request',
], 400);
}
}

View File

@ -31,11 +31,11 @@ class QuoteExport extends BaseExport
private Decorator $decorator;
private array $decorate_keys = [
'client',
'currency',
'invoice',
];
// private array $decorate_keys = [
// 'client',
// 'currency',
// 'invoice',
// ];
public function __construct(Company $company, array $input)
{

View File

@ -31,7 +31,7 @@ class TaskExport extends BaseExport
public string $date_key = 'created_at';
private string $date_format = 'YYYY-MM-DD';
private string $date_format = 'Y-m-d';
public Writer $csv;
@ -180,13 +180,7 @@ class TaskExport extends BaseExport
$logs = json_decode($task->time_log, 1);
$date_format_default = 'Y-m-d';
$date_format = DateFormat::find($task->company->settings->date_format_id);
if ($date_format) {
$date_format_default = $date_format->format;
}
$date_format_default = $this->date_format;
foreach ($logs as $key => $item) {
if (in_array('task.start_date', $this->input['report_keys']) || in_array('start_date', $this->input['report_keys'])) {
@ -236,7 +230,7 @@ class TaskExport extends BaseExport
*/
protected function addTaskStatusFilter(Builder $query, string $status): Builder
{
/** @var array $status_parameters */
$status_parameters = explode(',', $status);
if (in_array('all', $status_parameters) || count($status_parameters) == 0) {

View File

@ -171,16 +171,16 @@ class VendorExport extends BaseExport
return $entity;
}
private function calculateStatus($vendor)
{
if ($vendor->is_deleted) {
return ctrans('texts.deleted');
}
// private function calculateStatus($vendor)
// {
// if ($vendor->is_deleted) {
// return ctrans('texts.deleted');
// }
if ($vendor->deleted_at) {
return ctrans('texts.archived');
}
// if ($vendor->deleted_at) {
// return ctrans('texts.archived');
// }
return ctrans('texts.active');
}
// return ctrans('texts.active');
// }
}

View File

@ -82,11 +82,15 @@ class RecurringExpenseToExpenseFactory
} else {
$locale = $recurring_expense->company->locale();
$date_formats = Cache::get('date_formats');
//@deprecated
// $date_formats = Cache::get('date_formats');
$date_format = $date_formats->filter(function ($item) use ($recurring_expense) {
/** @var \Illuminate\Support\Collection<\App\Models\DateFormat> */
$date_formats = app('date_formats');
$date_format = $date_formats->first(function ($item) use ($recurring_expense) {
return $item->id == $recurring_expense->company->settings->date_format_id;
})->first()->format;
})->format;
}
Carbon::setLocale($locale);
@ -144,7 +148,7 @@ class RecurringExpenseToExpenseFactory
continue;
}
if (Str::contains($match, '|')) {
// if (Str::contains($match, '|')) {
$parts = explode('|', $match); // [ '[MONTH', 'MONTH+2]' ]
$left = substr($parts[0], 1); // 'MONTH'
@ -182,7 +186,7 @@ class RecurringExpenseToExpenseFactory
$value,
1
);
}
// }
}
// Second case with more common calculations.

View File

@ -79,7 +79,7 @@ class ExpenseFilters extends QueryFilters
$this->builder->where(function ($query) use ($status_parameters) {
if (in_array('logged', $status_parameters)) {
$query->orWhere(function ($query) {
$query->where('amount', '>', 0)
$query->where('amount', '>=', 0)
->whereNull('invoice_id')
->whereNull('payment_date')
->where('should_be_invoiced', false);

View File

@ -271,6 +271,7 @@ class InvoiceFilters extends QueryFilters
if (count($parts) != 2) {
return $this->builder;
}
try {
$start_date = Carbon::parse($parts[0]);
@ -281,7 +282,6 @@ class InvoiceFilters extends QueryFilters
return $this->builder;
}
return $this->builder;
}
/**
@ -307,7 +307,6 @@ class InvoiceFilters extends QueryFilters
return $this->builder;
}
return $this->builder;
}

View File

@ -204,7 +204,6 @@ class PaymentFilters extends QueryFilters
return $this->builder;
}
return $this->builder;
}
/**

View File

@ -133,6 +133,10 @@ class RecurringInvoiceFilters extends QueryFilters
return $this->builder->orderByRaw("REGEXP_REPLACE(number,'[^0-9]+','')+0 " . $dir);
}
if($sort_col[0] == 'status_id'){
return $this->builder->orderBy('status_id', $dir)->orderBy('last_sent_date', $dir);
}
if($sort_col[0] == 'next_send_datetime') {
$sort_col[0] = 'next_send_date';
}
@ -162,9 +166,10 @@ class RecurringInvoiceFilters extends QueryFilters
return $this->builder;
}
/** @var array $key_parameters */
$key_parameters = explode(',', $value);
if (count($key_parameters)) {
if (count($key_parameters) > 0) {
return $this->builder->where(function ($query) use ($key_parameters) {
foreach ($key_parameters as $key) {
$query->orWhereJsonContains('line_items', ['product_key' => $key]);
@ -183,6 +188,7 @@ class RecurringInvoiceFilters extends QueryFilters
*/
public function next_send_between(string $range = ''): Builder
{
/** @var array $parts */
$parts = explode('|', $range);
if (!isset($parts[0]) || !isset($parts[1])) {

View File

@ -175,6 +175,7 @@ class TaskFilters extends QueryFilters
return $this->builder;
}
/** @var array $status_parameters */
$status_parameters = explode(',', $value);
if(count($status_parameters) >= 1) {

View File

@ -156,21 +156,15 @@ class TransactionTransformer implements BankRevenueInterface
private function convertCurrency(string $code)
{
$currencies = Cache::get('currencies');
$currencies = app('currencies');
if (!$currencies) {
$this->buildCache(true);
}
$currency = $currencies->filter(function ($item) use ($code) {
$currency = $currencies->first(function ($item) use ($code) {
/** @var \App\Models\Currency $item */
return $item->code == $code;
})->first();
});
if ($currency) {
return $currency->id;
}
return 1;
/** @var \App\Models\Currency $currency */
return $currency ? $currency->id : 1; //@phpstan-ignore-line
}
@ -192,7 +186,7 @@ class TransactionTransformer implements BankRevenueInterface
}
try {
return Carbon::createFromFormat("d-m-Y", $input)->setTimezone($timezone_name)->format($date_format_default) ?? $input;
return Carbon::createFromFormat("d-m-Y", $input)->setTimezone($timezone_name)->format($date_format_default);
} catch (\Exception $e) {
return $input;
}

View File

@ -1,111 +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\Helpers\Bank\Yodlee\DTO;
use Illuminate\Support\Collection;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Data;
/**
* @deprecated
* [
"account": [
[
"CONTAINER": "bank",
"providerAccountId": 1005,
"accountName": "Business Acct",
"accountStatus": "ACTIVE",
"accountNumber": "1011",
"aggregationSource": "USER",
"isAsset": true,
"balance": [
"currency": "AUD",
"amount": 304.98,
],
"id": 10139315,
"includeInNetWorth": true,
"providerId": "3857",
"providerName": "Bank",
"isManual": false,
"availableBalance": {#2966
"currency": "AUD",
"amount": 304.98,
],
"currentBalance": [
"currency": "AUD",
"amount": 3044.98,
],
"accountType": "CHECKING",
"displayedName": "after David",
"createdDate": "2023-01-10T08:29:07Z",
"classification": "SMALL_BUSINESS",
"lastUpdated": "2023-08-01T23:50:13Z",
"nickname": "Business ",
"bankTransferCode": [
[
"id": "062",
"type": "BSB",
],
],
"dataset": [
[
"name": "BASIC_AGG_DATA",
"additionalStatus": "AVAILABLE_DATA_RETRIEVED",
"updateEligibility": "ALLOW_UPDATE",
"lastUpdated": "2023-08-01T23:49:53Z",
"lastUpdateAttempt": "2023-08-01T23:49:53Z",
"nextUpdateScheduled": "2023-08-03T14:45:14Z",
],
],
],
],
];
*/
class AccountSummary extends Data
{
public ?int $id;
#[MapInputName('CONTAINER')]
public ?string $account_type = '';
#[MapInputName('accountName')]
public ?string $account_name = '';
#[MapInputName('accountStatus')]
public ?string $account_status = '';
#[MapInputName('accountNumber')]
public ?string $account_number = '';
#[MapInputName('providerAccountId')]
public int $provider_account_id;
#[MapInputName('providerId')]
public ?string $provider_id = '';
#[MapInputName('providerName')]
public ?string $provider_name = '';
public ?string $nickname = '';
public ?float $current_balance = 0;
public ?string $account_currency = '';
public static function prepareForPipeline(Collection $properties): Collection
{
$properties->put('current_balance', $properties['currentBalance']['amount'] ?? 0);
$properties->put('account_currency', $properties['currentBalance']['currency'] ?? 0);
return $properties;
}
}

View File

@ -171,20 +171,16 @@ class IncomeTransformer implements BankRevenueInterface
private function convertCurrency(string $code)
{
$currencies = Cache::get('currencies');
if (! $currencies) {
$this->buildCache(true);
}
$currencies = app('currencies');
$currency = $currencies->filter(function ($item) use ($code) {
$currency = $currencies->first(function ($item) use ($code) {
/** @var \App\Models\Currency $item */
return $item->code == $code;
})->first();
});
if ($currency) {
return $currency->id;
}
/** @var \App\Models\Currency $currency */
return $currency ? $currency->id : 1; //@phpstan-ignore-line
return 1;
}
}

View File

@ -26,15 +26,15 @@ use BaconQrCode\Writer;
*/
class EpcQrGenerator
{
private array $sepa = [
'serviceTag' => 'BCD',
'version' => 2,
'characterSet' => 1,
'identification' => 'SCT',
'bic' => '',
'purpose' => '',
// private array $sepa = [
// 'serviceTag' => 'BCD',
// 'version' => 2,
// 'characterSet' => 1,
// 'identification' => 'SCT',
// 'bic' => '',
// 'purpose' => '',
];
// ];
public function __construct(protected Company $company, protected Invoice|RecurringInvoice $invoice, protected float $amount)
{
@ -61,12 +61,6 @@ class EpcQrGenerator
} catch(\Throwable $e) {
nlog("EPC QR failure => ".$e->getMessage());
return '';
} catch(\Exception $e) {
nlog("EPC QR failure => ".$e->getMessage());
return '';
} catch(InvalidArgumentException $e) {
nlog("EPC QR failure => ".$e->getMessage());
return '';
}
}

View File

@ -29,6 +29,7 @@ class InvoiceItemSum
use Discounter;
use Taxer;
//@phpstan-ignore-next-line
private array $eu_tax_jurisdictions = [
'AT', // Austria
'BE', // Belgium
@ -170,7 +171,7 @@ class InvoiceItemSum
private function shouldCalculateTax(): self
{
if (!$this->invoice->company?->calculate_taxes || $this->invoice->company->account->isFreeHostedClient()) {
if (!$this->invoice->company?->calculate_taxes || $this->invoice->company->account->isFreeHostedClient()) { //@phpstan-ignore-line
$this->calc_tax = false;
return $this;
}
@ -331,7 +332,7 @@ class InvoiceItemSum
public function setLineTotal($total)
{
$this->item->line_total = $total;
$this->item->line_total = (float) $total;
return $this;
}

View File

@ -27,7 +27,7 @@ class InvoiceItemSumInclusive
use Discounter;
use Taxer;
//@phpstan-ignore-next-line
private array $eu_tax_jurisdictions = [
'AT', // Austria
'BE', // Belgium
@ -98,6 +98,7 @@ class InvoiceItemSumInclusive
private $total_taxes;
/** @phpstan-ignore-next-line */
private $item;
private $line_items;
@ -399,7 +400,7 @@ class InvoiceItemSumInclusive
private function shouldCalculateTax(): self
{
if (!$this->invoice->company?->calculate_taxes || $this->invoice->company->account->isFreeHostedClient()) {
if (!$this->invoice->company?->calculate_taxes || $this->invoice->company->account->isFreeHostedClient()) {//@phpstan-ignore-line
$this->calc_tax = false;
return $this;
}

View File

@ -30,7 +30,7 @@ class ProRata
*/
public function refund(float $amount, Carbon $from_date, Carbon $to_date, int $frequency): float
{
$days = $from_date->copy()->diffInDays($to_date);
$days = intval(abs($from_date->copy()->diffInDays($to_date)));
$days_in_frequency = $this->getDaysInFrequency($frequency);
return round((($days / $days_in_frequency) * $amount), 2);
@ -48,7 +48,7 @@ class ProRata
*/
public function charge(float $amount, Carbon $from_date, Carbon $to_date, int $frequency): float
{
$days = $from_date->copy()->diffInDays($to_date);
$days = intval(abs($from_date->copy()->diffInDays($to_date)));
$days_in_frequency = $this->getDaysInFrequency($frequency);
return round((($days / $days_in_frequency) * $amount), 2);
@ -58,21 +58,21 @@ class ProRata
* Prepares the line items of an invoice
* to be pro rata refunded.
*
* @param Invoice $invoice
* @param ?Invoice $invoice
* @param bool $is_credit
* @return array
* @throws Exception
*/
public function refundItems(Invoice $invoice, $is_credit = false): array
public function refundItems(?Invoice $invoice, $is_credit = false): array
{
if (! $invoice) {
return [];
}
/** @var \App\Models\RecurringInvoice $recurring_invoice **/
$recurring_invoice = RecurringInvoice::find($invoice->recurring_id)->first();
$recurring_invoice = RecurringInvoice::find($invoice->recurring_id);
if (! $recurring_invoice) {
if (! $recurring_invoice) { // @phpstan-ignore-line
throw new \Exception("Invoice isn't attached to a recurring invoice");
}
@ -107,23 +107,23 @@ class ProRata
case RecurringInvoice::FREQUENCY_TWO_WEEKS:
return 14;
case RecurringInvoice::FREQUENCY_FOUR_WEEKS:
return now()->diffInDays(now()->addWeeks(4));
return intval(abs(now()->diffInDays(now()->addWeeks(4))));
case RecurringInvoice::FREQUENCY_MONTHLY:
return now()->diffInDays(now()->addMonthNoOverflow());
return intval(abs(now()->diffInDays(now()->addMonthNoOverflow())));
case RecurringInvoice::FREQUENCY_TWO_MONTHS:
return now()->diffInDays(now()->addMonthsNoOverflow(2));
return intval(abs(now()->diffInDays(now()->addMonthsNoOverflow(2))));
case RecurringInvoice::FREQUENCY_THREE_MONTHS:
return now()->diffInDays(now()->addMonthsNoOverflow(3));
return intval(abs(now()->diffInDays(now()->addMonthsNoOverflow(3))));
case RecurringInvoice::FREQUENCY_FOUR_MONTHS:
return now()->diffInDays(now()->addMonthsNoOverflow(4));
return intval(abs(now()->diffInDays(now()->addMonthsNoOverflow(4))));
case RecurringInvoice::FREQUENCY_SIX_MONTHS:
return now()->diffInDays(now()->addMonthsNoOverflow(6));
return intval(abs(now()->diffInDays(now()->addMonthsNoOverflow(6))));
case RecurringInvoice::FREQUENCY_ANNUALLY:
return now()->diffInDays(now()->addYear());
return intval(abs(now()->diffInDays(now()->addYear())));
case RecurringInvoice::FREQUENCY_TWO_YEARS:
return now()->diffInDays(now()->addYears(2));
return intval(abs(now()->diffInDays(now()->addYears(2))));
case RecurringInvoice::FREQUENCY_THREE_YEARS:
return now()->diffInDays(now()->addYears(3));
return intval(abs(now()->diffInDays(now()->addYears(3))));
default:
return 0;
}

View File

@ -11,19 +11,31 @@
namespace App\Http\Controllers;
use App\Http\Requests\Activity\DownloadHistoricalEntityRequest;
use App\Http\Requests\Activity\ShowActivityRequest;
use App\Models\Activity;
use App\Transformers\ActivityTransformer;
use App\Utils\HostedPDF\NinjaPdf;
use App\Utils\Ninja;
use App\Utils\PhantomJS\Phantom;
use App\Utils\Traits\MakesHash;
use App\Utils\Traits\Pdf\PageNumbering;
use App\Utils\Traits\Pdf\PdfMaker;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use stdClass;
use App\Utils\Ninja;
use App\Models\Client;
use App\Models\Invoice;
use App\Models\Activity;
use Illuminate\Http\Request;
use App\Utils\Traits\MakesHash;
use App\Utils\PhantomJS\Phantom;
use App\Utils\HostedPDF\NinjaPdf;
use App\Utils\Traits\Pdf\PdfMaker;
use App\Utils\Traits\Pdf\PageNumbering;
use Illuminate\Support\Facades\Storage;
use App\Transformers\ActivityTransformer;
use App\Http\Requests\Activity\StoreNoteRequest;
use App\Http\Requests\Activity\ShowActivityRequest;
use App\Http\Requests\Activity\DownloadHistoricalEntityRequest;
use App\Models\Credit;
use App\Models\Expense;
use App\Models\Payment;
use App\Models\PurchaseOrder;
use App\Models\Quote;
use App\Models\RecurringExpense;
use App\Models\RecurringInvoice;
use App\Models\Task;
use App\Models\Vendor;
class ActivityController extends BaseController
{
@ -177,4 +189,89 @@ class ActivityController extends BaseController
echo $pdf;
}, $filename, ['Content-Type' => 'application/pdf']);
}
public function note(StoreNoteRequest $request)
{
/** @var \App\Models\User $user */
$user = auth()->user();
$entity = $request->getEntity();
$activity = new Activity();
$activity->account_id = $user->account_id;
$activity->company_id = $user->company()->id;
$activity->notes = $request->notes;
$activity->user_id = $user->id;
$activity->ip = $request->ip();
$activity->activity_type_id = Activity::USER_NOTE;
switch (get_class($entity)) {
case Invoice::class:
$activity->invoice_id = $entity->id;
$activity->client_id = $entity->client_id;
$activity->project_id = $entity->project_id;
$activity->vendor_id = $entity->vendor_id;
break;
case Credit::class:
$activity->credit_id = $entity->id;
$activity->client_id = $entity->client_id;
$activity->project_id = $entity->project_id;
$activity->vendor_id = $entity->vendor_id;
$activity->invoice_id = $entity->invoice_id;
break;
case Client::class:
$activity->client_id = $entity->id;
break;
case Quote::class:
$activity->quote_id = $entity->id;
$activity->client_id = $entity->client_id;
$activity->project_id = $entity->project_id;
$activity->vendor_id = $entity->vendor_id;
break;
case RecurringInvoice::class:
$activity->recurring_invoice_id = $entity->id;
$activity->client_id = $entity->client_id;
break;
case Expense::class:
$activity->expense_id = $entity->id;
$activity->client_id = $entity->client_id;
$activity->project_id = $entity->project_id;
$activity->vendor_id = $entity->vendor_id;
break;
case RecurringExpense::class:
$activity->recurring_expense_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;
break;
case Vendor::class:
$activity->vendor_id = $entity->id;
break;
case PurchaseOrder::class:
$activity->purchase_order_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;
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;
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;
default:
# code...
break;
}
$activity->save();
return $this->itemResponse($activity);
}
}

View File

@ -118,7 +118,7 @@ class ContactForgotPasswordController extends Controller
})->first();
}
$response = false;
// $response = false;
if ($contact) {
/* Update all instances of the client */
@ -131,16 +131,16 @@ class ContactForgotPasswordController extends Controller
}
if ($request->ajax()) {
if ($response == Password::RESET_THROTTLED) {
if ($response == Password::RESET_THROTTLED) { // @phpstan-ignore-line
return response()->json(['message' => ctrans('passwords.throttled'), 'status' => false], 429);
}
return $response == Password::RESET_LINK_SENT
return $response == Password::RESET_LINK_SENT // @phpstan-ignore-line
? response()->json(['message' => 'Reset link sent to your email.', 'status' => true], 201)
: response()->json(['message' => 'Email not found', 'status' => false], 401);
}
return $response == Password::RESET_LINK_SENT
return $response == Password::RESET_LINK_SENT // @phpstan-ignore-line
? $this->sendResetLinkResponse($request, $response)
: $this->sendResetLinkFailedResponse($request, $response);
}

View File

@ -55,7 +55,7 @@ class ContactLoginController extends Controller
$company = Company::where('company_key', $company_key)->first();
}
/** @var \App\Models\Company $company **/
/** @var ?\App\Models\Company $company **/
if ($company) {
$account = $company->account;
} elseif (! $company && strpos($request->getHost(), config('ninja.app_domain')) !== false) {

View File

@ -137,8 +137,8 @@ class ResetPasswordController extends Controller
return redirect('/#/login');
}
return redirect($this->redirectPath())
->with('status', trans($response));
// return redirect($this->redirectPath())
// ->with('status', trans($response));
}
}

View File

@ -11,7 +11,6 @@
namespace App\Http\Controllers\Bank;
use App\Helpers\Bank\Yodlee\DTO\AccountSummary;
use App\Helpers\Bank\Yodlee\Yodlee;
use App\Http\Controllers\BaseController;
use App\Http\Requests\Yodlee\YodleeAdminRequest;
@ -301,8 +300,6 @@ class YodleeController extends BaseController
$summary = $yodlee->getAccountSummary($account_number);
//@todo remove laravel-data
// $transformed_summary = AccountSummary::from($summary[0]);
$transformed_summary = $this->transformSummary($summary[0]);
return response()->json($transformed_summary, 200);

View File

@ -270,7 +270,7 @@ class BankIntegrationController extends BaseController
$nordigen = new Nordigen();
BankIntegration::where("integration_type", BankIntegration::INTEGRATION_TYPE_NORDIGEN)->whereNotNull('nordigen_account_id')->each(function (BankIntegration $bank_integration) use ($nordigen) {
BankIntegration::where("integration_type", BankIntegration::INTEGRATION_TYPE_NORDIGEN)->where('account_id', $user->account_id)->whereNotNull('nordigen_account_id')->each(function (BankIntegration $bank_integration) use ($nordigen) {
$is_account_active = $nordigen->isAccountActive($bank_integration->nordigen_account_id);
$account = $nordigen->getAccount($bank_integration->nordigen_account_id);

View File

@ -1035,7 +1035,7 @@ class BaseController extends Controller
$resource = new Item($item, $transformer, $this->entity_type);
/** @var \App\Models\User $user */
/** @var ?\App\Models\User $user */
$user = auth()->user();
if ($user && request()->include_static) {
@ -1158,8 +1158,6 @@ class BaseController extends Controller
$data['path'] = $this->setBuild();
$this->buildCache();
if (Ninja::isSelfHost() && $account->set_react_as_default_ap) {
return response()->view('react.index', $data)->header('X-Frame-Options', 'SAMEORIGIN', false);
} else {

View File

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

View File

@ -141,7 +141,7 @@ class ClientController extends BaseController
return $request->disallowUpdate();
}
/** @var \App\Models\User $user */
/** @var ?\App\Models\User $user */
$user = auth()->user();
$client = $this->client_repo->save($request->all(), $client);
@ -426,7 +426,7 @@ class ClientController extends BaseController
try {
/** @var \Postmark\Models\DynamicResponseModel $response */
/** @var ?\Postmark\Models\DynamicResponseModel $response */
$response = $postmark->activateBounce((int)$bounce_id);
if($response && $response?->Message == 'OK' && !$response->Bounce->Inactive && $response->Bounce->Email) {

View File

@ -146,6 +146,7 @@ class InvitationController extends Controller
}
private function fireEntityViewedEvent($invitation, $entity_string)
{
switch ($entity_string) {

View File

@ -97,12 +97,12 @@ class InvoiceController extends Controller
$invitation = false;
match($data['entity_type'] ?? false) {
match($data['entity_type'] ?? 'invoice') {
'invoice' => $invitation = InvoiceInvitation::withTrashed()->find($data['invitation_id']),
'quote' => $invitation = QuoteInvitation::withTrashed()->find($data['invitation_id']),
'credit' => $invitation = CreditInvitation::withTrashed()->find($data['invitation_id']),
'recurring_invoice' => $invitation = RecurringInvoiceInvitation::withTrashed()->find($data['invitation_id']),
false => $invitation = false,
default => $invitation = false,
};
if (! $invitation) {

View File

@ -77,6 +77,7 @@ class PaymentController extends Controller
'EUR' => $data = $bt->formatDataforEur($payment_intent),
'JPY' => $data = $bt->formatDataforJp($payment_intent),
'GBP' => $data = $bt->formatDataforUk($payment_intent),
default => $data = $bt->formatDataforUk($payment_intent),
};
$gateway = $stripe;

View File

@ -90,15 +90,20 @@ class SubscriptionPurchaseController extends Controller
* Set locale for incoming request.
*
* @param string $locale
* @return string
*/
private function setLocale(string $locale): void
private function setLocale(string $locale): string
{
$record = Cache::get('languages')->filter(function ($item) use ($locale) {
return $item->locale == $locale;
})->first();
if ($record) {
App::setLocale($record->locale);
}
/** @var \Illuminate\Support\Collection<\App\Models\Language> */
$languages = app('languages');
$record = $languages->first(function ($item) use ($locale) {
/** @var \App\Models\Language $item */
return $item->locale == $locale;
});
return $record ? $record->locale : 'en';
}
}

View File

@ -693,7 +693,7 @@ class CompanyController extends BaseController
public function updateOriginTaxData(DefaultCompanyRequest $request, Company $company)
{
if($company->settings->country_id == "840" && !$company?->account->isFreeHostedClient()) {
if($company->settings->country_id == "840" && !$company->account->isFreeHostedClient()) {
try {
(new CompanyTaxRate($company))->handle();
} catch(\Exception $e) {

View File

@ -119,8 +119,6 @@ class CompanyUserController extends BaseController
if (! $company_user) {
throw new ModelNotFoundException(ctrans('texts.company_user_not_found'));
return;
}
if ($auth_user->isAdmin()) {
@ -152,7 +150,6 @@ class CompanyUserController extends BaseController
if (! $company_user) {
throw new ModelNotFoundException(ctrans('texts.company_user_not_found'));
return;
}
$this->entity_type = User::class;

View File

@ -594,13 +594,11 @@ class CreditController extends BaseController
$credit->service()->markPaid()->save();
return $this->itemResponse($credit);
break;
case 'clone_to_credit':
$credit = CloneCreditFactory::create($credit, auth()->user()->id);
return $this->itemResponse($credit);
break;
case 'history':
// code...
break;
@ -617,7 +615,7 @@ class CreditController extends BaseController
return response()->streamDownload(function () use ($file) {
echo $file;
}, $credit->numberFormatter() . '.pdf', ['Content-Type' => 'application/pdf']);
break;
case 'archive':
$this->credit_repository->archive($credit);
@ -655,7 +653,6 @@ class CreditController extends BaseController
default:
return response()->json(['message' => ctrans('texts.action_unavailable', ['action' => $action])], 400);
break;
}
}

View File

@ -139,19 +139,19 @@ class EmailController extends BaseController
return $this->itemResponse($entity_obj->fresh());
}
private function sendPurchaseOrder($entity_obj, $data, $template)
{
$this->entity_type = PurchaseOrder::class;
// private function sendPurchaseOrder($entity_obj, $data, $template)
// {
// $this->entity_type = PurchaseOrder::class;
$this->entity_transformer = PurchaseOrderTransformer::class;
// $this->entity_transformer = PurchaseOrderTransformer::class;
$data['template'] = $template;
// $data['template'] = $template;
PurchaseOrderEmail::dispatch($entity_obj, $entity_obj->company, $data);
$entity_obj->sendEvent(Webhook::EVENT_SENT_PURCHASE_ORDER, "vendor");
// PurchaseOrderEmail::dispatch($entity_obj, $entity_obj->company, $data);
// $entity_obj->sendEvent(Webhook::EVENT_SENT_PURCHASE_ORDER, "vendor");
return $this->itemResponse($entity_obj);
}
// return $this->itemResponse($entity_obj);
// }
private function resolveClass(string $entity): string
{

View File

@ -19,10 +19,12 @@ use App\Http\Requests\Expense\BulkExpenseRequest;
use App\Http\Requests\Expense\CreateExpenseRequest;
use App\Http\Requests\Expense\DestroyExpenseRequest;
use App\Http\Requests\Expense\EditExpenseRequest;
use App\Http\Requests\Expense\EDocumentRequest;
use App\Http\Requests\Expense\ShowExpenseRequest;
use App\Http\Requests\Expense\StoreExpenseRequest;
use App\Http\Requests\Expense\UpdateExpenseRequest;
use App\Http\Requests\Expense\UploadExpenseRequest;
use App\Jobs\EDocument\ImportEDocument;
use App\Models\Account;
use App\Models\Expense;
use App\Repositories\ExpenseRepository;
@ -581,4 +583,15 @@ class ExpenseController extends BaseController
return $this->itemResponse($expense->fresh());
}
public function edocument(EDocumentRequest $request): string
{
if ($request->hasFile("documents")) {
return (new ImportEDocument($request->file("documents")[0]->get(), $request->file("documents")[0]->getClientOriginalName()))->handle();
}
else {
return "No file found";
}
}
}

View File

@ -280,186 +280,172 @@ class MigrationController extends BaseController
}
}
if (app()->environment() === 'local') {
}
try {
return response()->json([
'_id' => Str::uuid(),
'method' => config('queue.default'),
'started_at' => now(),
], 200);
} finally {
// Controller logic here
foreach ($companies as $company) {
if (! is_array($company)) {
continue;
}
$company = (array) $company;
$user = auth()->user();
$company_count = $user->account->companies()->count();
$fresh_company = false;
// Look for possible existing company (based on company keys).
$existing_company = Company::query()->whereRaw('BINARY `company_key` = ?', [$company['company_key']])->first();
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations($user->account->companies()->first()->settings));
App::setLocale($user->account->companies()->first()->getLocale());
if (! $existing_company && $company_count >= 10) {
$nmo = new NinjaMailerObject();
$nmo->mailable = new MaxCompanies($user->account->companies()->first());
$nmo->company = $user->account->companies()->first();
$nmo->settings = $user->account->companies()->first()->settings;
$nmo->to_user = $user;
if(!$this->silent_migration) {
NinjaMailerJob::dispatch($nmo, true);
}
return;
} elseif ($existing_company && $company_count > 10) {
$nmo = new NinjaMailerObject();
$nmo->mailable = new MaxCompanies($user->account->companies()->first());
$nmo->company = $user->account->companies()->first();
$nmo->settings = $user->account->companies()->first()->settings;
$nmo->to_user = $user;
if(!$this->silent_migration) {
NinjaMailerJob::dispatch($nmo, true);
}
return;
}
$checks = [
'existing_company' => $existing_company ? (bool) 1 : false,
'force' => array_key_exists('force', $company) ? (bool) $company['force'] : false,
];
// If there's existing company and ** no ** force is provided - skip migration.
if ($checks['existing_company'] == true && $checks['force'] == false) {
nlog('Migrating: Existing company without force. (CASE_01)');
$nmo = new NinjaMailerObject();
$nmo->mailable = new ExistingMigration($existing_company);
$nmo->company = $user->account->companies()->first();
$nmo->settings = $user->account->companies()->first();
$nmo->to_user = $user;
if(!$this->silent_migration) {
NinjaMailerJob::dispatch($nmo, true);
}
return response()->json([
'_id' => Str::uuid(),
'method' => config('queue.default'),
'started_at' => now(),
], 200);
}
// If there's existing company and force ** is provided ** - purge the company and migrate again.
if ($checks['existing_company'] == true && $checks['force'] == true) {
nlog('purging the existing company here');
$this->purgeCompanyWithForceFlag($existing_company);
$account = auth()->user()->account;
$fresh_company = (new ImportMigrations())->getCompany($account);
$fresh_company->is_disabled = true;
$fresh_company->save();
$account->default_company_id = $fresh_company->id;
$account->save();
$fresh_company_token = new CompanyToken();
$fresh_company_token->user_id = $user->id;
$fresh_company_token->company_id = $fresh_company->id;
$fresh_company_token->account_id = $account->id;
$fresh_company_token->name = $request->token_name ?? Str::random(12);
$fresh_company_token->token = $request->token ?? Str::random(64);
$fresh_company_token->is_system = true;
$fresh_company_token->save();
/** @var \App\Models\User $user */
$user->companies()->attach($fresh_company->id, [
'account_id' => $account->id,
'is_owner' => 1,
'is_admin' => 1,
'is_locked' => 0,
'notifications' => CompanySettings::notificationDefaults(),
'permissions' => '',
'settings' => null,
]);
}
// If there's no existing company migrate just normally.
if ($checks['existing_company'] == false) {
nlog('creating fresh company');
$account = auth()->user()->account;
$fresh_company = (new ImportMigrations())->getCompany($account);
$fresh_company->is_disabled = true;
$fresh_company->save();
$fresh_company_token = new CompanyToken();
$fresh_company_token->user_id = $user->id;
$fresh_company_token->company_id = $fresh_company->id;
$fresh_company_token->account_id = $account->id;
$fresh_company_token->name = $request->token_name ?? Str::random(12);
$fresh_company_token->token = $request->token ?? Str::random(64);
$fresh_company_token->is_system = true;
$fresh_company_token->save();
/** @var \App\Models\User $user */
$user->companies()->attach($fresh_company->id, [
'account_id' => $account->id,
'is_owner' => 1,
'is_admin' => 1,
'is_locked' => 0,
'notifications' => CompanySettings::notificationDefaults(),
'permissions' => '',
'settings' => null,
]);
}
$migration_file = $request->file($company['company_index'])
->storeAs(
'migrations',
$request->file($company['company_index'])->getClientOriginalName(),
'public'
);
if (app()->environment() == 'testing') {
nlog('environment is testing = bailing out now');
return;
}
nlog('starting migration job');
nlog($migration_file);
if (Ninja::isHosted()) {
StartMigration::dispatch($migration_file, $user, $fresh_company, $this->silent_migration)->onQueue('migration');
} else {
StartMigration::dispatch($migration_file, $user, $fresh_company, $this->silent_migration);
}
foreach ($companies as $company) {
if (! is_array($company)) {
continue;
}
return response()->json([
'_id' => Str::uuid(),
'method' => config('queue.default'),
'started_at' => now(),
], 200);
$company = (array) $company;
$user = auth()->user();
$company_count = $user->account->companies()->count();
$fresh_company = false;
// Look for possible existing company (based on company keys).
$existing_company = Company::query()->whereRaw('BINARY `company_key` = ?', [$company['company_key']])->first();
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations($user->account->companies()->first()->settings));
App::setLocale($user->account->companies()->first()->getLocale());
if (! $existing_company && $company_count >= 10) {
$nmo = new NinjaMailerObject();
$nmo->mailable = new MaxCompanies($user->account->companies()->first());
$nmo->company = $user->account->companies()->first();
$nmo->settings = $user->account->companies()->first()->settings;
$nmo->to_user = $user;
if(!$this->silent_migration) {
NinjaMailerJob::dispatch($nmo, true);
}
return;
} elseif ($existing_company && $company_count > 10) {
$nmo = new NinjaMailerObject();
$nmo->mailable = new MaxCompanies($user->account->companies()->first());
$nmo->company = $user->account->companies()->first();
$nmo->settings = $user->account->companies()->first()->settings;
$nmo->to_user = $user;
if(!$this->silent_migration) {
NinjaMailerJob::dispatch($nmo, true);
}
return;
}
$checks = [
'existing_company' => $existing_company ? (bool) 1 : false,
'force' => array_key_exists('force', $company) ? (bool) $company['force'] : false,
];
// If there's existing company and ** no ** force is provided - skip migration.
if ($checks['existing_company'] == true && $checks['force'] == false) {
nlog('Migrating: Existing company without force. (CASE_01)');
$nmo = new NinjaMailerObject();
$nmo->mailable = new ExistingMigration($existing_company);
$nmo->company = $user->account->companies()->first();
$nmo->settings = $user->account->companies()->first();
$nmo->to_user = $user;
if(!$this->silent_migration) {
NinjaMailerJob::dispatch($nmo, true);
}
return response()->json([
'_id' => Str::uuid(),
'method' => config('queue.default'),
'started_at' => now(),
], 200);
}
// If there's existing company and force ** is provided ** - purge the company and migrate again.
if ($checks['existing_company'] == true && $checks['force'] == true) {
nlog('purging the existing company here');
$this->purgeCompanyWithForceFlag($existing_company);
$account = auth()->user()->account;
$fresh_company = (new ImportMigrations())->getCompany($account);
$fresh_company->is_disabled = true;
$fresh_company->save();
$account->default_company_id = $fresh_company->id;
$account->save();
$fresh_company_token = new CompanyToken();
$fresh_company_token->user_id = $user->id;
$fresh_company_token->company_id = $fresh_company->id;
$fresh_company_token->account_id = $account->id;
$fresh_company_token->name = $request->token_name ?? Str::random(12);
$fresh_company_token->token = $request->token ?? Str::random(64);
$fresh_company_token->is_system = true;
$fresh_company_token->save();
/** @var \App\Models\User $user */
$user->companies()->attach($fresh_company->id, [
'account_id' => $account->id,
'is_owner' => 1,
'is_admin' => 1,
'is_locked' => 0,
'notifications' => CompanySettings::notificationDefaults(),
'permissions' => '',
'settings' => null,
]);
}
// If there's no existing company migrate just normally.
if ($checks['existing_company'] == false) {
nlog('creating fresh company');
$account = auth()->user()->account;
$fresh_company = (new ImportMigrations())->getCompany($account);
$fresh_company->is_disabled = true;
$fresh_company->save();
$fresh_company_token = new CompanyToken();
$fresh_company_token->user_id = $user->id;
$fresh_company_token->company_id = $fresh_company->id;
$fresh_company_token->account_id = $account->id;
$fresh_company_token->name = $request->token_name ?? Str::random(12);
$fresh_company_token->token = $request->token ?? Str::random(64);
$fresh_company_token->is_system = true;
$fresh_company_token->save();
/** @var \App\Models\User $user */
$user->companies()->attach($fresh_company->id, [
'account_id' => $account->id,
'is_owner' => 1,
'is_admin' => 1,
'is_locked' => 0,
'notifications' => CompanySettings::notificationDefaults(),
'permissions' => '',
'settings' => null,
]);
}
$migration_file = $request->file($company['company_index'])
->storeAs(
'migrations',
$request->file($company['company_index'])->getClientOriginalName(),
'public'
);
if (app()->environment() == 'testing') {
nlog('environment is testing = bailing out now');
return;
}
nlog('starting migration job');
nlog($migration_file);
if (Ninja::isHosted()) {
StartMigration::dispatch($migration_file, $user, $fresh_company, $this->silent_migration)->onQueue('migration');
} else {
StartMigration::dispatch($migration_file, $user, $fresh_company, $this->silent_migration);
}
}
return response()->json([
'_id' => Str::uuid(),
'method' => config('queue.default'),
'started_at' => now(),
], 200);
}
}

View File

@ -22,8 +22,6 @@ use Illuminate\Support\Str;
class OneTimeTokenController extends BaseController
{
private $contexts = [
];
public function __construct()
{

View File

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

View File

@ -219,139 +219,6 @@ class PreviewPurchaseOrderController extends BaseController
}
public function livex(PreviewPurchaseOrderRequest $request)
{
/** @var \App\Models\User $user */
$user = auth()->user();
$company = $user->company();
$file_path = (new PreviewPdf('<html></html>', $company))->handle();
$response = Response::make($file_path, 200);
$response->header('Content-Type', 'application/pdf');
return $response;
MultiDB::setDb($company->db);
$repo = new PurchaseOrderRepository();
$entity_obj = PurchaseOrderFactory::create($company->id, $user->id);
$class = PurchaseOrder::class;
try {
DB::connection(config('database.default'))->beginTransaction();
if ($request->has('entity_id')) {
/** @var \App\Models\PurchaseOrder|\Illuminate\Contracts\Database\Eloquent\Builder $entity_obj **/
$entity_obj = \App\Models\PurchaseOrder::on(config('database.default'))
->with('vendor.company')
->where('id', $this->decodePrimaryKey($request->input('entity_id')))
->where('company_id', $company->id)
->withTrashed()
->first();
}
$entity_obj = $repo->save($request->all(), $entity_obj);
if (!$request->has('entity_id')) {
$entity_obj->service()->fillDefaults()->save();
}
App::forgetInstance('translator');
$t = app('translator');
App::setLocale($entity_obj->company->locale());
$t->replace(Ninja::transformTranslations($entity_obj->company->settings));
$html = new VendorHtmlEngine($entity_obj->invitations()->first());
/** @var \App\Models\Design $design */
$design = \App\Models\Design::withTrashed()->find($entity_obj->design_id);
/* Catch all in case migration doesn't pass back a valid design */
if (!$design) {
$design = \App\Models\Design::find(2);
}
if ($design->is_custom) {
$options = [
'custom_partials' => json_decode(json_encode($design->design), true)
];
$template = new PdfMakerDesign(PdfDesignModel::CUSTOM, $options);
} else {
$template = new PdfMakerDesign(strtolower($design->name));
}
$variables = $html->generateLabelsAndValues();
$state = [
'template' => $template->elements([
'client' => null,
'vendor' => $entity_obj->vendor,
'entity' => $entity_obj,
'pdf_variables' => (array) $entity_obj->company->settings->pdf_variables,
'variables' => $html->generateLabelsAndValues(),
'$product' => $design->design->product,
]),
'variables' => $html->generateLabelsAndValues(),
'options' => [
'client' => null,
'vendor' => $entity_obj->vendor,
'purchase_orders' => [$entity_obj],
'variables' => $html->generateLabelsAndValues(),
],
'process_markdown' => $entity_obj->company->markdown_enabled,
];
$maker = new PdfMaker($state);
$maker
->design($template)
->build();
DB::connection(config('database.default'))->rollBack();
if (request()->query('html') == 'true') {
return $maker->getCompiledHTML();
}
} catch(\Exception $e) {
DB::connection(config('database.default'))->rollBack();
return;
}
/** @var \App\Models\User $user */
$user = auth()->user();
//if phantom js...... inject here..
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
return (new Phantom())->convertHtmlToPdf($maker->getCompiledHTML(true));
}
if (config('ninja.invoiceninja_hosted_pdf_generation') || config('ninja.pdf_generator') == 'hosted_ninja') {
$pdf = (new NinjaPdf())->build($maker->getCompiledHTML(true));
$numbered_pdf = $this->pageNumbering($pdf, $user->company());
if ($numbered_pdf) {
$pdf = $numbered_pdf;
}
return $pdf;
}
$file_path = (new PreviewPdf($maker->getCompiledHTML(true), $company))->handle();
if (Ninja::isHosted()) {
LightLogs::create(new LivePreview())
->increment()
->batch();
}
$response = Response::make($file_path, 200);
$response->header('Content-Type', 'application/pdf');
return $response;
}
private function blankEntity()
{
/** @var \App\Models\User $user */

View File

@ -25,7 +25,6 @@ class ProtectedDownloadController extends BaseController
if (!$hashed_path) {
throw new SystemError('File no longer available', 404);
abort(404, 'File no longer available');
}
return response()->streamDownload(function () use ($hashed_path) {

View File

@ -495,7 +495,7 @@ class PurchaseOrderController extends BaseController
$purchase_orders = PurchaseOrder::withTrashed()->whereIn('id', $this->transformKeys($ids))->company()->get();
if (! $purchase_orders) {
if ($purchase_orders->count() == 0) {
return response()->json(['message' => 'No Purchase Orders Found']);
}
@ -717,7 +717,7 @@ class PurchaseOrderController extends BaseController
default:
return response()->json(['message' => ctrans('texts.action_unavailable', ['action' => $action])], 400);
break;
}
}

View File

@ -787,7 +787,6 @@ class QuoteController extends BaseController
break;
default:
return response()->json(['message' => ctrans('texts.action_unavailable', ['action' => $action])], 400);
break;
}
}

View File

@ -112,8 +112,7 @@ class SelfUpdateController extends BaseController
Artisan::call('view:clear');
Artisan::call('migrate', ['--force' => true]);
Artisan::call('config:clear');
$this->buildCache(true);
Artisan::call('cache:clear');
$this->runModelChecks();

View File

@ -159,8 +159,6 @@ class SetupController extends Controller
(new VersionCheck())->handle();
$this->buildCache(true);
return redirect('/');
} catch (Exception $e) {
nlog($e->getMessage());
@ -234,24 +232,6 @@ class SetupController extends Controller
}
}
private function testPhantom()
{
try {
$key = config('ninja.phantomjs_key');
$url = 'https://www.invoiceninja.org/';
$phantom_url = "https://phantomjscloud.com/api/browser/v2/{$key}/?request=%7Burl:%22{$url}%22,renderType:%22pdf%22%7D";
$pdf = CurlUtils::get($phantom_url);
Storage::disk(config('filesystems.default'))->put('test.pdf', $pdf);
Storage::disk('local')->put('test.pdf', $pdf);
return response(['url' => Storage::disk('local')->url('test.pdf')], 200);
} catch (Exception $e) {
return response([], 500);
}
}
public function clearCompiledCache()
{
$cacheCompiled = base_path('bootstrap/cache/compiled.php');
@ -305,8 +285,7 @@ class SetupController extends Controller
Artisan::call('migrate', ['--force' => true]);
Artisan::call('db:seed', ['--force' => true]);
$this->buildCache(true);
Artisan::call('cache:clear');
(new SchedulerCheck())->handle();

View File

@ -141,6 +141,7 @@ class StripeConnectController extends BaseController
$company_gateway->save();
}
} catch(\Exception $e) {
nlog("Exception:: StripeConnectController::" . $e->getMessage());
nlog("could not harvest stripe company name");
}

View File

@ -61,7 +61,7 @@ class PasswordProtection
return $next($request);
} elseif(strlen(auth()->user()->oauth_provider_id) > 2 && !auth()->user()->company()->oauth_password_required) {
return $next($request);
} elseif ($request->header('X-API-OAUTH-PASSWORD') && strlen($request->header('X-API-OAUTH-PASSWORD')) >= 1) {
} elseif ($request->header('X-API-OAUTH-PASSWORD') && strlen($request->header('X-API-OAUTH-PASSWORD')) > 1) {
//user is attempting to reauth with OAuth - check the token value
//todo expand this to include all OAuth providers
if (auth()->user()->oauth_provider_id == 'google') {
@ -93,6 +93,7 @@ class PasswordProtection
try {
$payload = json_decode(base64_decode(str_replace('_', '/', str_replace('-', '+', explode('.', request()->header('X-API-OAUTH-PASSWORD'))[1]))));
} catch(\Exception $e) {
nlog("Exception:: PasswordProtection::" . $e->getMessage());
nlog("could not decode microsoft response");
return response()->json(['message' => 'Could not decode the response from Microsoft'], 412);
}

View File

@ -1,109 +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\Http\Middleware;
use App\DataMapper\EmailTemplateDefaults;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Schema;
/**
* Class StartupCheck.
*/
class StartupCheck
{
/**
* Handle an incoming request.
* @deprecated
* @param Request $request
* @param Closure $next
*
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
// $start = microtime(true);
/* Make sure our cache is built */
$cached_tables = config('ninja.cached_tables');
foreach ($cached_tables as $name => $class) {
if ($request->has('clear_cache') || ! Cache::has($name)) {
// check that the table exists in case the migration is pending
if (! Schema::hasTable((new $class())->getTable())) {
continue;
}
if ($name == 'payment_terms') {
$orderBy = 'num_days';
} elseif ($name == 'fonts') {
$orderBy = 'sort_order';
} elseif (in_array($name, ['currencies', 'industries', 'languages', 'countries', 'banks'])) {
$orderBy = 'name';
} else {
$orderBy = 'id';
}
$tableData = $class::orderBy($orderBy)->get();
if ($tableData->count()) {
Cache::forever($name, $tableData);
}
}
}
/*Build template cache*/
if ($request->has('clear_cache') || ! Cache::has('templates')) {
$this->buildTemplates();
}
return $next($request);
}
private function buildTemplates($name = 'templates')
{
$data = [
'invoice' => [
'subject' => EmailTemplateDefaults::emailInvoiceSubject(),
'body' => EmailTemplateDefaults::emailInvoiceTemplate(),
],
'quote' => [
'subject' => EmailTemplateDefaults::emailQuoteSubject(),
'body' => EmailTemplateDefaults::emailQuoteTemplate(),
],
'payment' => [
'subject' => EmailTemplateDefaults::emailPaymentSubject(),
'body' => EmailTemplateDefaults::emailPaymentTemplate(),
],
'reminder1' => [
'subject' => EmailTemplateDefaults::emailReminder1Subject(),
'body' => EmailTemplateDefaults::emailReminder1Template(),
],
'reminder2' => [
'subject' => EmailTemplateDefaults::emailReminder2Subject(),
'body' => EmailTemplateDefaults::emailReminder2Template(),
],
'reminder3' => [
'subject' => EmailTemplateDefaults::emailReminder3Subject(),
'body' => EmailTemplateDefaults::emailReminder3Template(),
],
'reminder_endless' => [
'subject' => EmailTemplateDefaults::emailReminderEndlessSubject(),
'body' => EmailTemplateDefaults::emailReminderEndlessTemplate(),
],
'statement' => [
'subject' => EmailTemplateDefaults::emailStatementSubject(),
'body' => EmailTemplateDefaults::emailStatementTemplate(),
],
];
Cache::forever($name, $data);
}
}

View File

@ -0,0 +1,79 @@
<?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\Activity;
use Illuminate\Support\Str;
use App\Http\Requests\Request;
use Illuminate\Validation\Rule;
class StoreNoteRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return $this->checkAuthority();
}
public function rules()
{
/** @var \App\Models\User $user */
$user = auth()->user();
$rules = [
'entity' => 'required|bail|in:invoices,quotes,credits,recurring_invoices,clients,vendors,credits,payments,projects,tasks,expenses,recurring_expenses,bank_transactions,purchase_orders',
'entity_id' => ['required','bail', Rule::exists($this->entity, 'id')->where('company_id', $user->company()->id)],
'notes' => 'required|bail',
];
return $rules;
}
public function prepareForValidation()
{
$input = $this->all();
if(isset($input['entity_id']) && $input['entity_id'] != null) {
$input['entity_id'] = $this->decodePrimaryKey($input['entity_id']);
}
$this->replace($input);
}
public function checkAuthority(): bool
{
$this->error_message = ctrans('texts.authorization_failure');
/** @var \App\Models\User $user */
$user = auth()->user();
$entity = $this->getEntity();
return $user->isAdmin() || $user->can('view', $entity);
}
public function getEntity()
{
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

@ -51,9 +51,9 @@ class StoreBankTransactionRuleRequest extends Request
'applies_to' => 'bail|sometimes|string',
];
$rules['category_id'] = 'bail|sometimes|exists:expense_categories,id,company_id,'.$user->company()->id.',is_deleted,0';
$rules['vendor_id'] = 'bail|sometimes|exists:vendors,id,company_id,'.$user->company()->id.',is_deleted,0';
$rules['client_id'] = 'bail|sometimes|exists:clients,id,company_id,'.$user->company()->id.',is_deleted,0';
$rules['category_id'] = 'bail|sometimes|nullable|exists:expense_categories,id,company_id,'.$user->company()->id.',is_deleted,0';
$rules['vendor_id'] = 'bail|sometimes|nullable|exists:vendors,id,company_id,'.$user->company()->id.',is_deleted,0';
$rules['client_id'] = 'bail|sometimes|nullable|exists:clients,id,company_id,'.$user->company()->id.',is_deleted,0';
return $rules;
}

View File

@ -185,48 +185,44 @@ class StoreClientRequest extends Request
];
}
private function getLanguageId($language_code)
private function getLanguageId(string $language_code)
{
$languages = Cache::get('languages');
/** @var \Illuminate\Support\Collection<\App\Models\Language> */
$languages = app('languages');
$language = $languages->filter(function ($item) use ($language_code) {
$language = $languages->first(function ($item) use ($language_code) {
return $item->locale == $language_code;
})->first();
});
if ($language) {
return (string) $language->id;
}
return $language ? (string)$language->id : '';
return '';
}
private function getCountryCode($country_code)
private function getCountryCode(string $country_code)
{
$countries = Cache::get('countries');
$country = $countries->filter(function ($item) use ($country_code) {
/** @var \Illuminate\Support\Collection<\App\Models\Country> */
$countries = app('countries');
$country = $countries->first(function ($item) use ($country_code) {
return $item->iso_3166_2 == $country_code || $item->iso_3166_3 == $country_code;
})->first();
});
if ($country) {
return (string) $country->id;
}
return $country ? (string) $country->id : '';
return '';
}
private function getCurrencyCode($code)
{
$currencies = Cache::get('currencies');
$currency = $currencies->filter(function ($item) use ($code) {
/** @var \Illuminate\Support\Collection<\App\Models\Currency> */
$currencies = app('currencies');
$currency = $currencies->first(function ($item) use ($code) {
return $item->code == $code;
})->first();
});
if ($currency) {
return (string) $currency->id;
}
return $currency ? (string)$currency->id : '';
return '';
}
}

View File

@ -139,32 +139,28 @@ class UpdateClientRequest extends Request
private function getCountryCode($country_code)
{
$countries = Cache::get('countries');
$country = $countries->filter(function ($item) use ($country_code) {
/** @var \Illuminate\Support\Collection<\App\Models\Country> */
$countries = app('countries');
$country = $countries->first(function ($item) use ($country_code) {
return $item->iso_3166_2 == $country_code || $item->iso_3166_3 == $country_code;
})->first();
});
if ($country) {
return (string) $country->id;
}
return '';
return $country ? (string) $country->id : '';
}
private function getLanguageId($language_code)
{
$languages = Cache::get('languages');
$language = $languages->filter(function ($item) use ($language_code) {
/** @var \Illuminate\Support\Collection<\App\Models\Language> */
$languages = app('languages');
$language = $languages->first(function ($item) use ($language_code) {
return $item->locale == $language_code;
})->first();
});
if ($language) {
return (string) $language->id;
}
return '';
return $language ? (string) $language->id : '';
}
/**

View File

@ -65,6 +65,7 @@ class UpdateCompanyRequest extends Request
$rules['smtp_local_domain'] = 'sometimes|string|nullable';
// $rules['smtp_verify_peer'] = 'sometimes|string';
// $rules['e_invoice'] = ['sometimes','nullable', new ValidScheme()];
if (isset($input['portal_mode']) && ($input['portal_mode'] == 'domain' || $input['portal_mode'] == 'iframe')) {
$rules['portal_domain'] = 'bail|nullable|sometimes|url';
@ -120,11 +121,6 @@ class UpdateCompanyRequest extends Request
$input['smtp_verify_peer'] == 'true' ? true : false;
}
// if(isset($input['e_invoice'])){
// nlog("am i set?");
// $r = FatturaElettronica::validate($input['e_invoice']);
// }
$this->replace($input);
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Http\Requests\Expense;
use App\Http\Requests\Request;
use App\Models\User;
class EDocumentRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
/** @var User $user */
$user = auth()->user();
return $user->isAdmin();
}
public function rules()
{
$rules = [];
if ($this->file('documents') && is_array($this->file('documents'))) {
$rules['documents.*'] = $this->fileValidation();
} elseif ($this->file('documents')) {
$rules['documents'] = $this->fileValidation();
}
return $rules;
}
public function prepareForValidation()
{
$input = $this->all();
$this->replace($input);
}
}

View File

@ -11,6 +11,7 @@
namespace App\Http\Requests\Payment;
use App\Exceptions\DuplicatePaymentException;
use App\Http\Requests\Request;
use App\Http\ValidationRules\Credit\CreditsSumRule;
use App\Http\ValidationRules\Credit\ValidCreditsRules;
@ -79,6 +80,11 @@ 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))
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);
$input = $this->all();
$invoices_total = 0;
@ -130,7 +136,7 @@ class StorePaymentRequest extends Request
}
if (! isset($input['idempotency_key'])) {
$input['idempotency_key'] = substr(sha1(json_encode($input)).time()."{$input['date']}{$input['amount']}{$user->id}", 0, 64);
$input['idempotency_key'] = substr(time()."{$input['date']}{$input['amount']}{$credits_total}{$this->client_id}{$user->company()->company_key}", 0, 64);
}
$this->replace($input);

View File

@ -89,6 +89,7 @@ class PreviewInvoiceRequest extends Request
{
$invitation = false;
/** @phpstan-ignore-next-line */
if(! $this->entity_id ?? false) {
return $this->stubInvitation();
}
@ -98,6 +99,7 @@ class PreviewInvoiceRequest extends Request
'quote' => $invitation = QuoteInvitation::withTrashed()->where('quote_id', $this->entity_id)->first(),
'credit' => $invitation = CreditInvitation::withTrashed()->where('credit_id', $this->entity_id)->first(),
'recurring_invoice' => $invitation = RecurringInvoiceInvitation::withTrashed()->where('recurring_invoice_id', $this->entity_id)->first(),
default => $invitation = false,
};
if($invitation) {

View File

@ -24,7 +24,6 @@ class PreviewPurchaseOrderRequest extends Request
use CleanLineItems;
private ?Vendor $vendor = null;
private string $entity_plural = '';
/**
* Determine if the user is authorized to make this request.
@ -72,7 +71,7 @@ class PreviewPurchaseOrderRequest extends Request
{
$invitation = false;
if(! $this->entity_id ?? false) {
if(! isset($this->entity_id)) {
return $this->stubInvitation();
}
@ -130,12 +129,5 @@ class PreviewPurchaseOrderRequest extends Request
return $entity;
}
private function convertEntityPlural(string $entity): self
{
$this->entity_plural = 'purchase_orders';
return $this;
}
}

View File

@ -155,23 +155,27 @@ class StoreShopClientRequest extends Request
private function getCountryCode($country_code)
{
$countries = Cache::get('countries');
$country = $countries->filter(function ($item) use ($country_code) {
/** @var \Illuminate\Support\Collection<\App\Models\Country> */
$countries = app('countries');
$country = $countries->first(function ($item) use ($country_code) {
return $item->iso_3166_2 == $country_code || $item->iso_3166_3 == $country_code;
})->first();
});
return (string) $country->id;
return $country ? (string) $country->id : '';
}
private function getCurrencyCode($code)
{
$currencies = Cache::get('currencies');
$currency = $currencies->filter(function ($item) use ($code) {
/** @var \Illuminate\Support\Collection<\App\Models\Country> */
$currencies = app('currencies');
$currency = $currencies->first(function ($item) use ($code) {
return $item->code == $code;
})->first();
});
return (string) $currency->id;
return $currency ? (string) $currency->id : '';
}
}

View File

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

View File

@ -0,0 +1,62 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*1`
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\ValidationRules\EInvoice;
use Closure;
use InvoiceNinja\EInvoice\EInvoice;
use Illuminate\Validation\Validator;
use InvoiceNinja\EInvoice\Models\Peppol\Invoice;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Contracts\Validation\ValidatorAwareRule;
/**
* Class BlackListRule.
*/
class ValidScheme implements ValidationRule, ValidatorAwareRule
{
/**
* The validator instance.
*
* @var Validator
*/
protected $validator;
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$r = new EInvoice();
$errors = $r->validateRequest($value['Invoice'], Invoice::class);
foreach ($errors as $key => $msg) {
$this->validator->errors()->add(
"e_invoice.{$key}",
"{$key} - {$msg}"
);
}
}
/**
* Set the current validator.
*/
public function setValidator(Validator $validator): static
{
$this->validator = $validator;
return $this;
}
}

View File

@ -119,17 +119,18 @@ class BaseTransformer
{
$code = array_key_exists($key, $data) ? $data[$key] : false;
$currencies = Cache::get('currencies');
if(!$code)
return $this->company->settings->currency_id;
$currency = $currencies
->filter(function ($item) use ($code) {
return $item->code == $code;
})
->first();
/** @var \Illuminate\Support\Collection<\App\Models\Currency> */
$currencies = app('currencies');
$currency = $currencies->first(function ($item) use ($code) {
return $item->code == $code;
});
return $currency ? (string) $currency->id : $this->company->settings->currency_id;
return $currency
? $currency->id
: $this->company->settings->currency_id;
}
public function getFrequency($frequency = RecurringInvoice::FREQUENCY_MONTHLY): int
@ -653,12 +654,11 @@ class BaseTransformer
/**
* @param $name
*
* @return int|null
* @return int
*/
public function getExpenseCategoryId($name)
{
/** @var \App\Models\ExpenseCategory $ec */
/** @var ?\App\Models\ExpenseCategory $ec */
$ec = ExpenseCategory::query()->where('company_id', $this->company->id)
->where('is_deleted', false)
->whereRaw("LOWER(REPLACE(`name`, ' ' ,'')) = ?", [
@ -674,7 +674,7 @@ class BaseTransformer
$ec->name = $name;
$ec->save();
return $ec ? $ec->id : null;
return $ec->id;
}
public function getOrCreateExpenseCategry($name)

View File

@ -70,16 +70,18 @@ class ClientTransformer extends BaseTransformer
'custom_value2' => $this->getString($data, 'client.custom_value2'),
'custom_value3' => $this->getString($data, 'client.custom_value3'),
'custom_value4' => $this->getString($data, 'client.custom_value4'),
'balance' => preg_replace(
'/[^0-9,.]+/',
'',
$this->getFloat($data, 'client.balance')
),
'paid_to_date' => preg_replace(
'/[^0-9,.]+/',
'',
$this->getFloat($data, 'client.paid_to_date')
),
// 'balance' => preg_replace(
// '/[^0-9,.]+/',
// '',
// $this->getFloat($data, 'client.balance')
// ),
// 'paid_to_date' => preg_replace(
// '/[^0-9,.]+/',
// '',
// $this->getFloat($data, 'client.paid_to_date')
// ),
'paid_to_date' => 0,
'balance' => 0,
'credit_balance' => 0,
'settings' => $settings,
'client_hash' => Str::random(40),

View File

@ -277,7 +277,7 @@ class InvoiceTransformer extends BaseTransformer
if($key == 0) {
continue;
}
/** @var array $row */
if(is_array($row[5])) {
$csv = str_getcsv($row[5][0], ";");
$row[5] = array_combine(explode(",", $csv[0]), explode(",", $csv[1]));

View File

@ -50,7 +50,7 @@ class InvoiceTransformer extends BaseTransformer
'client_id' => $this->getClient($customer_name = $this->getString($invoice_data, $customer_key), null),
'number' => $invoice_number = $this->getString($invoice_data, 'Invoice Number'),
'date' => $this->parseDate($invoice_data[$date_key]) ?: now()->format('Y-m-d'), //27-01-2022
'currency_id' => $this->getCurrencyByCode($invoice_data, 'Currency'),
// 'currency_id' => $this->getCurrencyByCode($invoice_data, 'Currency'),
'status_id' => Invoice::STATUS_SENT,
'due_date' => array_key_exists('Due Date', $invoice_data) ? $this->parseDate($invoice_data['Due Date']) : null,
];

View File

@ -126,13 +126,12 @@ class MatchBankTransactions implements ShouldQueue
{
$collection = collect();
/** @array $invoices */
$invoices = explode(",", $invoice_hashed_ids);
if (count($invoices) >= 1) {
foreach ($invoices as $invoice) {
if (is_string($invoice) && strlen($invoice) > 1) {
$collection->push($this->decodePrimaryKey($invoice));
}
foreach ($invoices as $invoice) {
if (is_string($invoice) && strlen($invoice) > 1) {
$collection->push($this->decodePrimaryKey($invoice));
}
}
@ -189,7 +188,7 @@ class MatchBankTransactions implements ShouldQueue
private function coalesceExpenses($expense): string
{
if (!$this->bt->expense_id || strlen($this->bt->expense_id) < 1) {
if (!$this->bt->expense_id || strlen($this->bt->expense_id ?? '') < 2) {
return $expense;
}
@ -233,11 +232,12 @@ class MatchBankTransactions implements ShouldQueue
$_invoices = Invoice::query()
->withTrashed()
->where('company_id', $this->bt->company_id)
->whereIn('id', $this->getInvoices($input['invoice_ids']));
->whereIn('id', $this->getInvoices($input['invoice_ids']))
->get();
$amount = $this->bt->amount;
if ($_invoices && $this->checkPayable($_invoices)) {
if ($_invoices->count() >0 && $this->checkPayable($_invoices)) {
$this->createPayment($_invoices, $amount);
$this->bts->push($this->bt->id);
@ -312,8 +312,11 @@ class MatchBankTransactions implements ShouldQueue
if ($_amount) {
$this->attachable_invoices[] = ['id' => $this->invoice->id, 'amount' => $_amount];
$this->invoice->next_send_date = null;
$this->invoice
->service()
->applyNumber()
->setExchangeRate()
->updateBalance($_amount * -1)
->updatePaidToDate($_amount)
@ -323,6 +326,7 @@ class MatchBankTransactions implements ShouldQueue
});
}, 2);
// @phpstan-ignore-next-line
if (!$this->invoice) {
return;
}
@ -355,7 +359,7 @@ class MatchBankTransactions implements ShouldQueue
$this->setExchangeRate($payment);
/* Create a payment relationship to the invoice entity */
foreach ($this->attachable_invoices as $attachable_invoice) {
foreach ($this->attachable_invoices as $attachable_invoice) { // @phpstan-ignore-line
$payment->invoices()->attach($attachable_invoice['id'], [
'amount' => $attachable_invoice['amount'],
]);
@ -363,14 +367,6 @@ class MatchBankTransactions implements ShouldQueue
event('eloquent.created: App\Models\Payment', $payment);
$this->invoice->next_send_date = null;
$this->invoice
->service()
->applyNumber()
->deletePdf()
->save();
$payment->ledger()
->updatePaymentBalance($amount * -1);
@ -389,7 +385,13 @@ class MatchBankTransactions implements ShouldQueue
event(new PaymentWasCreated($payment, $payment->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
event(new InvoiceWasPaid($this->invoice, $payment, $payment->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
$this->bt->invoice_ids = $invoices->get()->pluck('hashed_id')->implode(',');
$hashed_keys = [];
foreach($this->attachable_invoices as $attachable_invoice){ //@phpstan-ignore-line
$hashed_keys[] = $this->encodePrimaryKey($attachable_invoice['id']);
}
$this->bt->invoice_ids = implode(",", $hashed_keys);
$this->bt->status_id = BankTransaction::STATUS_CONVERTED;
$this->bt->payment_id = $payment->id;
$this->bt->save();

View File

@ -70,6 +70,7 @@ class UpdateTaxData implements ShouldQueue
} catch(\Exception $e) {
nlog("Exception:: UpdateTaxData::" . $e->getMessage());
nlog("problem getting tax data => ".$e->getMessage());
}

View File

@ -1205,6 +1205,7 @@ class CompanyImport implements ShouldQueue
continue;
}
/** @var string $storage_url */
$storage_url = (object)$this->getObject('storage_url', true);
if (!Storage::exists($document->url) && is_string($storage_url)) {
@ -1351,45 +1352,31 @@ class CompanyImport implements ShouldQueue
switch ($type) {
case Company::class:
return $this->company->id;
break;
case Client::class:
return $this->transformId('clients', $id);
break;
case ClientContact::class:
return $this->transformId('client_contacts', $id);
break;
case Credit::class:
return $this->transformId('credits', $id);
break;
case Expense::class:
return $this->transformId('expenses', $id);
break;
case 'invoices':
return $this->transformId('invoices', $id);
break;
case Payment::class:
return $this->transformId('payments', $id);
break;
case Project::class:
return $this->transformId('projects', $id);
break;
case Product::class:
return $this->transformId('products', $id);
break;
case Quote::class:
return $this->transformId('quotes', $id);
break;
case RecurringInvoice::class:
return $this->transformId('recurring_invoices', $id);
break;
case Company::class:
return $this->transformId('clients', $id);
break;
default:
return false;
break;
}
}
@ -1420,10 +1407,10 @@ class CompanyImport implements ShouldQueue
switch ($type) {
case 'invoices':
return $this->transformId('invoices', $id);
break;
case Credit::class:
return $this->transformId('credits', $id);
break;
case Payment::class:
return $this->transformId('payments', $id);
default:

View File

@ -61,6 +61,7 @@ class CompanyTaxRate implements ShouldQueue
try {
$calculated_state = USStates::getState($this->company->settings->postal_code);
} catch(\Exception $e) {
nlog("Exception:: CompanyTaxRate::" . $e->getMessage());
nlog("could not calculate state from postal code => {$this->company->settings->postal_code} or from state {$this->company->settings->state}");
}

View File

@ -123,6 +123,7 @@ class CreateCompany
}
} catch(\Exception $e) {
nlog("Exception:: CreateCompany::" . $e->getMessage());
nlog("Could not resolve country => {$e->getMessage()}");
}
@ -156,6 +157,7 @@ class CreateCompany
return $company;
} catch(\Exception $e) {
nlog("Exception:: CreateCompany::" . $e->getMessage());
nlog("SETUP: could not complete setup for Spanish Locale");
}
@ -189,6 +191,7 @@ class CreateCompany
} catch(\Exception $e) {
nlog($e->getMessage());
nlog("Exception:: CreateCompany::" . $e->getMessage());
nlog("SETUP: could not complete setup for South African Locale");
}
@ -222,6 +225,7 @@ class CreateCompany
} catch(\Exception $e) {
nlog($e->getMessage());
nlog("Exception:: CreateCompany::" . $e->getMessage());
nlog("SETUP: could not complete setup for Australian Locale");
}

View File

@ -65,9 +65,9 @@ class SubscriptionCron
//Requires the crons to be updated and set to hourly @ 00:01
private function timezoneAware()
{
$grouped_company_ids =
Invoice::select('company_id')
Invoice::query()
->with('company')
->where('is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->where('balance', '>', 0)
@ -77,10 +77,11 @@ class SubscriptionCron
->whereNotNull('subscription_id')
->groupBy('company_id')
->cursor()
->each(function ($company_id) {
->each(function ($invoice) {
/** @var \App\Models\Company $company */
$company = Company::find($company_id);
// $company = Company::find($invoice->company_id);
$company = $invoice->company;
$timezone_now = now()->setTimezone($company->timezone()->name ?? 'Pacific/Midway');

View File

@ -0,0 +1,64 @@
<?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\EDocument;
use App\Models\Expense;
use App\Services\EDocument\Imports\ZugferdEDocument;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class ImportEDocument implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
public $deleteWhenMissingModels = true;
private string $file_name;
private readonly string $file_content;
public function __construct(string $file_content, string $file_name)
{
$this->file_content = $file_content;
$this->file_name = $file_name;
}
/**
* Execute the job.
*
* @return Expense
* @throws \Exception
*/
public function handle(): Expense
{
if (str_contains($this->file_name, ".xml")){
switch (true) {
case stristr($this->file_content, "urn:cen.eu:en16931:2017"):
case stristr($this->file_content, "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0"):
case stristr($this->file_content, "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_2.1"):
case stristr($this->file_content, "urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_2.0"):
return (new ZugferdEDocument($this->file_content, $this->file_name))->run();
default:
throw new Exception("E-Invoice standard not supported");
}
}
else {
throw new Exception("File type not supported");
}
}
}

View File

@ -40,7 +40,7 @@ class CreateRawPdf
public Invoice | Credit | Quote | RecurringInvoice | PurchaseOrder $entity;
public $company;
public \App\Models\Company $company;
public $contact;
@ -55,6 +55,7 @@ class CreateRawPdf
{
$this->invitation = $invitation;
$this->company = $invitation->company;
if ($invitation instanceof InvoiceInvitation) {
$this->entity = $invitation->invoice;
@ -110,12 +111,22 @@ class CreateRawPdf
try {
$pdf = $ps->boot()->getPdf();
} catch (\Exception) {
} catch (\Exception $e) {
echo "EXCEPTION::".PHP_EOL;
echo $e->getMessage().PHP_EOL;
throw new FilePermissionsFailure('Unable to generate the raw PDF');
}
if ($this->entity_string == "invoice" && $this->entity->client->getSetting("merge_e_invoice_to_pdf")) {
$pdf = (new MergeEDocument($this->entity, $pdf))->handle();
}
$merge_docs = isset($this->entity->client) ? $this->entity->client->getSetting('embed_documents') : $this->company->getSetting('embed_documents');
if($merge_docs && ($this->entity->documents()->where('is_public', true)->count() > 0 || $this->company->documents()->where('is_public', true)->count() > 0)) {
$pdf = $this->entity->documentMerge($pdf);
}
return $pdf;
}

View File

@ -192,7 +192,7 @@ class CreateUbl implements ShouldQueue
/**
* @param $item
* @param $invoice_total
* @return float|int
* @return float
*/
private function getItemTaxable($item, $invoice_total)
{

View File

@ -71,6 +71,7 @@ class AdjustEmailQuota implements ShouldQueue
try {
LightLogs::create(new EmailCount($email_count, $account->key))->send(); // this runs syncronously
} catch(\Exception $e) {
nlog("Exception:: AdjustEmailQuota::" . $e->getMessage());
nlog($e->getMessage());
}
}

View File

@ -93,6 +93,7 @@ class BankTransactionSync implements ShouldQueue
try {
(new ProcessBankTransactionsNordigen($bank_integration))->handle();
} catch(\Exception $e) {
nlog("Exception:: BankTransactioSync::" . $e->getMessage());
sleep(20);
}

View File

@ -77,7 +77,7 @@ class RefundCancelledAccount implements ShouldQueue
$end_date = Carbon::parse($plan_expires);
$now = Carbon::now();
$days_left = $now->diffInDays($end_date);
$days_left = intval(abs($now->diffInDays($end_date)));
$pro_rata_ratio = $days_left / 365;

View File

@ -63,7 +63,7 @@ class TaskScheduler implements ShouldQueue
//@var \App\Models\Schedule $scheduler
$scheduler->service()->runTask();
} catch(\Exception $e) {
nlog($e->getMessage());
nlog("Exception:: TaskScheduler:: Doing job {$scheduler->name}" . $e->getMessage());
}
});
@ -89,6 +89,7 @@ class TaskScheduler implements ShouldQueue
/** @var \App\Models\Scheduler $scheduler */
$scheduler->service()->runTask();
} catch(\Exception $e) {
nlog("Exception:: TaskScheduler::" . $e->getMessage());
nlog($e->getMessage());
}

View File

@ -60,7 +60,7 @@ class CleanStaleInvoiceOrder implements ShouldQueue
Invoice::query()
->withTrashed()
->where('status_id', Invoice::STATUS_SENT)
->where('created_at', '<', now()->subMinutes(30))
->where('updated_at', '<', now()->subHour())
->where('balance', '>', 0)
->whereJsonContains('line_items', ['type_id' => '3'])
->cursor()
@ -88,7 +88,7 @@ class CleanStaleInvoiceOrder implements ShouldQueue
Invoice::query()
->withTrashed()
->where('status_id', Invoice::STATUS_SENT)
->where('created_at', '<', now()->subMinutes(30))
->where('updated_at', '<', now()->subHour())
->where('balance', '>', 0)
->whereJsonContains('line_items', ['type_id' => '3'])
->cursor()

View File

@ -0,0 +1,71 @@
<?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\Task;
use App\Models\Task;
use App\Libraries\MultiDB;
use App\Services\Email\Email;
use Illuminate\Bus\Queueable;
use App\Services\Email\EmailObject;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use App\Utils\Traits\Notifications\UserNotifies;
class TaskAssigned implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
use UserNotifies;
/**
* Create a new job instance.
*
*/
public function __construct(private Task $task, private string $db)
{
}
public function handle(): void
{
MultiDB::setDb($this->db);
$company_user = $this->task->assignedCompanyUser();
if($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() : ' ']);
$mo->text_body = ctrans('texts.task_assigned_body',['task' => $this->task->number, 'description' => $this->task->description ?? '', 'client' => $this->task->client ? $this->task->client->present()->name() : ' ']);
$mo->company_key = $this->task->company->company_key;
$mo->html_template = 'email.template.generic';
$mo->to = [new Address($this->task->assigned_user->email, $this->task->assigned_user->present()->name())];
$mo->email_template_body = 'task_assigned_body';
$mo->email_template_subject = 'task_assigned_subject';
(new Email($mo, $this->task->company))->handle();
}
}
public function failed($exception = null)
{
}
}

View File

@ -56,7 +56,7 @@ class VerifyPhone implements ShouldQueue
$twilio = new \Twilio\Rest\Client($sid, $token);
$country = $this->user->account?->companies()?->first()?->country();
$country = $this->user->account?->companies()?->first()?->country(); //@phpstan-ignore-line
if (!$country || strlen($this->user->phone) < 2) {
return;
@ -73,7 +73,7 @@ class VerifyPhone implements ShouldQueue
return;
}
if ($phone_number && strlen($phone_number->phoneNumber) > 1) {
if ($phone_number && strlen($phone_number->phoneNumber) > 1) { //@phpstan-ignore-line
$this->user->phone = $phone_number->phoneNumber;
$this->user->verified_phone_number = true;
$this->user->save();

View File

@ -1455,30 +1455,23 @@ class Import implements ShouldQueue
switch ($status_id) {
case 1:
return $payment;
break;
case 2:
return $payment->service()->deletePayment();
break;
case 3:
return $payment->service()->deletePayment();
break;
case 4:
return $payment;
break;
case 5:
$payment->status_id = Payment::STATUS_PARTIALLY_REFUNDED;
$payment->save();
return $payment;
break;
case 6:
$payment->status_id = Payment::STATUS_REFUNDED;
$payment->save();
return $payment;
break;
default:
return $payment;
break;
}
}

View File

@ -0,0 +1,323 @@
<?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\Util;
use App\Utils\Ninja;
use App\Models\Quote;
use App\Models\Invoice;
use App\Models\Webhook;
use App\Libraries\MultiDB;
use Illuminate\Bus\Queueable;
use Illuminate\Support\Carbon;
use App\DataMapper\InvoiceItem;
use App\Factory\InvoiceFactory;
use App\Jobs\Entity\EmailEntity;
use App\Utils\Traits\MakesDates;
use Illuminate\Support\Facades\App;
use App\Utils\Traits\MakesReminders;
use Illuminate\Support\Facades\Auth;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use App\Events\Quote\QuoteReminderWasEmailed;
class QuoteReminderJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
use MakesReminders;
use MakesDates;
public $tries = 1;
public function __construct()
{
}
/**
* Execute the job.
*
* @return void
*/
public function handle(): void
{
set_time_limit(0);
Auth::logout();
if (! config('ninja.db.multi_db_enabled')) {
nrlog("Sending quote reminders on ".now()->format('Y-m-d h:i:s'));
Quote::query()
->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);
})
->whereHas('company', function ($query) {
$query->where('is_disabled', 0);
})
->with('invitations')->chunk(50, function ($quotes) {
foreach ($quotes as $quote) {
$this->sendReminderForQuote($quote);
}
sleep(1);
});
} else {
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
nrlog("Sending quote reminders on db {$db} ".now()->format('Y-m-d h:i:s'));
Quote::query()
->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);
})
->whereHas('company', function ($query) {
$query->where('is_disabled', 0);
})
->with('invitations')->chunk(50, function ($quotes) {
foreach ($quotes as $quote) {
$this->sendReminderForQuote($quote);
}
sleep(1);
});
}
}
}
private function sendReminderForQuote(Quote $quote)
{
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations($quote->client->getMergedSettings()));
App::setLocale($quote->client->locale());
if ($quote->isPayable()) {
//Attempts to prevent duplicates from sending
if ($quote->reminder_last_sent && Carbon::parse($quote->reminder_last_sent)->startOfDay()->eq(now()->startOfDay())) {
nrlog("caught a duplicate reminder for quote {$quote->number}");
return;
}
$reminder_template = $quote->calculateTemplate('invoice');
nrlog("#{$quote->number} => reminder template = {$reminder_template}");
$quote->service()->touchReminder($reminder_template)->save();
$fees = $this->calcLateFee($quote, $reminder_template);
if($quote->isLocked()) {
return $this->addFeeToNewQuote($quote, $reminder_template, $fees);
}
$quote = $this->setLateFee($quote, $fees[0], $fees[1]);
//20-04-2022 fixes for endless reminders - generic template naming was wrong
$enabled_reminder = 'enable_'.$reminder_template;
if ($reminder_template == 'endless_reminder') {
$enabled_reminder = 'enable_reminder_endless';
}
if (in_array($reminder_template, ['reminder1', 'reminder2', 'reminder3', 'reminder_endless', 'endless_reminder']) &&
$quote->client->getSetting($enabled_reminder) &&
$quote->client->getSetting('send_reminders') &&
(Ninja::isSelfHost() || $quote->company->account->isPaidHostedClient())) {
$quote->invitations->each(function ($invitation) use ($quote, $reminder_template) {
if ($invitation->contact && !$invitation->contact->trashed() && $invitation->contact->email) {
EmailEntity::dispatch($invitation, $invitation->company, $reminder_template);
nrlog("Firing reminder email for invoice {$quote->number} - {$reminder_template}");
$quote->entityEmailEvent($invitation, $reminder_template);
$quote->sendEvent(Webhook::EVENT_REMIND_INVOICE, "client");
}
});
}
$quote->service()->setReminder()->save();
} else {
$quote->next_send_date = null;
$quote->save();
}
}
private function addFeeToNewQuote(Quote $over_due_quote, string $reminder_template, array $fees)
{
$amount = $fees[0];
$percent = $fees[1];
$quote = false;
//2024-06-07 this early return prevented any reminders from sending for users who enabled lock_invoices.
if ($amount > 0 || $percent > 0) {
// return;
$fee = $amount;
if ($over_due_quote->partial > 0) {
$fee += round($over_due_quote->partial * $percent / 100, 2);
} else {
$fee += round($over_due_quote->balance * $percent / 100, 2);
}
/** @var \App\Models\Invoice $quote */
$quote = InvoiceFactory::create($over_due_quote->company_id, $over_due_quote->user_id);
$quote->client_id = $over_due_quote->client_id;
$quote->date = now()->format('Y-m-d');
$quote->due_date = now()->format('Y-m-d');
$quote_item = new InvoiceItem();
$quote_item->type_id = '5';
$quote_item->product_key = trans('texts.fee');
$quote_item->notes = ctrans('texts.late_fee_added_locked_invoice', ['invoice' => $over_due_quote->number, 'date' => $this->translateDate(now()->startOfDay(), $over_due_invoice->client->date_format(), $over_due_invoice->client->locale())]);
$quote_item->quantity = 1;
$quote_item->cost = $fee;
$quote_items = [];
$quote_items[] = $quote_item;
$quote->line_items = $quote_items;
/**Refresh Invoice values*/
$quote = $quote->calc()->getInvoice();
$quote->service()
->createInvitations()
->applyNumber()
->markSent()
->save();
}
if(!$quote) {
$quote = $over_due_quote;
}
$enabled_reminder = 'enable_'.$reminder_template;
// if ($reminder_template == 'endless_reminder') {
// $enabled_reminder = 'enable_reminder_endless';
// }
if (in_array($reminder_template, ['reminder1', 'reminder2', 'reminder3', 'reminder_endless', 'endless_reminder']) &&
$quote->client->getSetting($enabled_reminder) &&
$quote->client->getSetting('send_reminders') &&
(Ninja::isSelfHost() || $quote->company->account->isPaidHostedClient())) {
$quote->invitations->each(function ($invitation) use ($quote, $reminder_template) {
if ($invitation->contact && !$invitation->contact->trashed() && $invitation->contact->email) {
EmailEntity::dispatch($invitation, $invitation->company, $reminder_template);
nrlog("Firing reminder email for qipte {$quote->number} - {$reminder_template}");
event(new QuoteReminderWasEmailed($invitation, $invitation->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null), $reminder_template));
$quote->sendEvent(Webhook::EVENT_REMIND_QUOTE, "client");
}
});
}
$quote->service()->setReminder()->save();
}
/**
* Calculates the late if - if any - and rebuilds the invoice
*
* @param Invoice $quote
* @param string $template
* @return array
*/
private function calcLateFee($quote, $template): array
{
$late_fee_amount = 0;
$late_fee_percent = 0;
switch ($template) {
case 'reminder1':
$late_fee_amount = $quote->client->getSetting('late_fee_amount1');
$late_fee_percent = $quote->client->getSetting('late_fee_percent1');
break;
case 'reminder2':
$late_fee_amount = $quote->client->getSetting('late_fee_amount2');
$late_fee_percent = $quote->client->getSetting('late_fee_percent2');
break;
case 'reminder3':
$late_fee_amount = $quote->client->getSetting('late_fee_amount3');
$late_fee_percent = $quote->client->getSetting('late_fee_percent3');
break;
case 'endless_reminder':
$late_fee_amount = $quote->client->getSetting('late_fee_endless_amount');
$late_fee_percent = $quote->client->getSetting('late_fee_endless_percent');
break;
default:
$late_fee_amount = 0;
$late_fee_percent = 0;
break;
}
return [$late_fee_amount, $late_fee_percent];
}
/**
* Applies the late fee to the invoice line items
*
* @param Invoice $quote
* @param float $amount The fee amount
* @param float $percent The fee percentage amount
*
* @return Invoice
*/
private function setLateFee($quote, $amount, $percent): Invoice
{
$temp_invoice_balance = $quote->balance;
if ($amount <= 0 && $percent <= 0) {
return $quote;
}
$fee = $amount;
if ($quote->partial > 0) {
$fee += round($quote->partial * $percent / 100, 2);
} else {
$fee += round($quote->balance * $percent / 100, 2);
}
$quote_item = new InvoiceItem();
$quote_item->type_id = '5';
$quote_item->product_key = trans('texts.fee');
$quote_item->notes = ctrans('texts.late_fee_added', ['date' => $this->translateDate(now()->startOfDay(), $quote->client->date_format(), $quote->client->locale())]);
$quote_item->quantity = 1;
$quote_item->cost = $fee;
$quote_items = $quote->line_items;
$quote_items[] = $quote_item;
$quote->line_items = $quote_items;
/**Refresh Invoice values*/
$quote = $quote->calc()->getInvoice();
$quote->ledger()->updateInvoiceBalance($quote->balance - $temp_invoice_balance, "Late Fee Adjustment for invoice {$quote->number}");
$quote->client->service()->calculateBalance();
return $quote;
}
}

View File

@ -100,18 +100,6 @@ class CreatePurchaseOrderPdf implements ShouldQueue
return $ps->boot()->getPdf();
$pdf = $this->rawPdf();
if ($pdf) {
try {
Storage::disk($this->disk)->put($this->file_path, $pdf);
} catch(\Exception $e) {
throw new FilePermissionsFailure($e->getMessage());
}
}
return $this->file_path;
}
public function rawPdf()

View File

@ -477,6 +477,23 @@ class MultiDB
return false;
}
public static function findUserByReferralCode(string $referral_code): ?User
{
$current_db = config('database.default');
foreach (self::$dbs as $db) {
if ($user = User::on($db)->where('referral_code', $referral_code)->first()) {
self::setDb($db);
return $user;
}
}
self::setDB($current_db);
return null;
}
public static function findAndSetDbByClientId($client_id): ?Client
{
$current_db = config('database.default');
@ -587,7 +604,7 @@ class MultiDB
$current_db = config('database.default');
if (SMSNumbers::hasNumber($phone)) {
if (SMSNumbers::hasNumber($phone)) { // @phpstan-ignore-line
return true;
}

View File

@ -130,7 +130,6 @@ class OAuth
return $this;
default:
return null;
break;
}
}

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