diff --git a/app/Factory/ProjectFactory.php b/app/Factory/ProjectFactory.php new file mode 100644 index 000000000000..bb312e83ecd9 --- /dev/null +++ b/app/Factory/ProjectFactory.php @@ -0,0 +1,37 @@ +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/Filters/ProjectFilters.php b/app/Filters/ProjectFilters.php new file mode 100644 index 000000000000..a552904037c4 --- /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/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php new file mode 100644 index 000000000000..18dca5935c0c --- /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 7ebe32087817..000000000000 --- 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 000000000000..074c617c19f9 --- /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 000000000000..85148dd874a1 --- /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 000000000000..8a0aca71c6b1 --- /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 000000000000..7fdf5124faf3 --- /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 000000000000..f17afb073761 --- /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 000000000000..73b1c642e105 --- /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/Models/Gateway.php b/app/Models/Gateway.php index 448c522e3afb..4ac87da2facf 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -117,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 f3e0bf074489..62900d4e986f 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/Policies/ProjectPolicy.php b/app/Policies/ProjectPolicy.php new file mode 100644 index 000000000000..b18e12c94f57 --- /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 45cf785d4664..96687e579d31 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 000000000000..b1010bf71d65 --- /dev/null +++ b/app/Repositories/ProjectRepository.php @@ -0,0 +1,31 @@ + (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 aebd974094b6..8232603d14e3 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/composer.lock b/composer.lock index 83432f82fc5b..84904d40ef4d 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/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 e13d117bc586..c870ead51551 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/routes/api.php b/routes/api.php index 91ed5e17f3e3..8e0449643bf7 100644 --- a/routes/api.php +++ b/routes/api.php @@ -69,6 +69,9 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::post('expenses/bulk', 'ExpenseController@bulk')->name('expenses.bulk'); + Route::resource('projects', 'ProjectController'); // name = (projects. index / create / show / update / destroy / edit + Route::post('projects/bulk', 'ProjectController@bulk')->name('projects.bulk'); + Route::resource('vendors', 'VendorController'); // name = (vendors. index / create / show / update / destroy / edit Route::post('vendors/bulk', 'VendorController@bulk')->name('vendors.bulk'); @@ -157,13 +160,6 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::post('tasks/bulk', 'TaskController@bulk')->name('tasks.bulk'); - Route::resource('credits', 'CreditController'); // name = (credits. index / create / show / update / destroy / edit - - Route::post('credits/bulk', 'CreditController@bulk')->name('credits.bulk'); - - - - Route::get('settings', 'SettingsController@index')->name('user.settings'); */ Route::get('scheduler', 'SchedulerController@index'); diff --git a/tests/Feature/ProjectApiTest.php b/tests/Feature/ProjectApiTest.php new file mode 100644 index 000000000000..237cf08089f8 --- /dev/null +++ b/tests/Feature/ProjectApiTest.php @@ -0,0 +1,155 @@ +makeTestData(); + + Session::start(); + + $this->faker = \Faker\Factory::create(); + + Model::reguard(); + } + + public function testProjectGet() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/projects/'.$this->encodePrimaryKey($this->project->id)); + + $response->assertStatus(200); + } + + + public function testProjectPost() + { + $data = [ + 'name' => $this->faker->firstName, + 'client_id' => $this->client->hashed_id, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/projects', $data); + + $response->assertStatus(200); + } + + public function testProjectPut() + { + $data = [ + 'name' => $this->faker->firstName, + 'public_notes' => 'Coolio', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->put('/api/v1/projects/'.$this->encodePrimaryKey($this->project->id), $data); + + $response->assertStatus(200); + } + + + public function testProjectNotArchived() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/projects/'.$this->encodePrimaryKey($this->project->id)); + + $arr = $response->json(); + + $this->assertEquals(0, $arr['data']['archived_at']); + } + + public function testProjectArchived() + { + $data = [ + 'ids' => [$this->encodePrimaryKey($this->project->id)], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/projects/bulk?action=archive', $data); + + $arr = $response->json(); + + $this->assertNotNull($arr['data'][0]['archived_at']); + } + + public function testProjectRestored() + { + $data = [ + 'ids' => [$this->encodePrimaryKey($this->project->id)], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/projects/bulk?action=restore', $data); + + $arr = $response->json(); + + $this->assertEquals(0, $arr['data'][0]['archived_at']); + } + + public function testProjectDeleted() + { + $data = [ + 'ids' => [$this->encodePrimaryKey($this->project->id)], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/projects/bulk?action=delete', $data); + + $arr = $response->json(); + + $this->assertTrue($arr['data'][0]['is_deleted']); + } +} diff --git a/tests/MockAccountData.php b/tests/MockAccountData.php index d4a3bca6e25f..4d7209d7c369 100644 --- a/tests/MockAccountData.php +++ b/tests/MockAccountData.php @@ -35,6 +35,7 @@ use App\Models\Expense; use App\Models\GroupSetting; use App\Models\Invoice; use App\Models\InvoiceInvitation; +use App\Models\Project; use App\Models\Quote; use App\Models\QuoteInvitation; use App\Models\RecurringInvoice; @@ -200,7 +201,10 @@ trait MockAccountData 'send_email' => true, ]); - + $this->project = Project::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + ]); $this->expense = Expense::factory()->create([ 'user_id' => $this->user->id,