Merge branch 'v5-develop' into bank_rules

This commit is contained in:
David Bomba 2022-11-23 12:54:26 +11:00
commit 0f2e19a873
48 changed files with 134952 additions and 134525 deletions

View File

@ -1 +1 @@
5.5.41
5.5.42

View File

@ -22,6 +22,8 @@ use App\Jobs\Company\CreateCompanyTaskStatuses;
use App\Jobs\Ninja\CompanySizeCheck;
use App\Jobs\Util\VersionCheck;
use App\Models\Account;
use App\Models\BankIntegration;
use App\Models\BankTransaction;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\Company;
@ -223,6 +225,18 @@ class DemoMode extends Command
'company_id' => $company->id,
]);
$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,
]);
$this->info('Creating '.$this->count.' clients');
for ($x = 0; $x < $this->count; $x++) {

View File

@ -182,7 +182,7 @@ class Handler extends ExceptionHandler
} elseif ($exception instanceof FatalThrowableError && $request->expectsJson()) {
return response()->json(['message'=>'Fatal error'], 500);
} elseif ($exception instanceof AuthorizationException) {
return response()->json(['message'=>'You are not authorized to view or perform this action'], 401);
return response()->json(['message'=> $exception->getMessage()], 401);
} elseif ($exception instanceof TokenMismatchException) {
return redirect()
->back()

View File

@ -87,7 +87,8 @@ class AccountTransformer implements AccountTransformerInterface
return [
'id' => $account->id,
'account_type' => $account->CONTAINER,
'account_name' => $account->accountName,
// 'account_name' => $account->accountName,
'account_name' => property_exists($account, 'accountName') ? $account->accountName : $account->nickname,
'account_status' => $account->accountStatus,
'account_number' => property_exists($account, 'accountNumber') ? '**** ' . substr($account?->accountNumber, -7) : '',
'provider_account_id' => $account->providerAccountId,

View File

@ -48,8 +48,15 @@ class EpcQrGenerator
$this->validateFields();
$qr = $writer->writeString($this->encodeMessage());
try {
$qr = $writer->writeString($this->encodeMessage(), 'utf-8');
}
catch(\Throwable $e){
return '';
}
catch(\Exception $e){
return '';
}
return "<svg viewBox='0 0 200 200' width='200' height='200' x='0' y='0' xmlns='http://www.w3.org/2000/svg'>
<rect x='0' y='0' width='100%'' height='100%' />{$qr}</svg>";

View File

@ -87,10 +87,10 @@ class SwissQrGenerator
$qrBill->setUltimateDebtor(
QrBill\DataGroup\Element\StructuredAddress::createWithStreet(
substr($this->client->present()->name(), 0 , 70),
$this->client->address1 ? substr($this->client->address1, 0 , 70) : '',
$this->client->address2 ? substr($this->client->address2, 0 , 16) : '',
$this->client->postal_code ? substr($this->client->postal_code, 0, 16) : '',
$this->client->city ? substr($this->client->city, 0, 35) : '',
$this->client->address1 ? substr($this->client->address1, 0 , 70) : '_',
$this->client->address2 ? substr($this->client->address2, 0 , 16) : '_',
$this->client->postal_code ? substr($this->client->postal_code, 0, 16) : '_',
$this->client->city ? substr($this->client->city, 0, 35) : '_',
'CH'
));

View File

@ -164,7 +164,8 @@ class CompanyController extends BaseController
*/
public function create(CreateCompanyRequest $request)
{
$company = CompanyFactory::create(auth()->user()->company()->account->id);
$cf = new \App\Factory\CompanyFactory;
$company = $cf->create(auth()->user()->company()->account->id);
return $this->itemResponse($company);
}

View File

@ -777,7 +777,7 @@ class InvoiceController extends BaseController
case 'email':
//check query parameter for email_type and set the template else use calculateTemplate
if (request()->has('email_type') && property_exists($invoice->company->settings, request()->input('email_type'))) {
if (request()->has('email_type') && in_array(request()->input('email_type'), ['reminder1', 'reminder2', 'reminder3', 'reminder_endless', 'custom1', 'custom2', 'custom3'])) {
$this->reminder_template = $invoice->client->getSetting(request()->input('email_type'));
} else {
$this->reminder_template = $invoice->calculateTemplate('invoice');

View File

@ -41,6 +41,7 @@ class ContactKeyLogin
$request->session()->invalidate();
}
//magic links survive for 1 hour
if ($request->segment(2) && $request->segment(2) == 'magic_link' && $request->segment(3)) {
$payload = Cache::get($request->segment(3));
@ -66,7 +67,11 @@ class ContactKeyLogin
}
} elseif ($request->segment(3) && config('ninja.db.multi_db_enabled')) {
if (MultiDB::findAndSetDbByContactKey($request->segment(3))) {
if ($client_contact = ClientContact::where('contact_key', $request->segment(3))->first()) {
if ($client_contact = ClientContact::with('company')->where('contact_key', $request->segment(3))->first()) {
if($client_contact->company->settings->enable_client_portal_password)
return redirect()->route('client.login', ['company_key' => $client_contact->company->company_key]);
if (empty($client_contact->email)) {
$client_contact->email = Str::random(6).'@example.com';
}
@ -82,7 +87,11 @@ class ContactKeyLogin
}
}
} elseif ($request->segment(2) && $request->segment(2) == 'key_login' && $request->segment(3)) {
if ($client_contact = ClientContact::where('contact_key', $request->segment(3))->first()) {
if ($client_contact = ClientContact::with('company')->where('contact_key', $request->segment(3))->first()) {
if($client_contact->company->settings->enable_client_portal_password)
return redirect()->route('client.login', ['company_key' => $client_contact->company->company_key]);
if (empty($client_contact->email)) {
$client_contact->email = Str::random(6).'@example.com';
$client_contact->save();
@ -125,7 +134,11 @@ class ContactKeyLogin
return redirect($this->setRedirectPath());
}
} elseif ($request->segment(3)) {
if ($client_contact = ClientContact::where('contact_key', $request->segment(3))->first()) {
if ($client_contact = ClientContact::with('company')->where('contact_key', $request->segment(3))->first()) {
if($client_contact->company->settings->enable_client_portal_password)
return redirect()->route('client.login', ['company_key' => $client_contact->company->company_key]);
if (empty($client_contact->email)) {
$client_contact->email = Str::random(6).'@example.com';
$client_contact->save();

View File

@ -44,7 +44,7 @@ class StoreBankTransactionRequest extends Request
{
$input = $this->all();
if(array_key_exists('bank_integration_id', $input) && strlen($input['bank_integration_id']) > 1)
if(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

@ -108,6 +108,8 @@ class UpdateCompanyRequest extends Request
}
}
$settings['email_style_custom'] = str_replace("{{", "", $settings['email_style_custom']);
if (! $account->isFreeHostedClient()) {
return $settings;
}

View File

@ -12,6 +12,7 @@
namespace App\Http\Requests\Report;
use App\Http\Requests\Request;
use Illuminate\Validation\Rule;
class GenericReportRequest extends Request
{
@ -27,11 +28,14 @@ class GenericReportRequest extends Request
public function rules()
{
nlog($this->date_range);
return [
'start_date' => 'string|date',
'end_date' => 'string|date',
'date_key' => 'string',
'date_range' => 'sometimes|string',
'date_range' => 'bail|required|string',
// 'start_date' => [Rule::requiredIf($this->date_range === 'custom')],
// 'end_date' => [Rule::requiredIf($this->date_range === 'custom')],
'end_date' => 'bail|required_if:date_range,custom|nullable|date',
'start_date' => 'bail|required_if:date_range,custom|nullable|date',
'report_keys' => 'present|array',
'send_email' => 'required|bool',
];

View File

@ -28,8 +28,8 @@ class ProfitLossRequest extends Request
public function rules()
{
return [
'start_date' => 'string|date',
'end_date' => 'string|date',
'start_date' => 'required_if:date_range,custom|string|date',
'end_date' => 'required_if:date_range,custom|string|date',
'is_income_billed' => 'required|bail|bool',
'is_expense_billed' => 'bool',
'include_tax' => 'required|bail|bool',

View File

@ -15,6 +15,7 @@ use App\Http\Requests\Request;
use App\Models\Project;
use App\Utils\Traits\ChecksEntityStatus;
use App\Utils\Traits\MakesHash;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Validation\Rule;
class UpdateTaskRequest extends Request
@ -29,6 +30,10 @@ class UpdateTaskRequest extends Request
*/
public function authorize() : bool
{
//prevent locked tasks from updating
if($this->task->invoice_lock && $this->task->invoice_id)
return false;
return auth()->user()->can('edit', $this->task);
}
@ -87,4 +92,11 @@ class UpdateTaskRequest extends Request
$this->replace($input);
}
protected function failedAuthorization()
{
throw new AuthorizationException(ctrans('texts.task_update_authorization_error'));
}
}

View File

@ -293,7 +293,7 @@ class CompanyExport implements ShouldQueue
$this->export_data['payments'] = $this->company->payments()->orderBy('number', 'DESC')->cursor()->map(function ($payment){
$payment = $this->transformBasicEntities($payment);
$payment = $this->transformArrayOfKeys($payment, ['client_id','project_id', 'vendor_id', 'client_contact_id', 'invitation_id', 'company_gateway_id']);
$payment = $this->transformArrayOfKeys($payment, ['client_id','project_id', 'vendor_id', 'client_contact_id', 'invitation_id', 'company_gateway_id', 'transaction_id']);
$payment->paymentables = $this->transformPaymentable($payment);
@ -456,7 +456,6 @@ class CompanyExport implements ShouldQueue
})->all();
$this->export_data['purchase_order_invitations'] = PurchaseOrderInvitation::where('company_id', $this->company->id)->withTrashed()->cursor()->map(function ($purchase_order){
$purchase_order = $this->transformArrayOfKeys($purchase_order, ['company_id', 'user_id', 'vendor_contact_id', 'purchase_order_id']);
@ -466,6 +465,21 @@ class CompanyExport implements ShouldQueue
})->all();
$this->export_data['bank_integrations'] = $this->company->bank_integrations()->orderBy('id', 'ASC')->cursor()->map(function ($bank_integration){
$bank_integration = $this->transformArrayOfKeys($bank_integration, ['account_id','company_id', 'user_id']);
return $bank_integration->makeVisible(['id','user_id','company_id','account_id']);
})->all();
$this->export_data['bank_transactions'] = $this->company->bank_transactions()->orderBy('id', 'ASC')->cursor()->map(function ($bank_transaction){
$bank_transaction = $this->transformArrayOfKeys($bank_transaction, ['company_id', 'user_id','bank_integration_id','expense_id','category_id','ninja_category_id','vendor_id']);
return $bank_transaction->makeVisible(['id','user_id','company_id']);
})->all();
//write to tmp and email to owner();
@ -516,9 +530,6 @@ class CompanyExport implements ShouldQueue
$file_name = date('Y-m-d').'_'.str_replace([" ", "/"],["_",""], $this->company->present()->name() . '_' . $this->company->company_key .'.zip');
$path = 'backups';
// if(!Storage::disk(config('filesystems.default'))->exists($path))
// Storage::disk(config('filesystems.default'))->makeDirectory($path, 0775);
$zip_path = public_path('storage/backups/'.$file_name);
$zip = new \ZipArchive();

View File

@ -24,6 +24,8 @@ use App\Mail\Import\CompanyImportFailure;
use App\Mail\Import\ImportCompleted;
use App\Models\Activity;
use App\Models\Backup;
use App\Models\BankIntegration;
use App\Models\BankTransaction;
use App\Models\Client;
use App\Models\ClientContact;
use App\Models\ClientGatewayToken;
@ -142,15 +144,16 @@ class CompanyImport implements ShouldQueue
'expenses',
'tasks',
'payments',
// 'activities',
// 'backups',
'company_ledger',
'designs',
'documents',
'webhooks',
'system_logs',
'purchase_orders',
'purchase_order_invitations'
'purchase_order_invitations',
'bank_integrations',
'bank_transactions',
'payments',
];
private $company_properties = [
@ -527,6 +530,37 @@ class CompanyImport implements ShouldQueue
}
private function import_bank_integrations()
{
$this->genericImport(BankIntegration::class,
['assigned_user_id','account_id', 'company_id', 'id', 'hashed_id'],
[
['users' => 'user_id'],
],
'bank_integrations',
'description');
return $this;
}
private function import_bank_transactions()
{
$this->genericImport(BankTransaction::class,
['assigned_user_id','company_id', 'id', 'hashed_id', 'user_id'],
[
['users' => 'user_id'],
['expenses' => 'expense_id'],
['vendors' => 'vendor_id'],
['expense_categories' => 'ninja_category_id'],
['expense_categories' => 'category_id'],
['bank_integrations' => 'bank_integration_id']
],
'bank_transactions',
null);
return $this;
}
private function import_recurring_expenses()
{
//unset / transforms / object_property / match_key
@ -979,6 +1013,7 @@ class CompanyImport implements ShouldQueue
['vendors' => 'vendor_id'],
['invoice_invitations' => 'invitation_id'],
['company_gateways' => 'company_gateway_id'],
['bank_transactions' => 'transaction_id'],
],
'payments',
'number');
@ -1569,6 +1604,28 @@ class CompanyImport implements ShouldQueue
$obj_array,
);
}
elseif($class == 'App\Models\BankIntegration'){
$new_obj = new BankIntegration();
$new_obj->company_id = $this->company->id;
$new_obj->account_id = $this->account->id;
$new_obj->fill($obj_array);
$new_obj->save(['timestamps' => false]);
}
elseif($class == 'App\Models\BankTransaction'){
$new_obj = new BankTransaction();
$new_obj->company_id = $this->company->id;
$obj_array['invoice_ids'] = collect(explode(",",$obj_array['invoice_ids']))->map(function ($id) {
return $this->transformId('invoices', $id);
})->map(function ($encodeable){
return $this->encodePrimaryKey($encodeable);
})->implode(",");
$new_obj->fill($obj_array);
$new_obj->save(['timestamps' => false]);
}
else{
$new_obj = $class::withTrashed()->firstOrNew(
[$match_key => $obj->{$match_key}, 'company_id' => $this->company->id],

View File

@ -129,11 +129,8 @@ class NinjaMailerJob implements ShouldQueue
LightLogs::create(new EmailSuccess($this->nmo->company->company_key))
->send();
// nlog('Using ' . ((int) (memory_get_usage(true) / (1024 * 1024))) . 'MB ');
$this->nmo = null;
$this->company = null;
app('queue.worker')->shouldQuit = 1;
} catch (\Exception | \RuntimeException | \Google\Service\Exception $e) {

View File

@ -19,6 +19,9 @@ use stdClass;
class InvoiceEmailFailedActivity implements ShouldQueue
{
// public $delay = 10;
protected $activity_repo;
/**

View File

@ -18,13 +18,18 @@ use App\Libraries\MultiDB;
use App\Mail\Admin\EntityFailedSendObject;
use App\Notifications\Admin\EntitySentNotification;
use App\Utils\Traits\Notifications\UserNotifies;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class InvoiceFailedEmailNotification
{
use UserNotifies;
public $delay = 5;
use UserNotifies, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $delay = 10;
public function __construct()
{

View File

@ -124,6 +124,7 @@ class Company extends BaseModel
'enabled_expense_tax_rates',
'invoice_task_project',
'report_include_deleted',
'invoice_task_lock',
];
protected $hidden = [

View File

@ -40,6 +40,7 @@ class Task extends BaseModel
'number',
'is_date_based',
'status_order',
'invoice_lock'
];
protected $touches = [];

View File

@ -111,7 +111,10 @@ class PayPal
'paymentMethodNonce' => $gateway_response->nonce,
]);
return $payment_method->paymentMethod->token;
if($payment_method->success)
return $payment_method->paymentMethod->token;
else
throw new PaymentFailed(property_exists($payment_method, 'message') ? $payment_method->message : 'Undefined error storing payment token.', 0);
}
/**

View File

@ -306,10 +306,53 @@ class CheckoutComPaymentDriver extends BaseDriver
try {
$response = $this->gateway->getCustomersClient()->create($request);
} catch (\Exception $e) {
// API error
throw new PaymentFailed($e->getMessage(), $e->getCode());
}
catch (CheckoutApiException $e) {
// API error
$request_id = $e->request_id;
$http_status_code = $e->http_status_code;
$error_details = $e->error_details;
if(is_array($error_details)) {
$error_details = end($e->error_details['error_codes']);
}
$human_exception = $error_details ? new \Exception($error_details, 400) : $e;
throw new PaymentFailed($human_exception);
} catch (CheckoutArgumentException $e) {
// Bad arguments
$error_details = $e->error_details;
if(is_array($error_details)) {
$error_details = end($e->error_details['error_codes']);
}
$human_exception = $error_details ? new \Exception($error_details, 400) : $e;
throw new PaymentFailed($human_exception);
} catch (CheckoutAuthorizationException $e) {
// Bad Invalid authorization
$error_details = $e->error_details;
if(is_array($error_details)) {
$error_details = end($e->error_details['error_codes']);
}
$human_exception = $error_details ? new \Exception($error_details, 400) : $e;
throw new PaymentFailed($human_exception);
}
// catch (\Exception $e) {
// // API error
// throw new PaymentFailed($e->getMessage(), $e->getCode());
// }
return $response;
}

View File

@ -63,7 +63,11 @@ class ImportCustomers
$this->addCustomer($customer);
}
$starting_after = end($customers->data)['id'];
//handle
if(is_array($customers->data) && end($customers->data) && array_key_exists('id', end($customers->data)))
$starting_after = end($customers->data)['id'];
else
break;
} while ($customers->has_more);
}

View File

@ -93,7 +93,7 @@ class PaymentIntentWebhook implements ShouldQueue
return;
if(optional($this->stripe_request['object']['charges']['data'][0])['id']){
if(isset($this->stripe_request['object']['charges']) && optional($this->stripe_request['object']['charges']['data'][0])['id']){
$company = Company::where('company_key', $this->company_key)->first();

View File

@ -451,7 +451,7 @@ class EventServiceProvider extends ServiceProvider
PaymentEmailedActivity::class,
],
PaymentWasEmailedAndFailed::class => [
PaymentEmailFailureActivity::class,
// PaymentEmailFailureActivity::class,
],
PurchaseOrderWasArchived::class => [
PurchaseOrderArchivedActivity::class,

View File

@ -60,7 +60,7 @@ class ActivityRepository extends BaseRepository
$activity->save();
//rate limiter
// $this->createBackup($entity, $activity);
$this->createBackup($entity, $activity);
}
/**

View File

@ -104,7 +104,6 @@ class DeletePayment
$client = $this->payment
->client
->fresh()
->service()
->updateBalance($net_deletable)
->save();
@ -136,9 +135,8 @@ class DeletePayment
});
}
$client = $this->payment->client->fresh();
$client
$this->payment
->client
->service()
->updatePaidToDate(($this->payment->amount - $this->payment->refunded) * -1)
->save();
@ -146,7 +144,7 @@ class DeletePayment
$transaction = [
'invoice' => [],
'payment' => [],
'client' => $client->transaction_event(),
'client' => $this->payment->client->transaction_event(),
'credit' => [],
'metadata' => [],
];

View File

@ -62,12 +62,14 @@ class UpdateInvoicePayment
$paid_amount = $paid_invoice->amount;
}
$client->service()->updateBalanceAndPaidToDate($paid_amount*-1, $paid_amount);
$client->service()->updatePaidToDate($paid_amount); //always use the payment->amount
/* Need to determine here is we have an OVER payment - if YES only apply the max invoice amount */
if($paid_amount > $invoice->partial && $paid_amount > $invoice->balance)
$paid_amount = $invoice->balance;
$client->service()->updateBalance($paid_amount*-1); //only ever use the amount applied to the invoice
/*Improve performance here - 26-01-2022 - also change the order of events for invoice first*/
//caution what if we amount paid was less than partial - we wipe it!
$invoice->balance -= $paid_amount;

View File

@ -40,12 +40,8 @@ use Symfony\Component\HttpFoundation\Request;
class TaskSchedulerService
{
public Scheduler $scheduler;
public function __construct(Scheduler $scheduler)
{
$this->scheduler = $scheduler;
}
public function __construct(public Scheduler $scheduler) {}
public function store(Scheduler $scheduler, CreateScheduledTaskRequest $request)
{

View File

@ -188,6 +188,7 @@ class CompanyTransformer extends EntityTransformer
'enabled_expense_tax_rates' => (int) $company->enabled_expense_tax_rates,
'invoice_task_project' => (bool) $company->invoice_task_project,
'report_include_deleted' => (bool) $company->report_include_deleted,
'invoice_task_lock' => (bool) $company->invoice_task_lock,
];
}

View File

@ -72,7 +72,6 @@ class TaskTransformer extends EntityTransformer
'user_id' => (string) $this->encodePrimaryKey($task->user_id),
'assigned_user_id' => (string) $this->encodePrimaryKey($task->assigned_user_id),
'number' => (string) $task->number ?: '',
// 'start_time' => (int) $task->start_time,
'description' => (string) $task->description ?: '',
'duration' => (int) $task->duration ?: 0,
'rate' => (float) $task->rate ?: 0,
@ -93,6 +92,7 @@ class TaskTransformer extends EntityTransformer
'status_sort_order' => (int) $task->status_sort_order, //deprecated 5.0.34
'is_date_based' => (bool) $task->is_date_based,
'status_order' => is_null($task->status_order) ? null : (int) $task->status_order,
'invoice_lock' => (bool) $task->invoice_lock,
];
}
}

View File

@ -134,6 +134,30 @@ class Helpers
$replacements = [
'literal' => [
':MONTH_BEFORE' => \sprintf(
'%s %s %s',
Carbon::now()->subMonth(1)->translatedFormat($entity->date_format()),
ctrans('texts.to'),
Carbon::now()->subDay(1)->translatedFormat($entity->date_format()),
),
':YEAR_BEFORE' => \sprintf(
'%s %s %s',
Carbon::now()->subYear(1)->translatedFormat($entity->date_format()),
ctrans('texts.to'),
Carbon::now()->subDay(1)->translatedFormat($entity->date_format()),
),
':MONTH_AFTER' => \sprintf(
'%s %s %s',
Carbon::now()->translatedFormat($entity->date_format()),
ctrans('texts.to'),
Carbon::now()->addMonth(1)->subDay(1)->translatedFormat($entity->date_format()),
),
':YEAR_AFTER' => \sprintf(
'%s %s %s',
Carbon::now()->translatedFormat($entity->date_format()),
ctrans('texts.to'),
Carbon::now()->addYear(1)->subDay(1)->translatedFormat($entity->date_format()),
),
':MONTHYEAR' => \sprintf(
'%s %s',
Carbon::createFromDate(now()->month)->translatedFormat('F'),
@ -150,15 +174,15 @@ class Helpers
),
':WEEK_AHEAD' => \sprintf(
'%s %s %s',
Carbon::now()->addDays(6)->translatedFormat($entity->date_format()),
Carbon::now()->addDays(7)->translatedFormat($entity->date_format()),
ctrans('texts.to'),
Carbon::now()->addDays(13)->translatedFormat($entity->date_format())
),
':WEEK' => \sprintf(
'%s %s %s',
Carbon::now()->subDays(7)->translatedFormat($entity->date_format()),
Carbon::now()->translatedFormat($entity->date_format()),
ctrans('texts.to'),
Carbon::now()->addDays(13)->translatedFormat($entity->date_format())
Carbon::now()->addDays(6)->translatedFormat($entity->date_format())
),
],
'raw' => [

View File

@ -95,10 +95,14 @@ class SystemHealth
if(strlen(config('ninja.currency_converter_api_key')) == 0){
try{
$cs = DB::table('clients')
->select('settings->currency_id as id')
->get();
}
catch(\Exception $e){
return true; //fresh installs, there may be no DB connection, nor migrations could have run yet.
}
$currency_count = $cs->unique('id')->filter(function ($value){
return !is_null($value->id);

View File

@ -14,8 +14,8 @@ return [
'require_https' => env('REQUIRE_HTTPS', true),
'app_url' => rtrim(env('APP_URL', ''), '/'),
'app_domain' => env('APP_DOMAIN', 'invoicing.co'),
'app_version' => '5.5.41',
'app_tag' => '5.5.41',
'app_version' => '5.5.42',
'app_tag' => '5.5.42',
'minimum_client_version' => '5.0.16',
'terms_version' => '1.0.1',
'api_secret' => env('API_SECRET', ''),

View File

@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('tasks', function (Blueprint $table)
{
$table->boolean('invoice_lock')->default(false);
});
Schema::table('companies', function (Blueprint $table)
{
$table->boolean('invoice_task_lock')->default(false);
});
Schema::table('bank_transactions', function (Blueprint $table)
{
$table->bigInteger('bank_rule_id')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
//
}
};

View File

@ -4843,6 +4843,11 @@ $LANG = array(
'refresh_accounts' => 'Refresh Accounts',
'upgrade_to_connect_bank_account' => 'Upgrade to Enterprise to connect your bank account',
'click_here_to_connect_bank_account' => 'Click here to connect your bank account',
'task_update_authorization_error' => 'Insufficient permissions, or task may be locked',
'cash_vs_accrual' => 'Accrual accounting',
'cash_vs_accrual_help' => 'Turn on for accrual reporting, turn off for cash basis reporting.',
'expense_paid_report' => 'Expensed reporting',
'expense_paid_report_help' => 'Turn on for reporting all expenses, turn off for reporting only paid expenses',
);
return $LANG;

View File

@ -11,9 +11,9 @@ const RESOURCES = {
"favicon.png": "dca91c54388f52eded692718d5a98b8b",
"favicon.ico": "51636d3a390451561744c42188ccd628",
"flutter.js": "f85e6fb278b0fd20c349186fb46ae36d",
"/": "112f22769207bffb3936c08dec3ffa4d",
"/": "9d48b2826c07eb42f0817289b6a7b7ca",
"manifest.json": "ef43d90e57aa7682d7e2cfba2f484a40",
"main.dart.js": "bddfba2a7d482fece1e7a9ff84429256",
"main.dart.js": "3568e02ff28e4dae78f695088a6e21c5",
"assets/AssetManifest.json": "759f9ef9973f7e26c2a51450b55bb9fa",
"assets/FontManifest.json": "087fb858dc3cbfbf6baf6a30004922f1",
"assets/NOTICES": "1a34e70168d56fad075adfb4bdbb20eb",

131504
public/main.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

129170
public/main.foss.dart.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -257,7 +257,7 @@
overflow-wrap: break-word;
}
.stamp {
.stamp {
transform: rotate(12deg);
color: #555;
font-size: 3rem;

View File

@ -249,6 +249,16 @@
opacity: 0.2;
z-index:200 !important;
position: fixed;
}
.project-header {
font-size: 1.5em;
margin-top: 0.1em;
margin-bottom: 0.1em;
margin-left: 0;
margin-right: 0;
font-weight: bold;
color: #505050;
}
/** Useful snippets, uncomment to enable. **/

View File

@ -31,7 +31,7 @@
{{ ctrans('texts.type') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
{{ $payment_method->meta?->brand }}
{{ property_exists($payment_method->meta, 'brand') ? $payment_method->meta?->brand : ''}}
{{ property_exists($payment_method->meta, 'scheme') ? $payment_method->meta?->scheme : '' }}
</dd>
</div>

View File

@ -186,7 +186,7 @@ Route::group(['middleware' => ['throttle:300,1', 'api_db', 'token_auth', 'locale
Route::put('expenses/{expense}/upload', [ExpenseController::class, 'upload']);
Route::post('expenses/bulk', [ExpenseController::class, 'bulk'])->name('expenses.bulk');
Route::post('export', [ExportController::class, 'index'])->name('export.index');
Route::post('export', [ExportController::class, 'index'])->middleware('throttle:2,1')->name('export.index');
Route::resource('expense_categories', ExpenseCategoryController::class); // name = (expense_categories. index / create / show / update / destroy / edit
Route::post('expense_categories/bulk', [ExpenseCategoryController::class, 'bulk'])->name('expense_categories.bulk');
@ -196,7 +196,7 @@ Route::group(['middleware' => ['throttle:300,1', 'api_db', 'token_auth', 'locale
Route::put('group_settings/{group_setting}/upload', [GroupSettingController::class, 'upload'])->name('group_settings.upload');
Route::post('import', [ImportController::class, 'import'])->name('import.import');
Route::post('import_json', [ImportJsonController::class, 'import'])->name('import.import_json');
Route::post('import_json', [ImportJsonController::class, 'import'])->middleware('throttle:2,1')->name('import.import_json');
Route::post('preimport', [ImportController::class, 'preimport'])->name('import.preimport');
Route::resource('invoices', InvoiceController::class); // name = (invoices. index / create / show / update / destroy / edit

View File

@ -11,6 +11,7 @@
namespace Tests\Feature;
use App\Models\Task;
use App\Utils\Traits\MakesHash;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Testing\DatabaseTransactions;
@ -42,6 +43,90 @@ class TaskApiTest extends TestCase
Model::reguard();
}
public function testTaskLockingGate()
{
$data = [
'timelog' => [[1,2],[3,4]],
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/tasks', $data);
$arr = $response->json();
$response->assertStatus(200);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/tasks/' . $arr['data']['id'], $data);
$arr = $response->json();
$response->assertStatus(200);
$task = Task::find($this->decodePrimaryKey($arr['data']['id']));
$task->invoice_id = $this->invoice->id;
$task->save();
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/tasks/' . $arr['data']['id'], $data);
$arr = $response->json();
$response->assertStatus(200);
$task = Task::find($this->decodePrimaryKey($arr['data']['id']));
$task->invoice_lock =true;
$task->invoice_id = $this->invoice->id;
$task->save();
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/tasks/' . $arr['data']['id'], $data);
$arr = $response->json();
$response->assertStatus(401);
}
public function testTaskLocking()
{
$data = [
'timelog' => [[1,2],[3,4]],
'invoice_lock' => true
];
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->post('/api/v1/tasks', $data);
$arr = $response->json();
$response->assertStatus(200);
$response = $this->withHeaders([
'X-API-SECRET' => config('ninja.api_secret'),
'X-API-TOKEN' => $this->token,
])->putJson('/api/v1/tasks/' . $arr['data']['id'], $data);
$arr = $response->json();
$response->assertStatus(200);
}
public function testTimeLogValidation()
{
$data = [
@ -75,9 +160,10 @@ class TaskApiTest extends TestCase
$arr = $response->json();
$response->assertStatus(200);
}
public function testTimeLogValidation2()
{
$data = [