Merge pull request #8035 from turbo124/preview

Preview
This commit is contained in:
David Bomba 2022-12-05 17:47:42 +11:00 committed by GitHub
commit 3577170731
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
192 changed files with 264141 additions and 62422 deletions

View File

@ -1 +1 @@
5.5.42
5.5.45

View File

@ -25,6 +25,9 @@ use App\Helpers\Invoice\InvoiceSum;
use App\Jobs\Company\CreateCompanyTaskStatuses;
use App\Libraries\MultiDB;
use App\Models\Account;
use App\Models\BankIntegration;
use App\Models\BankTransaction;
use App\Models\BankTransactionRule;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Company;
@ -47,6 +50,7 @@ use App\Utils\Ninja;
use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\MakesHash;
use Carbon\Carbon;
use Database\Factories\BankTransactionRuleFactory;
use Faker\Factory;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
@ -87,7 +91,7 @@ class CreateSingleAccount extends Command
MultiDB::setDb($this->option('database'));
$this->info(date('r').' Create Single Sample Account...');
$this->count = 1;
$this->count = 5;
$this->gateway = $this->argument('gateway');
$this->info('Warming up cache');
@ -179,6 +183,23 @@ class CreateSingleAccount extends Command
'rate' => 5
]);
$bi = BankIntegration::factory()->create([
'account_id' => $account->id,
'company_id' => $company->id,
'user_id' => $user->id,
]);
BankTransaction::factory()->count(50)->create([
'bank_integration_id' => $bi->id,
'user_id' => $user->id,
'company_id' => $company->id,
]);
$btr = BankTransactionRule::factory()->create([
'user_id' => $user->id,
'company_id' => $company->id,
'applies_to' => (bool)rand(0,1) ? 'CREDIT' : 'DEBIT',
]);
$this->info('Creating '.$this->count.' clients');
@ -358,7 +379,7 @@ class CreateSingleAccount extends Command
private function createExpense($client)
{
Expense::factory()->count(rand(1, 2))->create([
Expense::factory()->count(rand(1, 20))->create([
'user_id' => $client->user->id,
'client_id' => $client->id,
'company_id' => $client->company->id,
@ -590,7 +611,6 @@ class CreateSingleAccount extends Command
$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;
@ -608,7 +628,6 @@ class CreateSingleAccount extends Command
if ($tableData->count()) {
Cache::forever($name, $tableData);
}
}
}
}

View File

@ -41,6 +41,7 @@ use App\Models\Vendor;
use App\Models\VendorContact;
use App\Repositories\InvoiceRepository;
use App\Utils\Ninja;
use App\Utils\Traits\AppSetup;
use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\MakesHash;
use Carbon\Carbon;
@ -53,7 +54,7 @@ use Illuminate\Support\Str;
class DemoMode extends Command
{
use MakesHash, GeneratesCounter;
use MakesHash, GeneratesCounter, AppSetup;
protected $signature = 'ninja:demo-mode';
@ -83,34 +84,14 @@ class DemoMode extends Command
$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);
}
}
}
$this->info('Migrating');
Artisan::call('migrate:fresh --force');
$this->info('Seeding');
Artisan::call('db:seed --force');
$this->buildCache(true);
$this->info('Seeding Random Data');
$this->createSmallAccount();

View File

@ -26,6 +26,7 @@ use App\Utils\Traits\MakesReminders;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\App;
//@deprecated 27-11-2022 - only ever should be used for testing
class SendRemindersCron extends Command
{
use MakesReminders, MakesDates;

View File

@ -54,13 +54,13 @@ class Kernel extends ConsoleKernel
$schedule->job(new ReminderJob)->hourly()->withoutOverlapping()->name('reminder-job')->onOneServer();
/* Returns the number of jobs in the queue */
$schedule->job(new QueueSize)->everyFiveMinutes()->withoutOverlapping();
$schedule->job(new QueueSize)->everyFiveMinutes()->withoutOverlapping()->name('queue-size-job')->onOneServer();
/* Checks for large companies and marked them as is_large */
$schedule->job(new CompanySizeCheck)->dailyAt('23:20')->withoutOverlapping()->name('company-size-job')->onOneServer();
/* Pulls in the latest exchange rates */
$schedule->job(new UpdateExchangeRates)->dailyAt('23:30')->withoutOverlapping();
$schedule->job(new UpdateExchangeRates)->dailyAt('23:30')->withoutOverlapping()->name('exchange-rate-job')->onOneServer();
/* Runs cleanup code for subscriptions */
$schedule->job(new SubscriptionCron)->dailyAt('00:01')->withoutOverlapping()->name('subscription-job')->onOneServer();
@ -105,11 +105,11 @@ class Kernel extends ConsoleKernel
//not used @deprecate
// $schedule->job(new SendFailedEmails)->daily()->withoutOverlapping();
$schedule->command('ninja:check-data --database=db-ninja-01')->daily('02:00')->withoutOverlapping();
$schedule->command('ninja:check-data --database=db-ninja-01')->dailyAt('02:10')->withoutOverlapping()->name('check-data-db-1-job')->onOneServer();
$schedule->command('ninja:check-data --database=db-ninja-02')->dailyAt('02:05')->withoutOverlapping();
$schedule->command('ninja:check-data --database=db-ninja-02')->dailyAt('02:20')->withoutOverlapping()->name('check-data-db-2-job')->onOneServer();
$schedule->command('ninja:s3-cleanup')->dailyAt('23:15')->withoutOverlapping();
$schedule->command('ninja:s3-cleanup')->dailyAt('23:15')->withoutOverlapping()->name('s3-cleanup-job')->onOneServer();
}

View File

@ -0,0 +1,50 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\DataMapper\Analytics;
use Turbo124\Beacon\ExampleMetric\GenericMixedMetric;
class BankAccountsCreated extends GenericMixedMetric
{
/**
* The type of Sample.
*
* Monotonically incrementing counter
*
* - counter
*
* @var string
*/
public $type = 'mixed_metric';
/**
* The name of the counter.
* @var string
*/
public $name = 'bank_accounts.created';
/**
* The datetime of the counter measurement.
*
* date("Y-m-d H:i:s")
*
* @var DateTime
*/
public $datetime;
public $int_metric1 = 0;
public function __construct($int_metric1)
{
$this->int_metric1 = $int_metric1;
}
}

View File

@ -32,6 +32,7 @@ use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Schema;
use Illuminate\Validation\ValidationException;
use PDOException;
use Sentry\Laravel\Integration;
use Sentry\State\Scope;
use Swift_TransportException;
use Symfony\Component\Console\Exception\CommandNotFoundException;
@ -84,7 +85,7 @@ class Handler extends ExceptionHandler
}
if (Ninja::isHosted() && ! ($exception instanceof ValidationException)) {
app('sentry')->configureScope(function (Scope $scope): void {
Integration::configureScope(function (Scope $scope): void {
$name = 'hosted@invoiceninja.com';
if (auth()->guard('contact') && auth()->guard('contact')->user()) {
@ -104,9 +105,9 @@ class Handler extends ExceptionHandler
]);
});
app('sentry')->captureException($exception);
Integration::captureUnhandledException($exception);
} elseif (app()->bound('sentry') && $this->shouldReport($exception)) {
app('sentry')->configureScope(function (Scope $scope): void {
Integration::configureScope(function (Scope $scope): void {
if (auth()->guard('contact') && auth()->guard('contact')->user() && auth()->guard('contact')->user()->company->account->report_errors) {
$scope->setUser([
'id' => auth()->guard('contact')->user()->company->account->key,
@ -123,7 +124,7 @@ class Handler extends ExceptionHandler
});
if ($this->validException($exception)) {
app('sentry')->captureException($exception);
Integration::captureUnhandledException($exception);
}
}

View File

@ -76,6 +76,7 @@ class ClientExport extends BaseExport
'contact_custom_value3' => 'contact.custom_value3',
'contact_custom_value4' => 'contact.custom_value4',
'email' => 'contact.email',
'status' => 'status'
];
private array $decorate_keys = [
@ -173,6 +174,19 @@ class ClientExport extends BaseExport
$entity['industry_id'] = $client->industry ? ctrans("texts.industry_{$client->industry->name}") : '';
}
$entity['status'] = $this->calculateStatus($client);
return $entity;
}
private function calculateStatus($client)
{
if($client->is_deleted)
return ctrans('texts.deleted');
if($client->deleted_at)
return ctrans('texts.arcvived');
return ctrans('texts.active');
}
}

View File

@ -54,6 +54,7 @@ class RecurringInvoiceExport extends BaseExport
'po_number' => 'po_number',
'private_notes' => 'private_notes',
'public_notes' => 'public_notes',
'next_send_date' => 'next_send_date',
'status' => 'status_id',
'tax_name1' => 'tax_name1',
'tax_name2' => 'tax_name2',
@ -66,6 +67,7 @@ class RecurringInvoiceExport extends BaseExport
'currency' => 'currency_id',
'vendor' => 'vendor_id',
'project' => 'project_id',
'frequency' => 'frequency_id'
];
private array $decorate_keys = [
@ -162,6 +164,8 @@ class RecurringInvoiceExport extends BaseExport
$entity['vendor'] = $invoice->vendor ? $invoice->vendor->name : '';
}
$entity['frequency'] = $invoice->frequencyForKey($invoice->frequency_id);
return $entity;
}
}

View File

@ -29,6 +29,7 @@ class RecurringInvoiceFactory
$invoice->private_notes = '';
$invoice->date = null;
$invoice->due_date = null;
$invoice->due_date_days = 'terms';
$invoice->partial_due_date = null;
$invoice->is_deleted = false;
$invoice->line_items = json_encode([]);

View File

@ -11,6 +11,7 @@
namespace App\Filters;
use App\Models\Company;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
@ -54,6 +55,15 @@ class DocumentFilters extends QueryFilters
return $this->builder->orderBy($sort_col[0], $sort_col[1]);
}
public function company_documents($value = 'false')
{
if($value == 'true')
return $this->builder->where('documentable_type', Company::class);
return $this->builder;
}
/**
* Returns the base query.
*

View File

@ -56,8 +56,6 @@ class InvoiceFilters extends QueryFilters
if (in_array('unpaid', $status_parameters)) {
$this->builder->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL]);
}
//->where('due_date', '>', Carbon::now())
//->orWhere('partial_due_date', '>', Carbon::now());
if (in_array('overdue', $status_parameters)) {
$this->builder->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
@ -167,7 +165,7 @@ class InvoiceFilters extends QueryFilters
->orderBy('due_date', 'ASC');
}
public function payable(string $client_id)
public function payable(string $client_id = '')
{
if (strlen($client_id) == 0) {
return $this->builder;
@ -185,7 +183,7 @@ class InvoiceFilters extends QueryFilters
* @param string sort formatted as column|asc
* @return Builder
*/
public function sort(string $sort) : Builder
public function sort(string $sort = '') : Builder
{
$sort_col = explode('|', $sort);

View File

@ -539,7 +539,7 @@ class BaseController extends Controller
$query->where('bank_integrations.user_id', $user->id);
}
},
'company.bank_transaction_rules'=> function ($query) use ($updated_at, $user) {
'company.bank_transaction_rules'=> function ($query) use ($user) {
if (! $user->isAdmin()) {
$query->where('bank_transaction_rules.user_id', $user->id);

View File

@ -131,22 +131,22 @@ class EmailController extends BaseController
if(Ninja::isHosted() && !$entity_obj->company->account->account_sms_verified)
return response(['message' => 'Please verify your account to send emails.'], 400);
nlog($entity);
if($entity == 'purchaseOrder' || $entity == 'purchase_order' || $template == 'purchase_order' || $entity == 'App\Models\PurchaseOrder'){
return $this->sendPurchaseOrder($entity_obj, $data, $template);
}
$entity_obj->invitations->each(function ($invitation) use ($data, $entity_string, $entity_obj, $template) {
if (! $invitation->contact->trashed() && $invitation->contact->email) {
$entity_obj->service()->markSent()->save();
EmailEntity::dispatch($invitation->fresh(), $invitation->company, $template, $data);
EmailEntity::dispatch($invitation->fresh(), $invitation->company, $template, $data)->delay(now()->addSeconds(2));
}
});
$entity_obj = $entity_obj->fresh();
$entity_obj->last_sent_date = now();
$entity_obj->save();
/*Only notify the admin ONCE, not once per contact/invite*/
@ -194,7 +194,7 @@ class EmailController extends BaseController
$data['template'] = $template;
PurchaseOrderEmail::dispatch($entity_obj, $entity_obj->company, $data);
PurchaseOrderEmail::dispatch($entity_obj, $entity_obj->company, $data)->delay(now()->addSeconds(2));
return $this->itemResponse($entity_obj);

View File

@ -136,6 +136,8 @@ class ImportController extends Controller
}
$csv = Reader::createFromString($csvfile);
$csvdelimiter = self::detectDelimiter($csvfile);
$csv->setDelimiter($csvdelimiter);
$stmt = new Statement();
$data = iterator_to_array($stmt->process($csv));
@ -156,4 +158,17 @@ class ImportController extends Controller
return $data;
}
public function detectDelimiter($csvfile)
{
$delimiters = array(',', '.', ';');
$bestDelimiter = false;
$count = 0;
foreach ($delimiters as $delimiter)
if (substr_count($csvfile, $delimiter) > $count) {
$count = substr_count($csvfile, $delimiter);
$bestDelimiter = $delimiter;
}
return $bestDelimiter;
}
}

View File

@ -428,13 +428,13 @@ class InvoiceController extends BaseController
event(new InvoiceWasUpdated($invoice, $invoice->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null)));
$transaction = [
'invoice' => $invoice->transaction_event(),
'payment' => [],
'client' => $invoice->client->transaction_event(),
'credit' => [],
'metadata' => [],
];
// $transaction = [
// 'invoice' => $invoice->transaction_event(),
// 'payment' => [],
// 'client' => $invoice->client->transaction_event(),
// 'credit' => [],
// 'metadata' => [],
// ];
// TransactionLog::dispatch(TransactionEvent::INVOICE_UPDATED, $transaction, $invoice->company->db);

View File

@ -183,7 +183,7 @@ class MigrationController extends BaseController
$company->expenses()->forceDelete();
$company->bank_transaction_rules()->forceDelete();
$company->bank_transactions()->forceDelete();
$company->bank_integrations()->forceDelete();
// $company->bank_integrations()->forceDelete();
$company->all_activities()->forceDelete();

View File

@ -26,7 +26,7 @@
* @OA\Property(property="city", type="string", example="", description="________"),
* @OA\Property(property="state", type="string", example="", description="________"),
* @OA\Property(property="postal_code", type="string", example="", description="________"),
* @OA\Property(property="work_phone", type="string", example="555-3434-3434", description="The client phone number"),
* @OA\Property(property="phone", type="string", example="555-3434-3434", description="The client phone number"),
* @OA\Property(property="country_id", type="string", example="", description="________"),
* @OA\Property(property="currency_id", type="string", example="4", description="________"),
* @OA\Property(property="custom_value1", type="string", example="", description="________"),

View File

@ -11,6 +11,7 @@
namespace App\Http\Controllers;
use App\Http\Requests\TwoFactor\EnableTwoFactorRequest;
use App\Models\User;
use App\Transformers\UserTransformer;
use Crypt;
@ -51,17 +52,16 @@ class TwoFactorController extends BaseController
return response()->json(['data' => $data], 200);
}
public function enableTwoFactor()
public function enableTwoFactor(EnableTwoFactorRequest $request)
{
$google2fa = new Google2FA();
$user = auth()->user();
$secret = request()->input('secret');
$oneTimePassword = request()->input('one_time_password');
$secret = $request->input('secret');
$oneTimePassword = $request->input('one_time_password');
if ($google2fa->verifyKey($secret, $oneTimePassword) && $user->phone && $user->email_verified_at) {
$user->google_2fa_secret = encrypt($secret);
$user->save();
return response()->json(['message' => ctrans('texts.enabled_two_factor')], 200);
@ -72,6 +72,11 @@ class TwoFactorController extends BaseController
return response()->json(['message' => 'No phone record or user is not confirmed'], 400);
}
/*
* @param App\Models\User $user
* @param App\Models\User auth()->user()
*/
public function disableTwoFactor()
{
$user = auth()->user();

View File

@ -39,14 +39,6 @@ class BillingPortalPurchasev2 extends Component
*/
public $hash;
/**
* Top level text on the left side of billing page.
*
* @var string
*/
public $heading_text;
/**
* E-mail address model for user input.
*
@ -80,9 +72,13 @@ class BillingPortalPurchasev2 extends Component
*
* @var \string[][]
*/
protected $rules = [
'email' => ['required', 'email'],
];
// protected $rules = [
// 'email' => ['required', 'email'],
// 'data' => ['required', 'array'],
// 'data.*.recurring_qty' => ['required', 'between:100,1000'],
// 'data.*.optional_recurring_qty' => ['required', 'between:100,1000'],
// 'data.*.optional_qty' => ['required', 'between:100,1000'],
// ];
/**
* Id for CompanyGateway record.
@ -120,6 +116,8 @@ class BillingPortalPurchasev2 extends Component
'payment_required' => true,
];
public $data = [];
/**
* List of payment methods fetched from client.
*
@ -187,6 +185,8 @@ class BillingPortalPurchasev2 extends Component
$this->quantity = 1;
$this->data = [];
$this->price = $this->subscription->price;
if (request()->query('coupon')) {
@ -198,6 +198,44 @@ class BillingPortalPurchasev2 extends Component
}
}
public function updatingData()
{
nlog('updating');
// nlog($this->data);
}
public function updatedData()
{
nlog('updated');
nlog($this->data);
$validatedData = $this->validate();
nlog( $validatedData );
}
public function updated($propertyName)
{
nlog("validating {$propertyName}");
$this->errors = $this->validateOnly($propertyName);
nlog($this->errors);
$validatedData = $this->validate();
nlog( $validatedData );
}
public function rules()
{
$rules = [
'email' => ['required', 'email'],
'data' => ['required', 'array'],
'data.*.recurring_qty' => ['required', 'between:100,1000'],
'data.*.optional_recurring_qty' => ['required', 'between:100,1000'],
'data.*.optional_qty' => ['required', 'between:100,1000'],
];
return $rules;
}
/**
* Handle user authentication
*
@ -265,9 +303,6 @@ class BillingPortalPurchasev2 extends Component
}
}
// nlog($this->subscription->group_settings->settings);
// nlog($this->subscription->group_settings->settings->currency_id);
if(array_key_exists('currency_id', $this->request_data)) {
$currency = Cache::get('currencies')->filter(function ($item){
@ -525,4 +560,11 @@ class BillingPortalPurchasev2 extends Component
return render('components.livewire.billing-portal-purchasev2');
}
public function changeData()
{
nlog($this->data);
}
}

View File

@ -44,7 +44,7 @@ class StoreBankIntegrationRequest extends Request
{
$input = $this->all();
if(!array_key_exists('provider_name', $input) || strlen($input['provider_name']) == 0)
if((!array_key_exists('provider_name', $input) || strlen($input['provider_name']) == 0) && array_key_exists('bank_account_name', $input))
$input['provider_name'] = $input['bank_account_name'];
$this->replace($input);

View File

@ -13,6 +13,8 @@ namespace App\Http\Requests\BankTransaction;
use App\Http\Requests\Request;
use App\Models\BankTransaction;
use App\Models\Expense;
use App\Models\Payment;
class MatchBankTransactionRequest extends Request
{
@ -35,8 +37,10 @@ class MatchBankTransactionRequest extends Request
];
$rules['transactions.*.ninja_category_id'] = 'bail|nullable|sometimes|exists:expense_categories,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
$rules['transactions.*.vendor_id'] = 'bail|sometimes|exists:vendors,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
$rules['transactions.*.vendor_id'] = 'bail|nullable|sometimes|exists:vendors,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
$rules['transactions.*.id'] = 'bail|required|exists:bank_transactions,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
$rules['transactions.*.payment_id'] = 'bail|sometimes|nullable|exists:payments,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
$rules['transactions.*.expense_id'] = 'bail|sometimes|nullable|exists:expenses,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
return $rules;
@ -58,7 +62,26 @@ class MatchBankTransactionRequest extends Request
if(array_key_exists('vendor_id', $inputs['transactions'][$key]) && strlen($inputs['transactions'][$key]['vendor_id']) >= 1)
$inputs['transactions'][$key]['vendor_id'] = $this->decodePrimaryKey($inputs['transactions'][$key]['vendor_id']);
// $input = $this->decodePrimaryKeys($input);
if(array_key_exists('payment_id', $inputs['transactions'][$key]) && strlen($inputs['transactions'][$key]['payment_id']) >= 1){
$inputs['transactions'][$key]['payment_id'] = $this->decodePrimaryKey($inputs['transactions'][$key]['payment_id']);
$p = Payment::withTrashed()->find($inputs['transactions'][$key]['payment_id']);
/*Ensure we don't relink an existing payment*/
if(!$p || $p->transaction_id)
$inputs['transactions'][$key]['payment_id'] = null;
}
if(array_key_exists('expense_id', $inputs['transactions'][$key]) && strlen($inputs['transactions'][$key]['expense_id']) >= 1){
$inputs['transactions'][$key]['expense_id'] = $this->decodePrimaryKey($inputs['transactions'][$key]['expense_id']);
$e = Expense::withTrashed()->find($inputs['transactions'][$key]['expense_id']);
/*Ensure we don't relink an existing expense*/
if(!$e || $e->transaction_id)
$inputs['transactions'][$key]['expense_id'] = null;
}
}
$this->replace($inputs);

View File

@ -34,8 +34,7 @@ class StoreBankTransactionRequest extends Request
$rules = [];
if(isset($this->bank_integration_id))
$rules['bank_integration_id'] = 'bail|required|exists:bank_integrations,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
$rules['bank_integration_id'] = 'bail|required|exists:bank_integrations,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
return $rules;
}
@ -44,7 +43,9 @@ class StoreBankTransactionRequest extends Request
{
$input = $this->all();
if(array_key_exists('bank_integration_id', $input) && strlen($input['bank_integration_id']) > 1 && !is_numeric($input['bank_integration_id']))
if(array_key_exists('bank_integration_id', $input) && $input['bank_integration_id'] == "")
unset($input['bank_integration_id']);
elseif(array_key_exists('bank_integration_id', $input) && strlen($input['bank_integration_id']) > 1 && !is_numeric($input['bank_integration_id']))
$input['bank_integration_id'] = $this->decodePrimaryKey($input['bank_integration_id']);
$this->replace($input);

View File

@ -45,8 +45,7 @@ class UpdateBankTransactionRequest extends Request
if(isset($this->expense_id))
$rules['expense_id'] = 'bail|required|exists:expenses,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
if(isset($this->bank_integration_id))
$rules['bank_integration_id'] = 'bail|required|exists:bank_integrations,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
$rules['bank_integration_id'] = 'bail|required|exists:bank_integrations,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
return $rules;
@ -69,7 +68,9 @@ class UpdateBankTransactionRequest extends Request
if(array_key_exists('ninja_category_id', $input) && strlen($input['ninja_category_id']) > 1)
$input['ninja_category_id'] = $this->decodePrimaryKey($input['ninja_category_id']);
if(array_key_exists('bank_integration_id', $input) && strlen($input['bank_integration_id']) > 1)
if(array_key_exists('bank_integration_id', $input) && $input['bank_integration_id'] == "")
unset($input['bank_integration_id']);
elseif(array_key_exists('bank_integration_id', $input) && strlen($input['bank_integration_id']) > 1)
$input['bank_integration_id'] = $this->decodePrimaryKey($input['bank_integration_id']);
$this->replace($input);

View File

@ -15,6 +15,7 @@ use App\Http\Requests\Request;
use App\Http\ValidationRules\ValidCompanyGatewayFeesAndLimitsRule;
use App\Models\Gateway;
use App\Utils\Traits\CompanyGatewayFeesAndLimitsSaver;
use Illuminate\Validation\Rule;
class StoreCompanyGatewayRequest extends Request
{
@ -33,7 +34,7 @@ class StoreCompanyGatewayRequest extends Request
public function rules()
{
$rules = [
'gateway_key' => 'required|alpha_num',
'gateway_key' => ['bail', 'required','alpha_num',Rule::exists('gateways','key')],
'fees_and_limits' => new ValidCompanyGatewayFeesAndLimitsRule(),
];
@ -67,4 +68,5 @@ class StoreCompanyGatewayRequest extends Request
$this->replace($input);
}
}

View File

@ -45,6 +45,8 @@ class StoreExpenseRequest extends Request
$rules['client_id'] = 'bail|sometimes|exists:clients,id,company_id,'.auth()->user()->company()->id;
}
$rules['category_id'] = 'bail|nullable|sometimes|exists:expense_categories,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
return $this->globalRules($rules);
}
@ -54,10 +56,6 @@ class StoreExpenseRequest extends Request
$input = $this->decodePrimaryKeys($input);
if (array_key_exists('category_id', $input) && is_string($input['category_id'])) {
$input['category_id'] = $this->decodePrimaryKey($input['category_id']);
}
if (! array_key_exists('currency_id', $input) || strlen($input['currency_id']) == 0) {
$input['currency_id'] = (string) auth()->user()->company()->settings->currency_id;
}
@ -66,7 +64,6 @@ class StoreExpenseRequest extends Request
$input['color'] = '';
}
/* Ensure the project is related */
if (array_key_exists('project_id', $input) && isset($input['project_id'])) {
$project = Project::withTrashed()->where('id', $input['project_id'])->company()->first();

View File

@ -41,6 +41,8 @@ class UpdateExpenseRequest extends Request
$rules['number'] = Rule::unique('expenses')->where('company_id', auth()->user()->company()->id)->ignore($this->expense->id);
}
$rules['category_id'] = 'bail|sometimes|nullable|exists:expense_categories,id,company_id,'.auth()->user()->company()->id.',is_deleted,0';
return $this->globalRules($rules);
}
@ -50,10 +52,6 @@ class UpdateExpenseRequest extends Request
$input = $this->decodePrimaryKeys($input);
if (array_key_exists('category_id', $input) && is_string($input['category_id'])) {
$input['category_id'] = $this->decodePrimaryKey($input['category_id']);
}
if (array_key_exists('documents', $input)) {
unset($input['documents']);
}

View File

@ -65,6 +65,7 @@ class StoreRecurringInvoiceRequest extends Request
$rules['tax_name1'] = 'bail|sometimes|string|nullable';
$rules['tax_name2'] = 'bail|sometimes|string|nullable';
$rules['tax_name3'] = 'bail|sometimes|string|nullable';
$rules['due_date_days'] = 'bail|sometimes|string';
return $rules;
}
@ -73,6 +74,10 @@ class StoreRecurringInvoiceRequest extends Request
{
$input = $this->all();
if (array_key_exists('due_date_days', $input) && is_null($input['due_date_days'])){
$input['due_date_days'] = 'terms';
}
if (array_key_exists('next_send_date', $input) && is_string($input['next_send_date'])) {
$input['next_send_date_client'] = $input['next_send_date'];
}

View File

@ -68,6 +68,10 @@ class UpdateRecurringInvoiceRequest extends Request
{
$input = $this->all();
if (array_key_exists('due_date_days', $input) && is_null($input['due_date_days'])){
$input['due_date_days'] = 'terms';
}
if (array_key_exists('next_send_date', $input) && is_string($input['next_send_date'])) {
$input['next_send_date_client'] = $input['next_send_date'];
}

View File

@ -0,0 +1,41 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2022. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Http\Requests\TwoFactor;
use App\Http\Requests\Request;
use Illuminate\Validation\Rule;
class EnableTwoFactorRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize() : bool
{
return true;;
}
public function rules()
{
return [
'secret' => 'bail|required|string',
'one_time_password' => 'bail|required|string',
];
}
public function prepareForValidation()
{
}
}

View File

@ -39,6 +39,6 @@ class ValidCompanyQuantity implements Rule
*/
public function message()
{
return ctrans('texts.company_limit_reached');
return ctrans('texts.company_limit_reached', ['limit' => Ninja::isSelfHost() ? 10 : auth()->user()->company()->account->hosted_company_count]);
}
}

View File

@ -38,7 +38,7 @@ class ClientTransformer extends BaseTransformer
return [
'company_id' => $this->company->id,
'name' => $this->getString($data, 'client.name'),
'work_phone' => $this->getString($data, 'client.phone'),
'phone' => $this->getString($data, 'client.phone'),
'address1' => $this->getString($data, 'client.address1'),
'address2' => $this->getString($data, 'client.address2'),
'postal_code' => $this->getString($data, 'client.postal_code'),

View File

@ -42,7 +42,7 @@ class ClientTransformer extends BaseTransformer
'company_id' => $this->company->id,
'name' => $this->getString($data, 'customer_name'),
'number' => $this->getValueOrNull($data, 'account_number'),
'work_phone' => $this->getString($data, 'phone'),
'phone' => $this->getString($data, 'phone'),
'website' => $this->getString($data, 'website'),
'country_id' => ! empty($data['country']) ? $this->getCountryId($data['country']) : null,
'state' => $this->getString($data, 'province/state'),

View File

@ -59,7 +59,7 @@ class BankTransformer extends BaseTransformer
if(array_key_exists('transaction.base_type', $transaction) && (($transaction['transaction.base_type'] == 'CREDIT') || strtolower($transaction['transaction.base_type']) == 'deposit'))
return 'CREDIT';
if(array_key_exists('transaction.base_type', $transaction) && (($transaction['transaction.base_type'] == 'DEBIT') || strtolower($transaction['transaction.bank_type']) == 'withdrawal'))
if(array_key_exists('transaction.base_type', $transaction) && (($transaction['transaction.base_type'] == 'DEBIT') || strtolower($transaction['transaction.base_type']) == 'withdrawal'))
return 'DEBIT';
if(array_key_exists('transaction.category_id', $transaction))

View File

@ -35,7 +35,7 @@ class ClientTransformer extends BaseTransformer
return [
'company_id' => $this->maps['company']->id,
'name' => $this->getString($data, 'client.name'),
'work_phone' => $this->getString($data, 'client.phone'),
'phone' => $this->getString($data, 'client.phone'),
'address1' => $this->getString($data, 'client.address1'),
'address2' => $this->getString($data, 'client.address2'),
'city' => $this->getString($data, 'client.city'),

View File

@ -37,7 +37,7 @@ class ClientTransformer extends BaseTransformer
return [
'company_id' => $this->maps['company']->id,
'name' => $this->getString($data, 'client.name'),
'work_phone' => $this->getString($data, 'client.phone'),
'phone' => $this->getString($data, 'client.phone'),
'address1' => $this->getString($data, 'client.address1'),
'address2' => $this->getString($data, 'client.address2'),
'postal_code' => $this->getString($data, 'client.postal_code'),

View File

@ -34,7 +34,7 @@ class ClientTransformer extends BaseTransformer
return [
'company_id' => $this->maps['company']->id,
'name' => $this->getString($data, 'Organization'),
'work_phone' => $this->getString($data, 'Phone'),
'phone' => $this->getString($data, 'Phone'),
'address1' => $this->getString($data, 'Street'),
'city' => $this->getString($data, 'City'),
'state' => $this->getString($data, 'Province/State'),

View File

@ -34,7 +34,7 @@ class ClientTransformer extends BaseTransformer
return [
'company_id' => $this->maps['company']->id,
'name' => $this->getString($data, 'Client Name'),
'work_phone' => $this->getString($data, 'Phone'),
'phone' => $this->getString($data, 'Phone'),
'country_id' => isset($data['Country']) ? $this->getCountryIdBy2($data['Country']) : null,
'credit_balance' => 0,
'settings' => new \stdClass,

View File

@ -42,7 +42,7 @@ class ClientTransformer extends BaseTransformer
'company_id' => $this->maps['company']->id,
'name' => $this->getString($data, 'customer_name'),
'number' => $this->getString($data, 'account_number'),
'work_phone' => $this->getString($data, 'phone'),
'phone' => $this->getString($data, 'phone'),
'website' => $this->getString($data, 'website'),
'country_id' => ! empty($data['country']) ? $this->getCountryId($data['country']) : null,
'state' => $this->getString($data, 'province/state'),

View File

@ -41,7 +41,7 @@ class ClientTransformer extends BaseTransformer
return [
'company_id' => $this->maps['company']->id,
'name' => $this->getString($data, 'Company Name'),
'work_phone' => $this->getString($data, 'Phone'),
'phone' => $this->getString($data, 'Phone'),
'private_notes' => $this->getString($data, 'Notes'),
'website' => $this->getString($data, 'Website'),
'id_number' => $this->getString($data, 'Customer ID'),

View File

@ -46,7 +46,7 @@ class InvoiceTransformer extends BaseTransformer
'due_date' => isset($invoice_data['Due Date']) ? date('Y-m-d', strtotime($invoice_data['Due Date'])) : null,
'po_number' => $this->getString($invoice_data, 'PurchaseOrder'),
'public_notes' => $this->getString($invoice_data, 'Notes'),
'currency_id' => $this->getCurrencyByCode($invoice_data, 'Currency'),
// 'currency_id' => $this->getCurrencyByCode($invoice_data, 'Currency'),
'amount' => $this->getFloat($invoice_data, 'Total'),
'balance' => $this->getFloat($invoice_data, 'Balance'),
'status_id' => $invoiceStatusMap[$status =

View File

@ -23,6 +23,7 @@ use App\Models\BankIntegration;
use App\Models\BankTransaction;
use App\Models\Company;
use App\Models\Currency;
use App\Models\Expense;
use App\Models\ExpenseCategory;
use App\Models\Invoice;
use App\Models\Payment;
@ -112,6 +113,10 @@ class MatchBankTransactions implements ShouldQueue
{
if(array_key_exists('invoice_ids', $input) && strlen($input['invoice_ids']) > 1)
$this->matchInvoicePayment($input);
elseif(array_key_exists('payment_id', $input) && strlen($input['payment_id']) > 1)
$this->linkPayment($input);
elseif(array_key_exists('expense_id', $input) && strlen($input['expense_id']) > 1)
$this->linkExpense($input);
else
$this->matchExpense($input);
}
@ -156,6 +161,55 @@ class MatchBankTransactions implements ShouldQueue
}
private function linkExpense($input)
{
$this->bt = BankTransaction::find($input['id']);
if(!$this->bt || $this->bt->status_id == BankTransaction::STATUS_CONVERTED)
return $this;
$expense = Expense::withTrashed()->find($input['expense_id']);
if($expense && !$expense->transaction_id) {
$expense->transaction_id = $this->bt->id;
$expense->save();
$this->bt->expense_id = $expense->id;
$this->bt->status_id = BankTransaction::STATUS_CONVERTED;
$this->bt->vendor_id = $expense->vendor_id;
$this->bt->ninja_category_id = $expense->category_id;
$this->bt->save();
}
}
private function linkPayment($input)
{
$this->bt = BankTransaction::find($input['id']);
if(!$this->bt || $this->bt->status_id == BankTransaction::STATUS_CONVERTED)
return $this;
$payment = Payment::withTrashed()->find($input['payment_id']);
if($payment && !$payment->transaction_id) {
$payment->transaction_id = $this->bt->id;
$payment->save();
$this->bt->payment_id = $payment->id;
$this->bt->status_id = BankTransaction::STATUS_CONVERTED;
$this->bt->invoice_ids = collect($payment->invoices)->pluck('hashed_id')->implode(',');
$this->bt->save();
}
}
private function matchInvoicePayment($input) :self
{
$this->bt = BankTransaction::find($input['id']);
@ -266,7 +320,7 @@ class MatchBankTransactions implements ShouldQueue
/* Create Payment */
$payment = PaymentFactory::create($this->invoice->company_id, $this->invoice->user_id);
$payment->amount = $amount;
$payment->amount = $this->bt->amount;
$payment->applied = $this->applied_amount;
$payment->status_id = Payment::STATUS_COMPLETED;
$payment->client_id = $this->invoice->client_id;
@ -315,7 +369,7 @@ class MatchBankTransactions implements ShouldQueue
$this->invoice
->client
->service()
->updateBalanceAndPaidToDate($amount*-1, $amount)
->updateBalanceAndPaidToDate($this->applied_amount*-1, $amount)
->save();
$this->invoice = $this->invoice

View File

@ -36,6 +36,8 @@ use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Storage;
use function Amp\call;
class CompanyExport implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, MakesHash;
@ -531,6 +533,15 @@ class CompanyExport implements ShouldQueue
$path = 'backups';
Storage::makeDirectory(public_path('storage/backups/'));
try {
mkdir(public_path('storage/backups/'));
}
catch(\Exception $e) {
nlog("could not create directory");
}
$zip_path = public_path('storage/backups/'.$file_name);
$zip = new \ZipArchive();

View File

@ -62,8 +62,15 @@ class AutoBillCron
nlog($auto_bill_partial_invoices->count().' partial invoices to auto bill');
$auto_bill_partial_invoices->cursor()->each(function ($invoice) {
AutoBill::dispatch($invoice->id, false);
$auto_bill_partial_invoices->chunk(400, function ($invoices) {
foreach($invoices as $invoice)
{
AutoBill::dispatch($invoice->id, false);
}
sleep(2);
});
$auto_bill_invoices = Invoice::whereDate('due_date', '<=', now())
@ -79,8 +86,15 @@ class AutoBillCron
nlog($auto_bill_invoices->count().' full invoices to auto bill');
$auto_bill_invoices->cursor()->each(function ($invoice) {
AutoBill::dispatch($invoice->id, false);
$auto_bill_invoices->chunk(400, function ($invoices) {
foreach($invoices as $invoice)
{
AutoBill::dispatch($invoice->id, false);
}
sleep(2);
});
} else {
//multiDB environment, need to
@ -100,8 +114,14 @@ class AutoBillCron
nlog($auto_bill_partial_invoices->count()." partial invoices to auto bill db = {$db}");
$auto_bill_partial_invoices->cursor()->each(function ($invoice) use ($db) {
AutoBill::dispatch($invoice->id, $db);
$auto_bill_partial_invoices->chunk(400, function ($invoices) use($db){
foreach($invoices as $invoice)
{
AutoBill::dispatch($invoice->id, $db);
}
sleep(2);
});
$auto_bill_invoices = Invoice::whereDate('due_date', '<=', now())
@ -117,10 +137,15 @@ class AutoBillCron
nlog($auto_bill_invoices->count()." full invoices to auto bill db = {$db}");
$auto_bill_invoices->cursor()->each(function ($invoice) use ($db) {
nlog($this->counter);
AutoBill::dispatch($invoice->id, $db);
$this->counter++;
$auto_bill_invoices->chunk(400, function ($invoices) use($db){
foreach($invoices as $invoice)
{
AutoBill::dispatch($invoice->id, $db);
}
sleep(2);
});
}

View File

@ -46,39 +46,62 @@ class RecurringExpensesCron
nlog('Sending recurring expenses '.Carbon::now()->format('Y-m-d h:i:s'));
if (! config('ninja.db.multi_db_enabled')) {
$this->getRecurringExpenses();
$recurring_expenses = RecurringExpense::where('next_send_date', '<=', now()->toDateTimeString())
->whereNotNull('next_send_date')
->whereNull('deleted_at')
->where('status_id', RecurringInvoice::STATUS_ACTIVE)
->where('remaining_cycles', '!=', '0')
->whereHas('company', function ($query) {
$query->where('is_disabled', 0);
})
->with('company')
->cursor();
nlog(now()->format('Y-m-d').' Generating Recurring Expenses. Count = '.$recurring_expenses->count());
$recurring_expenses->each(function ($recurring_expense, $key) {
nlog('Current date = '.now()->format('Y-m-d').' Recurring date = '.$recurring_expense->next_send_date);
if (! $recurring_expense->company->is_disabled) {
$this->generateExpense($recurring_expense);
}
});
} else {
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$this->getRecurringExpenses();
$recurring_expenses = RecurringExpense::where('next_send_date', '<=', now()->toDateTimeString())
->whereNotNull('next_send_date')
->whereNull('deleted_at')
->where('status_id', RecurringInvoice::STATUS_ACTIVE)
->where('remaining_cycles', '!=', '0')
->whereHas('company', function ($query) {
$query->where('is_disabled', 0);
})
->with('company')
->cursor();
nlog(now()->format('Y-m-d').' Generating Recurring Expenses. Count = '.$recurring_expenses->count());
$recurring_expenses->each(function ($recurring_expense, $key) {
nlog('Current date = '.now()->format('Y-m-d').' Recurring date = '.$recurring_expense->next_send_date);
if (! $recurring_expense->company->is_disabled) {
$this->generateExpense($recurring_expense);
}
});
}
}
}
private function getRecurringExpenses()
{
$recurring_expenses = RecurringExpense::where('next_send_date', '<=', now()->toDateTimeString())
->whereNotNull('next_send_date')
->whereNull('deleted_at')
->where('status_id', RecurringInvoice::STATUS_ACTIVE)
->where('remaining_cycles', '!=', '0')
->whereHas('company', function ($query) {
$query->where('is_disabled', 0);
})
->with('company')
->cursor();
nlog(now()->format('Y-m-d').' Generating Recurring Expenses. Count = '.$recurring_expenses->count());
$recurring_expenses->each(function ($recurring_expense, $key) {
nlog('Current date = '.now()->format('Y-m-d').' Recurring date = '.$recurring_expense->next_send_date);
if (! $recurring_expense->company->is_disabled) {
$this->generateExpense($recurring_expense);
}
});
//extracting this back to the if/else block to test duplicate crons
}
private function generateExpense(RecurringExpense $recurring_expense)

View File

@ -41,44 +41,62 @@ class SubscriptionCron
nlog('Subscription Cron');
if (! config('ninja.db.multi_db_enabled')) {
$this->loopSubscriptions();
$invoices = Invoice::where('is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->where('balance', '>', 0)
->whereDate('due_date', '<=', now()->addDay()->startOfDay())
->whereNull('deleted_at')
->whereNotNull('subscription_id')
->cursor();
$invoices->each(function ($invoice) {
$subscription = $invoice->subscription;
$body = [
'context' => 'plan_expired',
'client' => $invoice->client->hashed_id,
'invoice' => $invoice->hashed_id,
'subscription' => $subscription->hashed_id,
];
$this->sendLoad($subscription, $body);
//This will send the notification daily.
//We'll need to handle this by performing some action on the invoice to either archive it or delete it?
});
} else {
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$this->loopSubscriptions();
$invoices = Invoice::where('is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->where('balance', '>', 0)
->whereDate('due_date', '<=', now()->addDay()->startOfDay())
->whereNull('deleted_at')
->whereNotNull('subscription_id')
->cursor();
$invoices->each(function ($invoice) {
$subscription = $invoice->subscription;
$body = [
'context' => 'plan_expired',
'client' => $invoice->client->hashed_id,
'invoice' => $invoice->hashed_id,
'subscription' => $subscription->hashed_id,
];
$this->sendLoad($subscription, $body);
//This will send the notification daily.
//We'll need to handle this by performing some action on the invoice to either archive it or delete it?
});
}
}
}
private function loopSubscriptions()
{
$invoices = Invoice::where('is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->where('balance', '>', 0)
->whereDate('due_date', '<=', now()->addDay()->startOfDay())
->whereNull('deleted_at')
->whereNotNull('subscription_id')
->cursor();
$invoices->each(function ($invoice) {
$subscription = $invoice->subscription;
$body = [
'context' => 'plan_expired',
'client' => $invoice->client->hashed_id,
'invoice' => $invoice->hashed_id,
'subscription' => $subscription->hashed_id,
];
$this->sendLoad($subscription, $body);
//This will send the notification daily.
//We'll need to handle this by performing some action on the invoice to either archive it or delete it?
});
}
private function handleWebhook($invoice, $subscription)
{
}
}

View File

@ -102,6 +102,10 @@ class CreateRawPdf implements ShouldQueue
/* Set customized translations _NOW_ */
$t->replace(Ninja::transformTranslations($this->entity->client->getMergedSettings()));
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
return (new Phantom)->generate($this->invitation, true);
}
$entity_design_id = '';
if ($this->entity instanceof Invoice) {

View File

@ -90,7 +90,6 @@ class EmailEntity implements ShouldQueue
$this->template_data = $template_data;
$this->email_entity_builder = $this->resolveEmailBuilder();
}
/**
@ -99,13 +98,15 @@ class EmailEntity implements ShouldQueue
*
* @return void
*/
public function handle()
public function handle() :void
{
/* Don't fire emails if the company is disabled */
if ($this->company->is_disabled) {
return true;
return;
}
$this->email_entity_builder = $this->resolveEmailBuilder();
/* Set DB */
MultiDB::setDB($this->company->db);

View File

@ -44,38 +44,30 @@ class BankTransactionSync implements ShouldQueue
*/
public function handle()
{
// if (! Ninja::isHosted()) {
// return;
// }
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
foreach (MultiDB::$dbs as $db)
{
MultiDB::setDB($db);
nlog("syncing transactions");
$this->syncTransactions();
$a = Account::with('bank_integrations')->whereNotNull('bank_integration_account_id')->cursor()->each(function ($account){
// $queue = Ninja::isHosted() ? 'bank' : 'default';
if($account->isPaid() && $account->plan == 'enterprise')
{
$account->bank_integrations()->where('auto_sync', true)->cursor()->each(function ($bank_integration) use ($account){
ProcessBankTransactions::dispatchSync($account->bank_integration_account_id, $bank_integration);
});
}
});
}
}
public function syncTransactions()
{
$a = Account::with('bank_integrations')->whereNotNull('bank_integration_account_id')->cursor()->each(function ($account){
// $queue = Ninja::isHosted() ? 'bank' : 'default';
if($account->isPaid() && $account->plan == 'enterprise')
{
$account->bank_integrations()->where('auto_sync', true)->cursor()->each(function ($bank_integration) use ($account){
ProcessBankTransactions::dispatchSync($account->bank_integration_account_id, $bank_integration);
});
}
});
}
}

View File

@ -42,39 +42,54 @@ class CompanySizeCheck implements ShouldQueue
public function handle()
{
if (! config('ninja.db.multi_db_enabled')) {
$this->check();
Company::where('is_large', false)->withCount(['invoices', 'clients', 'products'])->cursor()->each(function ($company) {
if ($company->invoices_count > 500 || $company->products_count > 500 || $company->clients_count > 500) {
nlog("Marking company {$company->id} as large");
$company->account->companies()->update(['is_large' => true]);
}
});
nlog("updating all client credit balances");
Client::where('updated_at', '>', now()->subDay())
->cursor()
->each(function ($client){
$client->credit_balance = $client->service()->getCreditBalance();
$client->save();
});
} else {
//multiDB environment, need to
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$this->check();
nlog("Company size check db {$db}");
Company::where('is_large', false)->withCount(['invoices', 'clients', 'products'])->cursor()->each(function ($company) {
if ($company->invoices_count > 500 || $company->products_count > 500 || $company->clients_count > 500) {
nlog("Marking company {$company->id} as large");
$company->account->companies()->update(['is_large' => true]);
}
});
nlog("updating all client credit balances");
Client::where('updated_at', '>', now()->subDay())
->cursor()
->each(function ($client){
$client->credit_balance = $client->service()->getCreditBalance();
$client->save();
});
}
}
}
private function check()
{
nlog("Checking all company sizes");
Company::where('is_large', false)->withCount(['invoices', 'clients', 'products'])->cursor()->each(function ($company) {
if ($company->invoices_count > 500 || $company->products_count > 500 || $company->clients_count > 500) {
nlog("Marking company {$company->id} as large");
$company->account->companies()->update(['is_large' => true]);
}
});
nlog("updating all client credit balances");
Client::where('updated_at', '>', now()->subDay())
->cursor()
->each(function ($client){
$client->credit_balance = $client->service()->getCreditBalance();
$client->save();
});
}
}

View File

@ -40,7 +40,7 @@ class QueueSize implements ShouldQueue
*
* @return void
*/
public function handle()
public function handle() :void
{
LightLogs::create(new QueueSizeAnalytic(Queue::size()))
->send();

View File

@ -42,39 +42,61 @@ class QuoteCheckExpired implements ShouldQueue
*/
public function handle()
{
if (! config('ninja.db.multi_db_enabled'))
return $this->checkForExpiredQuotes();
foreach (MultiDB::$dbs as $db) {
if (! config('ninja.db.multi_db_enabled')){
MultiDB::setDB($db);
Quote::query()
->where('status_id', Quote::STATUS_SENT)
->where('is_deleted', false)
->whereNull('deleted_at')
->whereNotNull('due_date')
->whereHas('client', function ($query) {
$query->where('is_deleted', 0)
->where('deleted_at', null);
})
->whereHas('company', function ($query) {
$query->where('is_disabled', 0);
})
// ->where('due_date', '<='. now()->toDateTimeString())
->whereBetween('due_date', [now()->subDay()->startOfDay(), now()->startOfDay()->subSecond()])
->cursor()
->each(function ($quote){
$this->queueExpiredQuoteNotification($quote);
});
$this->checkForExpiredQuotes();
}
else {
foreach (MultiDB::$dbs as $db)
{
MultiDB::setDB($db);
Quote::query()
->where('status_id', Quote::STATUS_SENT)
->where('is_deleted', false)
->whereNull('deleted_at')
->whereNotNull('due_date')
->whereHas('client', function ($query) {
$query->where('is_deleted', 0)
->where('deleted_at', null);
})
->whereHas('company', function ($query) {
$query->where('is_disabled', 0);
})
// ->where('due_date', '<='. now()->toDateTimeString())
->whereBetween('due_date', [now()->subDay()->startOfDay(), now()->startOfDay()->subSecond()])
->cursor()
->each(function ($quote){
$this->queueExpiredQuoteNotification($quote);
});
}
}
}
private function checkForExpiredQuotes()
{
Quote::query()
->where('status_id', Quote::STATUS_SENT)
->where('is_deleted', false)
->whereNull('deleted_at')
->whereNotNull('due_date')
->whereHas('client', function ($query) {
$query->where('is_deleted', 0)
->where('deleted_at', null);
})
->whereHas('company', function ($query) {
$query->where('is_disabled', 0);
})
// ->where('due_date', '<='. now()->toDateTimeString())
->whereBetween('due_date', [now()->subDay()->startOfDay(), now()->startOfDay()->subSecond()])
->cursor()
->each(function ($quote){
$this->queueExpiredQuoteNotification($quote);
});
}
private function queueExpiredQuoteNotification(Quote $quote)

View File

@ -161,6 +161,11 @@ class SendRecurring implements ShouldQueue
*/
private function createRecurringInvitations($invoice) :Invoice
{
if($this->recurring_invoice->invitations->count() == 0) {
$this->recurring_invoice = $this->recurring_invoice->service()->createInvitations()->save();
}
$this->recurring_invoice->invitations->each(function ($recurring_invitation) use ($invoice) {
$ii = InvoiceInvitationFactory::create($invoice->company_id, $invoice->user_id);
$ii->key = $this->createDbHash($invoice->company->db);

View File

@ -190,7 +190,7 @@ class Import implements ShouldQueue
public function middleware()
{
return [new WithoutOverlapping($this->company->company_key)];
return [new WithoutOverlapping("only_one_migration_at_a_time_ever")];
}
/**

View File

@ -44,92 +44,128 @@ class ReminderJob implements ShouldQueue
*
* @return void
*/
public function handle()
public function handle() :void
{
if (! config('ninja.db.multi_db_enabled')) {
$this->processReminders();
} else {
//multiDB environment, need to
/*
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
nlog("set db {$db}");
$this->processReminders();
}
*/
//24-11-2022 fix for potential memory leak during a long running process, the second reminder may run twice
foreach (config('ninja.dbs') as $db) {
MultiDB::setDB($db);
nlog("set db {$db}");
$this->processReminders();
}
}
}
private function processReminders()
{
nlog('Sending invoice reminders '.now()->format('Y-m-d h:i:s'));
set_time_limit(0);
Invoice::query()
->where('is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->whereNull('deleted_at')
->where('balance', '>', 0)
->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')->cursor()->each(function ($invoice) {
// if ($invoice->refresh() && $invoice->isPayable()) {
if ($invoice->isPayable()) {
if (! config('ninja.db.multi_db_enabled'))
{
//Attempts to prevent duplicates from sending
if($invoice->reminder_last_sent && Carbon::parse($invoice->reminder_last_sent)->startOfDay()->eq(now()->startOfDay())){
nlog("caught a duplicate reminder for invoice {$invoice->number}");
return;
nlog("Sending invoice reminders on ".now()->format('Y-m-d h:i:s'));
Invoice::query()
->where('is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->whereNull('deleted_at')
->where('balance', '>', 0)
->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 ($invoices) {
foreach($invoices as $invoice)
{
$this->sendReminderForInvoice($invoice);
}
$reminder_template = $invoice->calculateTemplate('invoice');
nlog("reminder template = {$reminder_template}");
$invoice->service()->touchReminder($reminder_template)->save();
$invoice = $this->calcLateFee($invoice, $reminder_template);
sleep(2);
//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';
}
});
//check if this reminder needs to be emailed
//15-01-2022 - insert addition if block if send_reminders is definitely set
if (in_array($reminder_template, ['reminder1', 'reminder2', 'reminder3', 'reminder_endless', 'endless_reminder']) &&
$invoice->client->getSetting($enabled_reminder) &&
$invoice->client->getSetting('send_reminders') &&
(Ninja::isSelfHost() || $invoice->company->account->isPaidHostedClient())) {
$invoice->invitations->each(function ($invitation) use ($invoice, $reminder_template) {
EmailEntity::dispatch($invitation, $invitation->company, $reminder_template);
nlog("Firing reminder email for invoice {$invoice->number} - {$reminder_template}");
});
} else {
//multiDB environment, need to
foreach (MultiDB::$dbs as $db)
{
if ($invoice->invitations->count() > 0) {
event(new InvoiceWasEmailed($invoice->invitations->first(), $invoice->company, Ninja::eventVars(), $reminder_template));
}
}
$invoice->service()->setReminder()->save();
} else {
$invoice->next_send_date = null;
$invoice->save();
MultiDB::setDB($db);
nlog("Sending invoice reminders on db {$db} ".now()->format('Y-m-d h:i:s'));
Invoice::query()
->where('is_deleted', 0)
->whereIn('status_id', [Invoice::STATUS_SENT, Invoice::STATUS_PARTIAL])
->whereNull('deleted_at')
->where('balance', '>', 0)
->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 ($invoices) {
// if ($invoice->refresh() && $invoice->isPayable()) {
foreach($invoices as $invoice)
{
$this->sendReminderForInvoice($invoice);
}
sleep(2);
});
}
}
}
private function sendReminderForInvoice($invoice) {
if ($invoice->isPayable()) {
//Attempts to prevent duplicates from sending
if($invoice->reminder_last_sent && Carbon::parse($invoice->reminder_last_sent)->startOfDay()->eq(now()->startOfDay())){
nlog("caught a duplicate reminder for invoice {$invoice->number}");
return;
}
$reminder_template = $invoice->calculateTemplate('invoice');
nlog("reminder template = {$reminder_template}");
$invoice->service()->touchReminder($reminder_template)->save();
$invoice = $this->calcLateFee($invoice, $reminder_template);
//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';
}
//check if this reminder needs to be emailed
//15-01-2022 - insert addition if block if send_reminders is definitely set
if (in_array($reminder_template, ['reminder1', 'reminder2', 'reminder3', 'reminder_endless', 'endless_reminder']) &&
$invoice->client->getSetting($enabled_reminder) &&
$invoice->client->getSetting('send_reminders') &&
(Ninja::isSelfHost() || $invoice->company->account->isPaidHostedClient())) {
$invoice->invitations->each(function ($invitation) use ($invoice, $reminder_template) {
if($invitation->contact && !$invitation->contact->trashed() && $invitation->contact->email) {
EmailEntity::dispatch($invitation, $invitation->company, $reminder_template)->delay(now()->addSeconds(3));
nlog("Firing reminder email for invoice {$invoice->number} - {$reminder_template}");
}
});
if ($invoice->invitations->count() > 0) {
event(new InvoiceWasEmailed($invoice->invitations->first(), $invoice->company, Ninja::eventVars(), $reminder_template));
}
}
$invoice->service()->setReminder()->save();
} else {
$invoice->next_send_date = null;
$invoice->save();
}
});
}
/**

View File

@ -34,19 +34,7 @@ class UpdateExchangeRates implements ShouldQueue
*
* @return void
*/
public function handle()
{
if (config('ninja.db.multi_db_enabled')) {
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$this->updateCurrencies();
}
} else {
$this->updateCurrencies();
}
}
private function updateCurrencies()
public function handle() :void
{
info('updating currencies');
@ -56,20 +44,47 @@ class UpdateExchangeRates implements ShouldQueue
$cc_endpoint = sprintf('https://openexchangerates.org/api/latest.json?app_id=%s', config('ninja.currency_converter_api_key'));
$client = new Client();
$response = $client->get($cc_endpoint);
if (config('ninja.db.multi_db_enabled')) {
foreach (MultiDB::$dbs as $db) {
MultiDB::setDB($db);
$client = new Client();
$response = $client->get($cc_endpoint);
$currency_api = json_decode($response->getBody());
$currency_api = json_decode($response->getBody());
/* Update all currencies */
Currency::all()->each(function ($currency) use ($currency_api) {
$currency->exchange_rate = $currency_api->rates->{$currency->code};
$currency->save();
});
/* Update all currencies */
Currency::all()->each(function ($currency) use ($currency_api) {
$currency->exchange_rate = $currency_api->rates->{$currency->code};
$currency->save();
});
/* Rebuild the cache */
$currencies = Currency::orderBy('name')->get();
/* Rebuild the cache */
$currencies = Currency::orderBy('name')->get();
Cache::forever('currencies', $currencies);
Cache::forever('currencies', $currencies);
}
} else {
$client = new Client();
$response = $client->get($cc_endpoint);
$currency_api = json_decode($response->getBody());
/* Update all currencies */
Currency::all()->each(function ($currency) use ($currency_api) {
$currency->exchange_rate = $currency_api->rates->{$currency->code};
$currency->save();
});
/* Rebuild the cache */
$currencies = Currency::orderBy('name')->get();
Cache::forever('currencies', $currencies);
}
}
}

View File

@ -127,7 +127,7 @@ class CreatePurchaseOrderPdf implements ShouldQueue
$t->replace(Ninja::transformTranslations($this->company->settings));
if (config('ninja.phantomjs_pdf_generation') || config('ninja.pdf_generator') == 'phantom') {
return (new Phantom)->generate($this->invitation);
return (new Phantom)->generate($this->invitation, true);
}
$entity_design_id = '';

View File

@ -40,7 +40,7 @@ class CreditEmailedNotification implements ShouldQueue
// $first_notification_sent = true;
$credit = $event->invitation->credit;
$credit = $event->invitation->credit->fresh();
$credit->last_sent_date = now();
$credit->saveQuietly();

View File

@ -42,7 +42,7 @@ class InvoiceEmailedNotification implements ShouldQueue
$first_notification_sent = true;
$invoice = $event->invitation->invoice;
$invoice = $event->invitation->invoice->fresh();
$invoice->last_sent_date = now();
$invoice->saveQuietly();

View File

@ -29,8 +29,6 @@ class InvoiceFailedEmailNotification
use UserNotifies, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $delay = 10;
public function __construct()
{
}
@ -48,7 +46,7 @@ class InvoiceFailedEmailNotification
$first_notification_sent = true;
$invoice = $event->invitation->invoice;
$invoice->update(['last_sent_date' => now()]);
// $invoice->update(['last_sent_date' => now()]);
$nmo = new NinjaMailerObject;
$nmo->mailable = new NinjaMailer((new EntityFailedSendObject($event->invitation, 'invoice', $event->template, $event->message))->build());

View File

@ -42,7 +42,7 @@ class QuoteEmailedNotification implements ShouldQueue
// $first_notification_sent = true;
$quote = $event->invitation->quote;
$quote = $event->invitation->quote->fresh();
$quote->last_sent_date = now();
$quote->saveQuietly();

View File

@ -11,6 +11,7 @@
namespace App\Mail\Engine;
use App\Jobs\Entity\CreateRawPdf;
use App\Models\Account;
use App\Utils\HtmlEngine;
use App\Utils\Ninja;
@ -117,11 +118,17 @@ class CreditEmailEngine extends BaseEmailEngine
->setTextBody($text_body);
if ($this->client->getSetting('pdf_email_attachment') !== false && $this->credit->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) {
if (Ninja::isHosted()) {
$this->setAttachments([$this->credit->pdf_file_path($this->invitation, 'url', true)]);
} else {
$this->setAttachments([$this->credit->pdf_file_path($this->invitation)]);
}
// if (Ninja::isHosted()) {
// $this->setAttachments([$this->credit->pdf_file_path($this->invitation, 'url', true)]);
// } else {
// $this->setAttachments([$this->credit->pdf_file_path($this->invitation)]);
// }
$pdf = ((new CreateRawPdf($this->invitation, $this->invitation->company->db))->handle());
$this->setAttachments([['file' => base64_encode($pdf), 'name' => $this->credit->numberFormatter().'.pdf']]);
}
//attach third party documents
@ -129,11 +136,11 @@ class CreditEmailEngine extends BaseEmailEngine
// Storage::url
foreach ($this->credit->documents as $document) {
$this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => null]]);
$this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => NULL, 'file' => base64_encode($document->getFile())]]);
}
foreach ($this->credit->company->documents as $document) {
$this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => null]]);
$this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => NULL, 'file' => base64_encode($document->getFile())]]);
}
}

View File

@ -13,6 +13,7 @@ namespace App\Mail\Engine;
use App\DataMapper\EmailTemplateDefaults;
use App\Jobs\Entity\CreateEntityPdf;
use App\Jobs\Entity\CreateRawPdf;
use App\Models\Account;
use App\Models\Expense;
use App\Models\Task;
@ -126,11 +127,10 @@ class InvoiceEmailEngine extends BaseEmailEngine
->setTextBody($text_body);
if ($this->client->getSetting('pdf_email_attachment') !== false && $this->invoice->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) {
if (Ninja::isHosted()) {
$this->setAttachments([$this->invoice->pdf_file_path($this->invitation, 'url', true)]);
} else {
$this->setAttachments([$this->invoice->pdf_file_path($this->invitation)]);
}
$pdf = ((new CreateRawPdf($this->invitation, $this->invitation->company->db))->handle());
$this->setAttachments([['file' => base64_encode($pdf), 'name' => $this->invoice->numberFormatter().'.pdf']]);
}
//attach third party documents
@ -138,11 +138,11 @@ class InvoiceEmailEngine extends BaseEmailEngine
// Storage::url
foreach ($this->invoice->documents as $document) {
$this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => NULL]]);
$this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => NULL, ]]);
}
foreach ($this->invoice->company->documents as $document) {
$this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => NULL]]);
$this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => NULL, ]]);
}
$line_items = $this->invoice->line_items;
@ -160,7 +160,7 @@ class InvoiceEmailEngine extends BaseEmailEngine
->cursor()
->each(function ($expense) {
foreach ($expense->documents as $document) {
$this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => NULL]]);
$this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => NULL, ]]);
}
});
}
@ -176,7 +176,7 @@ class InvoiceEmailEngine extends BaseEmailEngine
->cursor()
->each(function ($task) {
foreach ($task->documents as $document) {
$this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => NULL]]);
$this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => NULL, ]]);
}
});
}

View File

@ -12,6 +12,7 @@
namespace App\Mail\Engine;
use App\DataMapper\EmailTemplateDefaults;
use App\Jobs\Entity\CreateRawPdf;
use App\Models\Account;
use App\Utils\Helpers;
use App\Utils\Ninja;
@ -89,11 +90,15 @@ class PaymentEmailEngine extends BaseEmailEngine
if ($this->client->getSetting('pdf_email_attachment') !== false && $this->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) {
$this->payment->invoices->each(function ($invoice) {
if (Ninja::isHosted()) {
$this->setAttachments([$invoice->pdf_file_path($invoice->invitations->first(), 'url', true)]);
} else {
$this->setAttachments([$invoice->pdf_file_path($invoice->invitations->first())]);
}
// if (Ninja::isHosted()) {
// $this->setAttachments([$invoice->pdf_file_path($invoice->invitations->first(), 'url', true)]);
// } else {
// $this->setAttachments([$invoice->pdf_file_path($invoice->invitations->first())]);
// }
$pdf = ((new CreateRawPdf($invoice->invitations->first(), $invoice->company->db))->handle());
$this->setAttachments([['file' => base64_encode($pdf), 'name' => $invoice->numberFormatter().'.pdf']]);
});
}

View File

@ -13,6 +13,7 @@ namespace App\Mail\Engine;
use App\DataMapper\EmailTemplateDefaults;
use App\Jobs\Entity\CreateEntityPdf;
use App\Jobs\Vendor\CreatePurchaseOrderPdf;
use App\Models\Account;
use App\Models\Expense;
use App\Models\PurchaseOrder;
@ -125,11 +126,16 @@ class PurchaseOrderEmailEngine extends BaseEmailEngine
->setTextBody($text_body);
if ($this->vendor->getSetting('pdf_email_attachment') !== false && $this->purchase_order->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) {
if (Ninja::isHosted()) {
$this->setAttachments([$this->purchase_order->pdf_file_path($this->invitation, 'url', true)]);
} else {
$this->setAttachments([$this->purchase_order->pdf_file_path($this->invitation)]);
}
// if (Ninja::isHosted()) {
// $this->setAttachments([$this->purchase_order->pdf_file_path($this->invitation, 'url', true)]);
// } else {
// $this->setAttachments([$this->purchase_order->pdf_file_path($this->invitation)]);
// }
$pdf = (new CreatePurchaseOrderPdf($this->invitation))->rawPdf();
$this->setAttachments([['file' => base64_encode($pdf), 'name' => $this->purchase_order->numberFormatter().'.pdf']]);
}
//attach third party documents
@ -138,10 +144,12 @@ class PurchaseOrderEmailEngine extends BaseEmailEngine
// Storage::url
foreach ($this->purchase_order->documents as $document) {
$this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => null]]);
// $this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => NULL, 'file' => base64_encode($document->getFile())]]);
}
foreach ($this->purchase_order->company->documents as $document) {
$this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => null]]);
// $this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => NULL, 'file' => base64_encode($document->getFile())]]);
}
}

View File

@ -11,6 +11,7 @@
namespace App\Mail\Engine;
use App\Jobs\Entity\CreateRawPdf;
use App\Models\Account;
use App\Utils\HtmlEngine;
use App\Utils\Ninja;
@ -116,11 +117,15 @@ class QuoteEmailEngine extends BaseEmailEngine
->setTextBody($text_body);
if ($this->client->getSetting('pdf_email_attachment') !== false && $this->quote->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) {
if (Ninja::isHosted()) {
$this->setAttachments([$this->quote->pdf_file_path($this->invitation, 'url', true)]);
} else {
$this->setAttachments([$this->quote->pdf_file_path($this->invitation)]);
}
// if (Ninja::isHosted()) {
// $this->setAttachments([$this->quote->pdf_file_path($this->invitation, 'url', true)]);
// } else {
// $this->setAttachments([$this->quote->pdf_file_path($this->invitation)]);
// }
$pdf = ((new CreateRawPdf($this->invitation, $this->invitation->company->db))->handle());
$this->setAttachments([['file' => base64_encode($pdf), 'name' => $this->quote->numberFormatter().'.pdf']]);
}
//attach third party documents
@ -128,11 +133,11 @@ class QuoteEmailEngine extends BaseEmailEngine
// Storage::url
foreach ($this->quote->documents as $document) {
$this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => null]]);
$this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => NULL, ]]);
}
foreach ($this->quote->company->documents as $document) {
$this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => null]]);
$this->setAttachments([['path' => $document->filePath(), 'name' => $document->name, 'mime' => NULL, ]]);
}
}

View File

@ -118,47 +118,14 @@ class TemplateEmail extends Mailable
'logo' => $this->company->present()->logo($settings),
]);
/*In the hosted platform we need to slow things down a little for Storage to catch up.*/
if(Ninja::isHosted() && $this->invitation){
$path = false;
if($this->invitation->invoice)
$path = $this->client->invoice_filepath($this->invitation).$this->invitation->invoice->numberFormatter().'.pdf';
elseif($this->invitation->quote)
$path = $this->client->quote_filepath($this->invitation).$this->invitation->quote->numberFormatter().'.pdf';
elseif($this->invitation->credit)
$path = $this->client->credit_filepath($this->invitation).$this->invitation->credit->numberFormatter().'.pdf';
sleep(1);
if($path && !Storage::disk(config('filesystems.default'))->exists($path)){
sleep(2);
if(!Storage::disk(config('filesystems.default'))->exists($path)) {
(new CreateEntityPdf($this->invitation))->handle();
sleep(2);
}
}
}
//22-10-2022 - Performance - To improve the performance/reliability of sending emails, attaching as Data is much better, stubs in place
foreach ($this->build_email->getAttachments() as $file) {
if (is_string($file)) {
// nlog($file);
// $file_data = file_get_contents($file);
// $this->attachData($file_data, basename($file));
$this->attach($file);
} elseif (is_array($file)) {
// nlog($file['path']);
// $file_data = file_get_contents($file['path']);
// $this->attachData($file_data, $file['name']);
if(array_key_exists('file', $file))
$this->attachData(base64_decode($file['file']), $file['name']);
else
$this->attach($file['path'], ['as' => $file['name'], 'mime' => null]);
}
}
if ($this->invitation && $this->invitation->invoice && $settings->ubl_email_attachment && $this->company->account->hasFeature(Account::FEATURE_PDF_ATTACHMENT)) {

View File

@ -110,40 +110,15 @@ class VendorTemplateEmail extends Mailable
'whitelabel' => $this->vendor->user->account->isPaid() ? true : false,
'logo' => $this->company->present()->logo($settings),
]);
//->withSymfonyMessage(function ($message) {
// $message->getHeaders()->addTextHeader('Tag', $this->company->company_key);
// $message->invitation = $this->invitation;
//});
// ->tag($this->company->company_key);
if(Ninja::isHosted() && $this->invitation){
$path = false;
if($this->invitation->purchase_order)
$path = $this->vendor->purchase_order_filepath($this->invitation).$this->invitation->purchase_order->numberFormatter().'.pdf';
sleep(1);
if($path && !Storage::disk(config('filesystems.default'))->exists($path)){
sleep(2);
if(!Storage::disk(config('filesystems.default'))->exists($path)) {
(new CreatePurchaseOrderPdf($this->invitation))->handle();
sleep(2);
}
}
}
foreach ($this->build_email->getAttachments() as $file) {
if (is_string($file)) {
$this->attach($file);
} elseif (is_array($file)) {
if(array_key_exists('file', $file))
$this->attachData(base64_decode($file['file']), $file['name']);
else
$this->attach($file['path'], ['as' => $file['name'], 'mime' => null]);
}
}
return $this;

View File

@ -119,7 +119,7 @@ class CompanyPresenter extends EntityPresenter
$str .= e($country->name).'<br/>';
}
if ($settings->phone) {
$str .= ctrans('texts.work_phone').': '.e($settings->phone).'<br/>';
$str .= ctrans('texts.phone').': '.e($settings->phone).'<br/>';
}
if ($settings->email) {
$str .= ctrans('texts.work_email').': '.e($settings->email).'<br/>';

View File

@ -557,6 +557,7 @@ class RecurringInvoice extends BaseModel
switch ($this->due_date_days) {
case 'terms':
case '':
case '0':
return $this->calculateDateFromTerms($date);
break;

View File

@ -76,6 +76,9 @@ class Webhook extends BaseModel
const EVENT_DELETE_CREDIT = 29;
const EVENT_PROJECT_DELETE = 30;
public static $valid_events = [
self::EVENT_CREATE_CLIENT,
self::EVENT_CREATE_INVOICE,
@ -106,6 +109,7 @@ class Webhook extends BaseModel
self::EVENT_CREATE_CREDIT,
self::EVENT_UPDATE_CREDIT,
self::EVENT_DELETE_CREDIT,
self::EVENT_PROJECT_DELETE
];
protected $fillable = [

View File

@ -61,7 +61,14 @@ class ProjectObserver
*/
public function deleted(Project $project)
{
//
//EVENT_PROJECT_DELETE
$subscriptions = Webhook::where('company_id', $project->company_id)
->where('event_id', Webhook::EVENT_PROJECT_DELETE)
->exists();
if ($subscriptions) {
WebhookHandler::dispatch(Webhook::EVENT_PROJECT_DELETE, $project, $project->company, 'client')->delay(now()->addSeconds(2));
}
}
/**

View File

@ -94,8 +94,6 @@ class CreditCard implements MethodInterface
$customerRequest = $this->checkout->getCustomer();
nlog($customerRequest);
$request = $this->bootRequest($gateway_response->token);
$request->capture = false;
$request->reference = '$1 payment for authorization.';

View File

@ -34,6 +34,7 @@ use Checkout\CheckoutArgumentException;
use Checkout\CheckoutAuthorizationException;
use Checkout\CheckoutDefaultSdk;
use Checkout\CheckoutFourSdk;
use Checkout\Common\Phone;
use Checkout\Customers\CustomerRequest;
use Checkout\Customers\Four\CustomerRequest as FourCustomerRequest;
use Checkout\Environment;
@ -300,9 +301,13 @@ class CheckoutComPaymentDriver extends BaseDriver
$request = new CustomerRequest();
}
$request->email = $this->client->present()->email();
$request->name = $this->client->present()->name();
$request->phone = $this->client->present()->phone();
$phone = new Phone();
// $phone->number = $this->client->present()->phone();
$phone->number = substr(str_pad($this->client->present()->phone(),6, "0", STR_PAD_RIGHT), 0 , 24);
$request->email = $this->client->present()->email();
$request->name = $this->client->present()->name();
$request->phone = $phone;
try {
$response = $this->gateway->getCustomersClient()->create($request);

View File

@ -36,6 +36,7 @@ use Stripe\Exception\CardException;
use Stripe\Exception\InvalidRequestException;
use Stripe\Exception\RateLimitException;
use Stripe\PaymentIntent;
use App\Utils\Number;
class ACH
{
@ -172,9 +173,9 @@ class ACH
->first();
if ($invoice) {
$description = "Invoice {$invoice->number} for {$amount} for client {$this->stripe->client->present()->name()}";
$description = ctrans('texts.stripe_paymenttext', ['invoicenumber' => $invoice->number, 'amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()]);
} else {
$description = "Payment with no invoice for amount {$amount} for client {$this->stripe->client->present()->name()}";
$description = ctrans('texts.stripe_paymenttext_without_invoice', ['amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()]);
}
@ -210,9 +211,9 @@ class ACH
->first();
if ($invoice) {
$description = "Invoice {$invoice->number} for {$amount} for client {$this->stripe->client->present()->name()}";
$description = ctrans('texts.stripe_paymenttext', ['invoicenumber' => $invoice->number, 'amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()]);
} else {
$description = "Payment with no invoice for amount {$amount} for client {$this->stripe->client->present()->name()}";
$description = ctrans('texts.stripe_paymenttext_without_invoice', ['amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()]);
}
if (substr($cgt->token, 0, 2) === 'pm') {
@ -331,7 +332,7 @@ class ACH
$data = [
'gateway_type_id' => $cgt->gateway_type_id,
'payment_type' => PaymentType::ACH,
'transaction_reference' => $response->charges->data[0]->id,
'transaction_reference' => isset($response->latest_charge) ? $response->latest_charge : $response->charges->data[0]->id,
'amount' => $amount,
];
@ -454,9 +455,9 @@ class ACH
->first();
if ($invoice) {
$description = "Invoice {$invoice->number} for {$amount} for client {$this->stripe->client->present()->name()}";
$description = ctrans('texts.stripe_paymenttext', ['invoicenumber' => $invoice->number, 'amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()]);
} else {
$description = "Payment with no invoice for amount {$amount} for client {$this->stripe->client->present()->name()}";
$description = ctrans('texts.stripe_paymenttext_without_invoice', ['amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()]);
}
if (substr($source->token, 0, 2) === 'pm') {

View File

@ -135,7 +135,7 @@ class BrowserPay implements MethodInterface
'payment_method' => $gateway_response->payment_method,
'payment_type' => PaymentType::parseCardType(strtolower($payment_method->card->brand)),
'amount' => $this->stripe->convertFromStripeAmount($gateway_response->amount, $this->stripe->client->currency()->precision, $this->stripe->client->currency()),
'transaction_reference' => optional($payment_intent->charges->data[0])->id,
'transaction_reference' => isset($payment_intent->latest_charge) ? $payment_intent->latest_charge : $payment_intent->charges->data[0]->id,
'gateway_type_id' => GatewayType::APPLE_PAY,
];

View File

@ -32,6 +32,7 @@ use Stripe\Exception\CardException;
use Stripe\Exception\InvalidRequestException;
use Stripe\Exception\RateLimitException;
use Stripe\StripeClient;
use App\Utils\Number;
class Charge
{
@ -62,9 +63,9 @@ class Charge
$invoice = Invoice::whereIn('id', $this->transformKeys(array_column($payment_hash->invoices(), 'invoice_id')))->withTrashed()->first();
if ($invoice) {
$description = "Invoice {$invoice->number} for {$amount} for client {$this->stripe->client->present()->name()}";
$description = ctrans('texts.stripe_paymenttext', ['invoicenumber' => $invoice->number, 'amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()]);
} else {
$description = "Payment with no invoice for amount {$amount} for client {$this->stripe->client->present()->name()}";
$description = ctrans('texts.stripe_paymenttext_without_invoice', ['amount' => Number::formatMoney($amount, $this->stripe->client), 'client' => $this->stripe->client->present()->name()]);
}
$this->stripe->init();
@ -90,6 +91,11 @@ class Charge
$data['payment_method_types'] = ['sepa_debit'];
}
/* Should improve token billing with client not present */
if (!auth()->guard('contact')->check()) {
$data['off_session'] = true;
}
$response = $this->stripe->createPaymentIntent($data, $this->stripe->stripe_connect_auth);
SystemLogger::dispatch($response, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_STRIPE, $this->stripe->client, $this->stripe->client->company);
@ -141,20 +147,27 @@ class Charge
$payment_method_type = PaymentType::SEPA;
$status = Payment::STATUS_PENDING;
} else {
$payment_method_type = $response->charges->data[0]->payment_method_details->card->brand;
if(isset($response->latest_charge)) {
$charge = \Stripe\Charge::retrieve($response->latest_charge, $this->stripe->stripe_connect_auth);
$payment_method_type = $charge->payment_method_details->card->brand;
}
elseif(isset($response->charges->data[0]->payment_method_details->card->brand))
$payment_method_type = $response->charges->data[0]->payment_method_details->card->brand;
else
$payment_method_type = 'visa';
$status = Payment::STATUS_COMPLETED;
}
if($response?->status == 'processing'){
//allows us to jump over the next stage - used for SEPA
}elseif($response?->status != 'succeeded'){
if(!in_array($response?->status, ['succeeded', 'processing'])){
$this->stripe->processInternallyFailedPayment($this->stripe, new \Exception('Auto billing failed.',400));
}
$data = [
'gateway_type_id' => $cgt->gateway_type_id,
'payment_type' => $this->transformPaymentTypeToConstant($payment_method_type),
'transaction_reference' => $response->charges->data[0]->id,
'transaction_reference' => isset($response->latest_charge) ? $response->latest_charge : $response->charges->data[0]->id,
'amount' => $amount,
];
@ -162,6 +175,7 @@ class Charge
$payment->meta = $cgt->meta;
$payment->save();
$payment_hash->data = array_merge((array) $payment_hash->data, ['payment_intent' => $response, 'amount_with_fee' => $amount]);
$payment_hash->payment_id = $payment->id;
$payment_hash->save();

View File

@ -23,6 +23,7 @@ use App\PaymentDrivers\StripePaymentDriver;
use App\PaymentDrivers\Stripe\Jobs\UpdateCustomer;
use Stripe\PaymentIntent;
use Stripe\PaymentMethod;
use App\Utils\Number;
class CreditCard
{
@ -62,7 +63,7 @@ class CreditCard
// $description = $this->stripe->decodeUnicodeString(ctrans('texts.invoices') . ': ' . collect($data['invoices'])->pluck('invoice_number')) . " for client {$this->stripe->client->present()->name()}";
$invoice_numbers = collect($data['invoices'])->pluck('invoice_number')->implode(',');
$description = "Invoices: {$invoice_numbers} for {$data['total']['amount_with_fee']} for client {$this->stripe->client->present()->name()}";
$description = ctrans('texts.stripe_paymenttext', ['invoicenumber' => $invoice_numbers, 'amount' => Number::formatMoney($data['total']['amount_with_fee'], $this->stripe->client), 'client' => $this->stripe->client->present()->name()]);
$payment_intent_data = [
'amount' => $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision, $this->stripe->client->currency()),

View File

@ -26,6 +26,7 @@ use App\PaymentDrivers\StripePaymentDriver;
use App\Utils\Ninja;
use App\Utils\Traits\GeneratesCounter;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\QueryException;
use Stripe\Customer;
use Stripe\PaymentMethod;
@ -37,6 +38,8 @@ class ImportCustomers
/** @var StripePaymentDriver */
public $stripe;
private bool $completed = true;
public $update_payment_methods;
public function __construct(StripePaymentDriver $stripe)
@ -64,9 +67,14 @@ class ImportCustomers
}
//handle
if(is_array($customers->data) && end($customers->data) && array_key_exists('id', end($customers->data)))
$starting_after = end($customers->data)['id'];
else
// if(is_array($customers->data) && end($customers->data) && array_key_exists('id', end($customers->data)))
// $starting_after = end($customers->data)['id'];
// else
// break;
$starting_after = isset(end($customers->data)['id']) ? end($customers->data)['id'] : false;
if(!$starting_after)
break;
} while ($customers->has_more);
@ -132,10 +140,30 @@ class ImportCustomers
$client->name = $customer->name ? $customer->name : $customer->email;
if (! isset($client->number) || empty($client->number)) {
$client->number = $this->getNextClientNumber($client);
}
$client->save();
$x = 1;
do {
try {
$client->number = $this->getNextClientNumber($client);
$client->saveQuietly();
$this->completed = false;
} catch (QueryException $e) {
$x++;
if ($x > 10) {
$this->completed = false;
}
}
} while ($this->completed);
}
else{
$client->save();
}
$contact = ClientContactFactory::create($client->company_id, $client->user_id);
$contact->client_id = $client->id;

View File

@ -66,11 +66,9 @@ class PaymentIntentWebhook implements ShouldQueue
{
$payment = Payment::query()
->where('company_id', $company->id)
->where(function ($query) use ($transaction) {
$query->where('transaction_reference', $transaction['payment_intent'])
->orWhere('transaction_reference', $transaction['id']);
})
->where('transaction_reference', $transaction['payment_intent'])
->first();
}
else
{
@ -138,6 +136,9 @@ class PaymentIntentWebhook implements ShouldQueue
$hash = isset($charge['metadata']['payment_hash']) ? $charge['metadata']['payment_hash'] : false;
if(!$hash)
return;
$payment_hash = PaymentHash::where('hash', $hash)->first();
if(!$payment_hash)
@ -264,6 +265,39 @@ class PaymentIntentWebhook implements ShouldQueue
}
}
// private function updateSepaPayment($payment_hash, $client, $meta)
// {
// $company_gateway = CompanyGateway::find($this->company_gateway_id);
// $payment_method_type = GatewayType::SEPA;
// $driver = $company_gateway->driver($client)->init()->setPaymentMethod($payment_method_type);
// $payment_hash->data = array_merge((array) $payment_hash->data, $this->stripe_request);
// $payment_hash->save();
// $driver->setPaymentHash($payment_hash);
// $data = [
// 'payment_method' => $payment_hash->data->object->payment_method,
// 'payment_type' => PaymentType::parseCardType(strtolower($meta['card_details'])) ?: PaymentType::CREDIT_CARD_OTHER,
// 'amount' => $payment_hash->data->amount_with_fee,
// 'transaction_reference' => $meta['transaction_reference'],
// 'gateway_type_id' => GatewayType::CREDIT_CARD,
// ];
// $payment = $driver->createPayment($data, Payment::STATUS_COMPLETED);
// SystemLogger::dispatch(
// ['response' => $this->stripe_request, 'data' => $data],
// SystemLog::CATEGORY_GATEWAY_RESPONSE,
// SystemLog::EVENT_GATEWAY_SUCCESS,
// SystemLog::TYPE_STRIPE,
// $client,
// $client->company,
// );
// }
private function updateCreditCardPayment($payment_hash, $client, $meta)
{

View File

@ -84,7 +84,7 @@ class SEPA
$this->stripe->payment_hash->data = array_merge((array) $this->stripe->payment_hash->data, $request->all());
$this->stripe->payment_hash->save();
if (property_exists($gateway_response, 'status') && ($gateway_response->status == 'processing' || $gateway_response->status === 'succeeded')) {
if (property_exists($gateway_response, 'status') && ($gateway_response->status == 'processing' || $gateway_response->status == 'succeeded')) {
if ($request->store_card) {
$this->storePaymentMethod($gateway_response);
}

View File

@ -660,14 +660,22 @@ class StripePaymentDriver extends BaseDriver
], $this->stripe_connect_auth);
if ($charge->captured) {
$payment = Payment::query()
->where('transaction_reference', $transaction['payment_intent'])
->where('company_id', $request->getCompany()->id)
->where(function ($query) use ($transaction) {
$query->where('transaction_reference', $transaction['payment_intent'])
->orWhere('transaction_reference', $transaction['id']);
})
->first();
$payment = false;
if(isset($transaction['payment_intent']))
{
$payment = Payment::query()
->where('transaction_reference', $transaction['payment_intent'])
->where('company_id', $request->getCompany()->id)
->first();
}
elseif(isset($transaction['id'])) {
$payment = Payment::query()
->where('transaction_reference', $transaction['id'])
->where('company_id', $request->getCompany()->id)
->first();
}
if ($payment) {
$payment->status_id = Payment::STATUS_COMPLETED;

View File

@ -56,6 +56,8 @@ class ClientRepository extends BaseRepository
*/
public function save(array $data, Client $client) : ?Client
{
$contact_data = $data;
unset($data['contacts']);
/* When uploading documents, only the document array is sent, so we must return early*/
if (array_key_exists('documents', $data) && count($data['documents']) >= 1) {
@ -67,7 +69,7 @@ class ClientRepository extends BaseRepository
$client->fill($data);
if (array_key_exists('settings', $data)) {
$client->saveSettings($data['settings'], $client);
$client->settings = $client->saveSettings($data['settings'], $client);
}
if (! $client->country_id) {
@ -75,19 +77,9 @@ class ClientRepository extends BaseRepository
$client->country_id = $company->settings->country_id;
}
try{
$client->save();
}
catch(\Exception $e) {
nlog("client save failed");
nlog($data);
}
$client->save();
if (! isset($client->number) || empty($client->number) || strlen($client->number) == 0) {
// $client->number = $this->getNextClientNumber($client);
// $client->save();
$x = 1;
@ -111,7 +103,7 @@ class ClientRepository extends BaseRepository
$data['name'] = $client->present()->name();
}
$this->contact_repo->save($data, $client);
$this->contact_repo->save($contact_data, $client);
return $client;
}

View File

@ -31,32 +31,20 @@ use Illuminate\Support\Facades\Cache;
class BankMatchingService implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, GeneratesCounter;
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private Company $company;
public function __construct(protected int $company_id, protected string $db){
$this->company_id = $company_id;
$this->db = $db;
$this->middleware_key = "bank_match_rate:{$this->company_id}";
}
private $invoices;
public $deleteWhenMissingModels = true;
public function __construct(private int $company_id, private string $db){}
public function handle()
public function handle() :void
{
MultiDB::setDb($this->db);
$this->company = Company::find($this->company_id);
$this->matchTransactions();
}
private function matchTransactions()
{
BankTransaction::where('company_id', $this->company->id)
BankTransaction::where('company_id', $this->company_id)
->where('status_id', BankTransaction::STATUS_UNMATCHED)
->cursor()
->each(function ($bt){
@ -64,11 +52,11 @@ class BankMatchingService implements ShouldQueue
(new BankService($bt))->processRules();
});
}
public function middleware()
{
return [new WithoutOverlapping($this->company_id)];
return [new WithoutOverlapping($this->middleware_key)];
}
}

View File

@ -33,8 +33,6 @@ class ClientService
try {
\DB::connection(config('database.default'))->transaction(function () use($amount) {
nlog("inside transaction - updating balance by {$amount}");
$this->client = Client::withTrashed()->where('id', $this->client->id)->lockForUpdate()->first();
$this->client->balance += $amount;
$this->client->save();

View File

@ -45,7 +45,7 @@ class SendEmail
$this->credit->invitations->each(function ($invitation) {
if (! $invitation->contact->trashed() && $invitation->contact->email) {
EmailEntity::dispatch($invitation, $invitation->company, $this->reminder_template);
EmailEntity::dispatch($invitation, $invitation->company, $this->reminder_template)->delay(2);
}
});

View File

@ -112,8 +112,7 @@ class AddGatewayFee extends AbstractService
$this->invoice
->client
->service()
->updateBalance($adjustment)
->save();
->updateBalance($adjustment);
$this->invoice
->ledger()

View File

@ -44,11 +44,12 @@ class HandleRestore extends AbstractService
return $this->invoice;
}
//determine whether we need to un-delete payments OR just modify the payment amount /applied balances.
//cannot restore an invoice with a deleted payment
foreach ($this->invoice->payments as $payment) {
//restore the payment record
$this->invoice->restore();
if(($this->invoice->paid_to_date == 0) && $payment->is_deleted)
return $this->invoice;
}
//adjust ledger balance
@ -56,8 +57,7 @@ class HandleRestore extends AbstractService
$this->invoice->client
->service()
->updateBalance($this->invoice->balance)
->updatePaidToDate($this->invoice->paid_to_date)
->updateBalanceAndPaidToDate($this->invoice->balance,$this->invoice->paid_to_date)
->save();
$this->windBackInvoiceNumber();
@ -120,11 +120,11 @@ class HandleRestore extends AbstractService
if ($this->adjustment_amount == $this->total_payments) {
$this->invoice->payments()->update(['payments.deleted_at' => null, 'payments.is_deleted' => false]);
} else {
}
//adjust payments down by the amount applied to the invoice payment.
$this->invoice->payments->each(function ($payment) {
$this->invoice->payments->fresh()->each(function ($payment) {
$payment_adjustment = $payment->paymentables
->where('paymentable_type', '=', 'invoices')
->where('paymentable_id', $this->invoice->id)
@ -141,8 +141,7 @@ class HandleRestore extends AbstractService
$payment->restore();
$payment->save();
});
}
return $this;
}

View File

@ -300,10 +300,10 @@ class InvoiceService
} elseif ($this->invoice->balance > 0 && $this->invoice->balance < $this->invoice->amount) {
$this->setStatus(Invoice::STATUS_PARTIAL);
}
elseif($this->invoice->balance < 0) {
$this->setStatus(Invoice::STATUS_PARTIAL);
elseif ($this->invoice->balance < 0 || $this->invoice->balance > 0) {
$this->invoice->status_id = Invoice::STATUS_SENT;
}
return $this;
}
@ -318,7 +318,7 @@ class InvoiceService
} elseif ($this->invoice->balance > 0 && $this->invoice->balance < $this->invoice->amount) {
$this->invoice->status_id = Invoice::STATUS_PARTIAL;
}
elseif ($this->invoice->balance < 0) {
elseif ($this->invoice->balance < 0 || $this->invoice->balance > 0) {
$this->invoice->status_id = Invoice::STATUS_SENT;
}

View File

@ -53,16 +53,6 @@ class MarkInvoiceDeleted extends AbstractService
->adjustPaidToDateAndBalance()
->adjustLedger();
$transaction = [
'invoice' => $this->invoice->transaction_event(),
'payment' => $this->invoice->payments()->exists() ? $this->invoice->payments()->first()->transaction_event() : [],
'client' => $this->invoice->client->transaction_event(),
'credit' => [],
'metadata' => ['total_payments' => $this->total_payments, 'balance_adjustment' => $this->balance_adjustment, 'adjustment_amount' => $this->adjustment_amount],
];
// TransactionLog::dispatch(TransactionEvent::INVOICE_DELETED, $transaction, $this->invoice->company->db);
return $this->invoice;
}
@ -87,26 +77,17 @@ class MarkInvoiceDeleted extends AbstractService
return $this;
}
// @deprecated
private function adjustBalance()
{
// $client = $this->invoice->client->fresh();
// $client->balance += $this->balance_adjustment * -1;
// $client->save();
// $this->invoice->client->service()->updateBalance($this->balance_adjustment * -1)->save(); //reduces the client balance by the invoice amount.
return $this;
}
/* Adjust the payment amounts */
private function adjustPayments()
{
//if total payments = adjustment amount - that means we need to delete the payments as well.
if ($this->adjustment_amount == $this->total_payments) {
nlog($this->adjustment_amount);
nlog($this->total_payments);
if ($this->adjustment_amount == $this->total_payments)
$this->invoice->payments()->update(['payments.deleted_at' => now(), 'payments.is_deleted' => true]);
} else {
//adjust payments down by the amount applied to the invoice payment.
@ -125,7 +106,7 @@ class MarkInvoiceDeleted extends AbstractService
$payment->applied -= $payment_adjustment;
$payment->save();
});
}
return $this;
}

View File

@ -139,9 +139,9 @@ class UpdateReminder extends AbstractService
if ($this->invoice->last_sent_date &&
$this->settings->enable_reminder_endless &&
($this->invoice->reminder1_sent || $this->settings->schedule_reminder1 == "") &&
($this->invoice->reminder2_sent || $this->settings->schedule_reminder2 == "") &&
($this->invoice->reminder3_sent || $this->settings->schedule_reminder3 == "")) {
($this->invoice->reminder1_sent || $this->settings->schedule_reminder1 == "" || !$this->settings->enable_reminder1) &&
($this->invoice->reminder2_sent || $this->settings->schedule_reminder2 == "" || !$this->settings->enable_reminder2) &&
($this->invoice->reminder3_sent || $this->settings->schedule_reminder3 == "" || !$this->settings->enable_reminder3)) {
$reminder_date = $this->addTimeInterval($this->invoice->last_sent_date, (int) $this->settings->endless_reminder_frequency_id);
if ($reminder_date) {

View File

@ -36,7 +36,7 @@ class LedgerService
$this->entity->company_ledger()->save($company_ledger);
ClientLedgerBalanceUpdate::dispatch($this->entity->company, $this->entity->client)->delay(now()->addSeconds(300));
ClientLedgerBalanceUpdate::dispatch($this->entity->company, $this->entity->client)->delay(now()->addSeconds(rand(30,300)));
return $this;
}
@ -52,7 +52,7 @@ class LedgerService
$this->entity->company_ledger()->save($company_ledger);
ClientLedgerBalanceUpdate::dispatch($this->entity->company, $this->entity->client)->delay(now()->addSeconds(300));
ClientLedgerBalanceUpdate::dispatch($this->entity->company, $this->entity->client)->delay(now()->addSeconds(rand(30,300)));
return $this;
}
@ -68,7 +68,7 @@ class LedgerService
$this->entity->company_ledger()->save($company_ledger);
ClientLedgerBalanceUpdate::dispatch($this->entity->company, $this->entity->client)->delay(now()->addSeconds(300));
ClientLedgerBalanceUpdate::dispatch($this->entity->company, $this->entity->client)->delay(now()->addSeconds(rand(30,300)));
return $this;
}

View File

@ -37,7 +37,7 @@ class SendEmail
$contact = $this->payment->client->contacts()->first();
if ($contact?->email)
EmailPayment::dispatch($this->payment, $this->payment->company, $contact);
EmailPayment::dispatch($this->payment, $this->payment->company, $contact)->delay(now()->addSeconds(3));
}
}

View File

@ -785,7 +785,7 @@ class SubscriptionService
*/
public function triggerWebhook($context)
{
nlog("trigger webook");
nlog("trigger webhook");
if (empty($this->subscription->webhook_configuration['post_purchase_url']) || is_null($this->subscription->webhook_configuration['post_purchase_url']) || strlen($this->subscription->webhook_configuration['post_purchase_url']) < 1) {
return ["message" => "Success", "status_code" => 200];
@ -879,6 +879,53 @@ class SubscriptionService
}
/* OPTIONAL PRODUCTS*/
/**
* Get the single charge products for the
* subscription
*
*/
public function optional_products()
{
if(!$this->subscription->optional_product_ids)
return collect();
$keys = $this->transformKeys(explode(",", $this->subscription->optional_product_ids));
if(is_array($keys))
return Product::whereIn('id', $keys)->get();
else
return Product::where('id', $keys)->get();
}
/**
* Get the recurring products for the
* subscription
*
*/
public function optional_recurring_products()
{
if(!$this->subscription->optional_recurring_product_ids)
return collect();
$keys = $this->transformKeys(explode(",", $this->subscription->optional_recurring_product_ids));
if(is_array($keys)){
return Product::whereIn('id', $keys)->get();
}
else{
return Product::where('id', $keys)->get();
}
}
/**
* Get available upgrades & downgrades for the plan.
*
@ -901,6 +948,8 @@ class SubscriptionService
*/
public function handleCancellation(RecurringInvoice $recurring_invoice)
{
$invoice_start_date = false;
$refund_end_date = false;
//only refund if they are in the refund window.
$outstanding_invoice = Invoice::where('subscription_id', $this->subscription->id)
@ -909,8 +958,11 @@ class SubscriptionService
->orderBy('id', 'desc')
->first();
$invoice_start_date = Carbon::parse($outstanding_invoice->date);
$refund_end_date = $invoice_start_date->addSeconds($this->subscription->refund_period);
if($outstanding_invoice)
{
$invoice_start_date = Carbon::parse($outstanding_invoice->date);
$refund_end_date = $invoice_start_date->addSeconds($this->subscription->refund_period);
}
/* Stop the recurring invoice and archive */
$recurring_invoice->service()->stop()->save();
@ -918,7 +970,7 @@ class SubscriptionService
$recurring_invoice_repo->archive($recurring_invoice);
/* Refund only if we are in the window - and there is nothing outstanding on the invoice */
if($refund_end_date->greaterThan(now()) && (int)$outstanding_invoice->balance == 0)
if($refund_end_date && $refund_end_date->greaterThan(now()) && (int)$outstanding_invoice->balance == 0)
{
if($outstanding_invoice->payments()->exists())

View File

@ -39,6 +39,7 @@ class BankTransactionTransformer extends EntityTransformer
'company',
'account',
'expense',
'payment',
'vendor',
'bank_account',
];
@ -66,6 +67,7 @@ class BankTransactionTransformer extends EntityTransformer
'base_type' => (string) $bank_transaction->base_type ?: '',
'invoice_ids' => (string) $bank_transaction->invoice_ids ?: '',
'expense_id'=> (string) $this->encodePrimaryKey($bank_transaction->expense_id) ?: '',
'payment_id'=> (string) $this->encodePrimaryKey($bank_transaction->payment_id) ?: '',
'vendor_id'=> (string) $this->encodePrimaryKey($bank_transaction->vendor_id) ?: '',
'bank_transaction_rule_id' => (string) $this->encodePrimaryKey($bank_transaction->bank_transaction_rule_id) ?: '',
'is_deleted' => (bool) $bank_transaction->is_deleted,
@ -103,4 +105,11 @@ class BankTransactionTransformer extends EntityTransformer
return $this->includeItem($bank_transaction->vendor, $transformer, Vendor::class);
}
public function includePayment(BankTransaction $bank_transaction)
{
$transformer = new PaymentTransformer($this->serializer);
return $this->includeItem($bank_transaction->payment, $transformer, Payment::class);
}
}

View File

@ -106,9 +106,10 @@ class Helpers
*
* @param string $value
* @param Client|Company $entity
* @param null|Carbon $currentDateTime
* @return null|string
*/
public static function processReservedKeywords(?string $value, $entity): ?string
public static function processReservedKeywords(?string $value, $entity, $currentDateTime = null): ?string
{
if (! $value) {
return '';
@ -132,71 +133,75 @@ class Helpers
Carbon::setLocale($entity->locale());
if (!$currentDateTime) {
$currentDateTime = Carbon::now();
}
$replacements = [
'literal' => [
':MONTH_BEFORE' => \sprintf(
'%s %s %s',
Carbon::now()->subMonth(1)->translatedFormat($entity->date_format()),
$currentDateTime->copy()->subMonth(1)->translatedFormat($entity->date_format()),
ctrans('texts.to'),
Carbon::now()->subDay(1)->translatedFormat($entity->date_format()),
$currentDateTime->copy()->subDay(1)->translatedFormat($entity->date_format()),
),
':YEAR_BEFORE' => \sprintf(
'%s %s %s',
Carbon::now()->subYear(1)->translatedFormat($entity->date_format()),
$currentDateTime->copy()->subYear(1)->translatedFormat($entity->date_format()),
ctrans('texts.to'),
Carbon::now()->subDay(1)->translatedFormat($entity->date_format()),
$currentDateTime->copy()->subDay(1)->translatedFormat($entity->date_format()),
),
':MONTH_AFTER' => \sprintf(
'%s %s %s',
Carbon::now()->translatedFormat($entity->date_format()),
$currentDateTime->translatedFormat($entity->date_format()),
ctrans('texts.to'),
Carbon::now()->addMonth(1)->subDay(1)->translatedFormat($entity->date_format()),
$currentDateTime->copy()->addMonth(1)->subDay(1)->translatedFormat($entity->date_format()),
),
':YEAR_AFTER' => \sprintf(
'%s %s %s',
Carbon::now()->translatedFormat($entity->date_format()),
$currentDateTime->translatedFormat($entity->date_format()),
ctrans('texts.to'),
Carbon::now()->addYear(1)->subDay(1)->translatedFormat($entity->date_format()),
$currentDateTime->copy()->addYear(1)->subDay(1)->translatedFormat($entity->date_format()),
),
':MONTHYEAR' => \sprintf(
'%s %s',
Carbon::createFromDate(now()->month)->translatedFormat('F'),
now()->year,
Carbon::createFromDate($currentDateTime->month)->translatedFormat('F'),
$currentDateTime->year,
),
':MONTH' => Carbon::createFromDate(now()->year, now()->month)->translatedFormat('F'),
':YEAR' => now()->year,
':QUARTER' => 'Q'.now()->quarter,
':MONTH' => Carbon::createFromDate($currentDateTime->year, $currentDateTime->month)->translatedFormat('F'),
':YEAR' => $currentDateTime->year,
':QUARTER' => 'Q'.$currentDateTime->quarter,
':WEEK_BEFORE' => \sprintf(
'%s %s %s',
Carbon::now()->subDays(7)->translatedFormat($entity->date_format()),
$currentDateTime->copy()->subDays(7)->translatedFormat($entity->date_format()),
ctrans('texts.to'),
Carbon::now()->subDays(1)->translatedFormat($entity->date_format())
$currentDateTime->copy()->subDays(1)->translatedFormat($entity->date_format())
),
':WEEK_AHEAD' => \sprintf(
'%s %s %s',
Carbon::now()->addDays(7)->translatedFormat($entity->date_format()),
$currentDateTime->copy()->addDays(7)->translatedFormat($entity->date_format()),
ctrans('texts.to'),
Carbon::now()->addDays(13)->translatedFormat($entity->date_format())
$currentDateTime->copy()->addDays(13)->translatedFormat($entity->date_format())
),
':WEEK' => \sprintf(
'%s %s %s',
Carbon::now()->translatedFormat($entity->date_format()),
$currentDateTime->translatedFormat($entity->date_format()),
ctrans('texts.to'),
Carbon::now()->addDays(6)->translatedFormat($entity->date_format())
$currentDateTime->copy()->addDays(6)->translatedFormat($entity->date_format())
),
],
'raw' => [
':MONTHYEAR' => now()->month,
':MONTH' => now()->month,
':YEAR' => now()->year,
':QUARTER' => now()->quarter,
':MONTHYEAR' => $currentDateTime->month,
':MONTH' => $currentDateTime->month,
':YEAR' => $currentDateTime->year,
':QUARTER' => $currentDateTime->quarter,
],
'ranges' => [
'MONTHYEAR' => Carbon::createFromDate(now()->year, now()->month),
'MONTHYEAR' => Carbon::createFromDate($currentDateTime->year, $currentDateTime->month),
],
'ranges_raw' => [
'MONTH' => now()->month,
'YEAR' => now()->year,
'MONTH' => $currentDateTime->month,
'YEAR' => $currentDateTime->year,
],
];
@ -221,12 +226,12 @@ class Helpers
continue;
}
$_left = Carbon::createFromDate(now()->year, now()->month)->translatedFormat('F Y');
$_left = Carbon::createFromDate($currentDateTime->year, $currentDateTime->month)->translatedFormat('F Y');
$_right = '';
// If right side doesn't have any calculations, replace with raw ranges keyword.
if (! Str::contains($right, ['-', '+', '/', '*'])) {
$_right = Carbon::createFromDate(now()->year, now()->month)->translatedFormat('F Y');
$_right = Carbon::createFromDate($currentDateTime->year, $currentDateTime->month)->translatedFormat('F Y');
}
// If right side contains one of math operations, calculate.
@ -237,7 +242,7 @@ class Helpers
$_value = explode($_operation, $right); // [MONTHYEAR, 4]
$_right = Carbon::createFromDate(now()->year, now()->month)->addMonths($_value[1])->translatedFormat('F Y');
$_right = Carbon::createFromDate($currentDateTime->year, $currentDateTime->month)->addMonths($_value[1])->translatedFormat('F Y');
}
$replacement = sprintf('%s to %s', $_left, $_right);
@ -304,7 +309,7 @@ class Helpers
}
if ($matches->keys()->first() == ':MONTHYEAR') {
$final_date = now()->addMonths($output - now()->month);
$final_date = $currentDateTime->copy()->addMonths($output - $currentDateTime->month);
$output = \sprintf(
'%s %s',

View File

@ -28,6 +28,6 @@ class NinjaPdf
RequestOptions::JSON => ['html' => $html],
]);
return $response->getBody();
return $response->getBody()->getContents();
}
}

View File

@ -226,6 +226,12 @@ class HtmlEngine
$data['$view_url'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_credit')];
// $data['$view_link'] = ['value' => $this->invitation->getLink(), 'label' => ctrans('texts.view_credit')];
$data['$date'] = ['value' => $this->translateDate($this->entity->date, $this->client->date_format(), $this->client->locale()) ?: '&nbsp;', 'label' => ctrans('texts.credit_date')];
$data['$credit.custom1'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'credit1', $this->entity->custom_value1, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice1')];
$data['$credit.custom2'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'credit2', $this->entity->custom_value2, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice2')];
$data['$credit.custom3'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'credit3', $this->entity->custom_value3, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice3')];
$data['$credit.custom4'] = ['value' => $this->helpers->formatCustomFieldValue($this->company->custom_fields, 'credit4', $this->entity->custom_value4, $this->client) ?: '&nbsp;', 'label' => $this->helpers->makeCustomField($this->company->custom_fields, 'invoice4')];
}
$data['$portal_url'] = ['value' => $this->invitation->getPortalLink(), 'label' =>''];

View File

@ -42,7 +42,7 @@ class Phantom
*
* @param $invitation
*/
public function generate($invitation)
public function generate($invitation, $return_pdf = false)
{
$entity = false;
@ -112,6 +112,9 @@ class Phantom
$instance = Storage::disk(config('filesystems.default'))->put($file_path, $pdf);
if($return_pdf)
return $pdf;
return $file_path;
}

View File

@ -30,7 +30,7 @@ trait ClientGroupSettingsSaver
* Saves a setting object.
*
* Works for groups|clients|companies
* @param array $settings The request input settings array
* @param array|object $settings The request input settings array
* @param object $entity The entity which the settings belongs to
* @return void
*/
@ -64,19 +64,6 @@ trait ClientGroupSettingsSaver
$entity_settings->{$key} = $value;
}
$entity->settings = $entity_settings;
try{
$entity->save();
}
catch(\Exception $e){
nlog("client settings failure");
nlog($entity_settings);
nlog($e->getMessage());
}
return $entity_settings;
}

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