diff --git a/app/DataMapper/Analytics/AccountCreated.php b/app/DataMapper/Analytics/AccountCreated.php new file mode 100644 index 0000000000..67a50c68d4 --- /dev/null +++ b/app/DataMapper/Analytics/AccountCreated.php @@ -0,0 +1,49 @@ +company_id = $company_id; + $project->user_id = $user_id; + + $project->public_notes = ''; + $project->private_notes = ''; + $project->budgeted_hours = 0; + $project->task_rate = 0; + $project->name = ''; + $project->custom_value1 = ''; + $project->custom_value2 = ''; + $project->custom_value3 = ''; + $project->custom_value4 = ''; + $project->is_deleted = 0; + + return $project; + } +} diff --git a/app/Factory/RecurringInvoiceToInvoiceFactory.php b/app/Factory/RecurringInvoiceToInvoiceFactory.php index cafa7ee385..12c3227058 100644 --- a/app/Factory/RecurringInvoiceToInvoiceFactory.php +++ b/app/Factory/RecurringInvoiceToInvoiceFactory.php @@ -30,7 +30,7 @@ class RecurringInvoiceToInvoiceFactory $invoice->terms = $recurring_invoice->terms; $invoice->public_notes = $recurring_invoice->public_notes; $invoice->private_notes = $recurring_invoice->private_notes; - $invoice->date = date_create()->format($client->date_format()); + //$invoice->date = now()->format($client->date_format()); $invoice->due_date = $recurring_invoice->calculateDueDate($recurring_invoice->next_send_date); $invoice->is_deleted = $recurring_invoice->is_deleted; $invoice->line_items = $recurring_invoice->line_items; diff --git a/app/Filters/ProjectFilters.php b/app/Filters/ProjectFilters.php new file mode 100644 index 0000000000..a552904037 --- /dev/null +++ b/app/Filters/ProjectFilters.php @@ -0,0 +1,145 @@ +builder; + } + + return $this->builder->where(function ($query) use ($filter) { + $query->where('projects.name', 'like', '%'.$filter.'%') + ->orWhere('projects.public_notes', 'like', '%'.$filter.'%') + ->orWhere('projects.private_notes', 'like', '%'.$filter.'%'); + }); + } + + /** + * Filters the list based on the status + * archived, active, deleted. + * + * @param string filter + * @return Illuminate\Database\Query\Builder + */ + public function status(string $filter = '') : Builder + { + if (strlen($filter) == 0) { + return $this->builder; + } + + $table = 'projects'; + $filters = explode(',', $filter); + + return $this->builder->where(function ($query) use ($filters, $table) { + $query->whereNull($table.'.id'); + + if (in_array(parent::STATUS_ACTIVE, $filters)) { + $query->orWhereNull($table.'.deleted_at'); + } + + if (in_array(parent::STATUS_ARCHIVED, $filters)) { + $query->orWhere(function ($query) use ($table) { + $query->whereNotNull($table.'.deleted_at'); + + if (! in_array($table, ['users'])) { + $query->where($table.'.is_deleted', '=', 0); + } + }); + } + + if (in_array(parent::STATUS_DELETED, $filters)) { + $query->orWhere($table.'.is_deleted', '=', 1); + } + }); + } + + /** + * Sorts the list based on $sort. + * + * @param string sort formatted as column|asc + * @return Illuminate\Database\Query\Builder + */ + public function sort(string $sort) : Builder + { + $sort_col = explode('|', $sort); + + return $this->builder->orderBy($sort_col[0], $sort_col[1]); + } + + /** + * Returns the base query. + * + * @param int company_id + * @return Illuminate\Database\Query\Builder + * @deprecated + */ + public function baseQuery(int $company_id, User $user) : Builder + { + $query = DB::table('projects') + ->join('companies', 'companies.id', '=', 'projects.company_id') + ->where('projects.company_id', '=', $company_id) + //->whereRaw('(projects.name != "" or contacts.first_name != "" or contacts.last_name != "" or contacts.email != "")') // filter out buy now invoices + ->select( + 'projects.id', + 'projects.name', + 'projects.public_notes', + 'projects.private_notes', + 'projects.created_at', + 'projects.created_at as project_created_at', + 'projects.deleted_at', + 'projects.is_deleted', + 'projects.user_id', + 'projects.assigned_user_id', + ); + + /* + * If the user does not have permissions to view all invoices + * limit the user to only the invoices they have created + */ + if (Gate::denies('view-list', Project::class)) { + $query->where('projects.user_id', '=', $user->id); + } + + return $query; + } + + /** + * Filters the query by the users company ID. + * + * @param $company_id The company Id + * @return Illuminate\Database\Query\Builder + */ + public function entityFilter() + { + //return $this->builder->whereCompanyId(auth()->user()->company()->id); + return $this->builder->whereCompanyId(auth()->user()->company()->id)->orWhere('company_id', null); + } +} diff --git a/app/Helpers/Invoice/InvoiceItemSum.php b/app/Helpers/Invoice/InvoiceItemSum.php index eaecb3338c..ee66fddc67 100644 --- a/app/Helpers/Invoice/InvoiceItemSum.php +++ b/app/Helpers/Invoice/InvoiceItemSum.php @@ -230,7 +230,10 @@ class InvoiceItemSum continue; } - $amount = $this->item->line_total - ($this->item->line_total * ($this->invoice->discount / $this->sub_total)); + //$amount = $this->item->line_total - ($this->item->line_total * ($this->invoice->discount / $this->sub_total)); + $amount = ( $this->sub_total > 0 ) ? $this->item->line_total - ($this->item->line_total * ($this->invoice->discount / $this->sub_total)) : 0; + + $item_tax_rate1_total = $this->calcAmountLineTax($this->item->tax_rate1, $amount); $item_tax += $item_tax_rate1_total; @@ -260,7 +263,8 @@ class InvoiceItemSum } /** - * Sets default values for the line_items. + * Sets default casts for the values in the line_items. + * * @return $this */ private function cleanLineItem() diff --git a/app/Http/Controllers/CompanyController.php b/app/Http/Controllers/CompanyController.php index 70a143e333..a2cad28cc1 100644 --- a/app/Http/Controllers/CompanyController.php +++ b/app/Http/Controllers/CompanyController.php @@ -11,6 +11,7 @@ namespace App\Http\Controllers; +use App\DataMapper\Analytics\AccountDeleted; use App\DataMapper\CompanySettings; use App\DataMapper\DefaultSettings; use App\Http\Requests\Company\CreateCompanyRequest; @@ -40,6 +41,7 @@ use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; +use Turbo124\Beacon\Facades\LightLogs; /** * Class CompanyController. @@ -471,6 +473,11 @@ class CompanyController extends BaseController } $account->delete(); + + LightLogs::create(new AccountDeleted()) + ->increment() + ->batch(); + } else { $company_id = $company->id; $company->delete(); diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php new file mode 100644 index 0000000000..18dca5935c --- /dev/null +++ b/app/Http/Controllers/ProjectController.php @@ -0,0 +1,485 @@ +project_repo = $project_repo; + } + + /** + * @OA\Get( + * path="/api/v1/projects", + * operationId="getProjects", + * tags={"projects"}, + * summary="Gets a list of projects", + * description="Lists projects", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter(ref="#/components/parameters/index"), + * @OA\Response( + * response=200, + * description="A list of projects", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/Project"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function index(ProjectFilters $filters) + { + $projects = Project::filter($filters); + + return $this->listResponse($projects); + } + + /** + * Display the specified resource. + * + * @param int $id + * @return \Illuminate\Http\Response + * + * + * @OA\Get( + * path="/api/v1/projects/{id}", + * operationId="showProject", + * tags={"projects"}, + * summary="Shows a project", + * description="Displays a project by id", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter( + * name="id", + * in="path", + * description="The Project Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the expense object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/Project"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function show(ShowProjectRequest $request, Project $project) + { + return $this->itemResponse($project); + } + + /** + * Show the form for editing the specified resource. + * + * @param int $id + * @return \Illuminate\Http\Response + * + * + * @OA\Get( + * path="/api/v1/projects/{id}/edit", + * operationId="editProject", + * tags={"projects"}, + * summary="Shows a project for editting", + * description="Displays a project by id", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter( + * name="id", + * in="path", + * description="The Project Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the project object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/Project"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function edit(EditProjectRequest $request, Project $project) + { + return $this->itemResponse($project); + } + + /** + * Update the specified resource in storage. + * + * @param \Illuminate\Http\Request $request + * @param App\Models\Project $project + * @return \Illuminate\Http\Response + * + * + * + * @OA\Put( + * path="/api/v1/projects/{id}", + * operationId="updateProject", + * tags={"projects"}, + * summary="Updates a project", + * description="Handles the updating of a project by id", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter( + * name="id", + * in="path", + * description="The Project Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the project object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/Project"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function update(UpdateProjectRequest $request, Project $project) + { + if ($request->entityIsDeleted($project)) { + return $request->disallowUpdate(); + } + + $project->fill($request->all()); + $project->save(); + + return $this->itemResponse($project->fresh()); + } + + /** + * Show the form for creating a new resource. + * + * @return \Illuminate\Http\Response + * + * + * + * @OA\Get( + * path="/api/v1/projects/create", + * operationId="getProjectsCreate", + * tags={"projects"}, + * summary="Gets a new blank project object", + * description="Returns a blank object with default values", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Response( + * response=200, + * description="A blank project object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/Project"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function create(CreateProjectRequest $request) + { + $project = ProjectFactory::create(auth()->user()->company()->id, auth()->user()->id); + + return $this->itemResponse($project); + } + + /** + * Store a newly created resource in storage. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\Response + * + * + * + * @OA\Post( + * path="/api/v1/projects", + * operationId="storeProject", + * tags={"projects"}, + * summary="Adds a project", + * description="Adds an project to a company", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Response( + * response=200, + * description="Returns the saved project object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/Project"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function store(StoreProjectRequest $request) + { + $project = ProjectFactory::create(auth()->user()->company()->id, auth()->user()->id); + $project->fill($request->all()); + $project->save(); + + return $this->itemResponse($project->fresh()); + } + + /** + * Remove the specified resource from storage. + * + * @param int $id + * @return \Illuminate\Http\Response + * + * + * @OA\Delete( + * path="/api/v1/projects/{id}", + * operationId="deleteProject", + * tags={"projects"}, + * summary="Deletes a project", + * description="Handles the deletion of a project by id", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter( + * name="id", + * in="path", + * description="The Project Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns a HTTP status", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function destroy(DestroyProjectRequest $request, Project $project) + { + //may not need these destroy routes as we are using actions to 'archive/delete' + $project->is_deleted = true; + $project->delete(); + $project->save(); + + return response()->json([], 200); + } + + /** + * Perform bulk actions on the list view. + * + * @param BulkProjectRequest $request + * @return \Illuminate\Http\Response + * + * + * @OA\Post( + * path="/api/v1/projects/bulk", + * operationId="bulkProjects", + * tags={"projects"}, + * summary="Performs bulk actions on an array of projects", + * description="", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/index"), + * @OA\RequestBody( + * description="User credentials", + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema( + * type="array", + * @OA\Items( + * type="integer", + * description="Array of hashed IDs to be bulk 'actioned", + * example="[0,1,2,3]", + * ), + * ) + * ) + * ), + * @OA\Response( + * response=200, + * description="The Project User response", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/Project"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function bulk() + { + $action = request()->input('action'); + + $ids = request()->input('ids'); + + $projects = Project::withTrashed()->find($this->transformKeys($ids)); + + $projects->each(function ($project, $key) use ($action) { + if (auth()->user()->can('edit', $project)) { + $this->project_repo->{$action}($project); + } + }); + + return $this->listResponse(Project::withTrashed()->whereIn('id', $this->transformKeys($ids))); + } +} diff --git a/app/Http/Requests/Client/BulkClientRequest.php b/app/Http/Requests/Client/BulkClientRequest.php deleted file mode 100644 index 7ebe320878..0000000000 --- a/app/Http/Requests/Client/BulkClientRequest.php +++ /dev/null @@ -1,47 +0,0 @@ -has('action')) { - return false; - } - - if (! in_array($this->action, $this->getBulkOptions(), true)) { - return false; - } - - return auth()->user()->can(auth()->user()->isAdmin(), Client::class); - } - - /** - * Get the validation rules that apply to the request. - * - * @return array - */ - public function rules() - { - $rules = $this->getGlobalRules(); - - /* We don't require IDs on bulk storing. */ - if ($this->action !== self::$STORE_METHOD) { - $rules['ids'] = ['required']; - } - - return $rules; - } -} diff --git a/app/Http/Requests/Project/CreateProjectRequest.php b/app/Http/Requests/Project/CreateProjectRequest.php new file mode 100644 index 0000000000..074c617c19 --- /dev/null +++ b/app/Http/Requests/Project/CreateProjectRequest.php @@ -0,0 +1,28 @@ +user()->isAdmin(); + } +} diff --git a/app/Http/Requests/Project/DestroyProjectRequest.php b/app/Http/Requests/Project/DestroyProjectRequest.php new file mode 100644 index 0000000000..85148dd874 --- /dev/null +++ b/app/Http/Requests/Project/DestroyProjectRequest.php @@ -0,0 +1,27 @@ +user()->isAdmin(); + } +} diff --git a/app/Http/Requests/Project/EditProjectRequest.php b/app/Http/Requests/Project/EditProjectRequest.php new file mode 100644 index 0000000000..8a0aca71c6 --- /dev/null +++ b/app/Http/Requests/Project/EditProjectRequest.php @@ -0,0 +1,39 @@ +user()->isAdmin(); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + // + ]; + } +} diff --git a/app/Http/Requests/Project/ShowProjectRequest.php b/app/Http/Requests/Project/ShowProjectRequest.php new file mode 100644 index 0000000000..7fdf5124fa --- /dev/null +++ b/app/Http/Requests/Project/ShowProjectRequest.php @@ -0,0 +1,39 @@ +user()->isAdmin(); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return [ + // + ]; + } +} diff --git a/app/Http/Requests/Project/StoreProjectRequest.php b/app/Http/Requests/Project/StoreProjectRequest.php new file mode 100644 index 0000000000..f17afb0737 --- /dev/null +++ b/app/Http/Requests/Project/StoreProjectRequest.php @@ -0,0 +1,53 @@ +user()->can('create', Project::class); + } + + public function rules() + { + $rules = []; + + $rules['name'] ='required|unique:projects,name,null,null,company_id,'.auth()->user()->companyId(); + $rules['client_id'] = 'required|exists:clients,id,company_id,'.auth()->user()->company()->id; + + return $rules; + } + + protected function prepareForValidation() + { + $input = $this->all(); + + if (array_key_exists('client_id', $input) && is_string($input['client_id'])) { + $input['client_id'] = $this->decodePrimaryKey($input['client_id']); + } + + + $this->replace($input); + } +} diff --git a/app/Http/Requests/Project/UpdateProjectRequest.php b/app/Http/Requests/Project/UpdateProjectRequest.php new file mode 100644 index 0000000000..73b1c642e1 --- /dev/null +++ b/app/Http/Requests/Project/UpdateProjectRequest.php @@ -0,0 +1,43 @@ +user()->can('edit', $this->project); + } + + public function rules() + { + return []; + } + + protected function prepareForValidation() + { + $input = $this->all(); + + $this->replace($input); + } +} diff --git a/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php b/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php index 5b1e9111f3..7a6cbfbe65 100644 --- a/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php +++ b/app/Http/Requests/RecurringInvoice/UpdateRecurringInvoiceRequest.php @@ -54,7 +54,7 @@ class UpdateRecurringInvoiceRequest extends Request protected function prepareForValidation() { $input = $this->all(); - info($input); + if (array_key_exists('design_id', $input) && is_string($input['design_id'])) { $input['design_id'] = $this->decodePrimaryKey($input['design_id']); } @@ -91,16 +91,27 @@ class UpdateRecurringInvoiceRequest extends Request $this->replace($input); } - private function setAutoBillFlag($auto_bill) + /** + * if($auto_bill == '') + * off / optin / optout will reset the status of this field to off to allow + * the client to choose whether to auto_bill or not. + * + * @param enum $auto_bill off/always/optin/optout + * + * @return bool + */ + private function setAutoBillFlag($auto_bill) :bool { + if($auto_bill == 'always') return true; - if($auto_bill == 'off') - return false; - - //todo do we need to handle optin / optout here? + // if($auto_bill == '') + // off / optin / optout will reset the status of this field to off to allow + // the client to choose whether to auto_bill or not. + return false; + } } diff --git a/app/Http/ValidationRules/Invoice/UniqueInvoiceNumberRule.php b/app/Http/ValidationRules/Invoice/UniqueInvoiceNumberRule.php index 15523687bc..f8156b7a97 100644 --- a/app/Http/ValidationRules/Invoice/UniqueInvoiceNumberRule.php +++ b/app/Http/ValidationRules/Invoice/UniqueInvoiceNumberRule.php @@ -55,6 +55,9 @@ class UniqueInvoiceNumberRule implements Rule */ private function checkIfInvoiceNumberUnique() : bool { + if(empty($this->input['number'])) + return true; + $invoice = Invoice::where('client_id', $this->input['client_id']) ->where('number', $this->input['number']) ->withTrashed() diff --git a/app/Jobs/Account/CreateAccount.php b/app/Jobs/Account/CreateAccount.php index 794a275307..70b9d1be08 100644 --- a/app/Jobs/Account/CreateAccount.php +++ b/app/Jobs/Account/CreateAccount.php @@ -1,7 +1,17 @@ notification(new NewAccountCreated($spaa9f78, $sp035a66))->ninja(); + LightLogs::create(new AnalyticsAccountCreated()) + ->increment() + ->batch(); + return $sp794f3f; } } diff --git a/app/Jobs/Invoice/EmailInvoice.php b/app/Jobs/Invoice/EmailInvoice.php index 3e8180d068..4f84dd975a 100644 --- a/app/Jobs/Invoice/EmailInvoice.php +++ b/app/Jobs/Invoice/EmailInvoice.php @@ -11,6 +11,7 @@ namespace App\Jobs\Invoice; +use App\DataMapper\Analytics\EmailInvoiceFailure; use App\Events\Invoice\InvoiceWasEmailed; use App\Events\Invoice\InvoiceWasEmailedAndFailed; use App\Helpers\Email\InvoiceEmail; @@ -30,6 +31,7 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Mail; use Symfony\Component\Mime\Test\Constraint\EmailTextBodyContains; +use Turbo124\Beacon\Facades\LightLogs; /*Multi Mailer implemented*/ @@ -95,4 +97,17 @@ class EmailInvoice extends BaseMailerJob implements ShouldQueue /* Mark invoice sent */ $this->invoice_invitation->invoice->service()->markSent()->save(); } + + public function failed($exception = null) + { + info('the job failed'); + + $job_failure = new EmailInvoiceFailure(); + $job_failure->string_metric5 = get_class($this); + $job_failure->string_metric6 = $exception->getMessage(); + + LightLogs::create($job_failure) + ->batch(); + + } } diff --git a/app/Jobs/RecurringInvoice/SendRecurring.php b/app/Jobs/RecurringInvoice/SendRecurring.php index cb2d10f34a..ccbdc7ae94 100644 --- a/app/Jobs/RecurringInvoice/SendRecurring.php +++ b/app/Jobs/RecurringInvoice/SendRecurring.php @@ -11,6 +11,7 @@ namespace App\Jobs\RecurringInvoice; +use App\DataMapper\Analytics\SendRecurringFailure; use App\Events\Invoice\InvoiceWasEmailed; use App\Factory\RecurringInvoiceToInvoiceFactory; use App\Helpers\Email\InvoiceEmail; @@ -26,6 +27,7 @@ use Illuminate\Http\Request; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Carbon; +use Turbo124\Beacon\Facades\LightLogs; class SendRecurring implements ShouldQueue { @@ -58,7 +60,9 @@ class SendRecurring implements ShouldQueue // Generate Standard Invoice $invoice = RecurringInvoiceToInvoiceFactory::create($this->recurring_invoice, $this->recurring_invoice->client); - + + $invoice->date = now()->format('Y-m-d'); + $invoice = $invoice->service() ->markSent() ->applyNumber() @@ -71,9 +75,10 @@ class SendRecurring implements ShouldQueue $email_builder = (new InvoiceEmail())->build($invitation); - EmailInvoice::dispatch($email_builder, $invitation, $invoice->company); - - info("Firing email for invoice {$invoice->number}"); + if($invitation->contact && strlen($invitation->contact->email) >=1){ + EmailInvoice::dispatch($email_builder, $invitation, $invoice->company); + info("Firing email for invoice {$invoice->number}"); + } }); @@ -101,4 +106,18 @@ class SendRecurring implements ShouldQueue } + public function failed($exception = null) + { + info('the job failed'); + + $job_failure = new SendRecurringFailure(); + $job_failure->string_metric5 = get_class($this); + $job_failure->string_metric6 = $exception->getMessage(); + + LightLogs::create($job_failure) + ->batch(); + + info(print_r($exception->getMessage(), 1)); + } + } diff --git a/app/Jobs/Util/Import.php b/app/Jobs/Util/Import.php index 10c7911a70..b556b5c3d9 100644 --- a/app/Jobs/Util/Import.php +++ b/app/Jobs/Util/Import.php @@ -11,6 +11,7 @@ namespace App\Jobs\Util; +use App\DataMapper\Analytics\MigrationFailure; use App\DataMapper\CompanySettings; use App\Exceptions\MigrationValidatorFailed; use App\Exceptions\ResourceDependencyMissing; @@ -72,6 +73,7 @@ use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str; +use Turbo124\Beacon\Facades\LightLogs; class Import implements ShouldQueue { @@ -966,6 +968,15 @@ class Import implements ShouldQueue public function failed($exception = null) { info('the job failed'); + + $job_failure = new MigrationFailure(); + $job_failure->string_metric5 = get_class($this); + $job_failure->string_metric6 = $exception->getMessage(); + + LightLogs::create($job_failure) + ->batch(); + info(print_r($exception->getMessage(), 1)); } + } diff --git a/app/Models/Client.php b/app/Models/Client.php index fcf885357b..f651ca1f77 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -156,6 +156,11 @@ class Client extends BaseModel implements HasLocalePreference ->first(); } + public function credits() + { + return $this->hasMany(Credit::class)->withTrashed(); + } + public function activities() { return $this->hasMany(Activity::class)->orderBy('id', 'desc'); diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index 801d4d5eec..4ac87da2fa 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -37,6 +37,7 @@ class Gateway extends StaticModel /** * @return mixed + * @deprecated 5.0.17 No longer needs as we are removing omnipay dependence */ public function getFields() { @@ -116,7 +117,7 @@ class Gateway extends StaticModel return ['methods' => [GatewayType::CREDIT_CARD], 'refund' => true, 'token_billing' => true ]; //Checkout break; default: - return []; + return ['methods' => [], 'refund' => false, 'token_billing' => false]; break; } } diff --git a/app/Models/Project.php b/app/Models/Project.php index f3e0bf0744..62900d4e98 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\Filterable; use Illuminate\Database\Eloquent\SoftDeletes; use Laracasts\Presenter\PresentableTrait; @@ -13,7 +14,8 @@ class Project extends BaseModel // Expense Categories use SoftDeletes; use PresentableTrait; - + use Filterable; + /** * @var array */ @@ -24,17 +26,16 @@ class Project extends BaseModel */ protected $fillable = [ 'name', + 'client_id', 'task_rate', 'private_notes', + 'public_notes', 'due_date', 'budgeted_hours', 'custom_value1', 'custom_value2', - ]; - - protected $casts = [ - 'updated_at' => 'timestamp', - 'created_at' => 'timestamp', + 'custom_value3', + 'custom_value4', ]; public function getEntityType() diff --git a/app/Models/RecurringInvoice.php b/app/Models/RecurringInvoice.php index 70a9795c89..1c4e827102 100644 --- a/app/Models/RecurringInvoice.php +++ b/app/Models/RecurringInvoice.php @@ -44,20 +44,8 @@ class RecurringInvoice extends BaseModel const STATUS_PENDING = -1; /** - * Recurring intervals //todo MAP WHEN WE MIGRATE. + * Invoice Frequencies. */ - - /* Make sure we support overflow!!!!!!!!!! - $start = Carbon::today(); - $subscription = Carbon::parse('2017-12-31'); - - foreach (range(1, 12) as $month) { - $day = $start->addMonthNoOverflow()->thisDayOrLast($subscription->day); - - echo "You will be billed on {$day} in month {$month}\n"; - } - */ - const FREQUENCY_DAILY = 1; const FREQUENCY_WEEKLY = 2; const FREQUENCY_TWO_WEEKS = 3; diff --git a/app/Policies/ProjectPolicy.php b/app/Policies/ProjectPolicy.php new file mode 100644 index 0000000000..b18e12c94f --- /dev/null +++ b/app/Policies/ProjectPolicy.php @@ -0,0 +1,41 @@ +isAdmin() || $user->hasPermission('create_project') || $user->hasPermission('create_all'); + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 45cf785d46..96687e579d 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -25,6 +25,7 @@ use App\Models\Invoice; use App\Models\Payment; use App\Models\PaymentTerm; use App\Models\Product; +use App\Models\Project; use App\Models\Quote; use App\Models\RecurringInvoice; use App\Models\RecurringQuote; @@ -46,6 +47,7 @@ use App\Policies\InvoicePolicy; use App\Policies\PaymentPolicy; use App\Policies\PaymentTermPolicy; use App\Policies\ProductPolicy; +use App\Policies\ProjectPolicy; use App\Policies\QuotePolicy; use App\Policies\RecurringInvoicePolicy; use App\Policies\RecurringQuotePolicy; @@ -79,6 +81,7 @@ class AuthServiceProvider extends ServiceProvider Payment::class => PaymentPolicy::class, PaymentTerm::class => PaymentTermPolicy::class, Product::class => ProductPolicy::class, + Project::class => ProjectPolicy::class, Quote::class => QuotePolicy::class, RecurringInvoice::class => RecurringInvoicePolicy::class, RecurringQuote::class => RecurringQuotePolicy::class, diff --git a/app/Repositories/ProjectRepository.php b/app/Repositories/ProjectRepository.php new file mode 100644 index 0000000000..b1010bf71d --- /dev/null +++ b/app/Repositories/ProjectRepository.php @@ -0,0 +1,31 @@ +client->getSetting('counter_number_applied')) { case 'when_saved': - $this->invoice->number = $this->getNextInvoiceNumber($this->client); + $this->invoice->number = $this->getNextInvoiceNumber($this->client, $this->invoice); break; case 'when_sent': if ($this->invoice->status_id == Invoice::STATUS_SENT) { - $this->invoice->number = $this->getNextInvoiceNumber($this->client); + $this->invoice->number = $this->getNextInvoiceNumber($this->client, $this->invoice); } break; diff --git a/app/Services/Invoice/AutoBillInvoice.php b/app/Services/Invoice/AutoBillInvoice.php index 3dad55c4e8..10230eeb5b 100644 --- a/app/Services/Invoice/AutoBillInvoice.php +++ b/app/Services/Invoice/AutoBillInvoice.php @@ -30,6 +30,8 @@ class AutoBillInvoice extends AbstractService private $client; + private $payment; + public function __construct(Invoice $invoice) { $this->invoice = $invoice; @@ -39,32 +41,37 @@ class AutoBillInvoice extends AbstractService public function run() { - if (! $this->invoice->isPayable()) { + /* Is the invoice payable? */ + if (! $this->invoice->isPayable()) return $this->invoice; - } - + + /* Mark the invoice as sent */ $this->invoice = $this->invoice->service()->markSent()->save(); - if ($this->invoice->balance > 0) { - $gateway_token = $this->getGateway($this->invoice->balance); //todo what if it is only a partial amount? - } else { + /* Mark the invoice as paid if there is no balance */ + if ((int)$this->invoice->balance == 0) return $this->invoice->service()->markPaid()->save(); - } - if (! $gateway_token || ! $gateway_token->gateway->driver($this->client)->token_billing) { - return $this->invoice; - } + $this->applyCreditPayment(); - if ($this->invoice->partial > 0) { - $fee = $gateway_token->gateway->calcGatewayFee($this->invoice->partial); - // $amount = $this->invoice->partial + $fee; + /* Determine $amount */ + if ($this->invoice->partial > 0) $amount = $this->invoice->partial; - } else { - $fee = $gateway_token->gateway->calcGatewayFee($this->invoice->balance); - // $amount = $this->invoice->balance + $fee; + elseif($this->invoice->balance >0) $amount = $this->invoice->balance; - } + else + return $this->invoice; + $gateway_token = $this->getGateway($amount); + + /* Bail out if no payment methods available */ + if (! $gateway_token || ! $gateway_token->gateway->driver($this->client)->token_billing) + return $this->invoice; + + /* $gateway fee */ + $fee = $gateway_token->gateway->calcGatewayFee($this->invoice->partial); + + /* Build payment hash */ $payment_hash = PaymentHash::create([ 'hash' => Str::random(128), 'data' => ['invoice_id' => $this->invoice->hashed_id, 'amount' => $amount], @@ -72,11 +79,103 @@ class AutoBillInvoice extends AbstractService 'fee_invoice_id' => $this->invoice->id, ]); - $payment = $gateway_token->gateway->driver($this->client)->tokenBilling($gateway_token, $payment_hash); + $payment = $gateway_token->gateway + ->driver($this->client) + ->tokenBilling($gateway_token, $payment_hash); return $this->invoice; } + /** + * Applies credits to a payment prior to push + * to the payment gateway + * + * @return $this + */ + private function applyCreditPayment() + { + + $available_credits = $this->client + ->credits + ->where('is_deleted', false) + ->where('balance', '>', 0) + ->sortBy('created_at'); + + $available_credit_balance = $available_credits->sum('balance'); + + if((int)$available_credit_balance == 0) + return; + + $is_partial_amount = false; + + if ($this->invoice->partial > 0) { + $is_partial_amount = true; + } + + $this->payment = PaymentFactory::create($this->client->company_id, $this->client->user_id); + $this->payment->save(); + + $available_credits->each(function($credit) use($is_partial_amount){ + + //todo need to iterate until the partial or balance is completely consumed + //by the credit, any remaining balance is then dealt with by + //the gateway + //each time a credit is applied SAVE the invoice + + // if($credit->balance >= $amount){ + // //current credit covers the total amount + + // } + //return false to exit each loop + }); + + return $this; + } + + private function buildPayment($credit, $is_partial_amount) + { + if($is_partial_amount) { + + if($this->invoice->partial >= $credit->balance) { + + $amount = $this->invoice->partial - $credit->balance; + $this->invoice->partial -= $amount; + + $this->payment->credits()->attach([ + $credit->id => ['amount' => $amount] + ]); + + $this->payment->invoice()->attach([ + $this->invoice->id => ['amount' => $amount] + ]); + + $this->applyPaymentToCredit($credit, $amount); + } + } + + } + + + private function applyPaymentToCredit($credit, $amount) + { + + $credit_item = new InvoiceItem; + $credit_item->type_id = '1'; + $credit_item->product_key = ctrans('texts.credit'); + $credit_item->notes = ctrans('texts.credit_payment', ['invoice_number' => $this->invoice->number]); + $credit_item->quantity = 1; + $credit_item->cost = $amount * -1; + + $credit_items = $credit->line_items; + $credit_items[] = $credit_item; + + $credit->line_items = $credit_items; + + $credit = $credit->calc()->getCredit(); + + + } + /** * Harvests a client gateway token which passes the * necessary filters for an $amount. diff --git a/app/Services/Recurring/RecurringService.php b/app/Services/Recurring/RecurringService.php index a6cd1394cd..7cbfa0e8b1 100644 --- a/app/Services/Recurring/RecurringService.php +++ b/app/Services/Recurring/RecurringService.php @@ -49,8 +49,11 @@ class RecurringService public function start() { //make sure next_send_date is either now or in the future else return. - if(Carbon::parse($this->recurring_entity->next_send_date)->lt(now())) - return $this; + // if(Carbon::parse($this->recurring_entity->next_send_date)->lt(now())) + // return $this; + + if($this->recurring_entity->remaining_cycles == 0) + return $this; $this->recurring_entity->status_id = RecurringInvoice::STATUS_ACTIVE; diff --git a/app/Transformers/GatewayTransformer.php b/app/Transformers/GatewayTransformer.php index 821a75e002..56006f4d9b 100644 --- a/app/Transformers/GatewayTransformer.php +++ b/app/Transformers/GatewayTransformer.php @@ -51,6 +51,7 @@ class GatewayTransformer extends EntityTransformer 'fields' => (string) $gateway->fields ?: '', 'updated_at' => (int) $gateway->updated_at, 'created_at' => (int) $gateway->created_at, + 'options' => $gateway->getMethods(), ]; } } diff --git a/app/Transformers/ProjectTransformer.php b/app/Transformers/ProjectTransformer.php index aebd974094..8232603d14 100644 --- a/app/Transformers/ProjectTransformer.php +++ b/app/Transformers/ProjectTransformer.php @@ -34,20 +34,23 @@ class ProjectTransformer extends EntityTransformer { return [ 'id' => (string) $this->encodePrimaryKey($project->id), - 'name' => $project->name ?: '', + 'user_id' => (string) $this->encodePrimaryKey($project->user_id), + 'assigned_user_id' => (string) $this->encodePrimaryKey($project->assigned_user_id), 'client_id' => (string) $this->encodePrimaryKey($project->client_id), + 'name' => $project->name ?: '', 'created_at' => (int) $project->created_at, 'updated_at' => (int) $project->updated_at, 'archived_at' => (int) $project->deleted_at, 'is_deleted' => (bool) $project->is_deleted, 'task_rate' => (float) $project->task_rate, 'due_date' => $project->due_date ?: '', - 'private_notes' => $project->private_notes ?: '', + 'private_notes' => (string) $project->private_notes ?: '', + 'public_notes' => (string) $project->public_notes ?: '', 'budgeted_hours' => (float) $project->budgeted_hours, - 'custom_value1' => $project->custom_value1 ?: '', - 'custom_value2' => $project->custom_value2 ?: '', - 'custom_value3' => $project->custom_value3 ?: '', - 'custom_value4' => $project->custom_value4 ?: '', + 'custom_value1' => (string) $project->custom_value1 ?: '', + 'custom_value2' => (string) $project->custom_value2 ?: '', + 'custom_value3' => (string) $project->custom_value3 ?: '', + 'custom_value4' => (string) $project->custom_value4 ?: '', ]; } } diff --git a/app/Utils/Traits/GeneratesCounter.php b/app/Utils/Traits/GeneratesCounter.php index a94a231cfc..ad1dc4d989 100644 --- a/app/Utils/Traits/GeneratesCounter.php +++ b/app/Utils/Traits/GeneratesCounter.php @@ -37,7 +37,7 @@ trait GeneratesCounter * * @return string The next invoice number. */ - public function getNextInvoiceNumber(Client $client) :string + public function getNextInvoiceNumber(Client $client, ?Invoice $invoice) :string { //Reset counters if enabled $this->resetCounters($client); @@ -64,8 +64,12 @@ trait GeneratesCounter //Return a valid counter $pattern = $client->getSetting('invoice_number_pattern'); $padding = $client->getSetting('counter_padding'); + $prefix = ''; - $invoice_number = $this->checkEntityNumber(Invoice::class, $client, $counter, $padding, $pattern); + if($invoice && $invoice->recurring_id) + $prefix = $client->getSetting('recurring_number_prefix'); + + $invoice_number = $this->checkEntityNumber(Invoice::class, $client, $counter, $padding, $pattern, $prefix); $this->incrementCounter($counter_entity, 'invoice_number_counter'); @@ -140,6 +144,9 @@ trait GeneratesCounter $quote_number = $this->checkEntityNumber(Quote::class, $client, $counter, $padding, $pattern); + // if($this->recurring_id) + // $quote_number = $this->prefixCounter($quote_number, $client->getSetting('recurring_number_prefix')); + $this->incrementCounter($counter_entity, $used_counter); return $quote_number; @@ -168,7 +175,7 @@ trait GeneratesCounter $pattern = ''; $padding = $client->getSetting('counter_padding'); $invoice_number = $this->checkEntityNumber(RecurringInvoice::class, $client, $counter, $padding, $pattern); - $invoice_number = $this->prefixCounter($invoice_number, $client->getSetting('recurring_number_prefix')); + //$invoice_number = $this->prefixCounter($invoice_number, $client->getSetting('recurring_number_prefix')); //increment the correct invoice_number Counter (company vs client) if ($is_client_counter) { @@ -283,7 +290,7 @@ trait GeneratesCounter * * @return string The padded and prefixed entity number */ - private function checkEntityNumber($class, $entity, $counter, $padding, $pattern) + private function checkEntityNumber($class, $entity, $counter, $padding, $pattern, $prefix = '') { $check = false; @@ -292,6 +299,8 @@ trait GeneratesCounter $number = $this->applyNumberPattern($entity, $number, $pattern); + $number = $this->prefixCounter($number, $prefix); + if ($class == Invoice::class || $class == RecurringInvoice::class) $check = $class::whereCompanyId($entity->company_id)->whereNumber($number)->withTrashed()->first(); elseif ($class == Client::class || $class == Vendor::class) diff --git a/composer.lock b/composer.lock index 83432f82fc..84904d40ef 100644 --- a/composer.lock +++ b/composer.lock @@ -108,16 +108,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.158.2", + "version": "3.158.4", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "b80957465d94c127254e36061dd3d0c3ccc94cc1" + "reference": "c0c0df79edc0a646a7ccd6b2e8d1723ff4ba88e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b80957465d94c127254e36061dd3d0c3ccc94cc1", - "reference": "b80957465d94c127254e36061dd3d0c3ccc94cc1", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c0c0df79edc0a646a7ccd6b2e8d1723ff4ba88e2", + "reference": "c0c0df79edc0a646a7ccd6b2e8d1723ff4ba88e2", "shasum": "" }, "require": { @@ -189,7 +189,7 @@ "s3", "sdk" ], - "time": "2020-10-05T18:13:27+00:00" + "time": "2020-10-07T18:12:22+00:00" }, { "name": "brick/math", @@ -1814,16 +1814,16 @@ }, { "name": "google/auth", - "version": "v1.14.0", + "version": "v1.14.1", "source": { "type": "git", "url": "https://github.com/googleapis/google-auth-library-php.git", - "reference": "95c23ebd89a0a4d1b511aed81426f57388ab7268" + "reference": "2df57c61c2fd739a15a81f792b1ccedc3e06d2b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/95c23ebd89a0a4d1b511aed81426f57388ab7268", - "reference": "95c23ebd89a0a4d1b511aed81426f57388ab7268", + "url": "https://api.github.com/repos/googleapis/google-auth-library-php/zipball/2df57c61c2fd739a15a81f792b1ccedc3e06d2b6", + "reference": "2df57c61c2fd739a15a81f792b1ccedc3e06d2b6", "shasum": "" }, "require": { @@ -1862,7 +1862,7 @@ "google", "oauth2" ], - "time": "2020-10-02T22:20:36+00:00" + "time": "2020-10-06T18:10:43+00:00" }, { "name": "graham-campbell/result-type", @@ -2497,16 +2497,16 @@ }, { "name": "laravel/framework", - "version": "v8.8.0", + "version": "v8.9.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "0bdd5c6f12cb7cb6644e484169656245af417735" + "reference": "8a6bf870bcfa1597e514a9c7ee6df44db98abb54" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/0bdd5c6f12cb7cb6644e484169656245af417735", - "reference": "0bdd5c6f12cb7cb6644e484169656245af417735", + "url": "https://api.github.com/repos/laravel/framework/zipball/8a6bf870bcfa1597e514a9c7ee6df44db98abb54", + "reference": "8a6bf870bcfa1597e514a9c7ee6df44db98abb54", "shasum": "" }, "require": { @@ -2656,7 +2656,7 @@ "framework", "laravel" ], - "time": "2020-10-02T14:33:08+00:00" + "time": "2020-10-06T14:22:36+00:00" }, { "name": "laravel/slack-notification-channel", @@ -6504,12 +6504,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "04c3a31fe8ea94b42c9e2d1acc93d19782133b00" + "reference": "ae789a8a2ad189ce7e8216942cdb9b77319f5eb8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/04c3a31fe8ea94b42c9e2d1acc93d19782133b00", - "reference": "04c3a31fe8ea94b42c9e2d1acc93d19782133b00", + "url": "https://api.github.com/repos/symfony/console/zipball/ae789a8a2ad189ce7e8216942cdb9b77319f5eb8", + "reference": "ae789a8a2ad189ce7e8216942cdb9b77319f5eb8", "shasum": "" }, "require": { @@ -6589,7 +6589,7 @@ "type": "tidelift" } ], - "time": "2020-09-18T14:27:32+00:00" + "time": "2020-10-07T15:23:00+00:00" }, { "name": "symfony/css-selector", diff --git a/database/factories/CompanyFactory.php b/database/factories/CompanyFactory.php index b01724b8ac..bd6239854e 100644 --- a/database/factories/CompanyFactory.php +++ b/database/factories/CompanyFactory.php @@ -39,6 +39,7 @@ class CompanyFactory extends Factory 'db' => config('database.default'), 'settings' => CompanySettings::defaults(), 'is_large' => false, + 'enabled_modules' => config('ninja.enabled_modules'), 'custom_fields' => (object) [ //'invoice1' => 'Custom Date|date', // 'invoice2' => '2|switch', diff --git a/database/migrations/2020_09_27_215800_update_gateway_table_visible_column.php b/database/migrations/2020_09_27_215800_update_gateway_table_visible_column.php index e13d117bc5..c870ead515 100644 --- a/database/migrations/2020_09_27_215800_update_gateway_table_visible_column.php +++ b/database/migrations/2020_09_27_215800_update_gateway_table_visible_column.php @@ -40,6 +40,7 @@ class UpdateGatewayTableVisibleColumn extends Migration $t->text('public_notes')->nullable(); $t->dropColumn('description'); $t->decimal('budgeted_hours', 12,2)->change(); + $t->boolean('is_deleted')->default(0); }); } diff --git a/database/seeders/RandomDataSeeder.php b/database/seeders/RandomDataSeeder.php index e8b98bf6e6..109a3832a7 100644 --- a/database/seeders/RandomDataSeeder.php +++ b/database/seeders/RandomDataSeeder.php @@ -214,7 +214,7 @@ class RandomDataSeeder extends Seeder $invoice->ledger()->updateInvoiceBalance($invoice->balance); if (rand(0, 1)) { - $payment = App\Models\Payment::create([ + $payment = Payment::create([ 'date' => now(), 'user_id' => $user->id, 'company_id' => $company->id, diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 4d5e8e7e77..57881f1c32 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -3281,4 +3281,7 @@ return [ 'paused' => 'Paused', 'saved_at' => 'Saved at :time', + 'credit_payment' => 'Credit applied to Invoice :invoice_number', + + ]; diff --git a/resources/views/email/template/master.blade.php b/resources/views/email/template/master.blade.php index cf44f3f416..7ca372a82f 100644 --- a/resources/views/email/template/master.blade.php +++ b/resources/views/email/template/master.blade.php @@ -13,7 +13,11 @@ if(!isset($design)) $design = 'light';