From a0f6afec0f2cd0e4a23eb77a76352dcd30b9da3b Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 23 Aug 2021 09:38:55 +1000 Subject: [PATCH 01/24] Stubs for Recurring Expenses --- app/Factory/RecurringExpenseFactory.php | 45 ++ app/Filters/ExpenseFilters.php | 5 - app/Filters/RecurringExpenseFilters.php | 148 +++++ .../RecurringExpenseController.php | 578 ++++++++++++++++++ .../BulkRecurringExpenseRequest.php | 57 ++ .../CreateRecurringExpenseRequest.php | 28 + .../DestroyRecurringExpenseRequest.php | 27 + .../EditRecurringExpenseRequest.php | 28 + .../ShowRecurringExpenseRequest.php | 27 + .../StoreRecurringExpenseRequest.php | 73 +++ .../UpdateRecurringExpenseRequest.php | 79 +++ .../UploadRecurringExpenseRequest.php | 39 ++ app/Models/RecurringExpense.php | 104 ++++ app/Policies/RecurringExpensePolicy.php | 31 + app/Providers/AuthServiceProvider.php | 7 +- .../RecurringExpenseRepository.php | 60 ++ .../RecurringExpenseTransformer.php | 101 +++ app/Utils/Traits/GeneratesCounter.php | 22 + 18 files changed, 1452 insertions(+), 7 deletions(-) create mode 100644 app/Factory/RecurringExpenseFactory.php create mode 100644 app/Filters/RecurringExpenseFilters.php create mode 100644 app/Http/Controllers/RecurringExpenseController.php create mode 100644 app/Http/Requests/RecurringExpense/BulkRecurringExpenseRequest.php create mode 100644 app/Http/Requests/RecurringExpense/CreateRecurringExpenseRequest.php create mode 100644 app/Http/Requests/RecurringExpense/DestroyRecurringExpenseRequest.php create mode 100644 app/Http/Requests/RecurringExpense/EditRecurringExpenseRequest.php create mode 100644 app/Http/Requests/RecurringExpense/ShowRecurringExpenseRequest.php create mode 100644 app/Http/Requests/RecurringExpense/StoreRecurringExpenseRequest.php create mode 100644 app/Http/Requests/RecurringExpense/UpdateRecurringExpenseRequest.php create mode 100644 app/Http/Requests/RecurringExpense/UploadRecurringExpenseRequest.php create mode 100644 app/Models/RecurringExpense.php create mode 100644 app/Policies/RecurringExpensePolicy.php create mode 100644 app/Repositories/RecurringExpenseRepository.php create mode 100644 app/Transformers/RecurringExpenseTransformer.php diff --git a/app/Factory/RecurringExpenseFactory.php b/app/Factory/RecurringExpenseFactory.php new file mode 100644 index 000000000000..08562266f9c8 --- /dev/null +++ b/app/Factory/RecurringExpenseFactory.php @@ -0,0 +1,45 @@ +user_id = $user_id; + $recurring_expense->company_id = $company_id; + $recurring_expense->is_deleted = false; + $recurring_expense->should_be_invoiced = false; + $recurring_expense->tax_name1 = ''; + $recurring_expense->tax_rate1 = 0; + $recurring_expense->tax_name2 = ''; + $recurring_expense->tax_rate2 = 0; + $recurring_expense->tax_name3 = ''; + $recurring_expense->tax_rate3 = 0; + $recurring_expense->date = null; + $recurring_expense->payment_date = null; + $recurring_expense->amount = 0; + $recurring_expense->foreign_amount = 0; + $recurring_expense->private_notes = ''; + $recurring_expense->public_notes = ''; + $recurring_expense->transaction_reference = ''; + $recurring_expense->custom_value1 = ''; + $recurring_expense->custom_value2 = ''; + $recurring_expense->custom_value3 = ''; + $recurring_expense->custom_value4 = ''; + + return $recurring_expense; + } +} diff --git a/app/Filters/ExpenseFilters.php b/app/Filters/ExpenseFilters.php index 8992424d59f6..11d4ee46ae26 100644 --- a/app/Filters/ExpenseFilters.php +++ b/app/Filters/ExpenseFilters.php @@ -38,11 +38,6 @@ class ExpenseFilters extends QueryFilters return $this->builder->where(function ($query) use ($filter) { $query->where('expenses.name', 'like', '%'.$filter.'%') ->orWhere('expenses.id_number', 'like', '%'.$filter.'%') - ->orWhereHas('contacts', function ($query) use($filter){ - $query->where('expense_contacts.first_name', 'like', '%'.$filter.'%'); - $query->orWhere('expense_contacts.last_name', 'like', '%'.$filter.'%'); - $query->orWhere('expense_contacts.email', 'like', '%'.$filter.'%'); - }) ->orWhere('expenses.custom_value1', 'like', '%'.$filter.'%') ->orWhere('expenses.custom_value2', 'like', '%'.$filter.'%') ->orWhere('expenses.custom_value3', 'like', '%'.$filter.'%') diff --git a/app/Filters/RecurringExpenseFilters.php b/app/Filters/RecurringExpenseFilters.php new file mode 100644 index 000000000000..e8c3c4740436 --- /dev/null +++ b/app/Filters/RecurringExpenseFilters.php @@ -0,0 +1,148 @@ +builder; + } + + return $this->builder->where(function ($query) use ($filter) { + $query->where('recurring_expenses.name', 'like', '%'.$filter.'%') + ->orWhere('recurring_expenses.id_number', 'like', '%'.$filter.'%') + ->orWhere('recurring_expenses.custom_value1', 'like', '%'.$filter.'%') + ->orWhere('recurring_expenses.custom_value2', 'like', '%'.$filter.'%') + ->orWhere('recurring_expenses.custom_value3', 'like', '%'.$filter.'%') + ->orWhere('recurring_expenses.custom_value4', 'like', '%'.$filter.'%'); + }); + } + + /** + * Filters the list based on the status + * archived, active, deleted. + * + * @param string filter + * @return Builder + */ + public function status(string $filter = '') : Builder + { + if (strlen($filter) == 0) { + return $this->builder; + } + + $table = 'expenses'; + $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 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 + * @param User $user + * @return Builder + * @deprecated + */ + public function baseQuery(int $company_id, User $user) : Builder + { + $query = DB::table('recurring_expenses') + ->join('companies', 'companies.id', '=', 'recurring_expenses.company_id') + ->where('recurring_expenses.company_id', '=', $company_id) + ->select( + DB::raw('COALESCE(recurring_expenses.country_id, companies.country_id) country_id'), + 'recurring_expenses.id', + 'recurring_expenses.private_notes', + 'recurring_expenses.custom_value1', + 'recurring_expenses.custom_value2', + 'recurring_expenses.custom_value3', + 'recurring_expenses.custom_value4', + 'recurring_expenses.created_at', + 'recurring_expenses.created_at as expense_created_at', + 'recurring_expenses.deleted_at', + 'recurring_expenses.is_deleted', + 'recurring_expenses.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', RecurringExpense::class)) { + $query->where('recurring_expenses.user_id', '=', $user->id); + } + + return $query; + } + + /** + * Filters the query by the users company ID. + * + * @return Illuminate\Database\Query\Builder + */ + public function entityFilter() + { + return $this->builder->company(); + } +} diff --git a/app/Http/Controllers/RecurringExpenseController.php b/app/Http/Controllers/RecurringExpenseController.php new file mode 100644 index 000000000000..23e79186c1b9 --- /dev/null +++ b/app/Http/Controllers/RecurringExpenseController.php @@ -0,0 +1,578 @@ +recurring_expense_repo = $recurring_expense_repo; + } + + /** + * @OA\Get( + * path="/api/v1/recurring_expenses", + * operationId="getRecurringExpenses", + * tags={"recurring_expenses"}, + * summary="Gets a list of recurring_expenses", + * description="Lists recurring_expenses, search and filters allow fine grained lists to be generated. + + Query parameters can be added to performed more fine grained filtering of the recurring_expenses, these are handled by the RecurringExpenseFilters class which defines the methods available", + * @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 recurring_expenses", + * @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/RecurringExpense"), + * ), + * @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"), + * ), + * ) + * @param RecurringExpenseFilters $filters + * @return Response|mixed + */ + public function index(RecurringExpenseFilters $filters) + { + $recurring_expenses = RecurringExpense::filter($filters); + + return $this->listResponse($recurring_expenses); + } + + /** + * Display the specified resource. + * + * @param ShowRecurringExpenseRequest $request + * @param RecurringExpense $recurring_expense + * @return Response + * + * + * @OA\Get( + * path="/api/v1/recurring_expenses/{id}", + * operationId="showRecurringExpense", + * tags={"recurring_expenses"}, + * summary="Shows a client", + * description="Displays a client 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 RecurringExpense Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the recurring_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/RecurringExpense"), + * ), + * @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(ShowRecurringExpenseRequest $request, RecurringExpense $recurring_expense) + { + return $this->itemResponse($recurring_expense); + } + + /** + * Show the form for editing the specified resource. + * + * @param EditRecurringExpenseRequest $request + * @param RecurringExpense $recurring_expense + * @return Response + * + * + * @OA\Get( + * path="/api/v1/recurring_expenses/{id}/edit", + * operationId="editRecurringExpense", + * tags={"recurring_expenses"}, + * summary="Shows a client for editting", + * description="Displays a client 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 RecurringExpense Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the client 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/RecurringExpense"), + * ), + * @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(EditRecurringExpenseRequest $request, RecurringExpense $recurring_expense) + { + return $this->itemResponse($recurring_expense); + } + + /** + * Update the specified resource in storage. + * + * @param UpdateRecurringExpenseRequest $request + * @param RecurringExpense $recurring_expense + * @return Response + * + * + * + * @OA\Put( + * path="/api/v1/recurring_expenses/{id}", + * operationId="updateRecurringExpense", + * tags={"recurring_expenses"}, + * summary="Updates a client", + * description="Handles the updating of a client 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 RecurringExpense Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the client 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/RecurringExpense"), + * ), + * @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(UpdateRecurringExpenseRequest $request, RecurringExpense $recurring_expense) + { + if ($request->entityIsDeleted($recurring_expense)) { + return $request->disallowUpdate(); + } + + $recurring_expense = $this->recurring_expense_repo->save($request->all(), $recurring_expense); + + $this->uploadLogo($request->file('company_logo'), $recurring_expense->company, $recurring_expense); + + event(new RecurringExpenseWasUpdated($recurring_expense, $recurring_expense->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); + + return $this->itemResponse($recurring_expense->fresh()); + } + + /** + * Show the form for creating a new resource. + * + * @param CreateRecurringExpenseRequest $request + * @return Response + * + * + * + * @OA\Get( + * path="/api/v1/recurring_expenses/create", + * operationId="getRecurringExpensesCreate", + * tags={"recurring_expenses"}, + * summary="Gets a new blank client 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 client 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/RecurringExpense"), + * ), + * @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(CreateRecurringExpenseRequest $request) + { + $recurring_expense = RecurringExpenseFactory::create(auth()->user()->company()->id, auth()->user()->id); + + return $this->itemResponse($recurring_expense); + } + + /** + * Store a newly created resource in storage. + * + * @param StoreRecurringExpenseRequest $request + * @return Response + * + * + * + * @OA\Post( + * path="/api/v1/recurring_expenses", + * operationId="storeRecurringExpense", + * tags={"recurring_expenses"}, + * summary="Adds a client", + * description="Adds an client 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 client 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/RecurringExpense"), + * ), + * @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(StoreRecurringExpenseRequest $request) + { + $recurring_expense = $this->recurring_expense_repo->save($request->all(), RecurringExpenseFactory::create(auth()->user()->company()->id, auth()->user()->id)); + + event(new RecurringExpenseWasCreated($recurring_expense, $recurring_expense->company, Ninja::eventVars(auth()->user() ? auth()->user()->id : null))); + + return $this->itemResponse($recurring_expense); + } + + /** + * Remove the specified resource from storage. + * + * @param DestroyRecurringExpenseRequest $request + * @param RecurringExpense $recurring_expense + * @return Response + * + * + * @throws \Exception + * @OA\Delete( + * path="/api/v1/recurring_expenses/{id}", + * operationId="deleteRecurringExpense", + * tags={"recurring_expenses"}, + * summary="Deletes a client", + * description="Handles the deletion of a client 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 RecurringExpense 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(DestroyRecurringExpenseRequest $request, RecurringExpense $recurring_expense) + { + $this->recurring_expense_repo->delete($recurring_expense); + + return $this->itemResponse($recurring_expense->fresh()); + } + + /** + * Perform bulk actions on the list view. + * + * @return Response + * + * + * @OA\Post( + * path="/api/v1/recurring_expenses/bulk", + * operationId="bulkRecurringExpenses", + * tags={"recurring_expenses"}, + * summary="Performs bulk actions on an array of recurring_expenses", + * 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 RecurringExpense 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/RecurringExpense"), + * ), + * @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'); + $recurring_expenses = RecurringExpense::withTrashed()->find($this->transformKeys($ids)); + + $recurring_expenses->each(function ($recurring_expense, $key) use ($action) { + if (auth()->user()->can('edit', $recurring_expense)) { + $this->recurring_expense_repo->{$action}($recurring_expense); + } + }); + + return $this->listResponse(RecurringExpense::withTrashed()->whereIn('id', $this->transformKeys($ids))); + } + + /** + * Returns a client statement. + * + * @return void [type] [description] + */ + public function statement() + { + //todo + } + + /** + * Update the specified resource in storage. + * + * @param UploadRecurringExpenseRequest $request + * @param RecurringExpense $recurring_expense + * @return Response + * + * + * + * @OA\Put( + * path="/api/v1/recurring_expenses/{id}/upload", + * operationId="uploadRecurringExpense", + * tags={"recurring_expense"}, + * summary="Uploads a document to a recurring_expense", + * description="Handles the uploading of a document to a recurring_expense", + * @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 RecurringExpense Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the RecurringExpense 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/RecurringExpense"), + * ), + * @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 upload(UploadRecurringExpenseRequest $request, RecurringExpense $recurring_expense) + { + + if(!$this->checkFeature(Account::FEATURE_DOCUMENTS)) + return $this->featureFailure(); + + if ($request->has('documents')) + $this->saveDocuments($request->file('documents'), $recurring_expense); + + return $this->itemResponse($recurring_expense->fresh()); + + } +} diff --git a/app/Http/Requests/RecurringExpense/BulkRecurringExpenseRequest.php b/app/Http/Requests/RecurringExpense/BulkRecurringExpenseRequest.php new file mode 100644 index 000000000000..75f89a155b70 --- /dev/null +++ b/app/Http/Requests/RecurringExpense/BulkRecurringExpenseRequest.php @@ -0,0 +1,57 @@ +has('action')) { + return false; + } + + if (! in_array($this->action, $this->getBulkOptions(), true)) { + return false; + } + + return auth()->user()->can(auth()->user()->isAdmin(), RecurringExpense::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/RecurringExpense/CreateRecurringExpenseRequest.php b/app/Http/Requests/RecurringExpense/CreateRecurringExpenseRequest.php new file mode 100644 index 000000000000..2f720a322d0d --- /dev/null +++ b/app/Http/Requests/RecurringExpense/CreateRecurringExpenseRequest.php @@ -0,0 +1,28 @@ +user()->can('create', RecurringExpense::class); + } +} diff --git a/app/Http/Requests/RecurringExpense/DestroyRecurringExpenseRequest.php b/app/Http/Requests/RecurringExpense/DestroyRecurringExpenseRequest.php new file mode 100644 index 000000000000..ed7e7fa795bb --- /dev/null +++ b/app/Http/Requests/RecurringExpense/DestroyRecurringExpenseRequest.php @@ -0,0 +1,27 @@ +user()->can('edit', $this->recurring_expense); + } +} diff --git a/app/Http/Requests/RecurringExpense/EditRecurringExpenseRequest.php b/app/Http/Requests/RecurringExpense/EditRecurringExpenseRequest.php new file mode 100644 index 000000000000..e24e11b19c58 --- /dev/null +++ b/app/Http/Requests/RecurringExpense/EditRecurringExpenseRequest.php @@ -0,0 +1,28 @@ +user()->can('edit', $this->recurring_expense); + } + +} diff --git a/app/Http/Requests/RecurringExpense/ShowRecurringExpenseRequest.php b/app/Http/Requests/RecurringExpense/ShowRecurringExpenseRequest.php new file mode 100644 index 000000000000..500c4ae13f9f --- /dev/null +++ b/app/Http/Requests/RecurringExpense/ShowRecurringExpenseRequest.php @@ -0,0 +1,27 @@ +user()->can('view', $this->recurring_expense); + } +} diff --git a/app/Http/Requests/RecurringExpense/StoreRecurringExpenseRequest.php b/app/Http/Requests/RecurringExpense/StoreRecurringExpenseRequest.php new file mode 100644 index 000000000000..706962e7aa77 --- /dev/null +++ b/app/Http/Requests/RecurringExpense/StoreRecurringExpenseRequest.php @@ -0,0 +1,73 @@ +user()->can('create', RecurringExpense::class); + } + + public function rules() + { + $rules = []; + + if ($this->number) + $rules['number'] = Rule::unique('recurring_expenses')->where('company_id', auth()->user()->company()->id); + + if(!empty($this->client_id)) + $rules['client_id'] = 'bail|sometimes|exists:clients,id,company_id,'.auth()->user()->company()->id; + + return $this->globalRules($rules); + } + + protected function prepareForValidation() + { + $input = $this->all(); + + $input = $this->decodePrimaryKeys($input); + + if (array_key_exists('category_id', $input) && is_string($input['category_id'])) { + $input['category_id'] = $this->decodePrimaryKey($input['category_id']); + } + + if (! array_key_exists('currency_id', $input) || strlen($input['currency_id']) == 0) { + $input['currency_id'] = (string)auth()->user()->company()->settings->currency_id; + } + + if(array_key_exists('color', $input) && is_null($input['color'])) + $input['color'] = ''; + + $this->replace($input); + } + + public function messages() + { + return [ + 'unique' => ctrans('validation.unique', ['attribute' => 'email']), + ]; + } +} diff --git a/app/Http/Requests/RecurringExpense/UpdateRecurringExpenseRequest.php b/app/Http/Requests/RecurringExpense/UpdateRecurringExpenseRequest.php new file mode 100644 index 000000000000..c1be81d1053a --- /dev/null +++ b/app/Http/Requests/RecurringExpense/UpdateRecurringExpenseRequest.php @@ -0,0 +1,79 @@ +user()->can('edit', $this->recurring_expense); + } + + public function rules() + { + /* Ensure we have a client name, and that all emails are unique*/ + + $rules['country_id'] = 'integer|nullable'; + + $rules['contacts.*.email'] = 'nullable|distinct'; + + if (isset($this->number)) { + $rules['number'] = Rule::unique('recurring_expenses')->where('company_id', auth()->user()->company()->id)->ignore($this->recurring_expense->id); + } + + return $this->globalRules($rules); + } + + public function messages() + { + return [ + 'unique' => ctrans('validation.unique', ['attribute' => 'email']), + 'email' => ctrans('validation.email', ['attribute' => 'email']), + 'name.required' => ctrans('validation.required', ['attribute' => 'name']), + 'required' => ctrans('validation.required', ['attribute' => 'email']), + ]; + } + + protected function prepareForValidation() + { + $input = $this->all(); + + $input = $this->decodePrimaryKeys($input); + + if (array_key_exists('category_id', $input) && is_string($input['category_id'])) { + $input['category_id'] = $this->decodePrimaryKey($input['category_id']); + } + + if (array_key_exists('documents', $input)) { + unset($input['documents']); + } + + if (! array_key_exists('currency_id', $input) || strlen($input['currency_id']) == 0) { + $input['currency_id'] = (string)auth()->user()->company()->settings->currency_id; + } + + $this->replace($input); + } +} diff --git a/app/Http/Requests/RecurringExpense/UploadRecurringExpenseRequest.php b/app/Http/Requests/RecurringExpense/UploadRecurringExpenseRequest.php new file mode 100644 index 000000000000..e5fa858da8f7 --- /dev/null +++ b/app/Http/Requests/RecurringExpense/UploadRecurringExpenseRequest.php @@ -0,0 +1,39 @@ +user()->can('edit', $this->recurring_expense); + } + + public function rules() + { + + $rules = []; + + if($this->input('documents')) + $rules['documents'] = 'file|mimes:html,csv,png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:2000000'; + + return $rules; + + } +} diff --git a/app/Models/RecurringExpense.php b/app/Models/RecurringExpense.php new file mode 100644 index 000000000000..b1bdce29b7c8 --- /dev/null +++ b/app/Models/RecurringExpense.php @@ -0,0 +1,104 @@ + 'boolean', + 'updated_at' => 'timestamp', + 'created_at' => 'timestamp', + 'deleted_at' => 'timestamp', + ]; + + protected $touches = []; + + public function getEntityType() + { + return self::class; + } + + public function documents() + { + return $this->morphMany(Document::class, 'documentable'); + } + + public function user() + { + return $this->belongsTo(User::class)->withTrashed(); + } + + public function assigned_user() + { + return $this->belongsTo(User::class, 'assigned_user_id', 'id'); + } + + public function company() + { + return $this->belongsTo(Company::class); + } + + public function vendor() + { + return $this->belongsTo(Vendor::class); + } + + public function client() + { + return $this->belongsTo(Client::class); + } +} diff --git a/app/Policies/RecurringExpensePolicy.php b/app/Policies/RecurringExpensePolicy.php new file mode 100644 index 000000000000..032bf869fa19 --- /dev/null +++ b/app/Policies/RecurringExpensePolicy.php @@ -0,0 +1,31 @@ +isAdmin() || $user->hasPermission('create_recurring_expense') || $user->hasPermission('create_all'); + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index a73ed4842e07..b2a4560ba93b 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -12,7 +12,6 @@ namespace App\Providers; use App\Models\Activity; -use App\Models\Subscription; use App\Models\Client; use App\Models\Company; use App\Models\CompanyGateway; @@ -29,8 +28,10 @@ use App\Models\PaymentTerm; use App\Models\Product; use App\Models\Project; use App\Models\Quote; +use App\Models\RecurringExpense; use App\Models\RecurringInvoice; use App\Models\RecurringQuote; +use App\Models\Subscription; use App\Models\Task; use App\Models\TaskStatus; use App\Models\TaxRate; @@ -38,7 +39,6 @@ use App\Models\User; use App\Models\Vendor; use App\Models\Webhook; use App\Policies\ActivityPolicy; -use App\Policies\SubscriptionPolicy; use App\Policies\ClientPolicy; use App\Policies\CompanyGatewayPolicy; use App\Policies\CompanyPolicy; @@ -55,8 +55,10 @@ use App\Policies\PaymentTermPolicy; use App\Policies\ProductPolicy; use App\Policies\ProjectPolicy; use App\Policies\QuotePolicy; +use App\Policies\RecurringExpensePolicy; use App\Policies\RecurringInvoicePolicy; use App\Policies\RecurringQuotePolicy; +use App\Policies\SubscriptionPolicy; use App\Policies\TaskPolicy; use App\Policies\TaskStatusPolicy; use App\Policies\TaxRatePolicy; @@ -92,6 +94,7 @@ class AuthServiceProvider extends ServiceProvider Product::class => ProductPolicy::class, Project::class => ProjectPolicy::class, Quote::class => QuotePolicy::class, + RecurringExpense::class => RecurringExpensePolicy::class, RecurringInvoice::class => RecurringInvoicePolicy::class, RecurringQuote::class => RecurringQuotePolicy::class, Webhook::class => WebhookPolicy::class, diff --git a/app/Repositories/RecurringExpenseRepository.php b/app/Repositories/RecurringExpenseRepository.php new file mode 100644 index 000000000000..ead280cab40b --- /dev/null +++ b/app/Repositories/RecurringExpenseRepository.php @@ -0,0 +1,60 @@ +fill($data); + $recurring_expense->number = empty($recurring_expense->number) ? $this->getNextRecurringExpenseNumber($recurring_expense) : $recurring_expense->number; + $recurring_expense->save(); + + if (array_key_exists('documents', $data)) { + $this->saveDocuments($data['documents'], $recurring_expense); + } + + return $recurring_expense; + } + + /** + * Store recurring_expenses in bulk. + * + * @param array $recurring_expense + * @return \App\Models\RecurringExpense|null + */ + public function create($recurring_expense): ?RecurringExpense + { + return $this->save( + $recurring_expense, + RecurringExpenseFactory::create(auth()->user()->company()->id, auth()->user()->id) + ); + } +} diff --git a/app/Transformers/RecurringExpenseTransformer.php b/app/Transformers/RecurringExpenseTransformer.php new file mode 100644 index 000000000000..f2da8f6a0831 --- /dev/null +++ b/app/Transformers/RecurringExpenseTransformer.php @@ -0,0 +1,101 @@ +serializer); + + return $this->includeCollection($recurring_expense->documents, $transformer, Document::class); + } + + /** + * @param RecurringExpense $recurring_expense + * + * @return array + */ + public function transform(RecurringExpense $recurring_expense) + { + return [ + 'id' => $this->encodePrimaryKey($recurring_expense->id), + 'user_id' => $this->encodePrimaryKey($recurring_expense->user_id), + 'assigned_user_id' => $this->encodePrimaryKey($recurring_expense->assigned_user_id), + 'vendor_id' => $this->encodePrimaryKey($recurring_expense->vendor_id), + 'invoice_id' => $this->encodePrimaryKey($recurring_expense->invoice_id), + 'client_id' => $this->encodePrimaryKey($recurring_expense->client_id), + 'bank_id' => (string) $recurring_expense->bank_id ?: '', + 'invoice_currency_id' => (string) $recurring_expense->invoice_currency_id ?: '', + 'recurring_expense_currency_id' => '', //todo remove redundant in 5.0.25 + 'currency_id' => (string) $recurring_expense->currency_id ?: '', + 'category_id' => $this->encodePrimaryKey($recurring_expense->category_id), + 'payment_type_id' => (string) $recurring_expense->payment_type_id ?: '', + 'recurring_recurring_expense_id' => (string) $recurring_expense->recurring_recurring_expense_id ?: '', + 'is_deleted' => (bool) $recurring_expense->is_deleted, + 'should_be_invoiced' => (bool) $recurring_expense->should_be_invoiced, + 'invoice_documents' => (bool) $recurring_expense->invoice_documents, + 'amount' => (float) $recurring_expense->amount ?: 0, + 'foreign_amount' => (float) $recurring_expense->foreign_amount ?: 0, + 'exchange_rate' => (float) $recurring_expense->exchange_rate ?: 0, + 'tax_name1' => $recurring_expense->tax_name1 ? $recurring_expense->tax_name1 : '', + 'tax_rate1' => (float) $recurring_expense->tax_rate1, + 'tax_name2' => $recurring_expense->tax_name2 ? $recurring_expense->tax_name2 : '', + 'tax_rate2' => (float) $recurring_expense->tax_rate2, + 'tax_name3' => $recurring_expense->tax_name3 ? $recurring_expense->tax_name3 : '', + 'tax_rate3' => (float) $recurring_expense->tax_rate3, + 'private_notes' => (string) $recurring_expense->private_notes ?: '', + 'public_notes' => (string) $recurring_expense->public_notes ?: '', + 'transaction_reference' => (string) $recurring_expense->transaction_reference ?: '', + 'transaction_id' => (string) $recurring_expense->transaction_id ?: '', + 'date' => $recurring_expense->date ?: '', + //'recurring_expense_date' => $recurring_expense->date ?: '', + 'number' => (string)$recurring_expense->number ?: '', + 'payment_date' => $recurring_expense->payment_date ?: '', + 'custom_value1' => $recurring_expense->custom_value1 ?: '', + 'custom_value2' => $recurring_expense->custom_value2 ?: '', + 'custom_value3' => $recurring_expense->custom_value3 ?: '', + 'custom_value4' => $recurring_expense->custom_value4 ?: '', + 'updated_at' => (int) $recurring_expense->updated_at, + 'archived_at' => (int) $recurring_expense->deleted_at, + 'created_at' => (int) $recurring_expense->created_at, + 'project_id' => $this->encodePrimaryKey($recurring_expense->project_id), + 'tax_amount1' => (float) $recurring_expense->tax_amount1, + 'tax_amount2' => (float) $recurring_expense->tax_amount2, + 'tax_amount3' => (float) $recurring_expense->tax_amount3, + 'uses_inclusive_taxes' => (bool) $recurring_expense->uses_inclusive_taxes, + 'calculate_tax_by_amount' => (bool) $recurring_expense->calculate_tax_by_amount, + ]; + } +} diff --git a/app/Utils/Traits/GeneratesCounter.php b/app/Utils/Traits/GeneratesCounter.php index e478b4cbedba..c92d7675f139 100644 --- a/app/Utils/Traits/GeneratesCounter.php +++ b/app/Utils/Traits/GeneratesCounter.php @@ -19,6 +19,7 @@ use App\Models\Invoice; use App\Models\Payment; use App\Models\Project; use App\Models\Quote; +use App\Models\RecurringExpense; use App\Models\RecurringInvoice; use App\Models\Task; use App\Models\Timezone; @@ -312,6 +313,27 @@ trait GeneratesCounter return $expense_number; } + /** + * Gets the next expense number. + * + * @param RecurringExpense $expense The expense + * @return string The next expense number. + */ + public function getNextRecurringExpenseNumber(RecurringExpense $expense) :string + { + $this->resetCompanyCounters($expense->company); + + $counter = $expense->company->settings->recurring_expense_number_counter; + $setting_entity = $expense->company->settings->recurring_expense_number_counter; + + $expense_number = $this->checkEntityNumber(RecurringExpense::class, $expense, $counter, $expense->company->settings->counter_padding, $expense->company->settings->recurring_expense_number_pattern); + + $this->incrementCounter($expense->company, 'recurring_expense_number_counter'); + + return $expense_number; + } + + /** * Determines if it has shared counter. * From 899cd4d845984634053c65fb234ce292bb897aee Mon Sep 17 00:00:00 2001 From: = Date: Mon, 23 Aug 2021 21:10:35 +1000 Subject: [PATCH 02/24] Recurring Expenses Schema --- app/Models/RecurringExpense.php | 5 ++ ...08_23_101529_recurring_expenses_schema.php | 86 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 database/migrations/2021_08_23_101529_recurring_expenses_schema.php diff --git a/app/Models/RecurringExpense.php b/app/Models/RecurringExpense.php index b1bdce29b7c8..87e8b6911243 100644 --- a/app/Models/RecurringExpense.php +++ b/app/Models/RecurringExpense.php @@ -56,6 +56,11 @@ class RecurringExpense extends BaseModel 'tax_amount3', 'uses_inclusive_taxes', 'calculate_tax_by_amount', + 'frequency_id', + 'start_date', + 'last_sent_date', + 'next_send_date', + 'remaining_cycles', ]; protected $casts = [ diff --git a/database/migrations/2021_08_23_101529_recurring_expenses_schema.php b/database/migrations/2021_08_23_101529_recurring_expenses_schema.php new file mode 100644 index 000000000000..ae28e7a5ec0a --- /dev/null +++ b/database/migrations/2021_08_23_101529_recurring_expenses_schema.php @@ -0,0 +1,86 @@ +increments('id'); + $table->timestamps(6); + $table->softDeletes(); + + $table->unsignedInteger('company_id')->index(); + $table->unsignedInteger('vendor_id')->nullable(); + $table->unsignedInteger('user_id'); + + $table->unsignedInteger('invoice_id')->nullable(); + $table->unsignedInteger('client_id')->nullable(); + $table->unsignedInteger('bank_id')->nullable(); + $table->unsignedInteger('payment_type_id')->nullable(); + $table->unsignedInteger('recurring_expense_id')->nullable(); + $table->boolean('is_deleted')->default(false); + $table->string('tax_name1')->nullable(); + $table->string('tax_name2')->nullable(); + $table->string('tax_name3')->nullable(); + $table->date('date')->nullable(); + $table->date('payment_date')->nullable(); + $table->boolean('should_be_invoiced')->default(false); + $table->boolean('invoice_documents')->default(); + $table->string('transaction_id')->nullable(); + $table->string('custom_value1')->nullable(); + $table->string('custom_value2')->nullable(); + $table->string('custom_value3')->nullable(); + $table->string('custom_value4')->nullable(); + + $table->unsignedInteger('category_id')->nullable(); + $table->boolean('calculate_tax_by_amount')->default(false); + $table->decimal('tax_rate1', 20, 6); + $table->decimal('tax_rate2', 20, 6); + $table->decimal('tax_rate3', 20, 6); + $table->decimal('amount', 20, 6); + $table->decimal('foreign_amount', 20, 6); + $table->decimal('exchange_rate', 20, 6)->default(1); + $table->unsignedInteger('assigned_user_id')->nullable(); + $table->string('number')->nullable(); + $table->unsignedInteger('invoice_currency_id')->nullable(); + $table->unsignedInteger('expense_currency_id')->nullable(); + $table->text('private_notes')->nullable(); + $table->text('public_notes')->nullable(); + $table->text('transaction_reference')->nullable(); + + $table->unsignedInteger('frequency_id'); + $table->datetime('start_date')->nullable(); + $table->datetime('last_sent_date')->nullable(); + $table->datetime('next_send_date')->nullable(); + $table->unsignedInteger('remaining_cycles')->nullable(); + + $table->unique(['company_id', 'number']); + $table->index(['company_id', 'deleted_at']); + + // Relations + $table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade')->onUpdate('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade')->onUpdate('cascade'); + + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} From 174248e03d5c05fe77b9ffabebde701da826ee48 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 24 Aug 2021 11:23:53 +1000 Subject: [PATCH 03/24] Events --- .../RecurringExpenseWasArchived.php | 47 +++++ .../RecurringExpenseWasCreated.php | 47 +++++ .../RecurringExpenseWasDeleted.php | 47 +++++ .../RecurringExpenseWasRestored.php | 49 +++++ .../RecurringExpenseWasUpdated.php | 47 +++++ .../factories/RecurringExpenseFactory.php | 47 +++++ ...08_23_101529_recurring_expenses_schema.php | 7 +- routes/api.php | 1 + tests/Feature/RecurringExpenseApiTest.php | 175 ++++++++++++++++++ tests/MockAccountData.php | 13 ++ 10 files changed, 479 insertions(+), 1 deletion(-) create mode 100644 app/Events/RecurringExpense/RecurringExpenseWasArchived.php create mode 100644 app/Events/RecurringExpense/RecurringExpenseWasCreated.php create mode 100644 app/Events/RecurringExpense/RecurringExpenseWasDeleted.php create mode 100644 app/Events/RecurringExpense/RecurringExpenseWasRestored.php create mode 100644 app/Events/RecurringExpense/RecurringExpenseWasUpdated.php create mode 100644 database/factories/RecurringExpenseFactory.php create mode 100644 tests/Feature/RecurringExpenseApiTest.php diff --git a/app/Events/RecurringExpense/RecurringExpenseWasArchived.php b/app/Events/RecurringExpense/RecurringExpenseWasArchived.php new file mode 100644 index 000000000000..e446ee406bf3 --- /dev/null +++ b/app/Events/RecurringExpense/RecurringExpenseWasArchived.php @@ -0,0 +1,47 @@ +recurring_expense = $recurring_expense; + $this->company = $company; + $this->event_vars = $event_vars; + } +} diff --git a/app/Events/RecurringExpense/RecurringExpenseWasCreated.php b/app/Events/RecurringExpense/RecurringExpenseWasCreated.php new file mode 100644 index 000000000000..501c593efcfe --- /dev/null +++ b/app/Events/RecurringExpense/RecurringExpenseWasCreated.php @@ -0,0 +1,47 @@ +recurring_expense = $recurring_expense; + $this->company = $company; + $this->event_vars = $event_vars; + } +} diff --git a/app/Events/RecurringExpense/RecurringExpenseWasDeleted.php b/app/Events/RecurringExpense/RecurringExpenseWasDeleted.php new file mode 100644 index 000000000000..730628990a60 --- /dev/null +++ b/app/Events/RecurringExpense/RecurringExpenseWasDeleted.php @@ -0,0 +1,47 @@ +recurring_expense = $recurring_expense; + $this->company = $company; + $this->event_vars = $event_vars; + } +} diff --git a/app/Events/RecurringExpense/RecurringExpenseWasRestored.php b/app/Events/RecurringExpense/RecurringExpenseWasRestored.php new file mode 100644 index 000000000000..03f809cc9925 --- /dev/null +++ b/app/Events/RecurringExpense/RecurringExpenseWasRestored.php @@ -0,0 +1,49 @@ +recurring_expense = $recurring_expense; + $this->fromDeleted = $fromDeleted; + $this->company = $company; + $this->event_vars = $event_vars; + } +} diff --git a/app/Events/RecurringExpense/RecurringExpenseWasUpdated.php b/app/Events/RecurringExpense/RecurringExpenseWasUpdated.php new file mode 100644 index 000000000000..7fd385433937 --- /dev/null +++ b/app/Events/RecurringExpense/RecurringExpenseWasUpdated.php @@ -0,0 +1,47 @@ +recurring_expense = $recurring_expense; + $this->company = $company; + $this->event_vars = $event_vars; + } +} diff --git a/database/factories/RecurringExpenseFactory.php b/database/factories/RecurringExpenseFactory.php new file mode 100644 index 000000000000..b90ed0ebdb31 --- /dev/null +++ b/database/factories/RecurringExpenseFactory.php @@ -0,0 +1,47 @@ + $this->faker->numberBetween(1, 10), + 'custom_value1' => $this->faker->text(10), + 'custom_value2' => $this->faker->text(10), + 'custom_value3' => $this->faker->text(10), + 'custom_value4' => $this->faker->text(10), + 'exchange_rate' => $this->faker->randomFloat(2, 0, 1), + 'date' => $this->faker->date(), + 'is_deleted' => false, + 'public_notes' => $this->faker->text(50), + 'private_notes' => $this->faker->text(50), + 'transaction_reference' => $this->faker->text(5), + 'invoice_id' => null, + ]; + } +} diff --git a/database/migrations/2021_08_23_101529_recurring_expenses_schema.php b/database/migrations/2021_08_23_101529_recurring_expenses_schema.php index ae28e7a5ec0a..cc99d58d543d 100644 --- a/database/migrations/2021_08_23_101529_recurring_expenses_schema.php +++ b/database/migrations/2021_08_23_101529_recurring_expenses_schema.php @@ -26,9 +26,11 @@ class RecurringExpensesSchema extends Migration $table->unsignedInteger('invoice_id')->nullable(); $table->unsignedInteger('client_id')->nullable(); $table->unsignedInteger('bank_id')->nullable(); + $table->unsignedInteger('project_id')->nullable(); $table->unsignedInteger('payment_type_id')->nullable(); $table->unsignedInteger('recurring_expense_id')->nullable(); $table->boolean('is_deleted')->default(false); + $table->boolean('uses_inclusive_taxes')->default(true); $table->string('tax_name1')->nullable(); $table->string('tax_name2')->nullable(); $table->string('tax_name3')->nullable(); @@ -44,6 +46,9 @@ class RecurringExpensesSchema extends Migration $table->unsignedInteger('category_id')->nullable(); $table->boolean('calculate_tax_by_amount')->default(false); + $table->decimal('tax_amount1', 20, 6); + $table->decimal('tax_amount2', 20, 6); + $table->decimal('tax_amount3', 20, 6); $table->decimal('tax_rate1', 20, 6); $table->decimal('tax_rate2', 20, 6); $table->decimal('tax_rate3', 20, 6); @@ -53,7 +58,7 @@ class RecurringExpensesSchema extends Migration $table->unsignedInteger('assigned_user_id')->nullable(); $table->string('number')->nullable(); $table->unsignedInteger('invoice_currency_id')->nullable(); - $table->unsignedInteger('expense_currency_id')->nullable(); + $table->unsignedInteger('currency_id')->nullable(); $table->text('private_notes')->nullable(); $table->text('public_notes')->nullable(); $table->text('transaction_reference')->nullable(); diff --git a/routes/api.php b/routes/api.php index a0e63da14382..3f3c993b9df8 100644 --- a/routes/api.php +++ b/routes/api.php @@ -129,6 +129,7 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::post('quotes/bulk', 'QuoteController@bulk')->name('quotes.bulk'); Route::put('quotes/{quote}/upload', 'QuoteController@upload'); + Route::resource('recurring_expenses', 'RecurringExpenseController'); Route::resource('recurring_invoices', 'RecurringInvoiceController'); // name = (recurring_invoices. index / create / show / update / destroy / edit Route::post('recurring_invoices/bulk', 'RecurringInvoiceController@bulk')->name('recurring_invoices.bulk'); Route::put('recurring_invoices/{recurring_invoice}/upload', 'RecurringInvoiceController@upload'); diff --git a/tests/Feature/RecurringExpenseApiTest.php b/tests/Feature/RecurringExpenseApiTest.php new file mode 100644 index 000000000000..78a636d45d50 --- /dev/null +++ b/tests/Feature/RecurringExpenseApiTest.php @@ -0,0 +1,175 @@ +makeTestData(); + + Session::start(); + + $this->faker = \Faker\Factory::create(); + + Model::reguard(); + } + + public function testRecurringExpenseGet() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/recurring_expenses/'); + + $response->assertStatus(200); + } + + public function testRecurringExpenseGetSingleExpense() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/recurring_expenses/'.$this->recurring_expense->hashed_id); + + $response->assertStatus(200); + } + + public function testRecurringExpensePost() + { + $data = [ + 'amount' => 10, + 'client_id' => $this->client->hashed_id, + 'number' => '123321', + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/recurring_expenses', $data); + + $response->assertStatus(200); + } +// $arr = $response->json(); + +// $response = $this->withHeaders([ +// 'X-API-SECRET' => config('ninja.api_secret'), +// 'X-API-TOKEN' => $this->token, +// ])->put('/api/v1/recurring_expenses/'.$arr['data']['id'], $data)->assertStatus(200); + +// try{ +// $response = $this->withHeaders([ +// 'X-API-SECRET' => config('ninja.api_secret'), +// 'X-API-TOKEN' => $this->token, +// ])->post('/api/v1/recurring_expenses', $data); +// } +// catch(ValidationException $e){ +// $response->assertStatus(302); +// } + + + +// } + +// public function testRecurringExpensePut() +// { +// $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/recurring_expenses/'.$this->encodePrimaryKey($this->project->id), $data); + +// $response->assertStatus(200); +// } + + +// public function testRecurringExpenseNotArchived() +// { +// $response = $this->withHeaders([ +// 'X-API-SECRET' => config('ninja.api_secret'), +// 'X-API-TOKEN' => $this->token, +// ])->get('/api/v1/recurring_expenses/'.$this->encodePrimaryKey($this->project->id)); + +// $arr = $response->json(); + +// $this->assertEquals(0, $arr['data']['archived_at']); +// } + +// public function testRecurringExpenseArchived() +// { +// $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/recurring_expenses/bulk?action=archive', $data); + +// $arr = $response->json(); + +// $this->assertNotNull($arr['data'][0]['archived_at']); +// } + +// public function testRecurringExpenseRestored() +// { +// $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/recurring_expenses/bulk?action=restore', $data); + +// $arr = $response->json(); + +// $this->assertEquals(0, $arr['data'][0]['archived_at']); +// } + +// public function testRecurringExpenseDeleted() +// { +// $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/recurring_expenses/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 0e566900ad80..194b5a9ee2fa 100644 --- a/tests/MockAccountData.php +++ b/tests/MockAccountData.php @@ -36,6 +36,7 @@ use App\Models\Product; use App\Models\Project; use App\Models\Quote; use App\Models\QuoteInvitation; +use App\Models\RecurringExpense; use App\Models\RecurringInvoice; use App\Models\Task; use App\Models\TaskStatus; @@ -83,6 +84,11 @@ trait MockAccountData */ public $token; + /** + * @var + */ + public $recurring_expense; + /** * @var */ @@ -285,6 +291,13 @@ trait MockAccountData 'company_id' => $this->company->id, ]); + + $this->recurring_expense = RecurringExpense::factory()->create([ + 'user_id' => $user_id, + 'company_id' => $this->company->id, + ]); + + $this->task = Task::factory()->create([ 'user_id' => $user_id, 'company_id' => $this->company->id, From 2bafe5d1fc277dd916776d6bc4f8d603dd1fc99f Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 24 Aug 2021 11:29:56 +1000 Subject: [PATCH 04/24] Recurring Expenses --- app/DataMapper/CompanySettings.php | 5 + routes/api.php | 4 + tests/Feature/RecurringExpenseApiTest.php | 110 +++++++++++----------- 3 files changed, 63 insertions(+), 56 deletions(-) diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index b5afc076f092..a5bfd2580af8 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -98,6 +98,9 @@ class CompanySettings extends BaseSettings public $expense_number_pattern = ''; //@implemented public $expense_number_counter = 1; //@implemented + public $recurring_expense_number_pattern = ''; + public $recurring_expense_number_counter = 1; + public $vendor_number_pattern = ''; //@implemented public $vendor_number_counter = 1; //@implemented @@ -347,6 +350,8 @@ class CompanySettings extends BaseSettings 'task_number_counter' => 'int', 'expense_number_pattern' => 'string', 'expense_number_counter' => 'int', + 'recurring_expense_number_pattern' => 'string', + 'recurring_expense_number_counter' => 'int', 'vendor_number_pattern' => 'string', 'vendor_number_counter' => 'int', 'ticket_number_pattern' => 'string', diff --git a/routes/api.php b/routes/api.php index 3f3c993b9df8..35f42a9ed604 100644 --- a/routes/api.php +++ b/routes/api.php @@ -130,6 +130,10 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::put('quotes/{quote}/upload', 'QuoteController@upload'); Route::resource('recurring_expenses', 'RecurringExpenseController'); + Route::post('recurring_expenses/bulk', 'RecurringExpenseController@bulk')->name('recurring_expenses.bulk'); + Route::put('recurring_expenses/{recurring_expense}/upload', 'RecurringExpenseController@upload'); + + Route::resource('recurring_invoices', 'RecurringInvoiceController'); // name = (recurring_invoices. index / create / show / update / destroy / edit Route::post('recurring_invoices/bulk', 'RecurringInvoiceController@bulk')->name('recurring_invoices.bulk'); Route::put('recurring_invoices/{recurring_invoice}/upload', 'RecurringInvoiceController@upload'); diff --git a/tests/Feature/RecurringExpenseApiTest.php b/tests/Feature/RecurringExpenseApiTest.php index 78a636d45d50..e657efe98dc9 100644 --- a/tests/Feature/RecurringExpenseApiTest.php +++ b/tests/Feature/RecurringExpenseApiTest.php @@ -75,76 +75,74 @@ class RecurringExpenseApiTest extends TestCase ])->post('/api/v1/recurring_expenses', $data); $response->assertStatus(200); + + $arr = $response->json(); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->put('/api/v1/recurring_expenses/'.$arr['data']['id'], $data)->assertStatus(200); + + try{ + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/recurring_expenses', $data); + } + catch(ValidationException $e){ + $response->assertStatus(302); + } + } -// $arr = $response->json(); -// $response = $this->withHeaders([ -// 'X-API-SECRET' => config('ninja.api_secret'), -// 'X-API-TOKEN' => $this->token, -// ])->put('/api/v1/recurring_expenses/'.$arr['data']['id'], $data)->assertStatus(200); + public function testRecurringExpensePut() + { + $data = [ + 'amount' => 20, + 'public_notes' => 'Coolio', + ]; -// try{ -// $response = $this->withHeaders([ -// 'X-API-SECRET' => config('ninja.api_secret'), -// 'X-API-TOKEN' => $this->token, -// ])->post('/api/v1/recurring_expenses', $data); -// } -// catch(ValidationException $e){ -// $response->assertStatus(302); -// } + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->put('/api/v1/recurring_expenses/'.$this->encodePrimaryKey($this->recurring_expense->id), $data); + + $response->assertStatus(200); + } + public function testRecurringExpenseNotArchived() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/recurring_expenses/'.$this->encodePrimaryKey($this->recurring_expense->id)); -// } + $arr = $response->json(); -// public function testRecurringExpensePut() -// { -// $data = [ -// 'name' => $this->faker->firstName, -// 'public_notes' => 'Coolio', -// ]; + $this->assertEquals(0, $arr['data']['archived_at']); + } -// $response = $this->withHeaders([ -// 'X-API-SECRET' => config('ninja.api_secret'), -// 'X-API-TOKEN' => $this->token, -// ])->put('/api/v1/recurring_expenses/'.$this->encodePrimaryKey($this->project->id), $data); + public function testRecurringExpenseArchived() + { + $data = [ + 'ids' => [$this->encodePrimaryKey($this->recurring_expense->id)], + ]; -// $response->assertStatus(200); -// } + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/recurring_expenses/bulk?action=archive', $data); + $arr = $response->json(); -// public function testRecurringExpenseNotArchived() -// { -// $response = $this->withHeaders([ -// 'X-API-SECRET' => config('ninja.api_secret'), -// 'X-API-TOKEN' => $this->token, -// ])->get('/api/v1/recurring_expenses/'.$this->encodePrimaryKey($this->project->id)); - -// $arr = $response->json(); - -// $this->assertEquals(0, $arr['data']['archived_at']); -// } - -// public function testRecurringExpenseArchived() -// { -// $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/recurring_expenses/bulk?action=archive', $data); - -// $arr = $response->json(); - -// $this->assertNotNull($arr['data'][0]['archived_at']); -// } + $this->assertNotNull($arr['data'][0]['archived_at']); + } // public function testRecurringExpenseRestored() // { // $data = [ -// 'ids' => [$this->encodePrimaryKey($this->project->id)], +// 'ids' => [$this->encodePrimaryKey($this->recurring_expense->id)], // ]; // $response = $this->withHeaders([ @@ -160,7 +158,7 @@ class RecurringExpenseApiTest extends TestCase // public function testRecurringExpenseDeleted() // { // $data = [ -// 'ids' => [$this->encodePrimaryKey($this->project->id)], +// 'ids' => [$this->encodePrimaryKey($this->recurring_expense->id)], // ]; // $response = $this->withHeaders([ From 8d15e181c3846e06276b5085e37520e67dfade0a Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 24 Aug 2021 11:32:33 +1000 Subject: [PATCH 05/24] Tests for recurring Expenses --- tests/Feature/RecurringExpenseApiTest.php | 48 +++++++++++------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/Feature/RecurringExpenseApiTest.php b/tests/Feature/RecurringExpenseApiTest.php index e657efe98dc9..d566cb96e338 100644 --- a/tests/Feature/RecurringExpenseApiTest.php +++ b/tests/Feature/RecurringExpenseApiTest.php @@ -139,35 +139,35 @@ class RecurringExpenseApiTest extends TestCase $this->assertNotNull($arr['data'][0]['archived_at']); } -// public function testRecurringExpenseRestored() -// { -// $data = [ -// 'ids' => [$this->encodePrimaryKey($this->recurring_expense->id)], -// ]; + public function testRecurringExpenseRestored() + { + $data = [ + 'ids' => [$this->encodePrimaryKey($this->recurring_expense->id)], + ]; -// $response = $this->withHeaders([ -// 'X-API-SECRET' => config('ninja.api_secret'), -// 'X-API-TOKEN' => $this->token, -// ])->post('/api/v1/recurring_expenses/bulk?action=restore', $data); + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/recurring_expenses/bulk?action=restore', $data); -// $arr = $response->json(); + $arr = $response->json(); -// $this->assertEquals(0, $arr['data'][0]['archived_at']); -// } + $this->assertEquals(0, $arr['data'][0]['archived_at']); + } -// public function testRecurringExpenseDeleted() -// { -// $data = [ -// 'ids' => [$this->encodePrimaryKey($this->recurring_expense->id)], -// ]; + public function testRecurringExpenseDeleted() + { + $data = [ + 'ids' => [$this->encodePrimaryKey($this->recurring_expense->id)], + ]; -// $response = $this->withHeaders([ -// 'X-API-SECRET' => config('ninja.api_secret'), -// 'X-API-TOKEN' => $this->token, -// ])->post('/api/v1/recurring_expenses/bulk?action=delete', $data); + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/recurring_expenses/bulk?action=delete', $data); -// $arr = $response->json(); + $arr = $response->json(); -// $this->assertTrue($arr['data'][0]['is_deleted']); -// } + $this->assertTrue($arr['data'][0]['is_deleted']); + } } From ee855824db47f9b59c960d3a296b13031ae7f265 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 24 Aug 2021 12:57:46 +1000 Subject: [PATCH 06/24] Recurring Quotes --- .../StoreRecurringQuoteRequest.php | 85 +++- .../UpdateRecurringQuoteRequest.php | 82 +++- .../UploadRecurringQuoteRequest.php | 39 ++ .../UniqueRecurringQuoteNumberRule.php | 67 +++ .../Presenters/RecurringQuotePresenter.php | 31 ++ app/Models/RecurringQuote.php | 399 ++++++++++++++++-- app/Models/RecurringQuoteInvitation.php | 88 ++++ .../RecurringInvoiceTransformer.php | 30 -- .../RecurringQuoteTransformer.php | 144 ++++--- resources/lang/en/texts.php | 3 +- 10 files changed, 828 insertions(+), 140 deletions(-) create mode 100644 app/Http/Requests/RecurringQuote/UploadRecurringQuoteRequest.php create mode 100644 app/Http/ValidationRules/Recurring/UniqueRecurringQuoteNumberRule.php create mode 100644 app/Models/Presenters/RecurringQuotePresenter.php create mode 100644 app/Models/RecurringQuoteInvitation.php diff --git a/app/Http/Requests/RecurringQuote/StoreRecurringQuoteRequest.php b/app/Http/Requests/RecurringQuote/StoreRecurringQuoteRequest.php index 9da24cf99b2a..375b076aee24 100644 --- a/app/Http/Requests/RecurringQuote/StoreRecurringQuoteRequest.php +++ b/app/Http/Requests/RecurringQuote/StoreRecurringQuoteRequest.php @@ -12,9 +12,12 @@ namespace App\Http\Requests\RecurringQuote; use App\Http\Requests\Request; +use App\Http\ValidationRules\Recurring\UniqueRecurringQuoteNumberRule; +use App\Models\Client; use App\Models\RecurringQuote; use App\Utils\Traits\CleanLineItems; use App\Utils\Traits\MakesHash; +use Illuminate\Http\UploadedFile; class StoreRecurringQuoteRequest extends Request { @@ -33,17 +36,39 @@ class StoreRecurringQuoteRequest extends Request public function rules() { - return [ - 'documents' => 'mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx', - 'client_id' => 'required|exists:clients,id,company_id,'.auth()->user()->company()->id, - ]; + $rules = []; + + if ($this->input('documents') && is_array($this->input('documents'))) { + $documents = count($this->input('documents')); + + foreach (range(0, $documents) as $index) { + $rules['documents.'.$index] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; + } + + } elseif ($this->input('documents')) { + $rules['documents'] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; + } + + $rules['client_id'] = 'required|exists:clients,id,company_id,'.auth()->user()->company()->id; + + $rules['invitations.*.client_contact_id'] = 'distinct'; + + $rules['frequency_id'] = 'required|integer|digits_between:1,12'; + + $rules['number'] = new UniqueRecurringQuoteNumberRule($this->all()); + + return $rules; } protected function prepareForValidation() { $input = $this->all(); - if ($input['client_id']) { + if (array_key_exists('design_id', $input) && is_string($input['design_id'])) { + $input['design_id'] = $this->decodePrimaryKey($input['design_id']); + } + + if (array_key_exists('client_id', $input) && is_string($input['client_id'])) { $input['client_id'] = $this->decodePrimaryKey($input['client_id']); } @@ -51,8 +76,56 @@ class StoreRecurringQuoteRequest extends Request $input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']); } + if (isset($input['client_contacts'])) { + foreach ($input['client_contacts'] as $key => $contact) { + if (! array_key_exists('send_email', $contact) || ! array_key_exists('id', $contact)) { + unset($input['client_contacts'][$key]); + } + } + } + + if (isset($input['invitations'])) { + foreach ($input['invitations'] as $key => $value) { + if (isset($input['invitations'][$key]['id']) && is_numeric($input['invitations'][$key]['id'])) { + unset($input['invitations'][$key]['id']); + } + + if (isset($input['invitations'][$key]['id']) && is_string($input['invitations'][$key]['id'])) { + $input['invitations'][$key]['id'] = $this->decodePrimaryKey($input['invitations'][$key]['id']); + } + + if (is_string($input['invitations'][$key]['client_contact_id'])) { + $input['invitations'][$key]['client_contact_id'] = $this->decodePrimaryKey($input['invitations'][$key]['client_contact_id']); + } + } + } + $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; - //$input['line_items'] = json_encode($input['line_items']); + + if (isset($input['auto_bill'])) { + $input['auto_bill_enabled'] = $this->setAutoBillFlag($input['auto_bill']); + } else { + if ($client = Client::find($input['client_id'])) { + $input['auto_bill'] = $client->getSetting('auto_bill'); + $input['auto_bill_enabled'] = $this->setAutoBillFlag($input['auto_bill']); + } + } + $this->replace($input); } + + private function setAutoBillFlag($auto_bill) + { + if ($auto_bill == 'always' || $auto_bill == 'optout') { + return true; + } + + return false; + + } + + public function messages() + { + return []; + } } diff --git a/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php b/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php index 4a8cc51d87d1..7278d8d1b246 100644 --- a/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php +++ b/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php @@ -14,12 +14,15 @@ namespace App\Http\Requests\RecurringQuote; use App\Http\Requests\Request; use App\Utils\Traits\ChecksEntityStatus; use App\Utils\Traits\CleanLineItems; +use App\Utils\Traits\MakesHash; +use Illuminate\Http\UploadedFile; use Illuminate\Validation\Rule; class UpdateRecurringQuoteRequest extends Request { use ChecksEntityStatus; use CleanLineItems; + use MakesHash; /** * Determine if the user is authorized to make this request. @@ -33,24 +36,91 @@ class UpdateRecurringQuoteRequest extends Request public function rules() { - return [ - 'documents' => 'mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx', - ]; + $rules = []; + + if ($this->input('documents') && is_array($this->input('documents'))) { + $documents = count($this->input('documents')); + + foreach (range(0, $documents) as $index) { + $rules['documents.'.$index] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; + } + } elseif ($this->input('documents')) { + $rules['documents'] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; + } + + if($this->number) + $rules['number'] = Rule::unique('recurring_quotes')->where('company_id', auth()->user()->company()->id)->ignore($this->recurring_quote->id); + + + return $rules; } protected function prepareForValidation() { $input = $this->all(); + if (array_key_exists('design_id', $input) && is_string($input['design_id'])) { + $input['design_id'] = $this->decodePrimaryKey($input['design_id']); + } + + if (isset($input['client_id'])) { + $input['client_id'] = $this->decodePrimaryKey($input['client_id']); + } + if (array_key_exists('assigned_user_id', $input) && is_string($input['assigned_user_id'])) { $input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']); } - $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; + if (isset($input['invitations'])) { + foreach ($input['invitations'] as $key => $value) { + if (is_numeric($input['invitations'][$key]['id'])) { + unset($input['invitations'][$key]['id']); + } - if($this->number) - $rules['number'] = Rule::unique('recurring_quotes')->where('company_id', auth()->user()->company()->id)->ignore($this->recurring_quote->id); + if (array_key_exists('id', $input['invitations'][$key]) && is_string($input['invitations'][$key]['id'])) { + $input['invitations'][$key]['id'] = $this->decodePrimaryKey($input['invitations'][$key]['id']); + } + if (is_string($input['invitations'][$key]['client_contact_id'])) { + $input['invitations'][$key]['client_contact_id'] = $this->decodePrimaryKey($input['invitations'][$key]['client_contact_id']); + } + } + } + + if (isset($input['line_items'])) { + $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; + } + + if (isset($input['auto_bill'])) { + $input['auto_bill_enabled'] = $this->setAutoBillFlag($input['auto_bill']); + } + + if (array_key_exists('documents', $input)) { + unset($input['documents']); + } + $this->replace($input); } + + /** + * 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 / 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/Requests/RecurringQuote/UploadRecurringQuoteRequest.php b/app/Http/Requests/RecurringQuote/UploadRecurringQuoteRequest.php new file mode 100644 index 000000000000..72985f6f48e7 --- /dev/null +++ b/app/Http/Requests/RecurringQuote/UploadRecurringQuoteRequest.php @@ -0,0 +1,39 @@ +user()->can('edit', $this->recurring_quote); + } + + public function rules() + { + + $rules = []; + + if($this->input('documents')) + $rules['documents'] = 'file|mimes:html,csv,png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:2000000'; + + return $rules; + + } +} diff --git a/app/Http/ValidationRules/Recurring/UniqueRecurringQuoteNumberRule.php b/app/Http/ValidationRules/Recurring/UniqueRecurringQuoteNumberRule.php new file mode 100644 index 000000000000..66fca8061f3f --- /dev/null +++ b/app/Http/ValidationRules/Recurring/UniqueRecurringQuoteNumberRule.php @@ -0,0 +1,67 @@ +input = $input; + } + + /** + * @param string $attribute + * @param mixed $value + * @return bool + */ + public function passes($attribute, $value) + { + return $this->checkIfQuoteNumberUnique(); //if it exists, return false! + } + + /** + * @return string + */ + public function message() + { + return ctrans('texts.recurring_quote_number_taken', ['number' => $this->input['number']]); + } + + /** + * @return bool + */ + private function checkIfQuoteNumberUnique() : bool + { + if (empty($this->input['number'])) { + return true; + } + + $invoice = RecurringQuote::where('client_id', $this->input['client_id']) + ->where('number', $this->input['number']) + ->withTrashed() + ->exists(); + + if ($invoice) { + return false; + } + + return true; + } +} diff --git a/app/Models/Presenters/RecurringQuotePresenter.php b/app/Models/Presenters/RecurringQuotePresenter.php new file mode 100644 index 000000000000..e69058ce8f5d --- /dev/null +++ b/app/Models/Presenters/RecurringQuotePresenter.php @@ -0,0 +1,31 @@ + 'object', 'line_items' => 'object', 'backup' => 'object', - 'settings' => 'object', 'updated_at' => 'timestamp', 'created_at' => 'timestamp', 'deleted_at' => 'timestamp', ]; - protected $with = [ - // 'client', - // 'company', + protected $appends = [ + 'hashed_id', + 'status', ]; + protected $touches = []; + public function getEntityType() { return self::class; @@ -126,6 +159,16 @@ class RecurringQuote extends BaseModel return $value; } + public function activities() + { + return $this->hasMany(Activity::class)->orderBy('id', 'DESC')->take(50); + } + + public function history() + { + return $this->hasManyThrough(Backup::class, Activity::class); + } + public function company() { return $this->belongsTo(Company::class); @@ -136,6 +179,11 @@ class RecurringQuote extends BaseModel return $this->belongsTo(Client::class)->withTrashed(); } + public function project() + { + return $this->belongsTo(Project::class)->withTrashed(); + } + public function user() { return $this->belongsTo(User::class)->withTrashed(); @@ -146,8 +194,299 @@ class RecurringQuote extends BaseModel return $this->belongsTo(User::class, 'assigned_user_id', 'id')->withTrashed(); } + public function quotes() + { + return $this->hasMany(Quote::class, 'recurring_id', 'id')->withTrashed(); + } + public function invitations() { - $this->morphMany(RecurringQuoteInvitation::class); + return $this->hasMany(RecurringQuoteInvitation::class); } + + public function documents() + { + return $this->morphMany(Document::class, 'documentable'); + } + + public function getStatusAttribute() + { + if ($this->status_id == self::STATUS_ACTIVE && Carbon::parse($this->next_send_date)->isFuture()) { + return self::STATUS_PENDING; + } else { + return $this->status_id; + } + } + + public function nextSendDate() :?Carbon + { + if (!$this->next_send_date) { + return null; + } + + $offset = $this->client->timezone_offset(); + + /* + As we are firing at UTC+0 if our offset is negative it is technically firing the day before so we always need + to add ON a day - a day = 86400 seconds + */ + if($offset < 0) + $offset += 86400; + + switch ($this->frequency_id) { + case self::FREQUENCY_DAILY: + return Carbon::parse($this->next_send_date)->startOfDay()->addDay()->addSeconds($offset); + case self::FREQUENCY_WEEKLY: + return Carbon::parse($this->next_send_date)->startOfDay()->addWeek()->addSeconds($offset); + case self::FREQUENCY_TWO_WEEKS: + return Carbon::parse($this->next_send_date)->startOfDay()->addWeeks(2)->addSeconds($offset); + case self::FREQUENCY_FOUR_WEEKS: + return Carbon::parse($this->next_send_date)->startOfDay()->addWeeks(4)->addSeconds($offset); + case self::FREQUENCY_MONTHLY: + return Carbon::parse($this->next_send_date)->startOfDay()->addMonthNoOverflow()->addSeconds($offset); + case self::FREQUENCY_TWO_MONTHS: + return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(2)->addSeconds($offset); + case self::FREQUENCY_THREE_MONTHS: + return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(3)->addSeconds($offset); + case self::FREQUENCY_FOUR_MONTHS: + return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(4)->addSeconds($offset); + case self::FREQUENCY_SIX_MONTHS: + return Carbon::parse($this->next_send_date)->startOfDay()->addMonthsNoOverflow(6)->addSeconds($offset); + case self::FREQUENCY_ANNUALLY: + return Carbon::parse($this->next_send_date)->startOfDay()->addYear()->addSeconds($offset); + case self::FREQUENCY_TWO_YEARS: + return Carbon::parse($this->next_send_date)->startOfDay()->addYears(2)->addSeconds($offset); + case self::FREQUENCY_THREE_YEARS: + return Carbon::parse($this->next_send_date)->startOfDay()->addYears(3)->addSeconds($offset); + default: + return null; + } + } + + public function nextDateByFrequency($date) + { + $offset = $this->client->timezone_offset(); + + switch ($this->frequency_id) { + case self::FREQUENCY_DAILY: + return Carbon::parse($date)->startOfDay()->addDay()->addSeconds($offset); + case self::FREQUENCY_WEEKLY: + return Carbon::parse($date)->startOfDay()->addWeek()->addSeconds($offset); + case self::FREQUENCY_TWO_WEEKS: + return Carbon::parse($date)->startOfDay()->addWeeks(2)->addSeconds($offset); + case self::FREQUENCY_FOUR_WEEKS: + return Carbon::parse($date)->startOfDay()->addWeeks(4)->addSeconds($offset); + case self::FREQUENCY_MONTHLY: + return Carbon::parse($date)->startOfDay()->addMonthNoOverflow()->addSeconds($offset); + case self::FREQUENCY_TWO_MONTHS: + return Carbon::parse($date)->startOfDay()->addMonthsNoOverflow(2)->addSeconds($offset); + case self::FREQUENCY_THREE_MONTHS: + return Carbon::parse($date)->startOfDay()->addMonthsNoOverflow(3)->addSeconds($offset); + case self::FREQUENCY_FOUR_MONTHS: + return Carbon::parse($date)->startOfDay()->addMonthsNoOverflow(4)->addSeconds($offset); + case self::FREQUENCY_SIX_MONTHS: + return Carbon::parse($date)->addMonthsNoOverflow(6)->addSeconds($offset); + case self::FREQUENCY_ANNUALLY: + return Carbon::parse($date)->startOfDay()->addYear()->addSeconds($offset); + case self::FREQUENCY_TWO_YEARS: + return Carbon::parse($date)->startOfDay()->addYears(2)->addSeconds($offset); + case self::FREQUENCY_THREE_YEARS: + return Carbon::parse($date)->startOfDay()->addYears(3)->addSeconds($offset); + default: + return null; + } + } + + public function remainingCycles() : int + { + if ($this->remaining_cycles == 0) { + return 0; + } elseif ($this->remaining_cycles == -1) { + return -1; + } else { + return $this->remaining_cycles - 1; + } + } + + public function setCompleted() : void + { + $this->status_id = self::STATUS_COMPLETED; + $this->next_send_date = null; + $this->remaining_cycles = 0; + $this->save(); + } + + public static function badgeForStatus(int $status) + { + switch ($status) { + case self::STATUS_DRAFT: + return '

'.ctrans('texts.draft').'

'; + break; + case self::STATUS_PENDING: + return '

'.ctrans('texts.pending').'

'; + break; + case self::STATUS_ACTIVE: + return '

'.ctrans('texts.active').'

'; + break; + case self::STATUS_COMPLETED: + return '

'.ctrans('texts.status_completed').'

'; + break; + case self::STATUS_PAUSED: + return '

'.ctrans('texts.paused').'

'; + break; + default: + // code... + break; + } + } + + public static function frequencyForKey(int $frequency_id) :string + { + switch ($frequency_id) { + case self::FREQUENCY_DAILY: + return ctrans('texts.freq_daily'); + break; + case self::FREQUENCY_WEEKLY: + return ctrans('texts.freq_weekly'); + break; + case self::FREQUENCY_TWO_WEEKS: + return ctrans('texts.freq_two_weeks'); + break; + case self::FREQUENCY_FOUR_WEEKS: + return ctrans('texts.freq_four_weeks'); + break; + case self::FREQUENCY_MONTHLY: + return ctrans('texts.freq_monthly'); + break; + case self::FREQUENCY_TWO_MONTHS: + return ctrans('texts.freq_two_months'); + break; + case self::FREQUENCY_THREE_MONTHS: + return ctrans('texts.freq_three_months'); + break; + case self::FREQUENCY_FOUR_MONTHS: + return ctrans('texts.freq_four_months'); + break; + case self::FREQUENCY_SIX_MONTHS: + return ctrans('texts.freq_six_months'); + break; + case self::FREQUENCY_ANNUALLY: + return ctrans('texts.freq_annually'); + break; + case self::FREQUENCY_TWO_YEARS: + return ctrans('texts.freq_two_years'); + break; + default: + // code... + break; + } + } + + public function calc() + { + $invoice_calc = null; + + if ($this->uses_inclusive_taxes) { + $invoice_calc = new InvoiceSumInclusive($this); + } else { + $invoice_calc = new InvoiceSum($this); + } + + return $invoice_calc->build(); + } + + /* + * Important to note when playing with carbon dates - in order + * not to modify the original instance, always use a `->copy()` + * + */ + public function recurringDates() + { + + /* Return early if nothing to send back! */ + if ($this->status_id == self::STATUS_COMPLETED || + $this->remaining_cycles == 0 || + !$this->next_send_date) { + return []; + } + + /* Endless - lets send 10 back*/ + $iterations = $this->remaining_cycles; + + if ($this->remaining_cycles == -1) { + $iterations = 10; + } + + $data = []; + + if (!Carbon::parse($this->next_send_date)) { + return $data; + } + + $next_send_date = Carbon::parse($this->next_send_date)->copy(); + + for ($x=0; $x<$iterations; $x++) { + // we don't add the days... we calc the day of the month!! + $next_due_date = $this->calculateDueDate($next_send_date->copy()->format('Y-m-d')); + $next_due_date_string = $next_due_date ? $next_due_date->format('Y-m-d') : ''; + + $next_send_date = Carbon::parse($next_send_date); + + $data[] = [ + 'send_date' => $next_send_date->format('Y-m-d'), + 'due_date' => $next_due_date_string + ]; + + /* Fixes the timeshift in case the offset is negative which cause a infinite loop due to UTC +0*/ + if($this->client->timezone_offset() < 0){ + $next_send_date = $this->nextDateByFrequency($next_send_date->addDay()->format('Y-m-d')); + } + else + $next_send_date = $this->nextDateByFrequency($next_send_date->format('Y-m-d')); + } + + return $data; + } + + + public function calculateDueDate($date) + { + switch ($this->due_date_days) { + case 'terms': + return $this->calculateDateFromTerms($date); + break; + default: + return $this->setDayOfMonth($date, $this->due_date_days); + break; + } + } + + /** + * Calculates a date based on the client payment terms. + * + * @param Carbon $date A given date + * @return NULL|Carbon The date + */ + public function calculateDateFromTerms($date) + { + $new_date = Carbon::parse($date); + + $client_payment_terms = $this->client->getSetting('payment_terms'); + + if ($client_payment_terms == '') {//no due date! return null; + return null; + } + + return $new_date->addDays($client_payment_terms); //add the number of days in the payment terms to the date + } + + /** + * Service entry points. + */ + public function service() :RecurringService + { + return new RecurringService($this); + } + + } diff --git a/app/Models/RecurringQuoteInvitation.php b/app/Models/RecurringQuoteInvitation.php new file mode 100644 index 000000000000..feb01a14a7cf --- /dev/null +++ b/app/Models/RecurringQuoteInvitation.php @@ -0,0 +1,88 @@ +belongsTo(RecurringQuote::class)->withTrashed(); + } + + /** + * @return mixed + */ + public function contact() + { + return $this->belongsTo(ClientContact::class, 'client_contact_id', 'id')->withTrashed(); + } + + /** + * @return mixed + */ + public function user() + { + return $this->belongsTo(User::class)->withTrashed(); + } + + /** + * @return BelongsTo + */ + public function company() + { + return $this->belongsTo(Company::class); + } + + public function markViewed() + { + $this->viewed_date = now(); + $this->save(); + } + + public function markOpened() + { + $this->opened_date = now(); + $this->save(); + } +} diff --git a/app/Transformers/RecurringInvoiceTransformer.php b/app/Transformers/RecurringInvoiceTransformer.php index 6d6a4eaf732d..97a13f78141d 100644 --- a/app/Transformers/RecurringInvoiceTransformer.php +++ b/app/Transformers/RecurringInvoiceTransformer.php @@ -37,36 +37,6 @@ class RecurringInvoiceTransformer extends EntityTransformer // 'history', // 'client', ]; - - /* - public function includeInvoiceItems(Invoice $invoice) - { - $transformer = new InvoiceItemTransformer($this->serializer); - - return $this->includeCollection($invoice->invoice_items, $transformer, ENTITY_INVOICE_ITEM); - } - - public function includeInvitations(Invoice $invoice) - { - $transformer = new InvitationTransformer($this->account, $this->serializer); - - return $this->includeCollection($invoice->invitations, $transformer, ENTITY_INVITATION); - } - - public function includePayments(Invoice $invoice) - { - $transformer = new PaymentTransformer($this->account, $this->serializer, $invoice); - - return $this->includeCollection($invoice->payments, $transformer, ENTITY_PAYMENT); - } - - public function includeClient(Invoice $invoice) - { - $transformer = new ClientTransformer($this->account, $this->serializer); - - return $this->includeItem($invoice->client, $transformer, ENTITY_CLIENT); - } - */ public function includeHistory(RecurringInvoice $invoice) { diff --git a/app/Transformers/RecurringQuoteTransformer.php b/app/Transformers/RecurringQuoteTransformer.php index 1e5d84d03762..c74c30fb5524 100644 --- a/app/Transformers/RecurringQuoteTransformer.php +++ b/app/Transformers/RecurringQuoteTransformer.php @@ -11,7 +11,14 @@ namespace App\Transformers; +use App\Models\Activity; +use App\Models\Backup; +use App\Models\Document; +use App\Models\Quote; use App\Models\RecurringQuote; +use App\Models\RecurringQuoteInvitation; +use App\Transformers\ActivityTransformer; +use App\Transformers\QuoteHistoryTransformer; use App\Utils\Traits\MakesHash; class RecurringQuoteTransformer extends EntityTransformer @@ -19,107 +26,110 @@ class RecurringQuoteTransformer extends EntityTransformer use MakesHash; protected $defaultIncludes = [ - // 'invoice_items', + 'invitations', + 'documents', ]; protected $availableIncludes = [ - // 'invitations', - // 'payments', + 'invitations', + 'documents', + 'activities', + // 'history', // 'client', - // 'documents', ]; + + public function includeHistory(RecurringQuote $quote) + { + $transformer = new QuoteHistoryTransformer($this->serializer); - /* - public function includeInvoiceItems(Invoice $quote) - { - $transformer = new InvoiceItemTransformer($this->serializer); + return $this->includeCollection($quote->history, $transformer, Backup::class); + } + + public function includeActivities(RecurringQuote $quote) + { + $transformer = new ActivityTransformer($this->serializer); - return $this->includeCollection($quote->invoice_items, $transformer, ENTITY_INVOICE_ITEM); - } + return $this->includeCollection($quote->activities, $transformer, Activity::class); + } - public function includeInvitations(Invoice $quote) - { - $transformer = new InvitationTransformer($this->account, $this->serializer); + public function includeInvitations(RecurringQuote $quote) + { + $transformer = new RecurringQuoteInvitationTransformer($this->serializer); - return $this->includeCollection($quote->invitations, $transformer, ENTITY_INVITATION); - } + return $this->includeCollection($quote->invitations, $transformer, RecurringQuoteInvitation::class); + } - public function includePayments(Invoice $quote) - { - $transformer = new PaymentTransformer($this->account, $this->serializer, $quote); + public function includeDocuments(RecurringQuote $quote) + { + $transformer = new DocumentTransformer($this->serializer); - return $this->includeCollection($quote->payments, $transformer, ENTITY_PAYMENT); - } - - public function includeClient(Invoice $quote) - { - $transformer = new ClientTransformer($this->account, $this->serializer); - - return $this->includeItem($quote->client, $transformer, ENTITY_CLIENT); - } - - public function includeExpenses(Invoice $quote) - { - $transformer = new ExpenseTransformer($this->account, $this->serializer); - - return $this->includeCollection($quote->expenses, $transformer, ENTITY_EXPENSE); - } - - public function includeDocuments(Invoice $quote) - { - $transformer = new DocumentTransformer($this->account, $this->serializer); - - $quote->documents->each(function ($document) use ($quote) { - $document->setRelation('invoice', $quote); - }); - - return $this->includeCollection($quote->documents, $transformer, ENTITY_DOCUMENT); - } - */ + return $this->includeCollection($quote->documents, $transformer, Document::class); + } + public function transform(RecurringQuote $quote) { return [ 'id' => $this->encodePrimaryKey($quote->id), 'user_id' => $this->encodePrimaryKey($quote->user_id), + 'project_id' => $this->encodePrimaryKey($quote->project_id), 'assigned_user_id' => $this->encodePrimaryKey($quote->assigned_user_id), - 'amount' => (float) $quote->amount ?: '', - 'balance' => (float) $quote->balance ?: '', - 'client_id' => (string) $quote->client_id, + 'amount' => (float) $quote->amount, + 'balance' => (float) $quote->balance, + 'client_id' => (string) $this->encodePrimaryKey($quote->client_id), + 'vendor_id' => (string) $this->encodePrimaryKey($quote->vendor_id), 'status_id' => (string) ($quote->status_id ?: 1), + 'design_id' => (string) $this->encodePrimaryKey($quote->design_id), 'created_at' => (int) $quote->created_at, 'updated_at' => (int) $quote->updated_at, 'archived_at' => (int) $quote->deleted_at, - 'discount' => (float) $quote->discount ?: '', + 'is_deleted' => (bool) $quote->is_deleted, + 'number' => $quote->number ?: '', + 'discount' => (float) $quote->discount, 'po_number' => $quote->po_number ?: '', - 'quote_date' => $quote->quote_date ?: '', - 'valid_until' => $quote->valid_until ?: '', + 'date' => $quote->date ?: '', + 'last_sent_date' => $quote->last_sent_date ?: '', + 'next_send_date' => $quote->next_send_date ?: '', + 'due_date' => $quote->due_date ?: '', 'terms' => $quote->terms ?: '', 'public_notes' => $quote->public_notes ?: '', 'private_notes' => $quote->private_notes ?: '', - 'is_deleted' => (bool) $quote->is_deleted, + 'uses_inclusive_taxes' => (bool) $quote->uses_inclusive_taxes, 'tax_name1' => $quote->tax_name1 ? $quote->tax_name1 : '', - 'tax_rate1' => (float) $quote->tax_rate1 ?: '', + 'tax_rate1' => (float) $quote->tax_rate1, 'tax_name2' => $quote->tax_name2 ? $quote->tax_name2 : '', - 'tax_rate2' => (float) $quote->tax_rate2 ?: '', + 'tax_rate2' => (float) $quote->tax_rate2, 'tax_name3' => $quote->tax_name3 ? $quote->tax_name3 : '', - 'tax_rate3' => (float) $quote->tax_rate3 ?: '', + 'tax_rate3' => (float) $quote->tax_rate3, + 'total_taxes' => (float) $quote->total_taxes, 'is_amount_discount' => (bool) ($quote->is_amount_discount ?: false), - 'quote_footer' => $quote->quote_footer ?: '', + 'footer' => $quote->footer ?: '', 'partial' => (float) ($quote->partial ?: 0.0), 'partial_due_date' => $quote->partial_due_date ?: '', - 'custom_value1' => (float) $quote->custom_value1 ?: '', - 'custom_value2' => (float) $quote->custom_value2 ?: '', - 'custom_taxes1' => (bool) $quote->custom_taxes1 ?: '', - 'custom_taxes2' => (bool) $quote->custom_taxes2 ?: '', + 'custom_value1' => (string) $quote->custom_value1 ?: '', + 'custom_value2' => (string) $quote->custom_value2 ?: '', + 'custom_value3' => (string) $quote->custom_value3 ?: '', + 'custom_value4' => (string) $quote->custom_value4 ?: '', 'has_tasks' => (bool) $quote->has_tasks, 'has_expenses' => (bool) $quote->has_expenses, - 'custom_text_value1' => $quote->custom_text_value1 ?: '', - 'custom_text_value2' => $quote->custom_text_value2 ?: '', - 'settings' => $quote->settings ?: '', - 'frequency_id' => (int) $quote->frequency_id, - 'last_sent_date' => $quote->last_sent_date ?: '', - 'next_send_date' => $quote->next_send_date ?: '', + 'custom_surcharge1' => (float) $quote->custom_surcharge1, + 'custom_surcharge2' => (float) $quote->custom_surcharge2, + 'custom_surcharge3' => (float) $quote->custom_surcharge3, + 'custom_surcharge4' => (float) $quote->custom_surcharge4, + 'exchange_rate' => (float) $quote->exchange_rate, + 'custom_surcharge_tax1' => (bool) $quote->custom_surcharge_tax1, + 'custom_surcharge_tax2' => (bool) $quote->custom_surcharge_tax2, + 'custom_surcharge_tax3' => (bool) $quote->custom_surcharge_tax3, + 'custom_surcharge_tax4' => (bool) $quote->custom_surcharge_tax4, + 'line_items' => $quote->line_items ?: (array) [], + 'entity_type' => 'recurringQuote', + 'frequency_id' => (string) $quote->frequency_id, 'remaining_cycles' => (int) $quote->remaining_cycles, + 'recurring_dates' => (array) $quote->recurringDates(), + 'auto_bill' => (string) $quote->auto_bill, + 'auto_bill_enabled' => (bool) $quote->auto_bill_enabled, + 'due_date_days' => (string) $quote->due_date_days ?: '', + 'paid_to_date' => (float) $quote->paid_to_date, + 'subscription_id' => (string)$this->encodePrimaryKey($quote->subscription_id), ]; } } diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 18fd55344cb3..25bb62e617c0 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -4297,7 +4297,8 @@ $LANG = array( 'lang_Latvian' => 'Latvian', 'expiry_date' => 'Expiry date', 'cardholder_name' => 'Card holder name', - + 'recurring_quote_number_taken' => 'Recurring Quote number :number already taken', + ); return $LANG; From bab9f222ff568a028e08429cd3755ffda863fc66 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 24 Aug 2021 15:05:35 +1000 Subject: [PATCH 07/24] Recurring Quotes Tests --- app/Factory/RecurringQuoteFactory.php | 2 + .../StoreRecurringQuoteRequest.php | 37 +--- .../UpdateRecurringQuoteRequest.php | 29 +-- .../RecurringQuoteInvitationTransformer.php | 38 ++++ ...08_23_101529_recurring_expenses_schema.php | 43 ++++ routes/api.php | 2 + tests/Feature/RecurringQuotesTest.php | 195 ++++++++++++++++++ tests/MockAccountData.php | 13 +- 8 files changed, 294 insertions(+), 65 deletions(-) create mode 100644 app/Transformers/RecurringQuoteInvitationTransformer.php create mode 100644 tests/Feature/RecurringQuotesTest.php diff --git a/app/Factory/RecurringQuoteFactory.php b/app/Factory/RecurringQuoteFactory.php index 7b76b6dbfc9b..cb326e2917e0 100644 --- a/app/Factory/RecurringQuoteFactory.php +++ b/app/Factory/RecurringQuoteFactory.php @@ -22,6 +22,7 @@ class RecurringQuoteFactory $quote->discount = 0; $quote->is_amount_discount = true; $quote->po_number = ''; + $quote->number = ''; $quote->footer = ''; $quote->terms = ''; $quote->public_notes = ''; @@ -48,6 +49,7 @@ class RecurringQuoteFactory $quote->last_sent_date = null; $quote->next_send_date = null; $quote->remaining_cycles = 0; + $quote->paid_to_date = 0; return $quote; } diff --git a/app/Http/Requests/RecurringQuote/StoreRecurringQuoteRequest.php b/app/Http/Requests/RecurringQuote/StoreRecurringQuoteRequest.php index 375b076aee24..51ee3917501a 100644 --- a/app/Http/Requests/RecurringQuote/StoreRecurringQuoteRequest.php +++ b/app/Http/Requests/RecurringQuote/StoreRecurringQuoteRequest.php @@ -63,42 +63,7 @@ class StoreRecurringQuoteRequest extends Request protected function prepareForValidation() { $input = $this->all(); - - if (array_key_exists('design_id', $input) && is_string($input['design_id'])) { - $input['design_id'] = $this->decodePrimaryKey($input['design_id']); - } - - if (array_key_exists('client_id', $input) && is_string($input['client_id'])) { - $input['client_id'] = $this->decodePrimaryKey($input['client_id']); - } - - if (array_key_exists('assigned_user_id', $input) && is_string($input['assigned_user_id'])) { - $input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']); - } - - if (isset($input['client_contacts'])) { - foreach ($input['client_contacts'] as $key => $contact) { - if (! array_key_exists('send_email', $contact) || ! array_key_exists('id', $contact)) { - unset($input['client_contacts'][$key]); - } - } - } - - if (isset($input['invitations'])) { - foreach ($input['invitations'] as $key => $value) { - if (isset($input['invitations'][$key]['id']) && is_numeric($input['invitations'][$key]['id'])) { - unset($input['invitations'][$key]['id']); - } - - if (isset($input['invitations'][$key]['id']) && is_string($input['invitations'][$key]['id'])) { - $input['invitations'][$key]['id'] = $this->decodePrimaryKey($input['invitations'][$key]['id']); - } - - if (is_string($input['invitations'][$key]['client_contact_id'])) { - $input['invitations'][$key]['client_contact_id'] = $this->decodePrimaryKey($input['invitations'][$key]['client_contact_id']); - } - } - } + $input = $this->decodePrimaryKeys($input); $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; diff --git a/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php b/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php index 7278d8d1b246..2f620cfcce1e 100644 --- a/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php +++ b/app/Http/Requests/RecurringQuote/UpdateRecurringQuoteRequest.php @@ -58,34 +58,7 @@ class UpdateRecurringQuoteRequest extends Request protected function prepareForValidation() { $input = $this->all(); - - if (array_key_exists('design_id', $input) && is_string($input['design_id'])) { - $input['design_id'] = $this->decodePrimaryKey($input['design_id']); - } - - if (isset($input['client_id'])) { - $input['client_id'] = $this->decodePrimaryKey($input['client_id']); - } - - if (array_key_exists('assigned_user_id', $input) && is_string($input['assigned_user_id'])) { - $input['assigned_user_id'] = $this->decodePrimaryKey($input['assigned_user_id']); - } - - if (isset($input['invitations'])) { - foreach ($input['invitations'] as $key => $value) { - if (is_numeric($input['invitations'][$key]['id'])) { - unset($input['invitations'][$key]['id']); - } - - if (array_key_exists('id', $input['invitations'][$key]) && is_string($input['invitations'][$key]['id'])) { - $input['invitations'][$key]['id'] = $this->decodePrimaryKey($input['invitations'][$key]['id']); - } - - if (is_string($input['invitations'][$key]['client_contact_id'])) { - $input['invitations'][$key]['client_contact_id'] = $this->decodePrimaryKey($input['invitations'][$key]['client_contact_id']); - } - } - } + $input = $this->decodePrimaryKeys($input); if (isset($input['line_items'])) { $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; diff --git a/app/Transformers/RecurringQuoteInvitationTransformer.php b/app/Transformers/RecurringQuoteInvitationTransformer.php new file mode 100644 index 000000000000..db03289b0678 --- /dev/null +++ b/app/Transformers/RecurringQuoteInvitationTransformer.php @@ -0,0 +1,38 @@ + $this->encodePrimaryKey($invitation->id), + 'client_contact_id' => $this->encodePrimaryKey($invitation->client_contact_id), + 'key' => $invitation->key, + 'link' => $invitation->getLink() ?: '', + 'sent_date' => $invitation->sent_date ?: '', + 'viewed_date' => $invitation->viewed_date ?: '', + 'opened_date' => $invitation->opened_date ?: '', + 'updated_at' => (int) $invitation->updated_at, + 'archived_at' => (int) $invitation->deleted_at, + 'created_at' => (int) $invitation->created_at, + 'email_status' => $invitation->email_status ?: '', + 'email_error' => (string)$invitation->email_error, + ]; + } +} diff --git a/database/migrations/2021_08_23_101529_recurring_expenses_schema.php b/database/migrations/2021_08_23_101529_recurring_expenses_schema.php index cc99d58d543d..93adb406f418 100644 --- a/database/migrations/2021_08_23_101529_recurring_expenses_schema.php +++ b/database/migrations/2021_08_23_101529_recurring_expenses_schema.php @@ -77,6 +77,49 @@ class RecurringExpensesSchema extends Migration $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade')->onUpdate('cascade'); }); + + + Schema::create('recurring_quote_invitations', function ($t) { + $t->increments('id'); + $t->unsignedInteger('company_id'); + $t->unsignedInteger('user_id'); + $t->unsignedInteger('client_contact_id'); + $t->unsignedInteger('recurring_quote_id')->index(); + $t->string('key')->index(); + + $t->foreign('recurring_quote_id')->references('id')->on('recurring_invoices')->onDelete('cascade')->onUpdate('cascade'); + $t->foreign('user_id')->references('id')->on('users')->onDelete('cascade')->onUpdate('cascade'); + $t->foreign('client_contact_id')->references('id')->on('client_contacts')->onDelete('cascade')->onUpdate('cascade'); + $t->foreign('company_id')->references('id')->on('companies')->onDelete('cascade')->onUpdate('cascade'); + + $t->string('transaction_reference')->nullable(); + $t->string('message_id')->nullable(); + $t->mediumText('email_error')->nullable(); + $t->text('signature_base64')->nullable(); + $t->datetime('signature_date')->nullable(); + + $t->datetime('sent_date')->nullable(); + $t->datetime('viewed_date')->nullable(); + $t->datetime('opened_date')->nullable(); + $t->enum('email_status', ['delivered', 'bounced', 'spam'])->nullable(); + + $t->timestamps(6); + $t->softDeletes('deleted_at', 6); + + $t->index(['deleted_at', 'recurring_quote_id', 'company_id'], 'rec_co_del_q'); + $t->unique(['client_contact_id', 'recurring_quote_id'], 'cli_rec_q'); + }); + + + + + + + + + + + } /** diff --git a/routes/api.php b/routes/api.php index 35f42a9ed604..3b41ab6888fb 100644 --- a/routes/api.php +++ b/routes/api.php @@ -138,6 +138,8 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::post('recurring_invoices/bulk', 'RecurringInvoiceController@bulk')->name('recurring_invoices.bulk'); Route::put('recurring_invoices/{recurring_invoice}/upload', 'RecurringInvoiceController@upload'); Route::resource('recurring_quotes', 'RecurringQuoteController'); // name = (recurring_invoices. index / create / show / update / destroy / edit + Route::post('recurring_quotes/bulk', 'RecurringQuoteController@bulk')->name('recurring_quotes.bulk'); + Route::put('recurring_quotes/{recurring_quote}/upload', 'RecurringQuoteController@upload'); Route::post('recurring_quotes/bulk', 'RecurringQuoteController@bulk')->name('recurring_quotes.bulk'); diff --git a/tests/Feature/RecurringQuotesTest.php b/tests/Feature/RecurringQuotesTest.php new file mode 100644 index 000000000000..ee66395827f3 --- /dev/null +++ b/tests/Feature/RecurringQuotesTest.php @@ -0,0 +1,195 @@ +faker = \Faker\Factory::create(); + + Model::reguard(); + + $this->withoutMiddleware( + ThrottleRequests::class + ); + + $this->makeTestData(); + } + + public function testRecurringQuoteList() + { + // Client::factory()->create(['user_id' => $this->user->id, 'company_id' => $this->company->id])->each(function ($c) { + // ClientContact::factory()->create([ + // 'user_id' => $this->user->id, + // 'client_id' => $c->id, + // 'company_id' => $this->company->id, + // 'is_primary' => 1, + // ]); + + // ClientContact::factory()->create([ + // 'user_id' => $this->user->id, + // 'client_id' => $c->id, + // 'company_id' => $this->company->id, + // ]); + // }); + + // $client = Client::all()->first(); + + // RecurringQuote::factory()->create(['user_id' => $this->user->id, 'company_id' => $this->company->id, 'client_id' => $this->client->id]); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/recurring_quotes'); + + $response->assertStatus(200); + } + + public function testRecurringQuoteRESTEndPoints() + { + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/recurring_quotes/'.$this->recurring_quote->hashed_id); + + $response->assertStatus(200); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/recurring_quotes/'.$this->recurring_quote->hashed_id.'/edit'); + + $response->assertStatus(200); + + $RecurringQuote_update = [ + 'status_id' => RecurringQuote::STATUS_DRAFT, + 'number' => 'customnumber' + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->put('/api/v1/recurring_quotes/'.$this->recurring_quote->hashed_id, $RecurringQuote_update); + + $response->assertStatus(200); + $arr = $response->json(); + + $this->assertEquals('customnumber', $arr['data']['number']); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->put('/api/v1/recurring_quotes/'.$this->recurring_quote->hashed_id, $RecurringQuote_update) + ->assertStatus(200); + + $RecurringQuote_update = [ + 'status_id' => RecurringQuote::STATUS_DRAFT, + 'client_id' => $this->recurring_quote->hashed_id, + 'number' => 'customnumber' + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/recurring_quotes/', $RecurringQuote_update) + ->assertStatus(302); + + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->delete('/api/v1/recurring_quotes/'.$this->encodePrimaryKey($this->recurring_quote->id)); + + $response->assertStatus(200); + } + + public function testSubscriptionIdPassesToQuote() + { + $recurring_invoice = QuoteToRecurringQuoteFactory::create($this->invoice); + $recurring_invoice->user_id = $this->user->id; + $recurring_invoice->next_send_date = \Carbon\Carbon::now()->addDays(10); + $recurring_invoice->status_id = RecurringQuote::STATUS_ACTIVE; + $recurring_invoice->remaining_cycles = 2; + $recurring_invoice->next_send_date = \Carbon\Carbon::now()->addDays(10); + $recurring_invoice->save(); + + $recurring_invoice->number = $this->getNextRecurringQuoteNumber($this->invoice->client, $this->invoice); + $recurring_invoice->subscription_id = 10; + $recurring_invoice->save(); + + $invoice = RecurringQuoteToQuoteFactory::create($recurring_invoice, $this->client); + + $this->assertEquals(10, $invoice->subscription_id); + } + + public function testSubscriptionIdPassesToQuoteIfNull() + { + $recurring_invoice = QuoteToRecurringQuoteFactory::create($this->invoice); + $recurring_invoice->user_id = $this->user->id; + $recurring_invoice->next_send_date = \Carbon\Carbon::now()->addDays(10); + $recurring_invoice->status_id = RecurringQuote::STATUS_ACTIVE; + $recurring_invoice->remaining_cycles = 2; + $recurring_invoice->next_send_date = \Carbon\Carbon::now()->addDays(10); + $recurring_invoice->save(); + + $recurring_invoice->number = $this->getNextRecurringQuoteNumber($this->invoice->client, $this->invoice); + $recurring_invoice->save(); + + $invoice = RecurringQuoteToQuoteFactory::create($recurring_invoice, $this->client); + + $this->assertEquals(null, $invoice->subscription_id); + } + + public function testSubscriptionIdPassesToQuoteIfNothingSet() + { + $recurring_invoice = QuoteToRecurringQuoteFactory::create($this->invoice); + $recurring_invoice->user_id = $this->user->id; + $recurring_invoice->next_send_date = \Carbon\Carbon::now()->addDays(10); + $recurring_invoice->status_id = RecurringQuote::STATUS_ACTIVE; + $recurring_invoice->remaining_cycles = 2; + $recurring_invoice->next_send_date = \Carbon\Carbon::now()->addDays(10); + $recurring_invoice->save(); + + $recurring_invoice->number = $this->getNextRecurringQuoteNumber($this->invoice->client, $this->invoice); + $recurring_invoice->save(); + + $invoice = RecurringQuoteToQuoteFactory::create($recurring_invoice, $this->client); + + $this->assertEquals(null, $invoice->subscription_id); + } +} diff --git a/tests/MockAccountData.php b/tests/MockAccountData.php index 194b5a9ee2fa..f63e3603193b 100644 --- a/tests/MockAccountData.php +++ b/tests/MockAccountData.php @@ -38,6 +38,7 @@ use App\Models\Quote; use App\Models\QuoteInvitation; use App\Models\RecurringExpense; use App\Models\RecurringInvoice; +use App\Models\RecurringQuote; use App\Models\Task; use App\Models\TaskStatus; use App\Models\User; @@ -88,7 +89,12 @@ trait MockAccountData * @var */ public $recurring_expense; - + + /** + * @var + */ + public $recurring_quote; + /** * @var */ @@ -297,6 +303,11 @@ trait MockAccountData 'company_id' => $this->company->id, ]); + $this->recurring_quote = RecurringQuote::factory()->create([ + 'user_id' => $user_id, + 'company_id' => $this->company->id, + 'client_id' => $this->client->id, + ]); $this->task = Task::factory()->create([ 'user_id' => $user_id, From 06157e95a6b922afd9c6bd9aa519c7913057f76a Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 24 Aug 2021 15:32:22 +1000 Subject: [PATCH 08/24] Recurring Quotes Tests --- app/DataMapper/CompanySettings.php | 3 + app/Factory/QuoteToRecurringQuoteFactory.php | 57 ++++++++++++++++++ app/Factory/RecurringQuoteToQuoteFactory.php | 60 +++++++++++++++++++ app/Utils/Traits/GeneratesCounter.php | 12 ++++ database/factories/RecurringQuoteFactory.php | 1 - ...08_23_101529_recurring_expenses_schema.php | 21 +++++++ tests/Feature/RecurringQuotesTest.php | 12 ++-- 7 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 app/Factory/QuoteToRecurringQuoteFactory.php create mode 100644 app/Factory/RecurringQuoteToQuoteFactory.php diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index a5bfd2580af8..fdf671cc304c 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -101,6 +101,9 @@ class CompanySettings extends BaseSettings public $recurring_expense_number_pattern = ''; public $recurring_expense_number_counter = 1; + public $recurring_quote_number_pattern = ''; + public $recurring_quote_number_counter = 1; + public $vendor_number_pattern = ''; //@implemented public $vendor_number_counter = 1; //@implemented diff --git a/app/Factory/QuoteToRecurringQuoteFactory.php b/app/Factory/QuoteToRecurringQuoteFactory.php new file mode 100644 index 000000000000..1931a852ddb5 --- /dev/null +++ b/app/Factory/QuoteToRecurringQuoteFactory.php @@ -0,0 +1,57 @@ +status_id = RecurringQuote::STATUS_DRAFT; + $recurring_quote->discount = $quote->discount; + $recurring_quote->number = ''; + $recurring_quote->is_amount_discount = $quote->is_amount_discount; + $recurring_quote->po_number = $quote->po_number; + $recurring_quote->footer = $quote->footer; + $recurring_quote->terms = $quote->terms; + $recurring_quote->public_notes = $quote->public_notes; + $recurring_quote->private_notes = $quote->private_notes; + $recurring_quote->date = date_create()->format($quote->client->date_format()); + $recurring_quote->due_date = $quote->due_date; //todo calculate based on terms + $recurring_quote->is_deleted = $quote->is_deleted; + $recurring_quote->line_items = $quote->line_items; + $recurring_quote->tax_name1 = $quote->tax_name1; + $recurring_quote->tax_rate1 = $quote->tax_rate1; + $recurring_quote->tax_name2 = $quote->tax_name2; + $recurring_quote->tax_rate2 = $quote->tax_rate2; + $recurring_quote->custom_value1 = $quote->custom_value1; + $recurring_quote->custom_value2 = $quote->custom_value2; + $recurring_quote->custom_value3 = $quote->custom_value3; + $recurring_quote->custom_value4 = $quote->custom_value4; + $recurring_quote->amount = $quote->amount; + // $recurring_quote->balance = $quote->balance; + $recurring_quote->user_id = $quote->user_id; + $recurring_quote->client_id = $quote->client_id; + $recurring_quote->company_id = $quote->company_id; + $recurring_quote->frequency_id = RecurringQuote::FREQUENCY_MONTHLY; + $recurring_quote->last_sent_date = null; + $recurring_quote->next_send_date = null; + $recurring_quote->remaining_cycles = 0; + $recurring_quote->paid_to_date = 0; + + return $recurring_quote; + } +} diff --git a/app/Factory/RecurringQuoteToQuoteFactory.php b/app/Factory/RecurringQuoteToQuoteFactory.php new file mode 100644 index 000000000000..9a615b05e66a --- /dev/null +++ b/app/Factory/RecurringQuoteToQuoteFactory.php @@ -0,0 +1,60 @@ +status_id = Quote::STATUS_DRAFT; + $quote->discount = $recurring_quote->discount; + $quote->is_amount_discount = $recurring_quote->is_amount_discount; + $quote->po_number = $recurring_quote->po_number; + $quote->footer = $recurring_quote->footer; + $quote->terms = $recurring_quote->terms; + $quote->public_notes = $recurring_quote->public_notes; + $quote->private_notes = $recurring_quote->private_notes; + //$quote->date = now()->format($client->date_format()); + //$quote->due_date = $recurring_quote->calculateDueDate(now()); + $quote->is_deleted = $recurring_quote->is_deleted; + $quote->line_items = $recurring_quote->line_items; + $quote->tax_name1 = $recurring_quote->tax_name1; + $quote->tax_rate1 = $recurring_quote->tax_rate1; + $quote->tax_name2 = $recurring_quote->tax_name2; + $quote->tax_rate2 = $recurring_quote->tax_rate2; + $quote->tax_name3 = $recurring_quote->tax_name3; + $quote->tax_rate3 = $recurring_quote->tax_rate3; + $quote->total_taxes = $recurring_quote->total_taxes; + $quote->subscription_id = $recurring_quote->subscription_id; + $quote->custom_value1 = $recurring_quote->custom_value1; + $quote->custom_value2 = $recurring_quote->custom_value2; + $quote->custom_value3 = $recurring_quote->custom_value3; + $quote->custom_value4 = $recurring_quote->custom_value4; + $quote->amount = $recurring_quote->amount; + // $quote->balance = $recurring_quote->balance; + $quote->user_id = $recurring_quote->user_id; + $quote->assigned_user_id = $recurring_quote->assigned_user_id; + $quote->company_id = $recurring_quote->company_id; + $quote->recurring_id = $recurring_quote->id; + $quote->client_id = $client->id; + $quote->auto_bill_enabled = $recurring_quote->auto_bill_enabled; + $quote->paid_to_date = 0; + $quote->design_id = $recurring_quote->design_id; + + return $quote; + } +} diff --git a/app/Utils/Traits/GeneratesCounter.php b/app/Utils/Traits/GeneratesCounter.php index c92d7675f139..8983461289ba 100644 --- a/app/Utils/Traits/GeneratesCounter.php +++ b/app/Utils/Traits/GeneratesCounter.php @@ -21,6 +21,7 @@ use App\Models\Project; use App\Models\Quote; use App\Models\RecurringExpense; use App\Models\RecurringInvoice; +use App\Models\RecurringQuote; use App\Models\Task; use App\Models\Timezone; use App\Models\Vendor; @@ -136,6 +137,12 @@ trait GeneratesCounter case RecurringInvoice::class: return 'recurring_invoice_number_counter'; break; + case RecurringQuote::class: + return 'recurring_quote_number_counter'; + break; + case RecurringExpense::class: + return 'recurring_expense_number_counter'; + break; case Payment::class: return 'payment_number_counter'; break; @@ -197,6 +204,11 @@ trait GeneratesCounter return $this->getNextEntityNumber(RecurringInvoice::class, $client); } + public function getNextRecurringQuoteNumber(Client $client) + { + return $this->getNextEntityNumber(RecurringQuote::class, $client); + } + /** * Gets the next Payment number. * diff --git a/database/factories/RecurringQuoteFactory.php b/database/factories/RecurringQuoteFactory.php index 17563725b88b..b40e43d416a0 100644 --- a/database/factories/RecurringQuoteFactory.php +++ b/database/factories/RecurringQuoteFactory.php @@ -50,7 +50,6 @@ class RecurringQuoteFactory extends Factory 'due_date' => $this->faker->date(), 'line_items' => false, 'frequency_id' => RecurringQuote::FREQUENCY_MONTHLY, - 'start_date' => $this->faker->date(), 'last_sent_date' => $this->faker->date(), 'next_send_date' => $this->faker->date(), 'remaining_cycles' => $this->faker->numberBetween(1, 10), diff --git a/database/migrations/2021_08_23_101529_recurring_expenses_schema.php b/database/migrations/2021_08_23_101529_recurring_expenses_schema.php index 93adb406f418..19d43dd3b43f 100644 --- a/database/migrations/2021_08_23_101529_recurring_expenses_schema.php +++ b/database/migrations/2021_08_23_101529_recurring_expenses_schema.php @@ -78,6 +78,27 @@ class RecurringExpensesSchema extends Migration }); + Schema::table('recurring_quotes', function ($table){ + $table->string('auto_bill')->default('off'); + $table->boolean('auto_bill_enabled')->default(0); + $table->decimal('paid_to_date', 20, 6)->default(0); + $table->decimal('custom_surcharge1', 20,6)->nullable(); + $table->decimal('custom_surcharge2', 20,6)->nullable(); + $table->decimal('custom_surcharge3', 20,6)->nullable(); + $table->decimal('custom_surcharge4', 20,6)->nullable(); + $table->boolean('custom_surcharge_tax1')->default(false); + $table->boolean('custom_surcharge_tax2')->default(false); + $table->boolean('custom_surcharge_tax3')->default(false); + $table->boolean('custom_surcharge_tax4')->default(false); + $table->string('due_date_days')->nullable(); + $table->decimal('exchange_rate', 13, 6)->default(1); + $table->decimal('partial', 16, 4)->nullable(); + $table->date('partial_due_date')->nullable(); + $table->unsignedInteger('remaining_cycles')->nullable()->change(); + $table->unsignedInteger('subscription_id')->nullable(); + $table->dropColumn('start_date'); + $table->boolean('uses_inclusive_taxes')->default(true); + }); Schema::create('recurring_quote_invitations', function ($t) { $t->increments('id'); diff --git a/tests/Feature/RecurringQuotesTest.php b/tests/Feature/RecurringQuotesTest.php index ee66395827f3..78da5112771c 100644 --- a/tests/Feature/RecurringQuotesTest.php +++ b/tests/Feature/RecurringQuotesTest.php @@ -140,7 +140,7 @@ class RecurringQuotesTest extends TestCase public function testSubscriptionIdPassesToQuote() { - $recurring_invoice = QuoteToRecurringQuoteFactory::create($this->invoice); + $recurring_invoice = QuoteToRecurringQuoteFactory::create($this->quote); $recurring_invoice->user_id = $this->user->id; $recurring_invoice->next_send_date = \Carbon\Carbon::now()->addDays(10); $recurring_invoice->status_id = RecurringQuote::STATUS_ACTIVE; @@ -148,7 +148,7 @@ class RecurringQuotesTest extends TestCase $recurring_invoice->next_send_date = \Carbon\Carbon::now()->addDays(10); $recurring_invoice->save(); - $recurring_invoice->number = $this->getNextRecurringQuoteNumber($this->invoice->client, $this->invoice); + $recurring_invoice->number = $this->getNextRecurringQuoteNumber($this->quote->client, $this->quote); $recurring_invoice->subscription_id = 10; $recurring_invoice->save(); @@ -159,7 +159,7 @@ class RecurringQuotesTest extends TestCase public function testSubscriptionIdPassesToQuoteIfNull() { - $recurring_invoice = QuoteToRecurringQuoteFactory::create($this->invoice); + $recurring_invoice = QuoteToRecurringQuoteFactory::create($this->quote); $recurring_invoice->user_id = $this->user->id; $recurring_invoice->next_send_date = \Carbon\Carbon::now()->addDays(10); $recurring_invoice->status_id = RecurringQuote::STATUS_ACTIVE; @@ -167,7 +167,7 @@ class RecurringQuotesTest extends TestCase $recurring_invoice->next_send_date = \Carbon\Carbon::now()->addDays(10); $recurring_invoice->save(); - $recurring_invoice->number = $this->getNextRecurringQuoteNumber($this->invoice->client, $this->invoice); + $recurring_invoice->number = $this->getNextRecurringQuoteNumber($this->quote->client, $this->quote); $recurring_invoice->save(); $invoice = RecurringQuoteToQuoteFactory::create($recurring_invoice, $this->client); @@ -177,7 +177,7 @@ class RecurringQuotesTest extends TestCase public function testSubscriptionIdPassesToQuoteIfNothingSet() { - $recurring_invoice = QuoteToRecurringQuoteFactory::create($this->invoice); + $recurring_invoice = QuoteToRecurringQuoteFactory::create($this->quote); $recurring_invoice->user_id = $this->user->id; $recurring_invoice->next_send_date = \Carbon\Carbon::now()->addDays(10); $recurring_invoice->status_id = RecurringQuote::STATUS_ACTIVE; @@ -185,7 +185,7 @@ class RecurringQuotesTest extends TestCase $recurring_invoice->next_send_date = \Carbon\Carbon::now()->addDays(10); $recurring_invoice->save(); - $recurring_invoice->number = $this->getNextRecurringQuoteNumber($this->invoice->client, $this->invoice); + $recurring_invoice->number = $this->getNextRecurringQuoteNumber($this->quote->client, $this->quote); $recurring_invoice->save(); $invoice = RecurringQuoteToQuoteFactory::create($recurring_invoice, $this->client); From 827dba0bed9650877eecbb6fc2b147728e72fcdf Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 24 Aug 2021 15:55:14 +1000 Subject: [PATCH 09/24] Recurring Quotes / Expenses events --- .../RecurringQuoteWasArchived.php | 47 +++++++++++++++ .../RecurringQuoteWasCreated.php | 47 +++++++++++++++ .../RecurringQuoteWasDeleted.php | 47 +++++++++++++++ .../RecurringQuoteWasRestored.php | 51 +++++++++++++++++ .../RecurringQuoteWasUpdated.php | 49 ++++++++++++++++ .../CreatedRecurringExpenseActivity.php | 57 +++++++++++++++++++ .../RecurringExpenseArchivedActivity.php | 57 +++++++++++++++++++ .../RecurringExpenseDeletedActivity.php | 55 ++++++++++++++++++ .../RecurringExpenseRestoredActivity.php | 55 ++++++++++++++++++ .../RecurringExpenseUpdatedActivity.php | 57 +++++++++++++++++++ .../CreateRecurringQuoteActivity.php | 56 ++++++++++++++++++ .../RecurringQuoteArchivedActivity.php | 57 +++++++++++++++++++ .../RecurringQuoteDeletedActivity.php | 55 ++++++++++++++++++ .../RecurringQuoteRestoredActivity.php | 55 ++++++++++++++++++ .../UpdateRecurringQuoteActivity.php | 55 ++++++++++++++++++ app/Models/Activity.php | 12 ++++ app/Providers/EventServiceProvider.php | 45 +++++++++++++++ routes/api.php | 2 - 18 files changed, 857 insertions(+), 2 deletions(-) create mode 100644 app/Events/RecurringQuote/RecurringQuoteWasArchived.php create mode 100644 app/Events/RecurringQuote/RecurringQuoteWasCreated.php create mode 100644 app/Events/RecurringQuote/RecurringQuoteWasDeleted.php create mode 100644 app/Events/RecurringQuote/RecurringQuoteWasRestored.php create mode 100644 app/Events/RecurringQuote/RecurringQuoteWasUpdated.php create mode 100644 app/Listeners/RecurringExpense/CreatedRecurringExpenseActivity.php create mode 100644 app/Listeners/RecurringExpense/RecurringExpenseArchivedActivity.php create mode 100644 app/Listeners/RecurringExpense/RecurringExpenseDeletedActivity.php create mode 100644 app/Listeners/RecurringExpense/RecurringExpenseRestoredActivity.php create mode 100644 app/Listeners/RecurringExpense/RecurringExpenseUpdatedActivity.php create mode 100644 app/Listeners/RecurringQuote/CreateRecurringQuoteActivity.php create mode 100644 app/Listeners/RecurringQuote/RecurringQuoteArchivedActivity.php create mode 100644 app/Listeners/RecurringQuote/RecurringQuoteDeletedActivity.php create mode 100644 app/Listeners/RecurringQuote/RecurringQuoteRestoredActivity.php create mode 100644 app/Listeners/RecurringQuote/UpdateRecurringQuoteActivity.php diff --git a/app/Events/RecurringQuote/RecurringQuoteWasArchived.php b/app/Events/RecurringQuote/RecurringQuoteWasArchived.php new file mode 100644 index 000000000000..769460b68864 --- /dev/null +++ b/app/Events/RecurringQuote/RecurringQuoteWasArchived.php @@ -0,0 +1,47 @@ +recurring_quote = $recurring_quote; + $this->company = $company; + $this->event_vars = $event_vars; + } +} diff --git a/app/Events/RecurringQuote/RecurringQuoteWasCreated.php b/app/Events/RecurringQuote/RecurringQuoteWasCreated.php new file mode 100644 index 000000000000..88814748b3f3 --- /dev/null +++ b/app/Events/RecurringQuote/RecurringQuoteWasCreated.php @@ -0,0 +1,47 @@ +recurring_quote = $recurring_quote; + $this->company = $company; + $this->event_vars = $event_vars; + } +} diff --git a/app/Events/RecurringQuote/RecurringQuoteWasDeleted.php b/app/Events/RecurringQuote/RecurringQuoteWasDeleted.php new file mode 100644 index 000000000000..3c27baa847f1 --- /dev/null +++ b/app/Events/RecurringQuote/RecurringQuoteWasDeleted.php @@ -0,0 +1,47 @@ +recurring_quote = $recurring_quote; + $this->company = $company; + $this->event_vars = $event_vars; + } +} diff --git a/app/Events/RecurringQuote/RecurringQuoteWasRestored.php b/app/Events/RecurringQuote/RecurringQuoteWasRestored.php new file mode 100644 index 000000000000..624c807af690 --- /dev/null +++ b/app/Events/RecurringQuote/RecurringQuoteWasRestored.php @@ -0,0 +1,51 @@ +recurring_quote = $recurring_quote; + $this->fromDeleted = $fromDeleted; + $this->company = $company; + $this->event_vars = $event_vars; + } +} diff --git a/app/Events/RecurringQuote/RecurringQuoteWasUpdated.php b/app/Events/RecurringQuote/RecurringQuoteWasUpdated.php new file mode 100644 index 000000000000..27c704fe2c4c --- /dev/null +++ b/app/Events/RecurringQuote/RecurringQuoteWasUpdated.php @@ -0,0 +1,49 @@ +recurring_quote = $recurring_quote; + $this->company = $company; + $this->event_vars = $event_vars; + } +} diff --git a/app/Listeners/RecurringExpense/CreatedRecurringExpenseActivity.php b/app/Listeners/RecurringExpense/CreatedRecurringExpenseActivity.php new file mode 100644 index 000000000000..12608e17183e --- /dev/null +++ b/app/Listeners/RecurringExpense/CreatedRecurringExpenseActivity.php @@ -0,0 +1,57 @@ +activity_repo = $activity_repo; + } + + /** + * Handle the event. + * + * @param object $event + * @return void + */ + public function handle($event) + { + MultiDB::setDb($event->company->db); + + $recurring_expense = $event->recurring_expense; + + $fields = new stdClass; + + $user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars['user_id'] : $event->recurring_expense->user_id; + + $fields->recurring_expense_id = $recurring_expense->id; + $fields->user_id = $user_id; + $fields->company_id = $recurring_expense->company_id; + $fields->activity_type_id = Activity::ARCHIVE_RECURRING_EXPENSE; + + $this->activity_repo->save($fields, $recurring_expense, $event->event_vars); + } +} diff --git a/app/Listeners/RecurringExpense/RecurringExpenseArchivedActivity.php b/app/Listeners/RecurringExpense/RecurringExpenseArchivedActivity.php new file mode 100644 index 000000000000..12608e17183e --- /dev/null +++ b/app/Listeners/RecurringExpense/RecurringExpenseArchivedActivity.php @@ -0,0 +1,57 @@ +activity_repo = $activity_repo; + } + + /** + * Handle the event. + * + * @param object $event + * @return void + */ + public function handle($event) + { + MultiDB::setDb($event->company->db); + + $recurring_expense = $event->recurring_expense; + + $fields = new stdClass; + + $user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars['user_id'] : $event->recurring_expense->user_id; + + $fields->recurring_expense_id = $recurring_expense->id; + $fields->user_id = $user_id; + $fields->company_id = $recurring_expense->company_id; + $fields->activity_type_id = Activity::ARCHIVE_RECURRING_EXPENSE; + + $this->activity_repo->save($fields, $recurring_expense, $event->event_vars); + } +} diff --git a/app/Listeners/RecurringExpense/RecurringExpenseDeletedActivity.php b/app/Listeners/RecurringExpense/RecurringExpenseDeletedActivity.php new file mode 100644 index 000000000000..236e73b9c420 --- /dev/null +++ b/app/Listeners/RecurringExpense/RecurringExpenseDeletedActivity.php @@ -0,0 +1,55 @@ +activity_repo = $activity_repo; + } + + /** + * Handle the event. + * + * @param object $event + * @return void + */ + public function handle($event) + { + MultiDB::setDb($event->company->db); + + $fields = new stdClass; + + $user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars['user_id'] : $event->recurring_expense->user_id; + + $fields->recurring_expense_id = $event->recurring_expense->id; + $fields->user_id = $user_id; + $fields->company_id = $event->recurring_expense->company_id; + $fields->activity_type_id = Activity::DELETE_RECURRING_EXPENSE; + + $this->activity_repo->save($fields, $event->recurring_expense, $event->event_vars); + } +} diff --git a/app/Listeners/RecurringExpense/RecurringExpenseRestoredActivity.php b/app/Listeners/RecurringExpense/RecurringExpenseRestoredActivity.php new file mode 100644 index 000000000000..d175d105a747 --- /dev/null +++ b/app/Listeners/RecurringExpense/RecurringExpenseRestoredActivity.php @@ -0,0 +1,55 @@ +activity_repo = $activity_repo; + } + + /** + * Handle the event. + * + * @param object $event + * @return void + */ + public function handle($event) + { + MultiDB::setDb($event->company->db); + + $fields = new stdClass; + + $user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars['user_id'] : $event->recurring_expense->user_id; + + $fields->recurring_expense_id = $event->recurring_expense->id; + $fields->user_id = $user_id; + $fields->company_id = $event->recurring_expense->company_id; + $fields->activity_type_id = Activity::RESTORE_RECURRING_EXPENSE; + + $this->activity_repo->save($fields, $event->recurring_expense, $event->event_vars); + } +} diff --git a/app/Listeners/RecurringExpense/RecurringExpenseUpdatedActivity.php b/app/Listeners/RecurringExpense/RecurringExpenseUpdatedActivity.php new file mode 100644 index 000000000000..dd46cc6df960 --- /dev/null +++ b/app/Listeners/RecurringExpense/RecurringExpenseUpdatedActivity.php @@ -0,0 +1,57 @@ +activity_repo = $activity_repo; + } + + /** + * Handle the event. + * + * @param object $event + * @return void + */ + public function handle($event) + { + MultiDB::setDb($event->company->db); + + $recurring_expense = $event->recurring_expense; + + $user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars['user_id'] : $event->recurring_expense->user_id; + + $fields = new stdClass; + + $fields->recurring_expense_id = $recurring_expense->id; + $fields->user_id = $user_id; + $fields->company_id = $recurring_expense->company_id; + $fields->activity_type_id = Activity::UPDATE_RECURRING_EXPENSE; + + $this->activity_repo->save($fields, $recurring_expense, $event->event_vars); + } +} diff --git a/app/Listeners/RecurringQuote/CreateRecurringQuoteActivity.php b/app/Listeners/RecurringQuote/CreateRecurringQuoteActivity.php new file mode 100644 index 000000000000..352bbfd33fd4 --- /dev/null +++ b/app/Listeners/RecurringQuote/CreateRecurringQuoteActivity.php @@ -0,0 +1,56 @@ +activity_repo = $activity_repo; + } + + /** + * Handle the event. + * + * @param object $event + * @return void + */ + public function handle($event) + { + MultiDB::setDb($event->company->db); + + $fields = new stdClass; + + $user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars['user_id'] : $event->recurring_quote->user_id; + + $fields->user_id = $user_id; + $fields->recurring_quote_id = $event->recurring_quote->id; + $fields->client_id = $event->recurring_quote->client_id; + $fields->company_id = $event->recurring_quote->company_id; + $fields->activity_type_id = Activity::CREATE_RECURRING_QUOTE; + + $this->activity_repo->save($fields, $event->recurring_quote, $event->event_vars); + } +} diff --git a/app/Listeners/RecurringQuote/RecurringQuoteArchivedActivity.php b/app/Listeners/RecurringQuote/RecurringQuoteArchivedActivity.php new file mode 100644 index 000000000000..1f977f963612 --- /dev/null +++ b/app/Listeners/RecurringQuote/RecurringQuoteArchivedActivity.php @@ -0,0 +1,57 @@ +activity_repo = $activity_repo; + } + + /** + * Handle the event. + * + * @param object $event + * @return void + */ + public function handle($event) + { + MultiDB::setDb($event->company->db); + + $event->recurring_quote->service()->deletePdf(); + + $fields = new stdClass; + $user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars['user_id'] : $event->recurring_quote->user_id; + + $fields->user_id = $user_id; + $fields->recurring_quote_id = $event->recurring_quote->id; + $fields->client_id = $event->recurring_quote->client_id; + $fields->company_id = $event->recurring_quote->company_id; + $fields->activity_type_id = Activity::ARCHIVE_RECURRING_QUOTE; + + $this->activity_repo->save($fields, $event->recurring_quote, $event->event_vars); + } +} diff --git a/app/Listeners/RecurringQuote/RecurringQuoteDeletedActivity.php b/app/Listeners/RecurringQuote/RecurringQuoteDeletedActivity.php new file mode 100644 index 000000000000..605195cbed2c --- /dev/null +++ b/app/Listeners/RecurringQuote/RecurringQuoteDeletedActivity.php @@ -0,0 +1,55 @@ +activity_repo = $activity_repo; + } + + /** + * Handle the event. + * + * @param object $event + * @return void + */ + public function handle($event) + { + MultiDB::setDb($event->company->db); + + $fields = new stdClass; + $user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars['user_id'] : $event->recurring_quote->user_id; + + $fields->user_id = $user_id; + $fields->recurring_quote_id = $event->recurring_quote->id; + $fields->client_id = $event->recurring_quote->client_id; + $fields->company_id = $event->recurring_quote->company_id; + $fields->activity_type_id = Activity::DELETE_RECURRING_QUOTE; + + $this->activity_repo->save($fields, $event->recurring_quote, $event->event_vars); + } +} diff --git a/app/Listeners/RecurringQuote/RecurringQuoteRestoredActivity.php b/app/Listeners/RecurringQuote/RecurringQuoteRestoredActivity.php new file mode 100644 index 000000000000..16972eb6cc21 --- /dev/null +++ b/app/Listeners/RecurringQuote/RecurringQuoteRestoredActivity.php @@ -0,0 +1,55 @@ +activity_repo = $activity_repo; + } + + /** + * Handle the event. + * + * @param object $event + * @return void + */ + public function handle($event) + { + MultiDB::setDb($event->company->db); + + $fields = new stdClass; + $user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars['user_id'] : $event->recurring_quote->user_id; + + $fields->user_id = $user_id; + $fields->recurring_quote_id = $event->recurring_quote->id; + $fields->client_id = $event->recurring_quote->client_id; + $fields->company_id = $event->recurring_quote->company_id; + $fields->activity_type_id = Activity::RESTORE_RECURRING_QUOTE; + + $this->activity_repo->save($fields, $event->recurring_quote, $event->event_vars); + } +} diff --git a/app/Listeners/RecurringQuote/UpdateRecurringQuoteActivity.php b/app/Listeners/RecurringQuote/UpdateRecurringQuoteActivity.php new file mode 100644 index 000000000000..fb81d3710bc6 --- /dev/null +++ b/app/Listeners/RecurringQuote/UpdateRecurringQuoteActivity.php @@ -0,0 +1,55 @@ +activity_repo = $activity_repo; + } + + /** + * Handle the event. + * + * @param object $event + * @return void + */ + public function handle($event) + { + MultiDB::setDB($event->company->db); + + $fields = new stdClass; + $user_id = array_key_exists('user_id', $event->event_vars) ? $event->event_vars['user_id'] : $event->recurring_quote->user_id; + + $fields->user_id = $user_id; + $fields->client_id = $event->recurring_quote->client_id; + $fields->company_id = $event->recurring_quote->company_id; + $fields->activity_type_id = Activity::UPDATE_RECURRING_QUOTE; + $fields->recurring_quote_id = $event->recurring_quote->id; + + $this->activity_repo->save($fields, $event->recurring_quote, $event->event_vars); + } +} diff --git a/app/Models/Activity.php b/app/Models/Activity.php index b51c9b8cb9b4..595601a1a4e4 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -96,6 +96,18 @@ class Activity extends StaticModel const DELETE_RECURRING_INVOICE = 103; const RESTORE_RECURRING_INVOICE = 104; + const CREATE_RECURRING_QUOTE = 110; + const UPDATE_RECURRING_QUOTE = 111; + const ARCHIVE_RECURRING_QUOTE = 112; + const DELETE_RECURRING_QUOTE = 113; + const RESTORE_RECURRING_QUOTE = 114; + + const CREATE_RECURRING_EXPENSE = 120; + const UPDATE_RECURRING_EXPENSE = 121; + const ARCHIVE_RECURRING_EXPENSE = 122; + const DELETE_RECURRING_EXPENSE = 123; + const RESTORE_RECURRING_EXPENSE = 124; + protected $casts = [ 'is_system' => 'boolean', 'updated_at' => 'timestamp', diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 24164a40e84c..da39760ffa91 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -68,11 +68,21 @@ use App\Events\Quote\QuoteWasEmailed; use App\Events\Quote\QuoteWasRestored; use App\Events\Quote\QuoteWasUpdated; use App\Events\Quote\QuoteWasViewed; +use App\Events\RecurringExpense\RecurringExpenseWasArchived; +use App\Events\RecurringExpense\RecurringExpenseWasCreated; +use App\Events\RecurringExpense\RecurringExpenseWasDeleted; +use App\Events\RecurringExpense\RecurringExpenseWasRestored; +use App\Events\RecurringExpense\RecurringExpenseWasUpdated; use App\Events\RecurringInvoice\RecurringInvoiceWasArchived; use App\Events\RecurringInvoice\RecurringInvoiceWasCreated; use App\Events\RecurringInvoice\RecurringInvoiceWasDeleted; use App\Events\RecurringInvoice\RecurringInvoiceWasRestored; use App\Events\RecurringInvoice\RecurringInvoiceWasUpdated; +use App\Events\RecurringQuote\RecurringQuoteWasArchived; +use App\Events\RecurringQuote\RecurringQuoteWasCreated; +use App\Events\RecurringQuote\RecurringQuoteWasDeleted; +use App\Events\RecurringQuote\RecurringQuoteWasRestored; +use App\Events\RecurringQuote\RecurringQuoteWasUpdated; use App\Events\Subscription\SubscriptionWasArchived; use App\Events\Subscription\SubscriptionWasCreated; use App\Events\Subscription\SubscriptionWasDeleted; @@ -174,6 +184,11 @@ use App\Listeners\RecurringInvoice\RecurringInvoiceArchivedActivity; use App\Listeners\RecurringInvoice\RecurringInvoiceDeletedActivity; use App\Listeners\RecurringInvoice\RecurringInvoiceRestoredActivity; use App\Listeners\RecurringInvoice\UpdateRecurringInvoiceActivity; +use App\Listeners\RecurringQuote\CreateRecurringQuoteActivity; +use App\Listeners\RecurringQuote\RecurringQuoteArchivedActivity; +use App\Listeners\RecurringQuote\RecurringQuoteDeletedActivity; +use App\Listeners\RecurringQuote\RecurringQuoteRestoredActivity; +use App\Listeners\RecurringQuote\UpdateRecurringQuoteActivity; use App\Listeners\SendVerificationNotification; use App\Listeners\User\ArchivedUserActivity; use App\Listeners\User\CreatedUserActivity; @@ -410,6 +425,36 @@ class EventServiceProvider extends ServiceProvider QuoteWasRestored::class => [ QuoteRestoredActivity::class, ], + RecurringExpenseWasCreated::class => [ + CreatedRecurringExpenseActivity::class, + ], + RecurringExpenseWasUpdated::class => [ + RecurringExpenseUpdatedActivity::class, + ], + RecurringExpenseWasArchived::class => [ + RecurringExpenseArchivedActivity::class, + ], + RecurringExpenseWasDeleted::class => [ + RecurringExpenseDeletedActivity::class, + ], + RecurringExpenseWasRestored::class => [ + RecurringExpenseRestoredActivity::class + ], + RecurringQuoteWasUpdated::class => [ + UpdateRecurringQuoteActivity::class, + ], + RecurringQuoteWasCreated::class => [ + CreateRecurringQuoteActivity::class, + ], + RecurringQuoteWasDeleted::class => [ + RecurringQuoteDeletedActivity::class, + ], + RecurringQuoteWasArchived::class => [ + RecurringQuoteArchivedActivity::class, + ], + RecurringQuoteWasRestored::class => [ + RecurringQuoteRestoredActivity::class, + ], RecurringInvoiceWasUpdated::class => [ UpdateRecurringInvoiceActivity::class, ], diff --git a/routes/api.php b/routes/api.php index 3b41ab6888fb..db788698b12b 100644 --- a/routes/api.php +++ b/routes/api.php @@ -141,8 +141,6 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::post('recurring_quotes/bulk', 'RecurringQuoteController@bulk')->name('recurring_quotes.bulk'); Route::put('recurring_quotes/{recurring_quote}/upload', 'RecurringQuoteController@upload'); - Route::post('recurring_quotes/bulk', 'RecurringQuoteController@bulk')->name('recurring_quotes.bulk'); - Route::post('refresh', 'Auth\LoginController@refresh'); Route::get('scheduler', 'SchedulerController@index'); From 7fe6dea1d3c6f93427eea9b0bdb53d351d579308 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 24 Aug 2021 15:56:12 +1000 Subject: [PATCH 10/24] Recurring quotes --- app/Http/Controllers/RecurringQuoteController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/RecurringQuoteController.php b/app/Http/Controllers/RecurringQuoteController.php index 6081035073ad..286fba9f7790 100644 --- a/app/Http/Controllers/RecurringQuoteController.php +++ b/app/Http/Controllers/RecurringQuoteController.php @@ -577,7 +577,7 @@ class RecurringQuoteController extends BaseController public function action(ActionRecurringQuoteRequest $request, RecurringQuote $recurring_quote, $action) { switch ($action) { - case 'clone_to_RecurringQuote': + case 'clone_to_recurring_quote': // $recurring_invoice = CloneRecurringQuoteFactory::create($recurring_invoice, auth()->user()->id); // return $this->itemResponse($recurring_invoice); break; From 4bd3f382cb36bd20ddb5fe38de1734f028da091f Mon Sep 17 00:00:00 2001 From: = Date: Tue, 24 Aug 2021 21:24:03 +1000 Subject: [PATCH 11/24] Recurring Expense Cron --- .../RecurringExpenseToExpenseFactory.php | 62 +++++++++++++ app/Jobs/Cron/RecurringExpensesCron.php | 88 +++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 app/Factory/RecurringExpenseToExpenseFactory.php create mode 100644 app/Jobs/Cron/RecurringExpensesCron.php diff --git a/app/Factory/RecurringExpenseToExpenseFactory.php b/app/Factory/RecurringExpenseToExpenseFactory.php new file mode 100644 index 000000000000..76a3b0456e9e --- /dev/null +++ b/app/Factory/RecurringExpenseToExpenseFactory.php @@ -0,0 +1,62 @@ +user_id = $recurring_expense->user_id; + $expense->assigned_user_id = $recurring_expense->assigned_user_id; + $expense->vendor_id = $recurring_expense->vendor_id; + $expense->invoice_id = $recurring_expense->invoice_id; + $expense->currency_id = $recurring_expense->currency_id; + $expense->company_id = $recurring_expense->company_id; + $expense->bank_id = $recurring_expense->bank_id; + $expense->exchange_rate = $recurring_expense->exchange_rate; + $expense->is_deleted = false; + $expense->should_be_invoiced = $recurring_expense->should_be_invoiced; + $expense->tax_name1 = $recurring_expense->tax_name1; + $expense->tax_rate1 = $recurring_expense->tax_rate1; + $expense->tax_name2 = $recurring_expense->tax_name2; + $expense->tax_rate2 = $recurring_expense->tax_rate2; + $expense->tax_name3 = $recurring_expense->tax_name3; + $expense->tax_rate3 = $recurring_expense->tax_rate3; + $expense->date = now()->format('Y-m-d'); + $expense->payment_date = $recurring_expense->payment_date; + $expense->amount = $recurring_expense->amount; + $expense->foreign_amount = $recurring_expense->foreign_amount; + $expense->private_notes = $recurring_expense->private_notes; + $expense->public_notes = $recurring_expense->public_notes; + $expense->transaction_reference = $recurring_expense->transaction_reference; + $expense->custom_value1 = $recurring_expense->custom_value1; + $expense->custom_value2 = $recurring_expense->custom_value2; + $expense->custom_value3 = $recurring_expense->custom_value3; + $expense->custom_value4 = $recurring_expense->custom_value4; + $expense->transaction_id = $recurring_expense->transaction_id; + $expense->category_id = $recurring_expense->category_id; + $expense->payment_type_id = $recurring_expense->payment_type_id; + $expense->project_id = $recurring_expense->project_id; + $expense->invoice_documents = $recurring_expense->invoice_documents; + $expense->tax_amount1 = $recurring_expense->tax_amount1; + $expense->tax_amount2 = $recurring_expense->tax_amount2; + $expense->tax_amount3 = $recurring_expense->tax_amount3; + $expense->uses_inclusive_taxes = $recurring_expense->uses_inclusive_taxes; + $expense->calculate_tax_by_amount = $recurring_expense->calculate_tax_by_amount; + + return $expense; + } +} diff --git a/app/Jobs/Cron/RecurringExpensesCron.php b/app/Jobs/Cron/RecurringExpensesCron.php new file mode 100644 index 000000000000..707cdbe3843c --- /dev/null +++ b/app/Jobs/Cron/RecurringExpensesCron.php @@ -0,0 +1,88 @@ +format('Y-m-d h:i:s')); + + if (! config('ninja.db.multi_db_enabled')) { + + $this->getRecurringExpenses(); + + } else { + //multiDB environment, need to + foreach (MultiDB::$dbs as $db) { + MultiDB::setDB($db); + + $this->getRecurringExpenses(); + } + } + } + + private function getRecurringExpenses() + { + $recurring_expenses = RecurringExpense::where('next_send_date', '<=', now()->toDateTimeString()) + ->whereNotNull('next_send_date') + ->whereNull('deleted_at') + ->where('remaining_cycles', '!=', '0') + // ->whereHas('client', function ($query) { + // $query->where('is_deleted',0) + // ->where('deleted_at', NULL); + // }) + ->with('company') + ->cursor(); + + nlog(now()->format('Y-m-d') . ' Generating Recurring Expenses. Count = '.$recurring_expenses->count()); + + $recurring_expenses->each(function ($recurring_expense, $key) { + nlog("Current date = " . now()->format("Y-m-d") . " Recurring date = " .$recurring_expense->next_send_date); + + if (!$recurring_expense->company->is_disabled) { + $this->generateExpense($recurring_expense); + } + }); + } + + private function generateExpense(RecurringExpense $recurring_expense) + { + + } + +} From 577eedb457e1ee6fc879a23cfa2d5e025048ce08 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 24 Aug 2021 21:57:51 +1000 Subject: [PATCH 12/24] Recurring expenses cron job --- app/Factory/RecurringExpenseFactory.php | 6 ++ app/Jobs/Cron/RecurringExpensesCron.php | 9 ++- ...08_23_101529_recurring_expenses_schema.php | 18 ++--- tests/Unit/RecurringExpenseCloneTest.php | 69 +++++++++++++++++++ 4 files changed, 92 insertions(+), 10 deletions(-) create mode 100644 tests/Unit/RecurringExpenseCloneTest.php diff --git a/app/Factory/RecurringExpenseFactory.php b/app/Factory/RecurringExpenseFactory.php index 08562266f9c8..bf72ac0997ed 100644 --- a/app/Factory/RecurringExpenseFactory.php +++ b/app/Factory/RecurringExpenseFactory.php @@ -21,6 +21,7 @@ class RecurringExpenseFactory $recurring_expense->user_id = $user_id; $recurring_expense->company_id = $company_id; $recurring_expense->is_deleted = false; + $recurring_expense->invoice_documents = false; $recurring_expense->should_be_invoiced = false; $recurring_expense->tax_name1 = ''; $recurring_expense->tax_rate1 = 0; @@ -28,6 +29,9 @@ class RecurringExpenseFactory $recurring_expense->tax_rate2 = 0; $recurring_expense->tax_name3 = ''; $recurring_expense->tax_rate3 = 0; + $recurring_expense->tax_amount1 = 0; + $recurring_expense->tax_amount2 = 0; + $recurring_expense->tax_amount3 = 0; $recurring_expense->date = null; $recurring_expense->payment_date = null; $recurring_expense->amount = 0; @@ -39,6 +43,8 @@ class RecurringExpenseFactory $recurring_expense->custom_value2 = ''; $recurring_expense->custom_value3 = ''; $recurring_expense->custom_value4 = ''; + $recurring_expense->uses_inclusive_taxes = true; + $recurring_expense->calculate_tax_by_amount = true; return $recurring_expense; } diff --git a/app/Jobs/Cron/RecurringExpensesCron.php b/app/Jobs/Cron/RecurringExpensesCron.php index 707cdbe3843c..a08f4eeedbeb 100644 --- a/app/Jobs/Cron/RecurringExpensesCron.php +++ b/app/Jobs/Cron/RecurringExpensesCron.php @@ -11,15 +11,18 @@ namespace App\Jobs\Cron; +use App\Factory\RecurringExpenseToExpenseFactory; use App\Jobs\RecurringInvoice\SendRecurring; use App\Libraries\MultiDB; use App\Models\RecurringExpense; +use App\Utils\Traits\GeneratesCounter; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Support\Carbon; class RecurringExpensesCron { use Dispatchable; + use GeneratesCounter; public $tries = 1; @@ -82,7 +85,11 @@ class RecurringExpensesCron private function generateExpense(RecurringExpense $recurring_expense) { + $expense = RecurringExpenseToExpenseFactory::create($recurring_expense); + $expense->save(); + $expense->number = $this->getNextExpenseNumber($expense); + $expense->save(); } - + } diff --git a/database/migrations/2021_08_23_101529_recurring_expenses_schema.php b/database/migrations/2021_08_23_101529_recurring_expenses_schema.php index 19d43dd3b43f..42d90a8d6927 100644 --- a/database/migrations/2021_08_23_101529_recurring_expenses_schema.php +++ b/database/migrations/2021_08_23_101529_recurring_expenses_schema.php @@ -37,7 +37,7 @@ class RecurringExpensesSchema extends Migration $table->date('date')->nullable(); $table->date('payment_date')->nullable(); $table->boolean('should_be_invoiced')->default(false); - $table->boolean('invoice_documents')->default(); + $table->boolean('invoice_documents')->default(false); $table->string('transaction_id')->nullable(); $table->string('custom_value1')->nullable(); $table->string('custom_value2')->nullable(); @@ -46,14 +46,14 @@ class RecurringExpensesSchema extends Migration $table->unsignedInteger('category_id')->nullable(); $table->boolean('calculate_tax_by_amount')->default(false); - $table->decimal('tax_amount1', 20, 6); - $table->decimal('tax_amount2', 20, 6); - $table->decimal('tax_amount3', 20, 6); - $table->decimal('tax_rate1', 20, 6); - $table->decimal('tax_rate2', 20, 6); - $table->decimal('tax_rate3', 20, 6); - $table->decimal('amount', 20, 6); - $table->decimal('foreign_amount', 20, 6); + $table->decimal('tax_amount1', 20, 6)->nullable(); + $table->decimal('tax_amount2', 20, 6)->nullable(); + $table->decimal('tax_amount3', 20, 6)->nullable(); + $table->decimal('tax_rate1', 20, 6)->nullable(); + $table->decimal('tax_rate2', 20, 6)->nullable(); + $table->decimal('tax_rate3', 20, 6)->nullable(); + $table->decimal('amount', 20, 6)->nullable(); + $table->decimal('foreign_amount', 20, 6)->nullable(); $table->decimal('exchange_rate', 20, 6)->default(1); $table->unsignedInteger('assigned_user_id')->nullable(); $table->string('number')->nullable(); diff --git a/tests/Unit/RecurringExpenseCloneTest.php b/tests/Unit/RecurringExpenseCloneTest.php new file mode 100644 index 000000000000..6dee502b112c --- /dev/null +++ b/tests/Unit/RecurringExpenseCloneTest.php @@ -0,0 +1,69 @@ +faker = \Faker\Factory::create(); + + } + + public function testBadBase64String() + { + $account = Account::factory()->create(); + $user = User::factory()->create(['account_id' => $account->id, 'email' => $this->faker->unique()->safeEmail]); + $company = Company::factory()->create(['account_id' => $account->id]); + + $client = Client::factory()->create([ + 'user_id' => $user->id, + 'company_id' => $company->id, + ]); + + $recurring_expense = RecurringExpenseFactory::create($company->id, $user->id); + $recurring_expense->date = now(); + $recurring_expense->amount = 10; + $recurring_expense->foreign_amount = 20; + $recurring_expense->exchange_rate = 0.5; + $recurring_expense->private_notes = "private"; + $recurring_expense->public_notes = "public"; + $recurring_expense->custom_value4 = "custom4"; + $recurring_expense->should_be_invoiced = true; + + $recurring_expense->save(); + + $expense = RecurringExpenseToExpenseFactory::create($recurring_expense); + $expense->save(); + + $this->assertNotNull($expense); + $this->assertEquals(20, $expense->foreign_amount); + $this->assertEquals(10, $expense->amount); + } + +} \ No newline at end of file From 8b57f386bb1b4cb5adb13a4b66e66e4da36a81c7 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 24 Aug 2021 21:59:21 +1000 Subject: [PATCH 13/24] Register Recurring Expenses Cron --- app/Console/Kernel.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 10cb1bb3f95c..d9bfd1342fb1 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -12,6 +12,7 @@ namespace App\Console; use App\Jobs\Cron\AutoBillCron; +use App\Jobs\Cron\RecurringExpensesCron; use App\Jobs\Cron\RecurringInvoicesCron; use App\Jobs\Cron\SubscriptionCron; use App\Jobs\Ninja\AdjustEmailQuota; @@ -61,7 +62,9 @@ class Kernel extends ConsoleKernel $schedule->job(new SubscriptionCron)->daily()->withoutOverlapping(); $schedule->job(new RecurringInvoicesCron)->hourly()->withoutOverlapping(); - + + $schedule->job(new RecurringExpensesCron)->dailyAt('23:45')->withoutOverlapping(); + $schedule->job(new AutoBillCron)->dailyAt('00:30')->withoutOverlapping(); $schedule->job(new SchedulerCheck)->daily()->withoutOverlapping(); From c7ef40a7afa8ceca4d9a55d355681a5856e595b1 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 27 Aug 2021 20:00:07 +1000 Subject: [PATCH 14/24] Rename file recurring expense activity --- .../RecurringExpense/CreatedRecurringExpenseActivity.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Listeners/RecurringExpense/CreatedRecurringExpenseActivity.php b/app/Listeners/RecurringExpense/CreatedRecurringExpenseActivity.php index 12608e17183e..b84c5baa48f8 100644 --- a/app/Listeners/RecurringExpense/CreatedRecurringExpenseActivity.php +++ b/app/Listeners/RecurringExpense/CreatedRecurringExpenseActivity.php @@ -17,7 +17,7 @@ use App\Repositories\ActivityRepository; use Illuminate\Contracts\Queue\ShouldQueue; use stdClass; -class RecurringExpenseArchivedActivity implements ShouldQueue +class CreatedRecurringExpenseActivity implements ShouldQueue { protected $activity_repo; @@ -50,7 +50,7 @@ class RecurringExpenseArchivedActivity implements ShouldQueue $fields->recurring_expense_id = $recurring_expense->id; $fields->user_id = $user_id; $fields->company_id = $recurring_expense->company_id; - $fields->activity_type_id = Activity::ARCHIVE_RECURRING_EXPENSE; + $fields->activity_type_id = Activity::CREATE_RECURRING_EXPENSE; $this->activity_repo->save($fields, $recurring_expense, $event->event_vars); } From da2070af5355f9a92c61510fa49b9a488afedf4d Mon Sep 17 00:00:00 2001 From: = Date: Fri, 27 Aug 2021 20:05:53 +1000 Subject: [PATCH 15/24] Fixes for Recurring Expense Transformer --- app/Models/RecurringExpense.php | 1 - app/Transformers/RecurringExpenseTransformer.php | 8 ++++++-- .../2021_08_23_101529_recurring_expenses_schema.php | 1 - 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/Models/RecurringExpense.php b/app/Models/RecurringExpense.php index 87e8b6911243..8e766cc66bc1 100644 --- a/app/Models/RecurringExpense.php +++ b/app/Models/RecurringExpense.php @@ -57,7 +57,6 @@ class RecurringExpense extends BaseModel 'uses_inclusive_taxes', 'calculate_tax_by_amount', 'frequency_id', - 'start_date', 'last_sent_date', 'next_send_date', 'remaining_cycles', diff --git a/app/Transformers/RecurringExpenseTransformer.php b/app/Transformers/RecurringExpenseTransformer.php index f2da8f6a0831..1323421a3a97 100644 --- a/app/Transformers/RecurringExpenseTransformer.php +++ b/app/Transformers/RecurringExpenseTransformer.php @@ -80,7 +80,6 @@ class RecurringExpenseTransformer extends EntityTransformer 'transaction_reference' => (string) $recurring_expense->transaction_reference ?: '', 'transaction_id' => (string) $recurring_expense->transaction_id ?: '', 'date' => $recurring_expense->date ?: '', - //'recurring_expense_date' => $recurring_expense->date ?: '', 'number' => (string)$recurring_expense->number ?: '', 'payment_date' => $recurring_expense->payment_date ?: '', 'custom_value1' => $recurring_expense->custom_value1 ?: '', @@ -96,6 +95,11 @@ class RecurringExpenseTransformer extends EntityTransformer 'tax_amount3' => (float) $recurring_expense->tax_amount3, 'uses_inclusive_taxes' => (bool) $recurring_expense->uses_inclusive_taxes, 'calculate_tax_by_amount' => (bool) $recurring_expense->calculate_tax_by_amount, + 'entity_type' => 'recurringExpense', + 'frequency_id' => (string) $recurring_expense->frequency_id, + 'remaining_cycles' => (int) $recurring_expense->remaining_cycles, + 'last_sent_date' => $recurring_expense->last_sent_date ?: '', + 'next_send_date' => $recurring_expense->next_send_date ?: '', ]; } -} +} \ No newline at end of file diff --git a/database/migrations/2021_08_23_101529_recurring_expenses_schema.php b/database/migrations/2021_08_23_101529_recurring_expenses_schema.php index 42d90a8d6927..48ac3a54115e 100644 --- a/database/migrations/2021_08_23_101529_recurring_expenses_schema.php +++ b/database/migrations/2021_08_23_101529_recurring_expenses_schema.php @@ -64,7 +64,6 @@ class RecurringExpensesSchema extends Migration $table->text('transaction_reference')->nullable(); $table->unsignedInteger('frequency_id'); - $table->datetime('start_date')->nullable(); $table->datetime('last_sent_date')->nullable(); $table->datetime('next_send_date')->nullable(); $table->unsignedInteger('remaining_cycles')->nullable(); From d3c4e1080a91440f6191b67846a58d99d9f4d36c Mon Sep 17 00:00:00 2001 From: = Date: Fri, 27 Aug 2021 20:45:09 +1000 Subject: [PATCH 16/24] Fixes for Recurring Expense Transformer --- app/Jobs/Util/Import.php | 3 ++- app/Providers/EventServiceProvider.php | 1 + app/Transformers/ExpenseTransformer.php | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/Jobs/Util/Import.php b/app/Jobs/Util/Import.php index 664c09afa9b1..cbcc7bd30b7c 100644 --- a/app/Jobs/Util/Import.php +++ b/app/Jobs/Util/Import.php @@ -132,8 +132,8 @@ class Import implements ShouldQueue 'projects', 'products', 'credits', - 'invoices', 'recurring_invoices', + 'invoices', 'quotes', 'payments', 'expense_categories', @@ -910,6 +910,7 @@ class Import implements ShouldQueue } $modified['client_id'] = $this->transformId('clients', $resource['client_id']); + $modified['recurring_id'] = $this->transformId('recurring_invoices', $resource['recurring_id']); $modified['user_id'] = $this->processUserId($resource); $modified['company_id'] = $this->company->id; $modified['line_items'] = $this->cleanItems($modified['line_items']); diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index da39760ffa91..d993288a5fb8 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -179,6 +179,7 @@ use App\Listeners\Quote\QuoteEmailedNotification; use App\Listeners\Quote\QuoteRestoredActivity; use App\Listeners\Quote\QuoteViewedActivity; use App\Listeners\Quote\ReachWorkflowSettings; +use App\Listeners\RecurringExpense\CreatedRecurringExpenseActivity; use App\Listeners\RecurringInvoice\CreateRecurringInvoiceActivity; use App\Listeners\RecurringInvoice\RecurringInvoiceArchivedActivity; use App\Listeners\RecurringInvoice\RecurringInvoiceDeletedActivity; diff --git a/app/Transformers/ExpenseTransformer.php b/app/Transformers/ExpenseTransformer.php index 21908884ba5e..4937fbb4f4fd 100644 --- a/app/Transformers/ExpenseTransformer.php +++ b/app/Transformers/ExpenseTransformer.php @@ -96,6 +96,7 @@ class ExpenseTransformer extends EntityTransformer 'tax_amount3' => (float) $expense->tax_amount3, 'uses_inclusive_taxes' => (bool) $expense->uses_inclusive_taxes, 'calculate_tax_by_amount' => (bool) $expense->calculate_tax_by_amount, + 'entity_type' => 'expense', ]; } } From a17227f2ca6ffe2d622533f364715d81bc01b7e5 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 29 Aug 2021 20:34:53 +1000 Subject: [PATCH 17/24] Recurring Invoices query --- app/Http/Controllers/BaseController.php | 8 ++++++++ app/Models/Company.php | 8 ++++++++ app/Transformers/CompanyTransformer.php | 12 +++++++++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php index 033e44816e95..da284f23e370 100644 --- a/app/Http/Controllers/BaseController.php +++ b/app/Http/Controllers/BaseController.php @@ -84,6 +84,7 @@ class BaseController extends Controller 'company.payments.documents', 'company.payment_terms.company', 'company.projects.documents', + 'company.recurring_expenses', 'company.recurring_invoices', 'company.recurring_invoices.invitations.contact', 'company.recurring_invoices.invitations.company', @@ -304,6 +305,13 @@ class BaseController extends Controller if(!$user->hasPermission('view_recurring_invoice')) $query->where('recurring_invoices.user_id', $user->id)->orWhere('recurring_invoices.assigned_user_id', $user->id); + }, + 'company.recurring_expenses'=> function ($query) use ($updated_at, $user) { + $query->where('updated_at', '>=', $updated_at)->with('documents'); + + if(!$user->hasPermission('view_recurring_expense')) + $query->where('recurring_expenses.user_id', $user->id)->orWhere('recurring_expenses.assigned_user_id', $user->id); + }, 'company.tasks'=> function ($query) use ($updated_at, $user) { $query->where('updated_at', '>=', $updated_at)->with('documents'); diff --git a/app/Models/Company.php b/app/Models/Company.php index 2d6e9badd87a..b4450370b0ce 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -259,6 +259,14 @@ class Company extends BaseModel return $this->hasMany(RecurringInvoice::class)->withTrashed(); } + /** + * @return HasMany + */ + public function recurring_expenses() + { + return $this->hasMany(RecurringExpense::class)->withTrashed(); + } + /** * @return HasMany */ diff --git a/app/Transformers/CompanyTransformer.php b/app/Transformers/CompanyTransformer.php index daf1f802941d..9183bbacf3aa 100644 --- a/app/Transformers/CompanyTransformer.php +++ b/app/Transformers/CompanyTransformer.php @@ -13,7 +13,6 @@ namespace App\Transformers; use App\Models\Account; use App\Models\Activity; -use App\Models\Subscription; use App\Models\Client; use App\Models\Company; use App\Models\CompanyGateway; @@ -31,13 +30,16 @@ use App\Models\PaymentTerm; use App\Models\Product; use App\Models\Project; use App\Models\Quote; +use App\Models\RecurringExpense; use App\Models\RecurringInvoice; +use App\Models\Subscription; use App\Models\SystemLog; use App\Models\Task; use App\Models\TaskStatus; use App\Models\TaxRate; use App\Models\User; use App\Models\Webhook; +use App\Transformers\RecurringExpenseTransformer; use App\Utils\Traits\MakesHash; use stdClass; @@ -92,6 +94,7 @@ class CompanyTransformer extends EntityTransformer 'expense_categories', 'task_statuses', 'subscriptions', + 'recurring_expenses', ]; /** @@ -295,6 +298,13 @@ class CompanyTransformer extends EntityTransformer return $this->includeCollection($company->recurring_invoices, $transformer, RecurringInvoice::class); } + public function includeRecurringExpenses(Company $company) + { + $transformer = new RecurringExpenseTransformer($this->serializer); + + return $this->includeCollection($company->recurring_expenses, $transformer, RecurringExpense::class); + } + public function includeQuotes(Company $company) { $transformer = new QuoteTransformer($this->serializer); From c4f50fadf71c17af32aa99e09706c39992308fd2 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 29 Aug 2021 22:18:15 +1000 Subject: [PATCH 18/24] Import Recurring Expenses --- app/Jobs/Util/Import.php | 64 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/app/Jobs/Util/Import.php b/app/Jobs/Util/Import.php index cbcc7bd30b7c..9f0f64690cd8 100644 --- a/app/Jobs/Util/Import.php +++ b/app/Jobs/Util/Import.php @@ -59,6 +59,7 @@ use App\Models\PaymentTerm; use App\Models\Product; use App\Models\Project; use App\Models\Quote; +use App\Models\RecurringExpense; use App\Models\RecurringInvoice; use App\Models\Task; use App\Models\TaskStatus; @@ -139,6 +140,7 @@ class Import implements ShouldQueue 'expense_categories', 'task_statuses', 'expenses', + 'recurring_expenses', 'tasks', 'documents', ]; @@ -814,6 +816,68 @@ class Import implements ShouldQueue $product_repository = null; } + private function processRecurringExpenses(array $data) :void + { + RecurringExpense::unguard(); + + $rules = [ + '*.amount' => ['numeric'], + ]; + + $validator = Validator::make($data, $rules); + + if ($validator->fails()) { + throw new MigrationValidatorFailed(json_encode($validator->errors())); + } + + foreach ($data as $resource) { + $modified = $resource; + + unset($modified['id']); + + $modified['company_id'] = $this->company->id; + $modified['user_id'] = $this->processUserId($resource); + + if (isset($resource['client_id'])) { + $modified['client_id'] = $this->transformId('clients', $resource['client_id']); + } + + if (isset($resource['category_id'])) { + $modified['category_id'] = $this->transformId('expense_categories', $resource['category_id']); + } + + if (isset($resource['vendor_id'])) { + $modified['vendor_id'] = $this->transformId('vendors', $resource['vendor_id']); + } + + $expense = RecurringExpense::create($modified); + + if(array_key_exists('created_at', $modified)) + $expense->created_at = Carbon::parse($modified['created_at']); + + if(array_key_exists('updated_at', $modified)) + $expense->updated_at = Carbon::parse($modified['updated_at']); + + $expense->save(['timestamps' => false]); + + $old_user_key = array_key_exists('user_id', $resource) ?? $this->user->id; + + $key = "recurring_expenses_{$resource['id']}"; + + $this->ids['recurring_expenses'][$key] = [ + 'old' => $resource['id'], + 'new' => $expense->id, + ]; + + } + + + RecurringExpense::reguard(); + + /*Improve memory handling by setting everything to null when we have finished*/ + $data = null; + } + private function processRecurringInvoices(array $data) :void { RecurringInvoice::unguard(); From d1ddfa16cdf2970b1c7aeb0b844e0bd8b697d8a5 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 29 Aug 2021 22:39:50 +1000 Subject: [PATCH 19/24] Recurring Expenses import --- app/Jobs/Util/Import.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/Jobs/Util/Import.php b/app/Jobs/Util/Import.php index 9f0f64690cd8..a7dbc17f9265 100644 --- a/app/Jobs/Util/Import.php +++ b/app/Jobs/Util/Import.php @@ -974,7 +974,10 @@ class Import implements ShouldQueue } $modified['client_id'] = $this->transformId('clients', $resource['client_id']); - $modified['recurring_id'] = $this->transformId('recurring_invoices', $resource['recurring_id']); + + if(array_key_exists('recurring_id', $resource) && !is_null($resource['recurring_id'])) + $modified['recurring_id'] = $this->transformId('recurring_invoices', (string)$resource['recurring_id']); + $modified['user_id'] = $this->processUserId($resource); $modified['company_id'] = $this->company->id; $modified['line_items'] = $this->cleanItems($modified['line_items']); From 8ee9261baa41977f7bd70ab39702d7b56535fc32 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 6 Sep 2021 06:59:00 +1000 Subject: [PATCH 20/24] Minor fix for store request validation --- .../Requests/RecurringExpense/StoreRecurringExpenseRequest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/Http/Requests/RecurringExpense/StoreRecurringExpenseRequest.php b/app/Http/Requests/RecurringExpense/StoreRecurringExpenseRequest.php index 706962e7aa77..e34918245585 100644 --- a/app/Http/Requests/RecurringExpense/StoreRecurringExpenseRequest.php +++ b/app/Http/Requests/RecurringExpense/StoreRecurringExpenseRequest.php @@ -41,6 +41,8 @@ class StoreRecurringExpenseRequest extends Request if(!empty($this->client_id)) $rules['client_id'] = 'bail|sometimes|exists:clients,id,company_id,'.auth()->user()->company()->id; + $rules['frequency_id'] = 'required|integer|digits_between:1,12'; + return $this->globalRules($rules); } From 1466ecfcfe54b32a6359f27101d8309b650eefa1 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 6 Sep 2021 07:48:41 +1000 Subject: [PATCH 21/24] Minor fixes for recurring expenses - add field to activities table --- ..._04_13_013424_add_subscription_id_to_activities_table.php | 3 --- .../2021_08_23_101529_recurring_expenses_schema.php | 5 +++++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/database/migrations/2021_04_13_013424_add_subscription_id_to_activities_table.php b/database/migrations/2021_04_13_013424_add_subscription_id_to_activities_table.php index 9b8a389ac5b4..db32a6854104 100644 --- a/database/migrations/2021_04_13_013424_add_subscription_id_to_activities_table.php +++ b/database/migrations/2021_04_13_013424_add_subscription_id_to_activities_table.php @@ -25,8 +25,5 @@ class AddSubscriptionIdToActivitiesTable extends Migration */ public function down() { - Schema::table('activities', function (Blueprint $table) { - // - }); } } diff --git a/database/migrations/2021_08_23_101529_recurring_expenses_schema.php b/database/migrations/2021_08_23_101529_recurring_expenses_schema.php index 42d90a8d6927..c57079f2e5a1 100644 --- a/database/migrations/2021_08_23_101529_recurring_expenses_schema.php +++ b/database/migrations/2021_08_23_101529_recurring_expenses_schema.php @@ -78,6 +78,11 @@ class RecurringExpensesSchema extends Migration }); + Schema::table('activities', function (Blueprint $table) { + $table->unsignedInteger('recurring_expense_id')->nullable(); + $table->unsignedInteger('recurring_quote_id')->nullable(); + }); + Schema::table('recurring_quotes', function ($table){ $table->string('auto_bill')->default('off'); $table->boolean('auto_bill_enabled')->default(0); From 65bc26ab79a1cd54f249989e4eaac90cedd1586d Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 7 Sep 2021 09:02:23 +1000 Subject: [PATCH 22/24] Start and Stop Recurring Expenses --- app/Factory/RecurringExpenseFactory.php | 2 + .../RecurringExpenseController.php | 54 ++++++++++++++++--- .../RecurringInvoiceController.php | 1 - app/Models/RecurringExpense.php | 9 ++++ app/Providers/EventServiceProvider.php | 4 ++ .../RecurringExpenseTransformer.php | 1 + ...08_23_101529_recurring_expenses_schema.php | 1 + tests/Feature/RecurringExpenseApiTest.php | 44 ++++++++++++++- tests/MockAccountData.php | 2 + 9 files changed, 108 insertions(+), 10 deletions(-) diff --git a/app/Factory/RecurringExpenseFactory.php b/app/Factory/RecurringExpenseFactory.php index bf72ac0997ed..8d179a33d113 100644 --- a/app/Factory/RecurringExpenseFactory.php +++ b/app/Factory/RecurringExpenseFactory.php @@ -12,12 +12,14 @@ namespace App\Factory; use App\Models\RecurringExpense; +use App\Models\RecurringInvoice; class RecurringExpenseFactory { public static function create(int $company_id, int $user_id) :RecurringExpense { $recurring_expense = new RecurringExpense(); + $recurring_expense->status_id = RecurringInvoice::STATUS_DRAFT; $recurring_expense->user_id = $user_id; $recurring_expense->company_id = $company_id; $recurring_expense->is_deleted = false; diff --git a/app/Http/Controllers/RecurringExpenseController.php b/app/Http/Controllers/RecurringExpenseController.php index 23e79186c1b9..a3bbf630f251 100644 --- a/app/Http/Controllers/RecurringExpenseController.php +++ b/app/Http/Controllers/RecurringExpenseController.php @@ -495,21 +495,59 @@ class RecurringExpenseController extends BaseController $recurring_expenses->each(function ($recurring_expense, $key) use ($action) { if (auth()->user()->can('edit', $recurring_expense)) { - $this->recurring_expense_repo->{$action}($recurring_expense); + $this->performAction($recurring_expense, $action, true); } }); return $this->listResponse(RecurringExpense::withTrashed()->whereIn('id', $this->transformKeys($ids))); } - /** - * Returns a client statement. - * - * @return void [type] [description] - */ - public function statement() + private function performAction(RecurringExpense $recurring_expense, string $action, $bulk = false) { - //todo + switch ($action) { + case 'archive': + $this->recurring_expense_repo->archive($recurring_expense); + + if (! $bulk) { + return $this->listResponse($recurring_expense); + } + break; + case 'restore': + $this->recurring_expense_repo->restore($recurring_expense); + + if (! $bulk) { + return $this->listResponse($recurring_expense); + } + break; + case 'delete': + $this->recurring_expense_repo->delete($recurring_expense); + + if (! $bulk) { + return $this->listResponse($recurring_expense); + } + break; + case 'email': + //dispatch email to queue + break; + case 'start': + $recurring_expense = $recurring_expense->service()->start()->save(); + + if (! $bulk) { + $this->itemResponse($recurring_expense); + } + break; + case 'stop': + $recurring_expense = $recurring_expense->service()->stop()->save(); + + if (! $bulk) { + $this->itemResponse($recurring_expense); + } + + break; + default: + // code... + break; + } } /** diff --git a/app/Http/Controllers/RecurringInvoiceController.php b/app/Http/Controllers/RecurringInvoiceController.php index 3bf2ea4e0809..cd81b2d51ba5 100644 --- a/app/Http/Controllers/RecurringInvoiceController.php +++ b/app/Http/Controllers/RecurringInvoiceController.php @@ -31,7 +31,6 @@ use App\Utils\Ninja; use App\Utils\Traits\MakesHash; use App\Utils\Traits\SavesDocuments; use Carbon\Carbon; -use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Storage; diff --git a/app/Models/RecurringExpense.php b/app/Models/RecurringExpense.php index 8e766cc66bc1..35cc6ecd0572 100644 --- a/app/Models/RecurringExpense.php +++ b/app/Models/RecurringExpense.php @@ -11,6 +11,7 @@ namespace App\Models; +use App\Services\Recurring\RecurringService; use Illuminate\Database\Eloquent\SoftDeletes; class RecurringExpense extends BaseModel @@ -105,4 +106,12 @@ class RecurringExpense extends BaseModel { return $this->belongsTo(Client::class); } + + /** + * Service entry points. + */ + public function service() :RecurringService + { + return new RecurringService($this); + } } diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index d993288a5fb8..960e71487a1d 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -180,6 +180,10 @@ use App\Listeners\Quote\QuoteRestoredActivity; use App\Listeners\Quote\QuoteViewedActivity; use App\Listeners\Quote\ReachWorkflowSettings; use App\Listeners\RecurringExpense\CreatedRecurringExpenseActivity; +use App\Listeners\RecurringExpense\RecurringExpenseArchivedActivity; +use App\Listeners\RecurringExpense\RecurringExpenseDeletedActivity; +use App\Listeners\RecurringExpense\RecurringExpenseRestoredActivity; +use App\Listeners\RecurringExpense\RecurringExpenseUpdatedActivity; use App\Listeners\RecurringInvoice\CreateRecurringInvoiceActivity; use App\Listeners\RecurringInvoice\RecurringInvoiceArchivedActivity; use App\Listeners\RecurringInvoice\RecurringInvoiceDeletedActivity; diff --git a/app/Transformers/RecurringExpenseTransformer.php b/app/Transformers/RecurringExpenseTransformer.php index 1323421a3a97..6457bb000792 100644 --- a/app/Transformers/RecurringExpenseTransformer.php +++ b/app/Transformers/RecurringExpenseTransformer.php @@ -53,6 +53,7 @@ class RecurringExpenseTransformer extends EntityTransformer 'id' => $this->encodePrimaryKey($recurring_expense->id), 'user_id' => $this->encodePrimaryKey($recurring_expense->user_id), 'assigned_user_id' => $this->encodePrimaryKey($recurring_expense->assigned_user_id), + 'status_id' => (string) ($recurring_expense->status_id ?: 1), 'vendor_id' => $this->encodePrimaryKey($recurring_expense->vendor_id), 'invoice_id' => $this->encodePrimaryKey($recurring_expense->invoice_id), 'client_id' => $this->encodePrimaryKey($recurring_expense->client_id), diff --git a/database/migrations/2021_08_23_101529_recurring_expenses_schema.php b/database/migrations/2021_08_23_101529_recurring_expenses_schema.php index e93fbf01ebf1..c52b8381b532 100644 --- a/database/migrations/2021_08_23_101529_recurring_expenses_schema.php +++ b/database/migrations/2021_08_23_101529_recurring_expenses_schema.php @@ -22,6 +22,7 @@ class RecurringExpensesSchema extends Migration $table->unsignedInteger('company_id')->index(); $table->unsignedInteger('vendor_id')->nullable(); $table->unsignedInteger('user_id'); + $table->unsignedInteger('status_id'); $table->unsignedInteger('invoice_id')->nullable(); $table->unsignedInteger('client_id')->nullable(); diff --git a/tests/Feature/RecurringExpenseApiTest.php b/tests/Feature/RecurringExpenseApiTest.php index d566cb96e338..586db2373426 100644 --- a/tests/Feature/RecurringExpenseApiTest.php +++ b/tests/Feature/RecurringExpenseApiTest.php @@ -10,13 +10,14 @@ */ namespace Tests\Feature; +use App\Models\RecurringInvoice; use App\Utils\Traits\MakesHash; use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Support\Facades\Session; +use Illuminate\Validation\ValidationException; use Tests\MockAccountData; use Tests\TestCase; -use Illuminate\Validation\ValidationException; /** * @test @@ -67,6 +68,7 @@ class RecurringExpenseApiTest extends TestCase 'amount' => 10, 'client_id' => $this->client->hashed_id, 'number' => '123321', + 'frequency_id' => 5, ]; $response = $this->withHeaders([ @@ -170,4 +172,44 @@ class RecurringExpenseApiTest extends TestCase $this->assertTrue($arr['data'][0]['is_deleted']); } + + public function testRecurringExpenseStart() + { + $data = [ + 'ids' => [$this->encodePrimaryKey($this->recurring_expense->id)], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/recurring_expenses/bulk?action=start', $data); + + $arr = $response->json(); +nlog($arr); + $this->assertEquals(RecurringInvoice::STATUS_ACTIVE, $arr['data'][0]['status_id']); + } + + + public function testRecurringExpensePaused() + { + $data = [ + 'ids' => [$this->encodePrimaryKey($this->recurring_expense->id)], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/recurring_expenses/bulk?action=start', $data); + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/recurring_expenses/bulk?action=stop', $data); + + $arr = $response->json(); +nlog($arr); + $this->assertEquals(RecurringInvoice::STATUS_PAUSED, $arr['data'][0]['status_id']); + } + + } diff --git a/tests/MockAccountData.php b/tests/MockAccountData.php index f63e3603193b..f48a4e8f49e5 100644 --- a/tests/MockAccountData.php +++ b/tests/MockAccountData.php @@ -301,6 +301,8 @@ trait MockAccountData $this->recurring_expense = RecurringExpense::factory()->create([ 'user_id' => $user_id, 'company_id' => $this->company->id, + 'frequency_id' => 5, + 'remaining_cycles' => 5, ]); $this->recurring_quote = RecurringQuote::factory()->create([ From 4e03a2f38cd0840f35b76456ccc9cb0ec2b569d8 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 14 Sep 2021 15:57:52 +1000 Subject: [PATCH 23/24] Fixes for column typeS --- .../migrations/2021_08_23_101529_recurring_expenses_schema.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/migrations/2021_08_23_101529_recurring_expenses_schema.php b/database/migrations/2021_08_23_101529_recurring_expenses_schema.php index c52b8381b532..eefea2ebed1e 100644 --- a/database/migrations/2021_08_23_101529_recurring_expenses_schema.php +++ b/database/migrations/2021_08_23_101529_recurring_expenses_schema.php @@ -67,7 +67,7 @@ class RecurringExpensesSchema extends Migration $table->unsignedInteger('frequency_id'); $table->datetime('last_sent_date')->nullable(); $table->datetime('next_send_date')->nullable(); - $table->unsignedInteger('remaining_cycles')->nullable(); + $table->integer('remaining_cycles')->nullable(); $table->unique(['company_id', 'number']); $table->index(['company_id', 'deleted_at']); From ec74eb7bdd1f9423fb19747afbab003d8f19a3c9 Mon Sep 17 00:00:00 2001 From: = Date: Fri, 17 Sep 2021 17:52:22 +1000 Subject: [PATCH 24/24] fixes for company settings casts --- app/DataMapper/CompanySettings.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index 837e63bc112c..a45639f65f0b 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -357,6 +357,8 @@ class CompanySettings extends BaseSettings 'expense_number_counter' => 'int', 'recurring_expense_number_pattern' => 'string', 'recurring_expense_number_counter' => 'int', + 'recurring_quote_number_pattern' => 'string', + 'recurring_quote_number_counter' => 'int', 'vendor_number_pattern' => 'string', 'vendor_number_counter' => 'int', 'ticket_number_pattern' => 'string',