diff --git a/VERSION.txt b/VERSION.txt index 36bb3ecc17..a07dffe0af 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -5.7.42 \ No newline at end of file +5.7.43 \ No newline at end of file diff --git a/app/Filters/ExpenseFilters.php b/app/Filters/ExpenseFilters.php index 264be55d03..c9cddb8dc7 100644 --- a/app/Filters/ExpenseFilters.php +++ b/app/Filters/ExpenseFilters.php @@ -41,6 +41,9 @@ class ExpenseFilters extends QueryFilters ->orWhere('custom_value4', 'like', '%'.$filter.'%') ->orWhereHas('category', function ($q) use ($filter) { $q->where('name', 'like', '%'.$filter.'%'); + }) + ->orWhereHas('vendor', function ($q) use ($filter) { + $q->where('name', 'like', '%'.$filter.'%'); }); }); } diff --git a/app/Filters/UserFilters.php b/app/Filters/UserFilters.php index b1ed138161..7034dd0cf4 100644 --- a/app/Filters/UserFilters.php +++ b/app/Filters/UserFilters.php @@ -154,23 +154,15 @@ class UserFilters extends QueryFilters $user = auth()->user(); if (in_array(self::STATUS_ACTIVE, $filters)) { - $query = $query->orWhereHas('company_users', function ($q) use($user){ - $q->where('company_id', $user->company()->id)->whereNull('deleted_at'); - }); + $query->orWhereNull('deleted_at'); } if (in_array(self::STATUS_ARCHIVED, $filters)) { - $query = $query->orWhereHas('company_users', function ($q) use($user){ - $q->where('company_id', $user->company()->id)->whereNotNull('deleted_at')->where('is_deleted', 0); - }); - + $query->orWhereNotNull('deleted_at')->where('is_deleted', 0); } if (in_array(self::STATUS_DELETED, $filters)) { - $query = $query->orWhereHas('company_users', function ($q) use($user){ - $q->where('company_id', $user->company()->id)->where('is_deleted', 1); - }); - + $query->orWhere('is_deleted', 1); } }); } diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php index bab418747b..933bb7c30c 100644 --- a/app/Http/Controllers/BaseController.php +++ b/app/Http/Controllers/BaseController.php @@ -1123,10 +1123,10 @@ class BaseController extends Controller $data['white_label'] = Ninja::isSelfHost() ? $account->isPaid() : false; //pass referral code to front end - $data['rc'] = request()->has('rc') ? request()->input('rc') : ''; - $data['build'] = request()->has('build') ? request()->input('build') : ''; - $data['login'] = request()->has('login') ? request()->input('login') : 'false'; - $data['signup'] = request()->has('signup') ? request()->input('signup') : 'false'; + $data['rc'] = request()->has('rc') && is_string(request()->input('rc')) ? request()->input('rc') : ''; + $data['build'] = request()->has('build') && is_string(request()->input('build')) ? request()->input('build') : ''; + $data['login'] = request()->has('login') && is_string(request()->input('input')) ? request()->input('login') : 'false'; + $data['signup'] = request()->has('signup') && is_string(request()->input('signup')) ? request()->input('signup') : 'false'; $data['canvas_path'] = $canvas_path; if (request()->session()->has('login')) { diff --git a/app/Http/Controllers/ClientPortal/InvoiceController.php b/app/Http/Controllers/ClientPortal/InvoiceController.php index e293eacbbd..6216f41cfc 100644 --- a/app/Http/Controllers/ClientPortal/InvoiceController.php +++ b/app/Http/Controllers/ClientPortal/InvoiceController.php @@ -181,6 +181,16 @@ class InvoiceController extends Controller ->with('message', ctrans('texts.no_payable_invoices_selected')); } + //ensure all stale fees are removed. + $invoices->each(function ($invoice) { + $invoice->service() + ->markSent() + ->removeUnpaidGatewayFees() + ->save(); + }); + + $invoices = $invoices->fresh(); + //iterate and sum the payable amounts either partial or balance $total = 0; foreach ($invoices as $invoice) { diff --git a/app/Http/Controllers/ExpenseController.php b/app/Http/Controllers/ExpenseController.php index f7bc4e9cfd..c70959f86e 100644 --- a/app/Http/Controllers/ExpenseController.php +++ b/app/Http/Controllers/ExpenseController.php @@ -11,27 +11,28 @@ namespace App\Http\Controllers; -use App\Events\Expense\ExpenseWasCreated; -use App\Events\Expense\ExpenseWasUpdated; +use App\Utils\Ninja; +use App\Models\Account; +use App\Models\Expense; +use Illuminate\Http\Response; use App\Factory\ExpenseFactory; use App\Filters\ExpenseFilters; -use App\Http\Requests\Expense\CreateExpenseRequest; -use App\Http\Requests\Expense\DestroyExpenseRequest; +use App\Utils\Traits\MakesHash; +use App\Utils\Traits\Uploadable; +use App\Utils\Traits\BulkOptions; +use App\Utils\Traits\SavesDocuments; +use App\Repositories\ExpenseRepository; +use App\Transformers\ExpenseTransformer; +use App\Events\Expense\ExpenseWasCreated; +use App\Events\Expense\ExpenseWasUpdated; +use App\Http\Requests\Expense\BulkExpenseRequest; use App\Http\Requests\Expense\EditExpenseRequest; use App\Http\Requests\Expense\ShowExpenseRequest; use App\Http\Requests\Expense\StoreExpenseRequest; +use App\Http\Requests\Expense\CreateExpenseRequest; use App\Http\Requests\Expense\UpdateExpenseRequest; use App\Http\Requests\Expense\UploadExpenseRequest; -use App\Models\Account; -use App\Models\Expense; -use App\Repositories\ExpenseRepository; -use App\Transformers\ExpenseTransformer; -use App\Utils\Ninja; -use App\Utils\Traits\BulkOptions; -use App\Utils\Traits\MakesHash; -use App\Utils\Traits\SavesDocuments; -use App\Utils\Traits\Uploadable; -use Illuminate\Http\Response; +use App\Http\Requests\Expense\DestroyExpenseRequest; /** * Class ExpenseController. @@ -321,7 +322,10 @@ class ExpenseController extends BaseController */ public function create(CreateExpenseRequest $request) { - $expense = ExpenseFactory::create(auth()->user()->company()->id, auth()->user()->id); + /** @var \App\Models\User $user */ + $user = auth()->user(); + + $expense = ExpenseFactory::create($user->company()->id, $user->id); return $this->itemResponse($expense); } @@ -366,9 +370,12 @@ class ExpenseController extends BaseController */ public function store(StoreExpenseRequest $request) { - $expense = $this->expense_repo->save($request->all(), ExpenseFactory::create(auth()->user()->company()->id, auth()->user()->id)); + /** @var \App\Models\User $user */ + $user = auth()->user(); - event(new ExpenseWasCreated($expense, $expense->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); + $expense = $this->expense_repo->save($request->all(), ExpenseFactory::create($user->company()->id, $user->id)); + + event(new ExpenseWasCreated($expense, $expense->company, Ninja::eventVars($user ? $user->id : null))); event('eloquent.created: App\Models\Expense', $expense); @@ -481,20 +488,25 @@ class ExpenseController extends BaseController * ), * ) */ - public function bulk() + public function bulk(BulkExpenseRequest $request) { - $action = request()->input('action'); + /** @var \App\Models\User $user */ + $user = auth()->user(); - $ids = request()->input('ids'); - $expenses = Expense::withTrashed()->find($this->transformKeys($ids)); + $expenses = Expense::withTrashed()->find($request->ids); - $expenses->each(function ($expense, $key) use ($action) { - if (auth()->user()->can('edit', $expense)) { - $this->expense_repo->{$action}($expense); + if($request->action == 'bulk_categorize' && $user->can('edit', $expenses->first())) { + $this->expense_repo->categorize($expenses, $request->category_id); + $expenses = collect([]); + } + + $expenses->each(function ($expense) use ($request, $user) { + if ($user->can('edit', $expense)) { + $this->expense_repo->{$request->action}($expense); } }); - return $this->listResponse(Expense::withTrashed()->whereIn('id', $this->transformKeys($ids))); + return $this->listResponse(Expense::withTrashed()->whereIn('id', $request->ids)); } /** diff --git a/app/Http/Controllers/ImportJsonController.php b/app/Http/Controllers/ImportJsonController.php index 309e70066b..4789c0221d 100644 --- a/app/Http/Controllers/ImportJsonController.php +++ b/app/Http/Controllers/ImportJsonController.php @@ -55,6 +55,9 @@ class ImportJsonController extends BaseController */ public function import(ImportJsonRequest $request) { + /** @var \App\Models\User $user */ + $user = auth()->user(); + $file_location = $request->file('files') ->storeAs( 'migrations', @@ -63,9 +66,9 @@ class ImportJsonController extends BaseController ); if (Ninja::isHosted()) { - CompanyImport::dispatch(auth()->user()->getCompany(), auth()->user(), $file_location, $request->except('files'))->onQueue('migration'); + CompanyImport::dispatch($user->company(), $user, $file_location, $request->except('files'))->onQueue('migration'); } else { - CompanyImport::dispatch(auth()->user()->getCompany(), auth()->user(), $file_location, $request->except('files')); + CompanyImport::dispatch($user->company(), $user, $file_location, $request->except('files')); } return response()->json(['message' => 'Processing'], 200); diff --git a/app/Http/Controllers/SubdomainController.php b/app/Http/Controllers/SubdomainController.php index 673bd642fb..910662d618 100644 --- a/app/Http/Controllers/SubdomainController.php +++ b/app/Http/Controllers/SubdomainController.php @@ -27,7 +27,7 @@ class SubdomainController extends BaseController public function index() { if (!MultiDB::checkDomainAvailable(request()->input('subdomain'))) { - return response()->json(['message' => 'Domain not available'], 401); + return response()->json(['message' => ctrans('texts.subdomain_is_not_available')], 401); } return response()->json(['message' => 'Domain available'], 200); diff --git a/app/Http/Requests/Expense/BulkExpenseRequest.php b/app/Http/Requests/Expense/BulkExpenseRequest.php index 2024ed39ed..e30ed90911 100644 --- a/app/Http/Requests/Expense/BulkExpenseRequest.php +++ b/app/Http/Requests/Expense/BulkExpenseRequest.php @@ -12,13 +12,10 @@ namespace App\Http\Requests\Expense; use App\Http\Requests\Request; -use App\Models\Expense; -use App\Utils\Traits\BulkOptions; +use Illuminate\Validation\Rule; class BulkExpenseRequest extends Request { - use BulkOptions; - /** * Determine if the user is authorized to make this request. * @@ -26,15 +23,7 @@ class BulkExpenseRequest extends Request */ public function authorize() { - if (! $this->has('action')) { - return false; - } - - if (! in_array($this->action, $this->getBulkOptions(), true)) { - return false; - } - - return auth()->user()->can(auth()->user()->isAdmin(), Expense::class); + return true; } /** @@ -44,13 +33,30 @@ class BulkExpenseRequest extends Request */ public function rules() { - $rules = $this->getGlobalRules(); + /** @var \App\Models\User $user */ + $user = auth()->user(); - /* We don't require IDs on bulk storing. */ - if ($this->action !== self::$STORE_METHOD) { - $rules['ids'] = ['required']; + return [ + 'ids' => ['required','bail','array', Rule::exists('expenses', 'id')->where('company_id', $user->company()->id)], + 'category_id' => ['sometimes', 'bail', Rule::exists('expense_categories', 'id')->where('company_id', $user->company()->id)], + 'action' => 'in:archive,restore,delete,bulk_categorize', + ]; + + + } + + public function prepareForValidation() + { + $input = $this->all(); + + if (isset($input['ids'])) { + $input['ids'] = $this->transformKeys($input['ids']); } - return $rules; + if (isset($input['category_id'])) { + $input['category_id'] = $this->transformKeys($input['category_id']); + } + + $this->replace($input); } } diff --git a/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php b/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php index 1ad00da30c..9ad7e9ec79 100644 --- a/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php +++ b/app/Http/Requests/RecurringInvoice/StoreRecurringInvoiceRequest.php @@ -82,6 +82,8 @@ class StoreRecurringInvoiceRequest extends Request public function prepareForValidation() { $input = $this->all(); + $input['amount'] = 0; + $input['balance'] = 0; if (array_key_exists('due_date_days', $input) && is_null($input['due_date_days'])) { $input['due_date_days'] = 'terms'; diff --git a/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php b/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php index 312e073099..754662331c 100644 --- a/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php +++ b/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php @@ -25,7 +25,10 @@ class StoreSchedulerRequest extends Request */ public function authorize(): bool { - return auth()->user()->isAdmin(); + /** @var \App\Models\User $user */ + $user = auth()->user(); + + return $user->isAdmin(); } public function rules() diff --git a/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php b/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php index 6a7750320f..f4054949fc 100644 --- a/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php +++ b/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php @@ -22,7 +22,10 @@ class UpdateSchedulerRequest extends Request */ public function authorize(): bool { - return auth()->user()->isAdmin() && $this->task_scheduler->company_id == auth()->user()->company()->id; + /** @var \App\Models\User $user */ + $user = auth()->user(); + + return $user->isAdmin() && $this->task_scheduler->company_id == $user->company()->id; } public function rules(): array diff --git a/app/Http/ValidationRules/Scheduler/ValidClientIds.php b/app/Http/ValidationRules/Scheduler/ValidClientIds.php index 64086bc5e9..c5b0f21a7d 100644 --- a/app/Http/ValidationRules/Scheduler/ValidClientIds.php +++ b/app/Http/ValidationRules/Scheduler/ValidClientIds.php @@ -28,7 +28,10 @@ class ValidClientIds implements Rule */ public function passes($attribute, $value) { - return Client::where('company_id', auth()->user()->company()->id)->whereIn('id', $this->transformKeys($value))->count() == count($value); + /** @var \App\Models\User $user */ + $user = auth()->user(); + + return Client::where('company_id', $user->company()->id)->whereIn('id', $this->transformKeys($value))->count() == count($value); } /** diff --git a/app/Jobs/Company/CompanyExport.php b/app/Jobs/Company/CompanyExport.php index b58e20f26a..6805c05a55 100644 --- a/app/Jobs/Company/CompanyExport.php +++ b/app/Jobs/Company/CompanyExport.php @@ -201,7 +201,7 @@ class CompanyExport implements ShouldQueue return $document->makeVisible(['id']); })->all(); - $this->export_data['expense_categories'] = $this->company->expense_categories->map(function ($expense_category) { + $this->export_data['expense_categories'] = $this->company->expense_categories()->cursor()->map(function ($expense_category) { $expense_category = $this->transformArrayOfKeys($expense_category, ['user_id', 'company_id']); return $expense_category->makeVisible(['id']); @@ -288,7 +288,7 @@ class CompanyExport implements ShouldQueue $this->export_data['recurring_expenses'] = $this->company->recurring_expenses()->orderBy('number', 'DESC')->cursor()->map(function ($expense) { $expense = $this->transformBasicEntities($expense); $expense = $this->transformArrayOfKeys($expense, ['vendor_id', 'invoice_id', 'client_id', 'category_id', 'project_id']); - + return $expense->makeVisible(['id']); })->all(); @@ -399,6 +399,12 @@ class CompanyExport implements ShouldQueue return $bank_transaction->makeVisible(['id','user_id','company_id']); })->all(); + $this->export_data['schedulers'] = $this->company->schedulers()->orderBy('id', 'ASC')->cursor()->map(function ($scheduler) { + $scheduler = $this->transformArrayOfKeys($scheduler, ['company_id', 'user_id']); + + return $scheduler->makeVisible(['id','user_id','company_id']); + })->all(); + //write to tmp and email to owner(); $this->zipAndSend(); @@ -440,7 +446,7 @@ class CompanyExport implements ShouldQueue $path = 'backups'; - Storage::makeDirectory(storage_path('backups/')); + // Storage::makeDirectory(storage_path('backups/')); try { mkdir(storage_path('backups/')); diff --git a/app/Jobs/Company/CompanyImport.php b/app/Jobs/Company/CompanyImport.php index c1962bec02..1d0a95a898 100644 --- a/app/Jobs/Company/CompanyImport.php +++ b/app/Jobs/Company/CompanyImport.php @@ -16,6 +16,7 @@ use App\Exceptions\NonExistingMigrationFile; use App\Factory\ClientContactFactory; use App\Jobs\Mail\NinjaMailerJob; use App\Jobs\Mail\NinjaMailerObject; +use App\Jobs\Ninja\TaskScheduler; use App\Libraries\MultiDB; use App\Mail\Import\CompanyImportFailure; use App\Mail\Import\ImportCompleted; @@ -111,6 +112,8 @@ class CompanyImport implements ShouldQueue private $file_path; + private string $import_version = ''; + private $importables = [ // 'company', 'users', @@ -151,6 +154,7 @@ class CompanyImport implements ShouldQueue 'bank_integrations', 'bank_transactions', 'payments', + 'schedulers', ]; private $company_properties = [ @@ -210,6 +214,22 @@ class CompanyImport implements ShouldQueue "convert_rate_to_client", ]; + private array $version_keys = [ + 'baseline' => [], + '5.7.35' => [ + Payment::class => [ + 'refund_meta', + 'category_id', + ], + User::class => [ + 'user_logged_in_notification', + ], + Design::class => [ + 'is_template', + ] + ], + ]; + /** * Create a new job instance. * @@ -407,9 +427,7 @@ class CompanyImport implements ShouldQueue $data = (object)$this->getObject('app_version', true); - if ($this->current_app_version != $data->app_version) { - //perform some magic here - } + $this->import_version = $data->app_version; if ($this->pre_flight_checks_pass === false) { $this->sendImportMail($this->message); @@ -439,10 +457,12 @@ class CompanyImport implements ShouldQueue $settings->project_number_counter = 1; $settings->purchase_order_number_counter = 1; $this->company->settings = $co->settings; - // $this->company->settings = $this->backup_file->company->settings; + + $this->company->saveSettings($co->settings, $this->company); + $this->company->save(); - return $this; + return $this; } private function purgeCompanyData() @@ -456,6 +476,10 @@ class CompanyImport implements ShouldQueue $this->company->expenses()->forceDelete(); $this->company->subscriptions()->forceDelete(); $this->company->purchase_orders()->forceDelete(); + $this->company->bank_integrations()->forceDelete(); + $this->company->bank_transactions()->forceDelete(); + $this->company->schedulers()->forceDelete(); + $this->company->system_log_relation()->forceDelete(); $this->company->save(); @@ -507,6 +531,20 @@ class CompanyImport implements ShouldQueue return $this; } + private function import_schedulers() + { + $this->genericNewClassImport( + \App\Models\Scheduler::class, + ['company_id', 'id', 'hashed_id'], + [ + ['users' => 'user_id'], + ], + 'schedulers' + ); + + return $this; + } + private function import_bank_integrations() { $this->genericImport( @@ -1131,7 +1169,6 @@ class CompanyImport implements ShouldQueue $new_document->documentable_type = $document->documentable_type; $new_document->save(['timestamps' => false]); - $storage_url = (object)$this->getObject('storage_url', true); @@ -1327,7 +1364,36 @@ class CompanyImport implements ShouldQueue } } + private function filterVersionProps($class, array $obj_array): array + { + if($this->current_app_version == $this->import_version) + return $obj_array; + + $version_index = 0; + $index = 0; + + $filters = collect($this->version_keys) + ->map(function ($value, $key) use (&$version_index, &$index) { + if($this->import_version == $key) { + $version_index = $index; + } + + $index++; + return $value; + + }) + ->when($version_index == 0, function ($collection) { + return collect([]); + }) + ->when($version_index > 0, function ($collection) use (&$version_index, $class) { + return $collection->slice($version_index)->pluck($class)->filter(); + }); + + return collect($obj_array)->diffKeys($filters->flatten()->flip())->toArray(); + + } + private function genericNewClassImport($class, $unset, $transforms, $object_property) { $class::unguard(); @@ -1348,6 +1414,10 @@ class CompanyImport implements ShouldQueue if (Ninja::isSelfHost() && $obj_array['gateway_key'] == 'd14dd26a47cecc30fdd65700bfb67b34') { $obj_array['gateway_key'] = 'd14dd26a37cecc30fdd65700bfb55b23'; } + + if(!isset($obj_array['fees_and_limits'])){ + $obj_array['fees_and_limits'] = \json_encode([]); + } } if (array_key_exists('deleted_at', $obj_array) && $obj_array['deleted_at'] > 1) { @@ -1385,8 +1455,29 @@ class CompanyImport implements ShouldQueue $obj_array['config'] = encrypt($obj_array['config']); } + if($class == 'App\Models\Scheduler') { + $parameters = $obj_array['parameters']; + + if(isset($parameters->clients)){ + + $parameters->clients = + collect($parameters->clients)->map(function ($client_hash){ + return $this->encodePrimaryKey($this->transformId('clients', $client_hash)); + })->toArray(); + + } + + if(isset($parameters->entity_id)){ + $parameters->entity_id = $this->encodePrimaryKey($this->transformId($parameters->entity."s", $parameters->entity_id)); + } + + $obj_array['parameters'] = $parameters; + } + $new_obj = new $class(); $new_obj->company_id = $this->company->id; + $obj_array = $this->filterVersionProps($class,$obj_array); + $new_obj->fill($obj_array); $new_obj->save(['timestamps' => false]); @@ -1430,6 +1521,8 @@ class CompanyImport implements ShouldQueue $obj_array['recurring_product_ids'] = $this->recordProductIds($obj_array['recurring_product_ids']); $obj_array['webhook_configuration'] = \json_encode($obj_array['webhook_configuration']); } + + $obj_array = $this->filterVersionProps($class, $obj_array); $new_obj = $class::firstOrNew( [$match_key => $obj->{$match_key}], @@ -1481,6 +1574,8 @@ class CompanyImport implements ShouldQueue $obj_array['recurring_product_ids'] = ''; $obj_array['product_ids'] = ''; } + + $obj_array = $this->filterVersionProps($class, $obj_array); /* Expenses that don't have a number will not be inserted - so need to override here*/ if ($class == 'App\Models\Expense' && is_null($obj->{$match_key})) { diff --git a/app/PaymentDrivers/BaseDriver.php b/app/PaymentDrivers/BaseDriver.php index 9631b3b97c..d1e2eddc6e 100644 --- a/app/PaymentDrivers/BaseDriver.php +++ b/app/PaymentDrivers/BaseDriver.php @@ -743,12 +743,26 @@ class BaseDriver extends AbstractPaymentDriver } $invoices_string = str_replace(["*","<",">","'",'"'], "-", $invoices_string); + // $invoices_string = "I-".$invoices_string; + // $invoices_string = substr($invoices_string, 0, 22); - $invoices_string = "I-".$invoices_string; +// 2023-11-02 - improve the statement descriptor for string +$company_name = $this->client->company->present()->name(); - $invoices_string = substr($invoices_string, 0, 22); - - $invoices_string = str_pad($invoices_string, 5, ctrans('texts.invoice'), STR_PAD_LEFT); +if(ctype_digit(substr($company_name, 0, 1))) + $company_name = "X" . $company_name; + +$suffix = strlen($invoices_string) + 1; + +$length = 22 - $suffix; + +$company_name = substr($company_name, 0, $length); + +$descriptor = "{$company_name} {$invoices_string}"; + +$invoices_string = str_pad($descriptor, 5, ctrans('texts.invoice'), STR_PAD_RIGHT); + + // $invoices_string = str_pad($invoices_string, 5, ctrans('texts.invoice'), STR_PAD_LEFT); return $invoices_string; diff --git a/app/PaymentDrivers/GoCardless/DirectDebit.php b/app/PaymentDrivers/GoCardless/DirectDebit.php index c99ef060d1..0eccde4785 100644 --- a/app/PaymentDrivers/GoCardless/DirectDebit.php +++ b/app/PaymentDrivers/GoCardless/DirectDebit.php @@ -51,34 +51,6 @@ class DirectDebit implements MethodInterface public function authorizeView(array $data) { return $this->billingRequestFlows($data); - // $session_token = \Illuminate\Support\Str::uuid()->toString(); - - // try { - // $redirect = $this->go_cardless->gateway->redirectFlows()->create([ - // 'params' => [ - // 'session_token' => $session_token, - // 'success_redirect_url' => route('client.payment_methods.confirm', [ - // 'method' => GatewayType::DIRECT_DEBIT, - // 'session_token' => $session_token, - // ]), - // 'prefilled_customer' => [ - // 'given_name' => auth()->guard('contact')->user()->first_name ?: '', - // 'family_name' => auth()->guard('contact')->user()->last_name ?: '', - // 'email' => auth()->guard('contact')->user()->email ?: '', - // 'address_line1' => auth()->guard('contact')->user()->client->address1 ?: '', - // 'city' => auth()->guard('contact')->user()->client->city ?: '', - // 'postal_code' => auth()->guard('contact')->user()->client->postal_code ?: '', - // 'country_code' => auth()->guard('contact')->user()->client->country->iso_3166_2, - // ], - // ], - // ]); - - // return redirect( - // $redirect->redirect_url - // ); - // } catch (\Exception $exception) { - // return $this->processUnsuccessfulAuthorization($exception); - // } } /** @@ -109,7 +81,7 @@ class DirectDebit implements MethodInterface "params" => [ "mandate_request" => [ "currency" => auth()->guard('contact')->user()->client->currency()->code, - "verify" => "recommended" + "verify" => "when_available" ] ] ]); diff --git a/app/PaymentDrivers/GoCardlessPaymentDriver.php b/app/PaymentDrivers/GoCardlessPaymentDriver.php index c91cd68d1f..6ab26b96fe 100644 --- a/app/PaymentDrivers/GoCardlessPaymentDriver.php +++ b/app/PaymentDrivers/GoCardlessPaymentDriver.php @@ -404,7 +404,9 @@ class GoCardlessPaymentDriver extends BaseDriver $this->init(); $mandate = $this->gateway->mandates()->get($token); - if ($mandate->status !== 'active') { + if(!in_array($mandate->status, ['pending_submission', 'submitted', 'active','pending_customer_approval'])) { + + // if ($mandate->status !== 'active') { throw new \Exception(ctrans('texts.gocardless_mandate_not_ready')); } } catch (\Exception $exception) { diff --git a/app/Repositories/BaseRepository.php b/app/Repositories/BaseRepository.php index 68ccb310cf..2fc13b4ff3 100644 --- a/app/Repositories/BaseRepository.php +++ b/app/Repositories/BaseRepository.php @@ -199,7 +199,7 @@ class BaseRepository }); } } - +nlog($model->toArray()); $model->saveQuietly(); /* Model now persisted, now lets do some child tasks */ diff --git a/app/Repositories/ExpenseRepository.php b/app/Repositories/ExpenseRepository.php index 04ac88e97b..d26e5f2965 100644 --- a/app/Repositories/ExpenseRepository.php +++ b/app/Repositories/ExpenseRepository.php @@ -12,13 +12,15 @@ namespace App\Repositories; -use App\Factory\ExpenseFactory; -use App\Libraries\Currency\Conversion\CurrencyApi; use App\Models\Expense; -use App\Utils\Traits\GeneratesCounter; -use Carbon\Exceptions\InvalidFormatException; -use Illuminate\Database\QueryException; use Illuminate\Support\Carbon; +use App\Factory\ExpenseFactory; +use App\Models\ExpenseCategory; +use App\Utils\Traits\GeneratesCounter; +use Illuminate\Database\QueryException; +use Carbon\Exceptions\InvalidFormatException; +use App\Libraries\Currency\Conversion\CurrencyApi; +use Illuminate\Database\Eloquent\Collection; /** * ExpenseRepository. @@ -158,4 +160,25 @@ class ExpenseRepository extends BaseRepository return $expense; } + + /** + * Categorize Expenses in bulk + * + * @param Collection $expenses + * @param int $category_id + * @return void + */ + public function categorize(Collection $expenses, int $category_id): void + { + $ec = ExpenseCategory::withTrashed()->find($category_id); + + $expenses->when($ec) + ->each(function ($expense) use($ec){ + + $expense->category_id = $ec->id; + $expense->save(); + + }); + } + } diff --git a/app/Services/Invoice/EInvoice/FacturaEInvoice.php b/app/Services/Invoice/EInvoice/FacturaEInvoice.php index 935d3ab638..36e76b0311 100644 --- a/app/Services/Invoice/EInvoice/FacturaEInvoice.php +++ b/app/Services/Invoice/EInvoice/FacturaEInvoice.php @@ -175,16 +175,14 @@ class FacturaEInvoice extends AbstractService ->setBillingPeriod() ->signDocument(); - $disk = config('filesystems.default'); + // $disk = config('filesystems.default'); - if (!Storage::disk($disk)->exists($this->invoice->client->e_invoice_filepath($this->invoice->invitations->first()))) { - Storage::makeDirectory($this->invoice->client->e_invoice_filepath($this->invoice->invitations->first())); - } + // if (!Storage::disk($disk)->exists($this->invoice->client->e_invoice_filepath($this->invoice->invitations->first()))) { + // Storage::makeDirectory($this->invoice->client->e_invoice_filepath($this->invoice->invitations->first())); + // } - // $this->fac->export(Storage::disk($disk)->path($this->invoice->client->e_invoice_filepath($this->invoice->invitations->first()) . $this->invoice->getFileName("xsig"))); return $this->fac->export(); - // return $this->invoice->client->e_invoice_filepath($this->invoice->invitations->first()) . $this->invoice->getFileName("xsig"); - + } /** Check if this is a public administration body */ @@ -546,8 +544,8 @@ class FacturaEInvoice extends AbstractService "isLegalEntity" => $this->invoice->client->classification === 'individual' ? false : true, "taxNumber" => $this->invoice->client->vat_number, "name" => substr($this->invoice->client->present()->name(), 0, 40), - "firstSurname" => substr($this->invoice->client->present()->first_name(), 0, 40), - "lastSurname" => substr($this->invoice->client->present()->last_name(), 0, 40), + // "firstSurname" => substr($this->invoice->client->present()->last_name(), 0, 40), + // "lastSurname" => substr($this->invoice->client->present()->last_name(), 0, 40), "address" => substr($this->invoice->client->address1, 0, 80), "postCode" => substr($this->invoice->client->postal_code, 0, 5), "town" => substr($this->invoice->client->city, 0, 50), @@ -563,6 +561,11 @@ class FacturaEInvoice extends AbstractService // "ineTownCode" => "280796" // Cód. de municipio del INE ]); + if($this->invoice->client->classification === 'individual') { + $buyer['name'] = $this->invoice->client->present()->first_name(); + $buyer['firstSurname'] = $this->invoice->client->present()->last_name(); + } + $this->fac->setBuyer($buyer); return $this; diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index 78fdf1cc27..e1947d9dbd 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -442,7 +442,6 @@ class InvoiceService $this->invoice->line_items = array_values($items); $this->invoice = $this->invoice->calc()->getInvoice(); - $this->deleteEInvoice(); //@deprecated /* 24-03-2022 */ $new_balance = $this->invoice->balance; diff --git a/app/Services/Template/TemplateService.php b/app/Services/Template/TemplateService.php index 6376533001..c5b783b513 100644 --- a/app/Services/Template/TemplateService.php +++ b/app/Services/Template/TemplateService.php @@ -522,7 +522,7 @@ class TemplateService 'balance_raw' => ($payment->amount - $payment->refunded - $payment->applied), 'date' => $this->translateDate($payment->date, $payment->client->date_format(), $payment->client->locale()), 'method' => $payment->translatedType(), - 'currency' => $payment->currency->code, + 'currency' => $payment->currency->code ?? $payment->company->currency()->code, 'exchange_rate' => $payment->exchange_rate, 'transaction_reference' => $payment->transaction_reference, 'is_manual' => $payment->is_manual, diff --git a/config/ninja.php b/config/ninja.php index bdfb27ba16..c07d03073f 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -15,11 +15,11 @@ return [ 'require_https' => env('REQUIRE_HTTPS', true), 'app_url' => rtrim(env('APP_URL', ''), '/'), 'app_domain' => env('APP_DOMAIN', 'invoicing.co'), - 'app_version' => env('APP_VERSION','5.7.42'), - 'app_tag' => env('APP_TAG','5.7.42'), + 'app_version' => env('APP_VERSION','5.7.43'), + 'app_tag' => env('APP_TAG','5.7.43'), 'minimum_client_version' => '5.0.16', 'terms_version' => '1.0.1', - 'api_secret' => env('API_SECRET', ''), + 'api_secret' => env('API_SECRET', false), 'google_maps_api_key' => env('GOOGLE_MAPS_API_KEY'), 'google_analytics_url' => env('GOOGLE_ANALYTICS_URL', 'https://www.google-analytics.com/collect'), 'key_length' => 32, diff --git a/database/factories/BankTransactionFactory.php b/database/factories/BankTransactionFactory.php index e47a822100..ce6b38006f 100644 --- a/database/factories/BankTransactionFactory.php +++ b/database/factories/BankTransactionFactory.php @@ -27,7 +27,7 @@ class BankTransactionFactory extends Factory 'amount' => $this->faker->randomFloat(2, 10, 10000) , 'currency_id' => '1', 'account_type' => 'creditCard', - 'category_id' => 1, + 'category_id' => null, 'category_type' => 'Random' , 'date' => $this->faker->date('Y-m-d') , 'bank_account_id' => 1 , diff --git a/tests/Feature/ExpenseApiTest.php b/tests/Feature/ExpenseApiTest.php index ef6fadf3d0..5a8b8d7b79 100644 --- a/tests/Feature/ExpenseApiTest.php +++ b/tests/Feature/ExpenseApiTest.php @@ -11,11 +11,12 @@ namespace Tests\Feature; -use App\Models\BankIntegration; -use App\Models\BankTransaction; use Tests\TestCase; use App\Models\Expense; use Tests\MockAccountData; +use App\Models\BankIntegration; +use App\Models\BankTransaction; +use App\Models\ExpenseCategory; use App\Utils\Traits\MakesHash; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Session; @@ -232,6 +233,42 @@ class ExpenseApiTest extends TestCase $this->assertTrue($arr['data'][0]['is_deleted']); } + public function testExpenseBulkCategorize() + { + + + $e = Expense::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + ]); + + + $ec = ExpenseCategory::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'name' => 'Test Category', + ]); + + nlog("expense category id = {$ec->hashed_id}"); + + $data = [ + 'category_id' => $ec->hashed_id, + 'action' => 'bulk_categorize', + 'ids' => [$this->encodePrimaryKey($e->id)], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/expenses/bulk', $data); + + $arr = $response->json(); + nlog($arr); + + $this->assertEquals($ec->hashed_id, $arr['data'][0]['category_id']); + } + + public function testAddingExpense() { $data = [ diff --git a/tests/Feature/UserTest.php b/tests/Feature/UserTest.php index e75ee11ac7..b030e4b3bf 100644 --- a/tests/Feature/UserTest.php +++ b/tests/Feature/UserTest.php @@ -195,7 +195,7 @@ class UserTest extends TestCase 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $company_token->token, 'X-API-PASSWORD' => 'ALongAndBriliantPassword', - ])->get("/api/v1/users?without={$company_token->user->hashed_id}&status=active"); + ])->get("/api/v1/users?status=active&without={$company_token->user->hashed_id}"); $response->assertStatus(200); $this->assertCount(0, $response->json()['data']); @@ -204,7 +204,7 @@ class UserTest extends TestCase 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $company_token->token, 'X-API-PASSWORD' => 'ALongAndBriliantPassword', - ])->get("/api/v1/users?without={$company_token->user->hashed_id}&status=archived"); + ])->get("/api/v1/users?status=archived&without={$company_token->user->hashed_id}"); $response->assertStatus(200); $this->assertCount(1, $response->json()['data']); @@ -213,7 +213,7 @@ class UserTest extends TestCase 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $company_token->token, 'X-API-PASSWORD' => 'ALongAndBriliantPassword', - ])->get("/api/v1/users?without={$company_token->user->hashed_id}&status=deleted"); + ])->get("/api/v1/users?status=deleted&without={$company_token->user->hashed_id}"); $response->assertStatus(200); $this->assertCount(0, $response->json()['data']); diff --git a/tests/Unit/ArrayFiltersTest.php b/tests/Unit/ArrayFiltersTest.php new file mode 100644 index 0000000000..5351ad1c09 --- /dev/null +++ b/tests/Unit/ArrayFiltersTest.php @@ -0,0 +1,242 @@ +[], + '5.7.34' => [ + Payment::class => [ + 'is_deleted', + 'amount', + ] + ], + '5.7.35' => [ + Payment::class => [ + 'date', + 'transaction_reference', + ], + User::class => [ + 'user_logged_in_notification', + 'first_name', + 'last_name', + ], + Design::class => [ + 'is_template', + ] + ], + '5.7.36' => [ + Payment::class => [ + 'type_id', + 'status_id', + ], + ], + '5.7.37' => [ + Payment::class => [ + 'currency_id', + 'hashed_id', + ], + ], + ]; + + protected function setUp() :void + { + parent::setUp(); + } + + public function testPaymentFilterFactory() + { + $p = Payment::factory()->make()->toArray(); + + $this->assertIsArray($p); + } + + public function testPaymentUnsetProps() + { + $p = Payment::factory()->make()->toArray(); + + $version = '5.7.36'; + $current_version = config('ninja.app_version'); + + $this->assertNotEquals($current_version, $version); + + $index = 0; + $version_index = 0; + + foreach($this->version_keys as $key => $value) + { + if($version == $key) + { + $version_index = $index; + } + + $index++; + } + + $this->assertEquals(3, $version_index); + + $filters = collect($this->version_keys)->slice($version_index); + + $this->assertEquals(2, $filters->count()); + + $x = collect($p)->diffKeys($filters->flatten()->flip()); + + $this->assertEquals(4, $x->count()); + } + + public function testPaymentUnsetPropsScenario2() + { + $p = Payment::factory()->make()->toArray(); + + $version = '5.7.35'; + $current_version = config('ninja.app_version'); + + $this->assertNotEquals($current_version, $version); + + $index = 0; + $version_index = 0; + + foreach($this->version_keys as $key => $value) + { + if($version == $key) + { + $version_index = $index; + } + + $index++; + } + + $this->assertEquals(2, $version_index); + + $index = 0; + $version_index = 0; + + $filters = collect($this->version_keys) + ->map(function ($value, $key) use ($version, &$version_index, &$index) { + if($version == $key) + $version_index = $index; + + $index++; + return $value; + + }) + ->slice($version_index) + ->pluck(Payment::class); + + $this->assertEquals(3, $filters->count()); + + $x = collect($p)->diffKeys($filters->flatten()->flip()); + + $this->assertEquals(2, $x->count()); + } + + public function testWhenScenario() + { + $p = Payment::factory()->make()->toArray(); + + $version = '5.7.35'; + $current_version = '5.7.35'; + + $filters = collect($this->version_keys) + ->map(function ($value, $key) use ($version, &$version_index, &$index) { + if($version == $key) + $version_index = $index; + + $index++; + return $value; + + }) + ->slice($version_index) + ->pluck(Payment::class); + + $this->assertEquals(3, $filters->count()); + } + + public function testWhenScenario2() + { + $p = Payment::factory()->make()->toArray(); + + $version = '5.7.33'; + $current_version = '5.7.35'; + + $filters = collect($this->version_keys) + ->map(function ($value, $key) use ($version, &$version_index, &$index) { + if($version == $key) { + $version_index = $index; + nlog("version = {$version_index}"); + } + $index++; + return $value; + + }) + ->slice($version_index ?? 0) + ->pluck(Payment::class); + + $x = collect($p)->diffKeys($filters->filter()->flatten()->flip()); + + $this->assertEquals(5, $filters->count()); + } + + + private function filterArray($class, array $obj_array) + { + $index = 0; + $version_index = 0; + + $filters = collect($this->version_keys) + ->map(function ($value, $key) use (&$version_index, &$index) { + if($this->import_version == $key) { + $version_index = $index; + } + + $index++; + return $value; + + }) + ->when($version_index == 0, function ($collection){ + return collect([]); + }) + ->when($version_index > 0, function ($collection) use (&$version_index, $class) { + return $collection->slice($version_index)->pluck($class)->filter(); + }); + + return collect($obj_array)->diffKeys($filters->flatten()->flip())->toArray(); + + // return $filters->count() > 0 ? collect($obj_array)->diffKeys($filters->flatten()->flip())->toArray() : $obj_array; + + } + + public function testFilterArrayOne() + { + $u = User::factory()->make()->toArray(); + + $prop_count = count($u); + + $this->import_version = '5.7.42'; + + $filtered_u = $this->filterArray(User::class, $u); + + $this->assertCount($prop_count, $filtered_u); + } + +}