diff --git a/app/Factory/ExpenseCategoryFactory.php b/app/Factory/ExpenseCategoryFactory.php new file mode 100644 index 000000000000..08bbb8e4e81d --- /dev/null +++ b/app/Factory/ExpenseCategoryFactory.php @@ -0,0 +1,31 @@ +user_id = $user_id; + $expense->company_id = $company_id; + $expense->name = ''; + $expense->is_deleted = false;; + + return $expense; + } +} diff --git a/app/Factory/ExpenseFactory.php b/app/Factory/ExpenseFactory.php index d6bf6701d764..ddade6b1c18a 100644 --- a/app/Factory/ExpenseFactory.php +++ b/app/Factory/ExpenseFactory.php @@ -31,7 +31,7 @@ class ExpenseFactory $expense->tax_rate2 = 0; $expense->tax_name3 = ''; $expense->tax_rate3 = 0; - $expense->expense_date = null; + $expense->date = null; $expense->payment_date = null; return $expense; diff --git a/app/Factory/PaymentFactory.php b/app/Factory/PaymentFactory.php index 09f377646ea4..c452f3edc699 100644 --- a/app/Factory/PaymentFactory.php +++ b/app/Factory/PaymentFactory.php @@ -19,13 +19,13 @@ use Illuminate\Support\Facades\Log; class PaymentFactory { - public static function create(int $company_id, int $user_id) :Payment + public static function create(int $company_id, int $user_id, int $client_id = 0) :Payment { $payment = new Payment; $payment->company_id = $company_id; $payment->user_id = $user_id; - $payment->client_id = 0; + $payment->client_id = $client_id; $payment->client_contact_id = null; $payment->invitation_id = null; $payment->company_gateway_id = null; diff --git a/app/Factory/TaskFactory.php b/app/Factory/TaskFactory.php new file mode 100644 index 000000000000..e0d43b02e94c --- /dev/null +++ b/app/Factory/TaskFactory.php @@ -0,0 +1,33 @@ +description = ''; + //$task->rate = ''; + $task->company_id = $company_id; + $task->user_id = $user_id; + $task->time_log = '[]'; + $task->is_running = false; + $task->is_deleted = false; + $task->duration = 0; + + return $task; + } +} diff --git a/app/Filters/TaskFilters.php b/app/Filters/TaskFilters.php new file mode 100644 index 000000000000..227d5b73231e --- /dev/null +++ b/app/Filters/TaskFilters.php @@ -0,0 +1,117 @@ +builder; + } + + return $this->builder->where(function ($query) use ($filter) { + $query->where('tasks.description', 'like', '%'.$filter.'%') + ->orWhere('tasks.custom_value1', 'like', '%'.$filter.'%') + ->orWhere('tasks.custom_value2', 'like', '%'.$filter.'%') + ->orWhere('tasks.custom_value3', 'like', '%'.$filter.'%') + ->orWhere('tasks.custom_value4', 'like', '%'.$filter.'%'); + }); + } + + /** + * Filters the list based on the status + * archived, active, deleted. + * + * @param string filter + * @return Illuminate\Database\Query\Builder + */ + public function status(string $filter = '') : Builder + { + if (strlen($filter) == 0) { + return $this->builder; + } + + $table = 'tasks'; + $filters = explode(',', $filter); + + return $this->builder->where(function ($query) use ($filters, $table) { + $query->whereNull($table.'.id'); + + if (in_array(parent::STATUS_ACTIVE, $filters)) { + $query->orWhereNull($table.'.deleted_at'); + } + + if (in_array(parent::STATUS_ARCHIVED, $filters)) { + $query->orWhere(function ($query) use ($table) { + $query->whereNotNull($table.'.deleted_at'); + + if (! in_array($table, ['users'])) { + $query->where($table.'.is_deleted', '=', 0); + } + }); + } + + if (in_array(parent::STATUS_DELETED, $filters)) { + $query->orWhere($table.'.is_deleted', '=', 1); + } + }); + } + + /** + * Sorts the list based on $sort. + * + * @param string sort formatted as column|asc + * @return Illuminate\Database\Query\Builder + */ + public function sort(string $sort) : Builder + { + $sort_col = explode('|', $sort); + + return $this->builder->orderBy($sort_col[0], $sort_col[1]); + } + + /** + * Returns the base query. + * + * @param int company_id + * @return Illuminate\Database\Query\Builder + * @deprecated + */ + public function baseQuery(int $company_id, User $user) : Builder + { + } + + /** + * Filters the query by the users company ID. + * + * @param $company_id The company Id + * @return Illuminate\Database\Query\Builder + */ + public function entityFilter() + { + return $this->builder->company(); + } +} diff --git a/app/Http/Controllers/ClientPortal/PaymentController.php b/app/Http/Controllers/ClientPortal/PaymentController.php index 6b7dc20d2a07..85220ddc2aff 100644 --- a/app/Http/Controllers/ClientPortal/PaymentController.php +++ b/app/Http/Controllers/ClientPortal/PaymentController.php @@ -12,6 +12,7 @@ namespace App\Http\Controllers\ClientPortal; +use App\Factory\PaymentFactory; use App\Http\Controllers\Controller; use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest; use App\Jobs\Invoice\InjectSignature; @@ -71,18 +72,25 @@ class PaymentController extends Controller { $gateway = CompanyGateway::findOrFail(request()->input('company_gateway_id')); - /*find invoices*/ + /** + * find invoices + * + * ['invoice_id' => xxx, 'amount' => 22.00] + * + */ - $payable_invoices = request()->payable_invoices; - $invoices = Invoice::whereIn('id', $this->transformKeys(array_column($payable_invoices, 'invoice_id')))->get(); + $payable_invoices = collect(request()->payable_invoices); + $invoices = Invoice::whereIn('id', $this->transformKeys($payable_invoices->pluck('invoice_id')->toArray()))->get(); + + /* pop non payable invoice from the $payable_invoices array */ + $payable_invoices = $payable_invoices->filter(function ($payable_invoice) use ($invoices){ + + return $invoices->where('hashed_id', $payable_invoice['invoice_id'])->first()->isPayable(); - /*filter only payable invoices*/ - $invoices = $invoices->filter(function ($invoice) { - return $invoice->isPayable(); }); /*return early if no invoices*/ - if ($invoices->count() == 0) { + if ($payable_invoices->count() == 0) { return redirect() ->route('client.invoices.index') ->with(['warning' => 'No payable invoices selected.']); @@ -91,10 +99,9 @@ class PaymentController extends Controller $settings = auth()->user()->client->getMergedSettings(); /*iterate through invoices and add gateway fees and other payment metadata*/ - foreach ($payable_invoices as $key => $payable_invoice) { - - $payable_invoices[$key]['amount'] = Number::parseFloat($payable_invoice['amount']); - $payable_invoice['amount'] = $payable_invoices[$key]['amount']; + $payable_invoices = $payable_invoices->map(function($payable_invoice) use($invoices, $settings){ + + $payable_invoice['amount'] = Number::parseFloat($payable_invoice['amount']); $invoice = $invoices->first(function ($inv) use ($payable_invoice) { return $payable_invoice['invoice_id'] == $inv->hashed_id; @@ -135,8 +142,8 @@ class PaymentController extends Controller } } // Make sure 'amount' from form is not higher than 'amount' from invoice. - $payable_invoices[$key]['due_date'] = $this->formatDate($invoice->due_date, $invoice->client->date_format()); - $payable_invoices[$key]['invoice_number'] = $invoice->number; + $payable_invoice['due_date'] = $this->formatDate($invoice->due_date, $invoice->client->date_format()); + $payable_invoice['invoice_number'] = $invoice->number; if (isset($invoice->po_number)) { $additional_info = $invoice->po_number; @@ -146,8 +153,11 @@ class PaymentController extends Controller $additional_info = $invoice->date; } - $payable_invoices[$key]['additional_info'] = $additional_info; - } + $payable_invoice['additional_info'] = $additional_info; + + return $payable_invoice; + + }); if ((bool) request()->signature) { $invoices->each(function ($invoice) { @@ -155,27 +165,16 @@ class PaymentController extends Controller }); } - //$payment_methods = auth()->user()->client->getPaymentMethods(array_sum(array_column($payable_invoices, 'amount_with_fee'))); - $payment_method_id = request()->input('payment_method_id'); - $invoice_totals = array_sum(array_column($payable_invoices, 'amount')); + $invoice_totals = $payable_invoices->sum('amount'); $first_invoice = $invoices->first(); + $credit_totals = $first_invoice->client->service()->getCreditBalance(); + $starting_invoice_amount = $first_invoice->amount; - // $fee_totals = round($gateway->calcGatewayFee($invoice_totals, true), $first_invoice->client->currency()->precision); - - // if (!$first_invoice->uses_inclusive_taxes) { - // $fee_tax = 0; - // $fee_tax += round(($first_invoice->tax_rate1 / 100) * $fee_totals, $first_invoice->client->currency()->precision); - // $fee_tax += round(($first_invoice->tax_rate2 / 100) * $fee_totals, $first_invoice->client->currency()->precision); - // $fee_tax += round(($first_invoice->tax_rate3 / 100) * $fee_totals, $first_invoice->client->currency()->precision); - - // $fee_totals += $fee_tax; - // } - $first_invoice->service()->addGatewayFee($gateway, $payment_method_id, $invoice_totals)->save(); /** @@ -188,15 +187,16 @@ class PaymentController extends Controller $payment_hash = new PaymentHash; $payment_hash->hash = Str::random(128); - $payment_hash->data = $payable_invoices; + $payment_hash->data = $payable_invoices->toArray(); $payment_hash->fee_total = $fee_totals; $payment_hash->fee_invoice_id = $first_invoice->id; $payment_hash->save(); $totals = [ + 'credit_totals' => $credit_totals, 'invoice_totals' => $invoice_totals, 'fee_total' => $fee_totals, - 'amount_with_fee' => $invoice_totals + $fee_totals, + 'amount_with_fee' => max(0, (($invoice_totals + $fee_totals) - $credit_totals)), ]; $data = [ @@ -219,24 +219,67 @@ class PaymentController extends Controller /*Payment Gateway*/ $gateway = CompanyGateway::find($request->input('company_gateway_id'))->firstOrFail(); - //REFACTOR - Entry point for the gateway response - we don't need to do anything at this point. - // - // - Inside each gateway driver, we should use have a generic code path (in BaseDriver.php)for successful/failed payment - // - // Success workflow - // - // - Rehydrate the hash and iterate through the invoices and update the balances - // - Update the type_id of the gateway fee to type_id 4 - // - Link invoices to payment - // - // Failure workflow - // - // - Rehydrate hash, iterate through invoices and remove type_id 3's - // - Recalcuate invoice totals - return $gateway ->driver(auth()->user()->client) ->setPaymentMethod($request->input('payment_method_id')) ->processPaymentResponse($request); } + + /** + * Pay for invoice/s using credits only. + * + * @param Request $request The request object + * @return Response The response view + */ + public function credit_response(Request $request) + { + $payment_hash = PaymentHash::whereRaw('BINARY `hash`= ?', [$request->input('payment_hash')])->first(); + + /* Hydrate the $payment */ + if($payment_hash->payment()->exists()) + $payment = $payment_hash->payment; + else { + $payment = PaymentFactory::create($payment_hash->fee_invoice->company_id, $payment_hash->fee_invoice->user_id); + $payment->client_id = $payment_hash->fee_invoice->client_id; + $payment->save(); + + $payment_hash->payment_id = $payment->id; + $payment_hash->save(); + } + + /* Iterate through the invoices and apply credits to them */ + collect($payment_hash->invoices())->each(function ($payable_invoice) use ($payment, $payment_hash){ + + $invoice = Invoice::find($this->decodePrimaryKey($payable_invoice->invoice_id)); + $amount = $payable_invoice->amount; + + $credits = $payment_hash->fee_invoice + ->client + ->service() + ->getCredits(); + + foreach($credits as $credit) + { + //starting invoice balance + $invoice_balance = $invoice->balance; + + //credit payment applied + $credit->service()->applyPayment($invoice, $amount, $payment); + + //amount paid from invoice calculated + $remaining_balance = ($invoice_balance - $invoice->fresh()->balance); + + //reduce the amount to be paid on the invoice from the NEXT credit + $amount -= $remaining_balance; + + //break if the invoice is no longer PAYABLE OR there is no more amount to be applied + if(!$invoice->isPayable() || (int)$amount == 0) + break; + } + + }); + + return redirect()->route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)]); + + } } diff --git a/app/Http/Controllers/ExpenseCategoryController.php b/app/Http/Controllers/ExpenseCategoryController.php new file mode 100644 index 000000000000..c49b50251574 --- /dev/null +++ b/app/Http/Controllers/ExpenseCategoryController.php @@ -0,0 +1,435 @@ +base_repo = $base_repo; + } + + /** + * @OA\Get( + * path="/api/v1/expense_categories", + * operationId="getExpenseCategorys", + * tags={"expense_categories"}, + * summary="Gets a list of expense_categories", + * description="Lists tax rates", + * @OA\Parameter(ref="#/components/parameters/index"), + * @OA\Response( + * response=200, + * description="A list of expense_categories", + * @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/ExpenseCategory"), + * ), + * @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"), + * ), + * ) + * + * + * Display a listing of the resource. + * + * @return \Illuminate\Http\Response + */ + public function index() + { + $expense_categories = ExpenseCategory::scope(); + + return $this->listResponse($expense_categories); + } + + /** + * Show the form for creating a new resource. + * + * @return \Illuminate\Http\Response + * + * + * + * @OA\Get( + * path="/api/v1/expense_categories/create", + * operationId="getExpenseCategoryCreate", + * tags={"expense_categories"}, + * summary="Gets a new blank Expens Category 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\Response( + * response=200, + * description="A blank Expens Category 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/ExpenseCategory"), + * ), + * @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(CreateExpenseCategoryRequest $request) + { + $expense_category = ExpenseCategoryFactory::create(auth()->user()->company()->id, auth()->user()->id); + + return $this->itemResponse($expense_category); + } + + /** + * Store a newly created resource in storage. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\Response + */ + public function store(StoreExpenseCategoryRequest $request) + { + $expense_category = ExpenseCategoryFactory::create(auth()->user()->company()->id, auth()->user()->id); + $expense_category->fill($request->all()); + $expense_category->save(); + + return $this->itemResponse($expense_category); + } + + /** + * Display the specified resource. + * + * @param int $id + * @return \Illuminate\Http\Response + * + * + * @OA\Get( + * path="/api/v1/expense_categories/{id}", + * operationId="showExpenseCategory", + * tags={"expense_categories"}, + * summary="Shows a Expens Category", + * description="Displays an ExpenseCategory 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( + * name="id", + * in="path", + * description="The ExpenseCategory Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the Expens Category 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/ExpenseCategory"), + * ), + * @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(ShowExpenseCategoryRequest $request, ExpenseCategory $expense_category) + { + return $this->itemResponse($expense_category); + } + + /** + * Show the form for editing the specified resource. + * + * @param int $id + * @return \Illuminate\Http\Response + * + * + * @OA\Get( + * path="/api/v1/expense_categories/{id}/edit", + * operationId="editExpenseCategory", + * tags={"expense_categories"}, + * summary="Shows a Expens Category for editting", + * description="Displays a Expens Category 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( + * name="id", + * in="path", + * description="The ExpenseCategory Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the Expens Category 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/ExpenseCategory"), + * ), + * @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(EditExpenseCategoryRequest $request, ExpenseCategory $expense_category) + { + return $this->itemResponse($expense_category); + } + + /** + * Update the specified resource in storage. + * + * @param \Illuminate\Http\Request $request + * @param App\Models\Client $client + * @return \Illuminate\Http\Response + * + * + * + * @OA\Put( + * path="/api/v1/expense_categories/{id}", + * operationId="updateExpenseCategory", + * tags={"expense_categories"}, + * summary="Updates a tax rate", + * description="Handles the updating of a tax rate 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( + * name="id", + * in="path", + * description="The ExpenseCategory Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the ExpenseCategory 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/ExpenseCategory"), + * ), + * @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(UpdateExpenseCategoryRequest $request, ExpenseCategory $expense_category) + { + $expense_category->fill($request->all()); + $expense_category->save(); + + return $this->itemResponse($expense_category); + } + + /** + * Remove the specified resource from storage. + * + * @param int $id + * @return \Illuminate\Http\Response + * + * + * @OA\Delete( + * path="/api/v1/expense_categories/{id}", + * operationId="deleteExpenseCategory", + * tags={"expense_categories"}, + * summary="Deletes a ExpenseCategory", + * description="Handles the deletion of an ExpenseCategory 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( + * name="id", + * in="path", + * description="The ExpenseCategory 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(DestroyExpenseCategoryRequest $request, ExpenseCategory $expense_category) + { + $expense_category->is_deleted = true; + $expense_category->save(); + $expense_category->delete(); + + return $this->itemResponse($expense_category); + } + + /** + * Perform bulk actions on the list view. + * + * @param BulkExpenseCategoryRequest $request + * @return \Illuminate\Http\Response + * + * + * @OA\Post( + * path="/api/v1/expense_categories/bulk", + * operationId="bulkExpenseCategorys", + * tags={"expense_categories"}, + * summary="Performs bulk actions on an array of ExpenseCategorys", + * 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="Expens Categorys", + * 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 ExpenseCategory List 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/Webhook"), + * ), + * @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'); + + $expense_categories = ExpenseCategory::withTrashed()->find($this->transformKeys($ids)); + + $expense_categories->each(function ($expense_category, $key) use ($action) { + if (auth()->user()->can('edit', $expense_category)) { + $this->base_repo->{$action}($expense_category); + } + }); + + return $this->listResponse(ExpenseCategory::withTrashed()->whereIn('id', $this->transformKeys($ids))); + } +} diff --git a/app/Http/Controllers/OpenAPI/ExpenseCategorySchema.php b/app/Http/Controllers/OpenAPI/ExpenseCategorySchema.php new file mode 100644 index 000000000000..f42ce8143905 --- /dev/null +++ b/app/Http/Controllers/OpenAPI/ExpenseCategorySchema.php @@ -0,0 +1,13 @@ +fill($request->all()); $project->save(); + if ($request->has('documents')) { + $this->saveDocuments($request->input('documents'), $project); + } + return $this->itemResponse($project->fresh()); } diff --git a/app/Http/Controllers/TaskController.php b/app/Http/Controllers/TaskController.php new file mode 100644 index 000000000000..0413a9e4a12f --- /dev/null +++ b/app/Http/Controllers/TaskController.php @@ -0,0 +1,504 @@ +task_repo = $task_repo; + } + + /** + * @OA\Get( + * path="/api/v1/tasks", + * operationId="getTasks", + * tags={"tasks"}, + * summary="Gets a list of tasks", + * description="Lists tasks, search and filters allow fine grained lists to be generated. + * + * Query parameters can be added to performed more fine grained filtering of the tasks, these are handled by the TaskFilters 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 tasks", + * @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/Task"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function index(TaskFilters $filters) + { + $tasks = Task::filter($filters); + + return $this->listResponse($tasks); + } + + /** + * Display the specified resource. + * + * @param int $id + * @return \Illuminate\Http\Response + * + * + * @OA\Get( + * path="/api/v1/tasks/{id}", + * operationId="showTask", + * tags={"tasks"}, + * 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 Task Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the task 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/Task"), + * ), + * @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(ShowTaskRequest $request, Task $task) + { + return $this->itemResponse($task); + } + + /** + * Show the form for editing the specified resource. + * + * @param int $id + * @return \Illuminate\Http\Response + * + * + * @OA\Get( + * path="/api/v1/tasks/{id}/edit", + * operationId="editTask", + * tags={"tasks"}, + * 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 Task 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/Task"), + * ), + * @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(EditTaskRequest $request, Task $task) + { + return $this->itemResponse($task); + } + + /** + * Update the specified resource in storage. + * + * @param \Illuminate\Http\Request $request + * @param App\Models\Task $task + * @return \Illuminate\Http\Response + * + * + * + * @OA\Put( + * path="/api/v1/tasks/{id}", + * operationId="updateTask", + * tags={"tasks"}, + * 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 Task 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/Task"), + * ), + * @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(UpdateTaskRequest $request, Task $task) + { + if ($request->entityIsDeleted($task)) { + return $request->disallowUpdate(); + } + + $task = $this->task_repo->save($request->all(), $task); + + return $this->itemResponse($task->fresh()); + } + + /** + * Show the form for creating a new resource. + * + * @return \Illuminate\Http\Response + * + * + * + * @OA\Get( + * path="/api/v1/tasks/create", + * operationId="getTasksCreate", + * tags={"tasks"}, + * 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/Task"), + * ), + * @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(CreateTaskRequest $request) + { + $task = TaskFactory::create(auth()->user()->company()->id, auth()->user()->id); + + return $this->itemResponse($task); + } + + /** + * Store a newly created resource in storage. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\Response + * + * + * + * @OA\Post( + * path="/api/v1/tasks", + * operationId="storeTask", + * tags={"tasks"}, + * 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/Task"), + * ), + * @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(StoreTaskRequest $request) + { + $task = $this->task_repo->save($request->all(), TaskFactory::create(auth()->user()->company()->id, auth()->user()->id)); + + return $this->itemResponse($task); + } + + /** + * Remove the specified resource from storage. + * + * @param int $id + * @return \Illuminate\Http\Response + * + * + * @OA\Delete( + * path="/api/v1/tasks/{id}", + * operationId="deleteTask", + * tags={"tasks"}, + * 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 Task 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(DestroyTaskRequest $request, Task $task) + { + //may not need these destroy routes as we are using actions to 'archive/delete' + $task->delete(); + + return response()->json([], 200); + } + + /** + * Perform bulk actions on the list view. + * + * @param BulkTaskRequest $request + * @return \Illuminate\Http\Response + * + * + * @OA\Post( + * path="/api/v1/tasks/bulk", + * operationId="bulkTasks", + * tags={"tasks"}, + * summary="Performs bulk actions on an array of tasks", + * 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 Task 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/Task"), + * ), + * @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'); + $tasks = Task::withTrashed()->find($this->transformKeys($ids)); + + $tasks->each(function ($task, $key) use ($action) { + if (auth()->user()->can('edit', $task)) { + $this->task_repo->{$action}($task); + } + }); + + return $this->listResponse(Task::withTrashed()->whereIn('id', $this->transformKeys($ids))); + } + + /** + * Returns a client statement. + * + * @return [type] [description] + */ + public function statement() + { + //todo + } +} diff --git a/app/Http/Requests/Account/CreateAccountRequest.php b/app/Http/Requests/Account/CreateAccountRequest.php index 27eea1ab3282..45cc0b250daa 100644 --- a/app/Http/Requests/Account/CreateAccountRequest.php +++ b/app/Http/Requests/Account/CreateAccountRequest.php @@ -38,7 +38,7 @@ class CreateAccountRequest extends Request 'first_name' => 'string|max:100', 'last_name' => 'string:max:100', 'password' => 'required|string|min:6', - 'email' => 'bail|required|email', + 'email' => 'bail|required|email:rfc,dns', 'email' => new NewUniqueUserRule(), 'privacy_policy' => 'required', 'terms_of_service' => 'required', diff --git a/app/Http/Requests/ClientPortal/RegisterRequest.php b/app/Http/Requests/ClientPortal/RegisterRequest.php index 39808840aed7..06726e740d46 100644 --- a/app/Http/Requests/ClientPortal/RegisterRequest.php +++ b/app/Http/Requests/ClientPortal/RegisterRequest.php @@ -28,7 +28,7 @@ class RegisterRequest extends FormRequest 'first_name' => ['required', 'string', 'max:255'], 'last_name' => ['required', 'string', 'max:255'], 'phone' => ['required', 'string', 'max:255'], - 'email' => ['required', 'string', 'email', 'max:255', 'unique:client_contacts'], + 'email' => ['required', 'string', 'email:rfc,dns', 'max:255', 'unique:client_contacts'], 'password' => ['required', 'string', 'min:6', 'confirmed'], ]; } diff --git a/app/Http/Requests/ClientPortal/UpdateContactRequest.php b/app/Http/Requests/ClientPortal/UpdateContactRequest.php index de9e61df9ab2..a4cdc50207de 100644 --- a/app/Http/Requests/ClientPortal/UpdateContactRequest.php +++ b/app/Http/Requests/ClientPortal/UpdateContactRequest.php @@ -34,7 +34,7 @@ class UpdateContactRequest extends Request return [ 'first_name' => 'required', 'last_name' => 'required', - 'email' => 'required|email|unique:client_contacts,email,'.auth()->user()->id, + 'email' => 'required|email:rfc,dns|unique:client_contacts,email,'.auth()->user()->id, 'password' => 'sometimes|nullable|min:6|confirmed', ]; } diff --git a/app/Http/Requests/ExpenseCategory/BulkExpenseCategoryRequest.php b/app/Http/Requests/ExpenseCategory/BulkExpenseCategoryRequest.php new file mode 100644 index 000000000000..92b72f5e5b13 --- /dev/null +++ b/app/Http/Requests/ExpenseCategory/BulkExpenseCategoryRequest.php @@ -0,0 +1,32 @@ +user()->->isAdmin(); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules() + { + return []; + } +} diff --git a/app/Http/Requests/ExpenseCategory/CreateExpenseCategoryRequest.php b/app/Http/Requests/ExpenseCategory/CreateExpenseCategoryRequest.php new file mode 100644 index 000000000000..615a5d6b439a --- /dev/null +++ b/app/Http/Requests/ExpenseCategory/CreateExpenseCategoryRequest.php @@ -0,0 +1,28 @@ +user()->can('create', ExpenseCategory::class); + } +} diff --git a/app/Http/Requests/ExpenseCategory/DestroyExpenseCategoryRequest.php b/app/Http/Requests/ExpenseCategory/DestroyExpenseCategoryRequest.php new file mode 100644 index 000000000000..126bb2e1ead3 --- /dev/null +++ b/app/Http/Requests/ExpenseCategory/DestroyExpenseCategoryRequest.php @@ -0,0 +1,28 @@ +user()->can('edit', $this->expense_category); + } +} diff --git a/app/Http/Requests/ExpenseCategory/EditExpenseCategoryRequest.php b/app/Http/Requests/ExpenseCategory/EditExpenseCategoryRequest.php new file mode 100644 index 000000000000..6046b3d8c1df --- /dev/null +++ b/app/Http/Requests/ExpenseCategory/EditExpenseCategoryRequest.php @@ -0,0 +1,38 @@ +user()->can('edit', $this->expense_category); + } + + // public function prepareForValidation() + // { + // $input = $this->all(); + + // //$input['id'] = $this->encodePrimaryKey($input['id']); + + // $this->replace($input); + + // } +} diff --git a/app/Http/Requests/ExpenseCategory/ShowExpenseCategoryRequest.php b/app/Http/Requests/ExpenseCategory/ShowExpenseCategoryRequest.php new file mode 100644 index 000000000000..7f8cf1413994 --- /dev/null +++ b/app/Http/Requests/ExpenseCategory/ShowExpenseCategoryRequest.php @@ -0,0 +1,28 @@ +user()->can('view', $this->expense_category); + } +} diff --git a/app/Http/Requests/ExpenseCategory/StoreExpenseCategoryRequest.php b/app/Http/Requests/ExpenseCategory/StoreExpenseCategoryRequest.php new file mode 100644 index 000000000000..4de8a3b06321 --- /dev/null +++ b/app/Http/Requests/ExpenseCategory/StoreExpenseCategoryRequest.php @@ -0,0 +1,39 @@ +user()->can('create', ExpenseCategory::class); + } + + public function rules() + { + $rules = []; + + $rules['name'] = 'required|unique:expense_categories,name,null,null,company_id,'.auth()->user()->companyId(); + + return $rules; + } + +} diff --git a/app/Http/Requests/ExpenseCategory/UpdateExpenseCategoryRequest.php b/app/Http/Requests/ExpenseCategory/UpdateExpenseCategoryRequest.php new file mode 100644 index 000000000000..27d1f2c83c2c --- /dev/null +++ b/app/Http/Requests/ExpenseCategory/UpdateExpenseCategoryRequest.php @@ -0,0 +1,41 @@ +user()->can('edit', $this->expense_category); + } + + public function rules() + { + + $rules = []; + + if ($this->input('name')) + $rules['name'] = 'unique:expense_categories,name,'.$this->id.',id,company_id,'.$this->expense_category->company_id; + + return $rules; + } + +} diff --git a/app/Http/Requests/GroupSetting/UpdateGroupSettingRequest.php b/app/Http/Requests/GroupSetting/UpdateGroupSettingRequest.php index 0e2aa6f45f76..16442d1e323f 100644 --- a/app/Http/Requests/GroupSetting/UpdateGroupSettingRequest.php +++ b/app/Http/Requests/GroupSetting/UpdateGroupSettingRequest.php @@ -34,7 +34,7 @@ class UpdateGroupSettingRequest extends Request { $rules['settings'] = new ValidClientGroupSettingsRule(); - $rules['name'] = 'unique:group_settings,name,'.$this->id.',id,company_id,'.$this->group_setting->company_id; +// $rules['name'] = 'unique:group_settings,name,'.$this->id.',id,company_id,'.$this->group_setting->company_id; return $rules; } diff --git a/app/Http/Requests/Project/StoreProjectRequest.php b/app/Http/Requests/Project/StoreProjectRequest.php index f17afb073761..ecee7100ecf6 100644 --- a/app/Http/Requests/Project/StoreProjectRequest.php +++ b/app/Http/Requests/Project/StoreProjectRequest.php @@ -33,7 +33,7 @@ class StoreProjectRequest extends Request { $rules = []; - $rules['name'] ='required|unique:projects,name,null,null,company_id,'.auth()->user()->companyId(); + //$rules['name'] ='required|unique:projects,name,null,null,company_id,'.auth()->user()->companyId(); $rules['client_id'] = 'required|exists:clients,id,company_id,'.auth()->user()->company()->id; return $rules; diff --git a/app/Http/Requests/Setup/StoreSetupRequest.php b/app/Http/Requests/Setup/StoreSetupRequest.php index 196b71145557..a3e7d6595aa3 100644 --- a/app/Http/Requests/Setup/StoreSetupRequest.php +++ b/app/Http/Requests/Setup/StoreSetupRequest.php @@ -48,7 +48,7 @@ class StoreSetupRequest extends Request 'terms_of_service' => 'required', 'first_name' => 'required', 'last_name' => 'required', - 'email' => 'required', + 'email' => 'required|email:rfc,dns', 'password' => 'required', ]; } diff --git a/app/Http/Requests/Task/BulkTaskRequest.php b/app/Http/Requests/Task/BulkTaskRequest.php new file mode 100644 index 000000000000..a3221713cb26 --- /dev/null +++ b/app/Http/Requests/Task/BulkTaskRequest.php @@ -0,0 +1,39 @@ +user()->can(auth()->user()->isAdmin(), Task::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/Task/CreateTaskRequest.php b/app/Http/Requests/Task/CreateTaskRequest.php new file mode 100644 index 000000000000..bd485c641a9a --- /dev/null +++ b/app/Http/Requests/Task/CreateTaskRequest.php @@ -0,0 +1,28 @@ +user()->can('create', Task::class); + } +} diff --git a/app/Http/Requests/Task/DestroyTaskRequest.php b/app/Http/Requests/Task/DestroyTaskRequest.php new file mode 100644 index 000000000000..3a72fc215aec --- /dev/null +++ b/app/Http/Requests/Task/DestroyTaskRequest.php @@ -0,0 +1,28 @@ +user()->can('edit', $this->task); + } +} diff --git a/app/Http/Requests/Task/EditTaskRequest.php b/app/Http/Requests/Task/EditTaskRequest.php new file mode 100644 index 000000000000..a2a52c466f18 --- /dev/null +++ b/app/Http/Requests/Task/EditTaskRequest.php @@ -0,0 +1,38 @@ +user()->can('edit', $this->task); + } + + // public function prepareForValidation() + // { + // $input = $this->all(); + + // //$input['id'] = $this->encodePrimaryKey($input['id']); + + // $this->replace($input); + + // } +} diff --git a/app/Http/Requests/Task/ShowTaskRequest.php b/app/Http/Requests/Task/ShowTaskRequest.php new file mode 100644 index 000000000000..f285b2c95625 --- /dev/null +++ b/app/Http/Requests/Task/ShowTaskRequest.php @@ -0,0 +1,28 @@ +user()->can('view', $this->task); + } +} diff --git a/app/Http/Requests/Task/StoreTaskRequest.php b/app/Http/Requests/Task/StoreTaskRequest.php new file mode 100644 index 000000000000..1c1a2b23d1f7 --- /dev/null +++ b/app/Http/Requests/Task/StoreTaskRequest.php @@ -0,0 +1,85 @@ +user()->can('create', Task::class); + } + + public function rules() + { + $rules = []; + /* Ensure we have a client name, and that all emails are unique*/ + //$rules['name'] = 'required|min:1'; + //$rules['client_id'] = 'required|exists:clients,id,company_id,'.auth()->user()->company()->id; + + // $rules['number'] = new UniqueTaskNumberRule($this->all()); + + + 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 (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 (array_key_exists('project_id', $input) && is_string($input['project_id'])) { + $input['project_id'] = $this->decodePrimaryKey($input['project_id']); + } + + if (array_key_exists('invoice_id', $input) && is_string($input['invoice_id'])) { + $input['invoice_id'] = $this->decodePrimaryKey($input['invoice_id']); + } + + $this->replace($input); + } + + // public function messages() + // { + // // return [ + // // 'unique' => ctrans('validation.unique', ['attribute' => 'email']), + // // //'required' => trans('validation.required', ['attribute' => 'email']), + // // 'contacts.*.email.required' => ctrans('validation.email', ['attribute' => 'email']), + // // ]; + // } +} diff --git a/app/Http/Requests/Task/UpdateTaskRequest.php b/app/Http/Requests/Task/UpdateTaskRequest.php new file mode 100644 index 000000000000..9d03ab13c4f6 --- /dev/null +++ b/app/Http/Requests/Task/UpdateTaskRequest.php @@ -0,0 +1,85 @@ +user()->can('edit', $this->task); + } + + public function rules() + { + $rules = []; + /* Ensure we have a client name, and that all emails are unique*/ + + if ($this->input('number')) { + $rules['number'] = 'unique:tasks,number,'.$this->id.',id,company_id,'.$this->taskss->company_id; + } + + return $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(); + + 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 (array_key_exists('project_id', $input) && is_string($input['project_id'])) { + $input['project_id'] = $this->decodePrimaryKey($input['project_id']); + } + + if (array_key_exists('invoice_id', $input) && is_string($input['invoice_id'])) { + $input['invoice_id'] = $this->decodePrimaryKey($input['invoice_id']); + } + + $this->replace($input); + } +} diff --git a/app/Http/Requests/User/UpdateUserRequest.php b/app/Http/Requests/User/UpdateUserRequest.php index bff4a46c0ab7..474145e11f52 100644 --- a/app/Http/Requests/User/UpdateUserRequest.php +++ b/app/Http/Requests/User/UpdateUserRequest.php @@ -32,7 +32,7 @@ class UpdateUserRequest extends Request $rules = []; if (isset($input['email'])) { - $rules['email'] = ['sometimes', new UniqueUserRule($this->user, $input['email'])]; + $rules['email'] = ['email:rfc,dns', 'sometimes', new UniqueUserRule($this->user, $input['email'])]; } return $rules; diff --git a/app/Models/Client.php b/app/Models/Client.php index f4af6cee2fab..e02fd1b5c898 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -517,7 +517,7 @@ class Client extends BaseModel implements HasLocalePreference $fee_label = $gateway->calcGatewayFeeLabel($amount, $this); $payment_urls[] = [ - 'label' => ctrans('texts.'.$gateway->getTypeAlias($gateway_type_id)).$fee_label, + 'label' => $gateway->getTypeAlias($gateway_type_id) . $fee_label, 'company_gateway_id' => $gateway_id, 'gateway_type_id' => $gateway_type_id, ]; diff --git a/app/Models/CompanyGateway.php b/app/Models/CompanyGateway.php index b05d20a94b05..fbf065fd19d8 100644 --- a/app/Models/CompanyGateway.php +++ b/app/Models/CompanyGateway.php @@ -79,7 +79,7 @@ class CompanyGateway extends BaseModel public function getTypeAlias($gateway_type_id) { - return GatewayType::find($gateway_type_id)->alias; + return GatewayType::getAlias($gateway_type_id); } /* This is the public entry point into the payment superclass */ diff --git a/app/Models/Expense.php b/app/Models/Expense.php index d9550a413293..693b74c3cc3c 100644 --- a/app/Models/Expense.php +++ b/app/Models/Expense.php @@ -25,7 +25,7 @@ class Expense extends BaseModel 'client_id', 'vendor_id', 'expense_currency_id', - 'expense_date', + 'date', 'invoice_currency_id', 'amount', 'foreign_amount', diff --git a/app/Models/GatewayType.php b/app/Models/GatewayType.php index 058fd116e8f5..5ed2e80c2fc5 100644 --- a/app/Models/GatewayType.php +++ b/app/Models/GatewayType.php @@ -37,4 +37,41 @@ class GatewayType extends StaticModel { return $this->hasMany(PaymentType::class); } + + public static function getAlias($type) + { + switch ($type) { + case self::CREDIT_CARD: + return ctrans('texts.credit_card'); + break; + case self::BANK_TRANSFER: + return ctrans('texts.bank_transfer'); + break; + case self::PAYPAL: + return ctrans('texts.paypal'); + break; + case self::CRYPTO: + return ctrans('texts.crypto'); + break; + case self::CUSTOM: + return ctrans('texts.custom'); + break; + case self::ALIPAY: + return ctrans('texts.alipay'); + break; + case self::SOFORT: + return ctrans('texts.sofort'); + break; + case self::APPLE_PAY: + return ctrans('texts.apple_pay'); + break; + case self::SEPA: + return ctrans('texts.sepa'); + break; + + default: + return 'Undefined.'; + break; + } + } } diff --git a/app/Models/PaymentHash.php b/app/Models/PaymentHash.php index 6c23c88a239f..2e16a8fc2631 100644 --- a/app/Models/PaymentHash.php +++ b/app/Models/PaymentHash.php @@ -25,4 +25,14 @@ class PaymentHash extends Model { return $this->data; } + + public function payment() + { + return $this->belongsTo(Payment::class)->withTrashed(); + } + + public function fee_invoice() + { + return $this->belongsTo(Invoice::class, 'fee_invoice_id', 'id'); + } } diff --git a/app/Models/Task.php b/app/Models/Task.php index f028773f8848..9a3345f7cfb0 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -11,6 +11,7 @@ namespace App\Models; +use App\Models\Filterable; use App\Utils\Traits\MakesHash; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; @@ -19,12 +20,16 @@ class Task extends BaseModel { use MakesHash; use SoftDeletes; - + use Filterable; + protected $fillable = [ 'client_id', 'invoice_id', + 'project_id', 'custom_value1', 'custom_value2', + 'custom_value3', + 'custom_value4', 'description', 'is_running', 'time_log', @@ -32,11 +37,6 @@ class Task extends BaseModel protected $touches = []; - protected $casts = [ - 'updated_at' => 'timestamp', - 'created_at' => 'timestamp', - ]; - public function getEntityType() { return self::class; @@ -66,4 +66,14 @@ class Task extends BaseModel { return $this->belongsTo(Client::class); } + + public function invoice() + { + return $this->belongsTo(Invoice::class); + } + + public function project() + { + return $this->belongsTo(Project::class); + } } diff --git a/app/PaymentDrivers/Stripe/Charge.php b/app/PaymentDrivers/Stripe/Charge.php index 4f76dc3e67d5..7e779e69403c 100644 --- a/app/PaymentDrivers/Stripe/Charge.php +++ b/app/PaymentDrivers/Stripe/Charge.php @@ -21,9 +21,12 @@ use App\Models\PaymentType; use App\Models\SystemLog; use App\PaymentDrivers\StripePaymentDriver; use App\Utils\Ninja; +use App\Utils\Traits\MakesHash; class Charge { + use MakesHash; + /** @var StripePaymentDriver */ public $stripe; @@ -39,7 +42,7 @@ class Charge public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash) { $amount = array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total; - $invoice = sInvoice::whereIn('id', $this->transformKeys(array_column($payment_hash->invoices(), 'invoice_id')))->first(); + $invoice = Invoice::whereIn('id', $this->transformKeys(array_column($payment_hash->invoices(), 'invoice_id')))->first(); if ($invoice) { $description = "Invoice {$invoice->number} for {$amount} for client {$this->stripe->client->present()->name()}"; diff --git a/app/Policies/ExpenseCategoryPolicy.php b/app/Policies/ExpenseCategoryPolicy.php new file mode 100644 index 000000000000..37529ece0486 --- /dev/null +++ b/app/Policies/ExpenseCategoryPolicy.php @@ -0,0 +1,32 @@ +isAdmin() || $user->hasPermission('create_expense_categories') || $user->hasPermission('create_all'); + } +} diff --git a/app/Policies/TaskPolicy.php b/app/Policies/TaskPolicy.php new file mode 100644 index 000000000000..40e4ae0cb921 --- /dev/null +++ b/app/Policies/TaskPolicy.php @@ -0,0 +1,26 @@ +isAdmin(); + } +} diff --git a/app/Policies/TaxRatePolicy.php b/app/Policies/TaxRatePolicy.php index ae2c5082a4a2..e589d36153bb 100644 --- a/app/Policies/TaxRatePolicy.php +++ b/app/Policies/TaxRatePolicy.php @@ -12,6 +12,7 @@ namespace App\Policies; use App\Models\TaxRate; +use App\Models\User; /** * Class TaxRatePolicy. diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 96687e579d31..d0c8d2978134 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -20,6 +20,7 @@ use App\Models\Credit; use App\Models\Design; use App\Models\Document; use App\Models\Expense; +use App\Models\ExpenseCategory; use App\Models\GroupSetting; use App\Models\Invoice; use App\Models\Payment; @@ -29,6 +30,7 @@ use App\Models\Project; use App\Models\Quote; use App\Models\RecurringInvoice; use App\Models\RecurringQuote; +use App\Models\Task; use App\Models\TaxRate; use App\Models\User; use App\Models\Vendor; @@ -41,6 +43,7 @@ use App\Policies\CompanyTokenPolicy; use App\Policies\CreditPolicy; use App\Policies\DesignPolicy; use App\Policies\DocumentPolicy; +use App\Policies\ExpenseCategoryPolicy; use App\Policies\ExpensePolicy; use App\Policies\GroupSettingPolicy; use App\Policies\InvoicePolicy; @@ -51,6 +54,7 @@ use App\Policies\ProjectPolicy; use App\Policies\QuotePolicy; use App\Policies\RecurringInvoicePolicy; use App\Policies\RecurringQuotePolicy; +use App\Policies\TaskPolicy; use App\Policies\TaxRatePolicy; use App\Policies\UserPolicy; use App\Policies\VendorPolicy; @@ -76,6 +80,7 @@ class AuthServiceProvider extends ServiceProvider Design::class => DesignPolicy::class, Document::class => DocumentPolicy::class, Expense::class => ExpensePolicy::class, + ExpenseCategory::class => ExpenseCategoryPolicy::class, GroupSetting::class => GroupSettingPolicy::class, Invoice::class => InvoicePolicy::class, Payment::class => PaymentPolicy::class, @@ -86,6 +91,7 @@ class AuthServiceProvider extends ServiceProvider RecurringInvoice::class => RecurringInvoicePolicy::class, RecurringQuote::class => RecurringQuotePolicy::class, Webhook::class => WebhookPolicy::class, + Task::class => TaskPolicy::class, TaxRate::class => TaxRatePolicy::class, User::class => UserPolicy::class, Vendor::class => VendorPolicy::class, diff --git a/app/Repositories/ExpenseRepository.php b/app/Repositories/ExpenseRepository.php index 64bfea705648..990cf0b93818 100644 --- a/app/Repositories/ExpenseRepository.php +++ b/app/Repositories/ExpenseRepository.php @@ -52,6 +52,11 @@ class ExpenseRepository extends BaseRepository $expense->save(); + + if (array_key_exists('documents', $data)) { + $this->saveDocuments($data['documents'], $expense); + } + // if ($expense->id_number == "" || !$expense->id_number) { // $expense->id_number = $this->getNextExpenseNumber($expense); // } //todo write tests for this and make sure that custom expense numbers also works as expected from here diff --git a/app/Repositories/TaskRepository.php b/app/Repositories/TaskRepository.php new file mode 100644 index 000000000000..2bf267e53edb --- /dev/null +++ b/app/Repositories/TaskRepository.php @@ -0,0 +1,75 @@ +fill($data); + $task->save(); + + if (array_key_exists('documents', $data)) { + $this->saveDocuments($data['documents'], $task); + } + + return $task; + } + + /** + * Store tasks in bulk. + * + * @param array $task + * @return task|null + */ + public function create($task): ?Task + { + return $this->save( + $task, + TaskFactory::create(auth()->user()->company()->id, auth()->user()->id) + ); + } +} diff --git a/app/Repositories/VendorRepository.php b/app/Repositories/VendorRepository.php index 2611e9bc0099..6f241f8187a5 100644 --- a/app/Repositories/VendorRepository.php +++ b/app/Repositories/VendorRepository.php @@ -75,6 +75,10 @@ class VendorRepository extends BaseRepository $data['name'] = $vendor->present()->name(); } + if (array_key_exists('documents', $data)) { + $this->saveDocuments($data['documents'], $vendor); + } + return $vendor; } diff --git a/app/Services/Client/ClientService.php b/app/Services/Client/ClientService.php index a79e5714db86..593091d3c915 100644 --- a/app/Services/Client/ClientService.php +++ b/app/Services/Client/ClientService.php @@ -12,6 +12,8 @@ namespace App\Services\Client; use App\Models\Client; +use App\Utils\Number; +use Illuminate\Database\Eloquent\Collection; class ClientService { @@ -43,6 +45,27 @@ class ClientService return $this; } + public function getCreditBalance() :float + { + $credits = $this->client->credits + ->where('is_deleted', false) + ->where('balance', '>', 0) + ->sortBy('created_at'); + + return Number::roundValue($credits->sum('balance'), $this->client->currency()->precision); + } + + public function getCredits() :Collection + { + + return $this->client->credits + ->where('is_deleted', false) + ->where('balance', '>', 0) + ->sortBy('created_at'); + + } + + public function save() :Client { $this->client->save(); diff --git a/app/Services/Credit/ApplyPayment.php b/app/Services/Credit/ApplyPayment.php new file mode 100644 index 000000000000..e9a5665e5f39 --- /dev/null +++ b/app/Services/Credit/ApplyPayment.php @@ -0,0 +1,152 @@ +credit = $credit; + $this->invoice = $invoice; + $this->amount = $amount; + $this->amount_applied = 0; + $this->payment = $payment->fresh(); + } + + public function run() :Credit + { + + //$available_credit_balance = $this->credit->balance; + $applicable_amount = min($this->amount, $this->credit->balance); + $invoice_balance = $this->invoice->balance; + + /* Check invoice partial for amount to be cleared first */ + if($this->invoice->partial > 0){ + + $partial_payment = min($this->invoice->partial, $applicable_amount); + + $this->invoice->partial -= $partial_payment; + $invoice_balance -= $partial_payment; + $this->amount -= $partial_payment; + // $this->credit->balance -= $partial_payment; + $applicable_amount -= $partial_payment; + $this->amount_applied += $partial_payment; + + } + + /* If there is remaining amount use it on the balance */ + if($this->amount > 0 && $applicable_amount > 0 && $invoice_balance > 0){ + + $balance_payment = min($invoice_balance, $this->amount); + + $invoice_balance -= $balance_payment; + $this->amount -= $balance_payment; + // $this->credit->balance -= $balance_payment; + $this->amount_applied += $balance_payment; + + } + + $this->applyPaymentToCredit(); + + $this->addPaymentToLedger(); + + return $this->credit; + + } + + private function applyPaymentToCredit() + { + + $credit_item = new InvoiceItem; + $credit_item->type_id = '1'; + $credit_item->product_key = ctrans('texts.credit'); + $credit_item->notes = ctrans('texts.credit_payment', ['invoice_number' => $this->invoice->number]); + $credit_item->quantity = 1; + $credit_item->cost = $this->amount_applied * -1; + + $credit_items = $this->credit->line_items; + $credit_items[] = $credit_item; + + $this->credit->line_items = $credit_items; + + $this->credit = $this->credit->calc()->getCredit(); + $this->credit->save(); + + } + + private function addPaymentToLedger() + { + + $this->payment->amount += $this->amount_applied; + $this->payment->applied += $this->amount_applied; + $this->payment->status_id = Payment::STATUS_COMPLETED; + $this->payment->currency_id = $this->credit->client->getSetting('currency_id'); + $this->payment->save(); + + $this->payment + ->invoices() + ->attach($this->invoice->id, ['amount' => $this->amount_applied]); + + $this->payment + ->credits() + ->attach($this->credit->id, ['amount' => $this->amount_applied]); + + $this->payment + ->ledger() + ->updatePaymentBalance($this->amount_applied * -1); + + $this->payment + ->client + ->service() + ->updateBalance($this->amount_applied * -1) + ->adjustCreditBalance($this->amount_applied * -1) + ->updatePaidToDate($this->amount_applied) + ->save(); + + $this->invoice + ->service() + ->updateBalance($this->amount_applied * -1) + ->updateStatus() + ->save(); + + $this->credit + ->ledger() + ->updateCreditBalance(($this->amount_applied * -1), "Credit payment applied to Invoice {$this->invoice->number}"); + + event(new InvoiceWasUpdated($this->invoice, $this->invoice->company, Ninja::eventVars())); + + if((int)$this->invoice->balance == 0){ + $this->invoice->service()->deletePdf(); + event(new InvoiceWasPaid($this->invoice, $this->payment->company, Ninja::eventVars())); + } + + } +} diff --git a/app/Services/Credit/CreditService.php b/app/Services/Credit/CreditService.php index e161ff6c2764..8b8dca70ae9d 100644 --- a/app/Services/Credit/CreditService.php +++ b/app/Services/Credit/CreditService.php @@ -12,6 +12,7 @@ namespace App\Services\Credit; use App\Models\Credit; +use App\Services\Credit\ApplyPayment; use App\Services\Credit\CreateInvitations; use App\Services\Credit\MarkSent; @@ -61,6 +62,13 @@ class CreditService return $this; } + public function applyPayment($invoice, $amount, $payment) + { + $this->credit = (new ApplyPayment($this->credit, $invoice, $amount, $payment))->run(); + + return $this; + } + /** * Saves the credit. * @return Credit object diff --git a/app/Services/Invoice/AutoBillInvoice.php b/app/Services/Invoice/AutoBillInvoice.php index 42bc0498e094..d3afe99dd3d1 100644 --- a/app/Services/Invoice/AutoBillInvoice.php +++ b/app/Services/Invoice/AutoBillInvoice.php @@ -84,7 +84,7 @@ class AutoBillInvoice extends AbstractService /* Build payment hash */ $payment_hash = PaymentHash::create([ 'hash' => Str::random(128), - 'data' => ['invoice_id' => $this->invoice->hashed_id, 'amount' => $amount], + 'data' => [['invoice_id' => $this->invoice->hashed_id, 'amount' => $amount]], 'fee_total' => $fee, 'fee_invoice_id' => $this->invoice->id, ]); @@ -104,10 +104,8 @@ class AutoBillInvoice extends AbstractService */ private function finalizePaymentUsingCredits() { - info("finalizing"); - info(print_r($this->used_credit,1)); + $amount = array_sum(array_column($this->used_credit, 'amount')); - info("amount {$amount}"); $payment = PaymentFactory::create($this->invoice->company_id, $this->invoice->user_id); $payment->amount = $amount; diff --git a/app/Transformers/ExpenseCategoryTransformer.php b/app/Transformers/ExpenseCategoryTransformer.php new file mode 100644 index 000000000000..9a5d522ef537 --- /dev/null +++ b/app/Transformers/ExpenseCategoryTransformer.php @@ -0,0 +1,55 @@ + $this->encodePrimaryKey($expense_category->id), + 'user_id' => $this->encodePrimaryKey($expense_category->user_id), + 'name' => (string) $expense_category->name ?: '', + 'is_deleted' => (bool) $expense_category->is_deleted, + 'updated_at' => (int) $expense_category->updated_at, + 'archived_at' => (int) $expense_category->deleted_at, + 'created_at' => (int) $expense_category->created_at, + ]; + } +} diff --git a/app/Transformers/ExpenseTransformer.php b/app/Transformers/ExpenseTransformer.php index 8dcde2bfd23d..e1d6c9ca6a17 100644 --- a/app/Transformers/ExpenseTransformer.php +++ b/app/Transformers/ExpenseTransformer.php @@ -58,7 +58,7 @@ class ExpenseTransformer extends EntityTransformer 'bank_id' => (string) $expense->bank_id ?: '', 'invoice_currency_id' => (string) $expense->invoice_currency_id ?: '', 'expense_currency_id' => (string) $expense->expense_currency_id ?: '', - 'invoice_category_id' => (string) $expense->invoice_category_id ?: '', + 'category_id' => (string) $expense->category_id ?: '', 'payment_type_id' => (string) $expense->payment_type_id ?: '', 'recurring_expense_id' => (string) $expense->recurring_expense_id ?: '', 'is_deleted' => (bool) $expense->is_deleted, @@ -77,7 +77,8 @@ class ExpenseTransformer extends EntityTransformer 'public_notes' => (string) $expense->public_notes ?: '', 'transaction_reference' => (string) $expense->transaction_reference ?: '', 'transaction_id' => (string) $expense->transaction_id ?: '', - 'expense_date' => $expense->expense_date ?: '', + //'date' => $expense->date ?: '', + 'expense_date' => $expense->date ?: '', 'payment_date' => $expense->payment_date ?: '', 'custom_value1' => $expense->custom_value1 ?: '', 'custom_value2' => $expense->custom_value2 ?: '', diff --git a/app/Transformers/TaskTransformer.php b/app/Transformers/TaskTransformer.php index d5b21a4eacd9..fd2cc483ae01 100644 --- a/app/Transformers/TaskTransformer.php +++ b/app/Transformers/TaskTransformer.php @@ -44,6 +44,10 @@ class TaskTransformer extends EntityTransformer { return [ 'id' => (string) $this->encodePrimaryKey($task->id), + 'user_id' => (string) $this->encodePrimaryKey($task->user_id), + 'assigned_user_id' => (string) $this->encodePrimaryKey($task->assigned_user_id), + 'number' => (string) $task->number ?: '', + 'start_time' => (int) $task->start_time, 'description' => $task->description ?: '', 'duration' => 0, 'created_at' => (int) $task->created_at, diff --git a/app/Utils/SystemHealth.php b/app/Utils/SystemHealth.php index 9766640c9a9e..7c02be66e752 100644 --- a/app/Utils/SystemHealth.php +++ b/app/Utils/SystemHealth.php @@ -35,6 +35,7 @@ class SystemHealth 'xml', 'bcmath', 'mysqlnd', + //'intl', //todo double check whether we need this for email dns validation ]; private static $php_version = 7.3; diff --git a/config/ninja.php b/config/ninja.php index c21ae376ba0f..7c8633f845f6 100644 --- a/config/ninja.php +++ b/config/ninja.php @@ -89,7 +89,7 @@ return [ 'date_formats' => App\Models\DateFormat::class, 'datetime_formats' => App\Models\DatetimeFormat::class, 'gateways' => App\Models\Gateway::class, - 'gateway_types' => App\Models\GatewayType::class, + //'gateway_types' => App\Models\GatewayType::class, 'industries' => App\Models\Industry::class, 'languages' => App\Models\Language::class, 'payment_types' => App\Models\PaymentType::class, diff --git a/database/factories/ExpenseCategoryFactory.php b/database/factories/ExpenseCategoryFactory.php new file mode 100644 index 000000000000..38a1c07aa7fd --- /dev/null +++ b/database/factories/ExpenseCategoryFactory.php @@ -0,0 +1,37 @@ + $this->faker->text(10), + ]; + } +} diff --git a/database/factories/ExpenseFactory.php b/database/factories/ExpenseFactory.php index 675b801328f5..ea5716ca6402 100644 --- a/database/factories/ExpenseFactory.php +++ b/database/factories/ExpenseFactory.php @@ -37,7 +37,7 @@ class ExpenseFactory extends Factory 'custom_value3' => $this->faker->text(10), 'custom_value4' => $this->faker->text(10), 'exchange_rate' => $this->faker->randomFloat(2, 0, 1), - 'expense_date' => $this->faker->date(), + 'date' => $this->faker->date(), 'is_deleted' => false, 'public_notes' => $this->faker->text(50), 'private_notes' => $this->faker->text(50), diff --git a/database/migrations/2014_10_13_000000_create_users_table.php b/database/migrations/2014_10_13_000000_create_users_table.php index b4aa48dde235..edd0af5e83f9 100644 --- a/database/migrations/2014_10_13_000000_create_users_table.php +++ b/database/migrations/2014_10_13_000000_create_users_table.php @@ -1244,11 +1244,12 @@ class CreateUsersTable extends Migration $table->increments('id'); $table->unsignedInteger('user_id'); $table->unsignedInteger('company_id')->index(); + $table->string('name')->nullable(); $table->timestamps(6); $table->softDeletes(); - $table->string('name')->nullable(); $table->index(['company_id', 'deleted_at']); + $table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade')->onUpdate('cascade'); }); Schema::create('expenses', function (Blueprint $table) { diff --git a/database/migrations/2020_10_12_204517_project_number_column.php b/database/migrations/2020_10_12_204517_project_number_column.php new file mode 100644 index 000000000000..9dc9740191df --- /dev/null +++ b/database/migrations/2020_10_12_204517_project_number_column.php @@ -0,0 +1,38 @@ +string('number')->nullable(); + }); + + Schema::table('expenses', function ($t){ + $t->renameColumn('expense_date', 'date'); + }); + + Schema::table('expense_categories', function ($t){ + $t->boolean('is_deleted')->default(false); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/database/seeders/RandomDataSeeder.php b/database/seeders/RandomDataSeeder.php index 109a3832a77f..d2b75f333c52 100644 --- a/database/seeders/RandomDataSeeder.php +++ b/database/seeders/RandomDataSeeder.php @@ -100,7 +100,7 @@ class RandomDataSeeder extends Seeder $account->save(); $user = User::factory()->create([ - 'email' => $faker->email, + 'email' => $faker->freeEmail, 'account_id' => $account->id, 'confirmation_code' => $this->createDbHash(config('database.default')), ]); diff --git a/resources/views/portal/default/gateways/pay_now.blade.php b/resources/views/portal/default/gateways/pay_now.blade.php index 21d10106488c..6e97da3731b4 100644 --- a/resources/views/portal/default/gateways/pay_now.blade.php +++ b/resources/views/portal/default/gateways/pay_now.blade.php @@ -38,7 +38,12 @@
  • {{ ctrans('texts.total')}}

    {{ $amount }}

  • - @if($fee) + @if($credit_totals > 0) +
  • {{ ctrans('texts.credit_amount')}} +

    {{ $credit_totals }}

    +
  • + @endifs + @if($fee > 0)
  • {{ ctrans('texts.gateway_fee')}}

    {{ $fee }}

  • diff --git a/resources/views/portal/ninja2020/gateways/stripe/credit_card.blade.php b/resources/views/portal/ninja2020/gateways/stripe/credit_card.blade.php index c2cbe6d6e62a..6248f80a52eb 100644 --- a/resources/views/portal/ninja2020/gateways/stripe/credit_card.blade.php +++ b/resources/views/portal/ninja2020/gateways/stripe/credit_card.blade.php @@ -16,6 +16,10 @@ +
    + @csrf + +
    @@ -39,21 +43,35 @@
    {{ App\Utils\Number::formatMoney($total['invoice_totals'], $client) }}
    + @if($total['fee_total'] > 0)
    {{ ctrans('texts.gateway_fees') }}
    {{ App\Utils\Number::formatMoney($total['fee_total'], $client) }}
    + @endif + @if($total['credit_totals'] > 0)
    - {{ ctrans('texts.total') }} + {{ ctrans('texts.credit_amount') }} +
    +
    + {{ App\Utils\Number::formatMoney($total['credit_totals'], $client) }} +
    + @endif +
    + {{ ctrans('texts.amount_due') }}
    {{ App\Utils\Number::formatMoney($total['amount_with_fee'], $client) }}
    - - @if($token) + @if((int)$total['amount_with_fee'] == 0) + +
    + +
    + @elseif($token)
    {{ ctrans('texts.credit_card') }} diff --git a/routes/api.php b/routes/api.php index 8e0449643bf7..711dcc25088e 100644 --- a/routes/api.php +++ b/routes/api.php @@ -69,6 +69,14 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::post('expenses/bulk', 'ExpenseController@bulk')->name('expenses.bulk'); + Route::resource('expense_categories', 'ExpenseCategoryController'); // name = (expense_categories. index / create / show / update / destroy / edit + + Route::post('expense_categories/bulk', 'ExpenseCategoryController@bulk')->name('expense_categories.bulk'); + + Route::resource('tasks', 'TaskController'); // name = (tasks. index / create / show / update / destroy / edit + + Route::post('tasks/bulk', 'TaskController@bulk')->name('tasks.bulk'); + Route::resource('projects', 'ProjectController'); // name = (projects. index / create / show / update / destroy / edit Route::post('projects/bulk', 'ProjectController@bulk')->name('projects.bulk'); @@ -125,7 +133,7 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::resource('group_settings', 'GroupSettingController'); Route::post('group_settings/bulk', 'GroupSettingController@bulk'); - Route::resource('tax_rates', 'TaxRateController'); // name = (tasks. index / create / show / update / destroy / edit + Route::resource('tax_rates', 'TaxRateController'); // name = (tax_rates. index / create / show / update / destroy / edit Route::post('tax_rates/bulk', 'TaxRateController@bulk')->name('tax_rates.bulk'); Route::post('refresh', 'Auth\LoginController@refresh'); diff --git a/routes/client.php b/routes/client.php index a7be93d29df2..685482d0ed5b 100644 --- a/routes/client.php +++ b/routes/client.php @@ -35,6 +35,8 @@ Route::group(['middleware' => ['auth:contact', 'locale'], 'prefix' => 'client', Route::get('recurring_invoices/{recurring_invoice}/request_cancellation', 'ClientPortal\RecurringInvoiceController@requestCancellation')->name('recurring_invoices.request_cancellation'); Route::post('payments/process', 'ClientPortal\PaymentController@process')->name('payments.process'); + Route::post('payments/credit_response', 'ClientPortal\PaymentController@credit_response')->name('payments.credit_response'); + Route::get('payments', 'ClientPortal\PaymentController@index')->name('payments.index')->middleware('portal_enabled'); Route::get('payments/{payment}', 'ClientPortal\PaymentController@show')->name('payments.show'); Route::post('payments/process/response', 'ClientPortal\PaymentController@response')->name('payments.response'); diff --git a/tests/Feature/DocumentsApiTest.php b/tests/Feature/DocumentsApiTest.php index 1b0ad8454674..67976152f39a 100644 --- a/tests/Feature/DocumentsApiTest.php +++ b/tests/Feature/DocumentsApiTest.php @@ -140,18 +140,18 @@ class DocumentsApiTest extends TestCase } - // public function testTaskDocuments() - // { + public function testTaskDocuments() + { - // $response = $this->withHeaders([ - // 'X-API-SECRET' => config('ninja.api_secret'), - // 'X-API-TOKEN' => $this->token, - // ])->get('/api/v1/tasks'); + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/tasks'); - // $response->assertStatus(200); - // $arr = $response->json(); - // $this->assertArrayHasKey('documents', $arr['data'][0]); + $response->assertStatus(200); + $arr = $response->json(); + $this->assertArrayHasKey('documents', $arr['data'][0]); - // } + } } diff --git a/tests/Feature/ExpenseCategoryApiTest.php b/tests/Feature/ExpenseCategoryApiTest.php new file mode 100644 index 000000000000..82eed58ccc29 --- /dev/null +++ b/tests/Feature/ExpenseCategoryApiTest.php @@ -0,0 +1,151 @@ +makeTestData(); + + Session::start(); + + $this->faker = \Faker\Factory::create(); + + Model::reguard(); + } + + public function testExpenseCategoryPost() + { + $data = [ + 'name' => $this->faker->firstName, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/expense_categories', $data); + + $response->assertStatus(200); + } + + public function testExpenseCategoryPut() + { + $data = [ + 'name' => $this->faker->firstName, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->put('/api/v1/expense_categories/'.$this->encodePrimaryKey($this->expense_category->id), $data); + + $response->assertStatus(200); + } + + public function testExpenseCategoryGet() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/expense_categories/'.$this->encodePrimaryKey($this->expense_category->id)); + + $response->assertStatus(200); + } + + public function testExpenseCategoryNotArchived() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/expense_categories/'.$this->encodePrimaryKey($this->expense_category->id)); + + $arr = $response->json(); + + $this->assertEquals(0, $arr['data']['archived_at']); + } + + public function testExpenseCategoryArchived() + { + $data = [ + 'ids' => [$this->encodePrimaryKey($this->expense_category->id)], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/expense_categories/bulk?action=archive', $data); + + $arr = $response->json(); + + $this->assertNotNull($arr['data'][0]['archived_at']); + } + + public function testExpenseCategoryRestored() + { + $data = [ + 'ids' => [$this->encodePrimaryKey($this->expense_category->id)], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/expense_categories/bulk?action=restore', $data); + + $arr = $response->json(); + + $this->assertEquals(0, $arr['data'][0]['archived_at']); + } + + public function testExpenseCategoryDeleted() + { + $data = [ + 'ids' => [$this->encodePrimaryKey($this->expense_category->id)], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/expense_categories/bulk?action=delete', $data); + + $arr = $response->json(); + + $this->assertTrue($arr['data'][0]['is_deleted']); + } +} diff --git a/tests/Feature/TaskApiTest.php b/tests/Feature/TaskApiTest.php new file mode 100644 index 000000000000..b6ed93936754 --- /dev/null +++ b/tests/Feature/TaskApiTest.php @@ -0,0 +1,151 @@ +makeTestData(); + + Session::start(); + + $this->faker = \Faker\Factory::create(); + + Model::reguard(); + } + + public function testTaskPost() + { + $data = [ + 'description' => $this->faker->firstName, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/tasks', $data); + + $response->assertStatus(200); + } + + public function testTaskPut() + { + $data = [ + 'description' => $this->faker->firstName, + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->put('/api/v1/tasks/'.$this->encodePrimaryKey($this->task->id), $data); + + $response->assertStatus(200); + } + + public function testTaskGet() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/tasks/'.$this->encodePrimaryKey($this->task->id)); + + $response->assertStatus(200); + } + + public function testTaskNotArchived() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/tasks/'.$this->encodePrimaryKey($this->task->id)); + + $arr = $response->json(); + + $this->assertEquals(0, $arr['data']['archived_at']); + } + + public function testTaskArchived() + { + $data = [ + 'ids' => [$this->encodePrimaryKey($this->task->id)], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/tasks/bulk?action=archive', $data); + + $arr = $response->json(); + + $this->assertNotNull($arr['data'][0]['archived_at']); + } + + public function testTaskRestored() + { + $data = [ + 'ids' => [$this->encodePrimaryKey($this->task->id)], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/tasks/bulk?action=restore', $data); + + $arr = $response->json(); + + $this->assertEquals(0, $arr['data'][0]['archived_at']); + } + + public function testTaskDeleted() + { + $data = [ + 'ids' => [$this->encodePrimaryKey($this->task->id)], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->post('/api/v1/tasks/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 27acbaf2b934..b1056c26630c 100644 --- a/tests/MockAccountData.php +++ b/tests/MockAccountData.php @@ -32,6 +32,7 @@ use App\Models\CompanyGateway; use App\Models\CompanyToken; use App\Models\Credit; use App\Models\Expense; +use App\Models\ExpenseCategory; use App\Models\GroupSetting; use App\Models\Invoice; use App\Models\InvoiceInvitation; @@ -40,6 +41,7 @@ use App\Models\Project; use App\Models\Quote; use App\Models\QuoteInvitation; use App\Models\RecurringInvoice; +use App\Models\Task; use App\Models\User; use App\Models\Vendor; use App\Models\VendorContact; @@ -77,6 +79,12 @@ trait MockAccountData public $expense; + public $task; + + public $expense_category; + + public $cu; + public function makeTestData() { @@ -143,10 +151,10 @@ trait MockAccountData $this->user->password = Hash::make('ALongAndBriliantPassword'); - $cu = CompanyUserFactory::create($this->user->id, $this->company->id, $this->account->id); - $cu->is_owner = true; - $cu->is_admin = true; - $cu->save(); + $this->cu = CompanyUserFactory::create($this->user->id, $this->company->id, $this->account->id); + $this->cu->is_owner = true; + $this->cu->is_admin = true; + $this->cu->save(); $this->token = \Illuminate\Support\Str::random(64); @@ -160,6 +168,8 @@ trait MockAccountData $company_token->save(); + //todo create one token withe token name TOKEN - use firstOrCreate + Product::factory()->create([ 'user_id' => $this->user->id, 'company_id' => $this->company->id, @@ -217,6 +227,16 @@ trait MockAccountData 'company_id' => $this->company->id, ]); + $this->task = Task::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + ]); + + $this->expense_category = ExpenseCategory::factory()->create([ + 'user_id' => $this->user->id, + 'company_id' => $this->company->id, + ]); + $gs = new GroupSetting; $gs->name = 'Test'; $gs->company_id = $this->client->company_id; diff --git a/tests/Unit/FactoryCreationTest.php b/tests/Unit/FactoryCreationTest.php index a025cd7cb24f..1340156d0b4c 100644 --- a/tests/Unit/FactoryCreationTest.php +++ b/tests/Unit/FactoryCreationTest.php @@ -143,7 +143,7 @@ class FactoryCreationTest extends TestCase public function testUserCreate() { $new_user = UserFactory::create($this->account->id); - $new_user->email = $this->faker->email; + $new_user->email = $this->faker->freeEmail; $new_user->save(); $this->assertNotNull($new_user);