From 84642bf035a57fa129274beeacc738a448f46b0c Mon Sep 17 00:00:00 2001 From: David Bomba Date: Mon, 20 Jan 2020 12:31:58 +1100 Subject: [PATCH] Expense and Vendors (#3226) * add expenses, vendors and vendor_contacts along with factories and test data * padding out vendors, expenses * Minor fixes * Add Expense and Company TransformerS --- app/Console/Commands/CreateTestData.php | 45 +- app/DataMapper/EmailTemplateDefaults.php | 25 +- app/Factory/ExpenseFactory.php | 39 ++ app/Factory/InvoiceFactory.php | 2 + app/Factory/VendorContactFactory.php | 28 + app/Factory/VendorFactory.php | 37 ++ app/Filters/VendorFilters.php | 196 +++++++ app/Http/Controllers/BaseController.php | 2 + app/Http/Controllers/ExpenseController.php | 510 ++++++++++++++++++ .../Support/Messages/SendingController.php | 2 +- app/Http/Controllers/TemplateController.php | 9 +- app/Http/Controllers/VendorController.php | 509 +++++++++++++++++ .../Requests/Vendor/BulkVendorRequest.php | 47 ++ .../Requests/Vendor/CreateVendorRequest.php | 29 + .../Requests/Vendor/DestroyVendorRequest.php | 29 + .../Requests/Vendor/EditVendorRequest.php | 39 ++ .../Requests/Vendor/ShowVendorRequest.php | 29 + .../Requests/Vendor/StoreVendorRequest.php | 79 +++ .../Requests/Vendor/UpdateVendorRequest.php | 73 +++ app/Models/Company.php | 8 + app/Models/Expense.php | 49 +- app/Models/ExpenseCategory.php | 34 ++ app/Models/Vendor.php | 73 +++ app/Models/VendorContact.php | 135 +++++ app/Policies/ExpensePolicy.php | 33 ++ app/Policies/VendorPolicy.php | 33 ++ app/Providers/AuthServiceProvider.php | 6 + app/Repositories/InvoiceRepository.php | 3 +- app/Repositories/VendorContactRepository.php | 76 +++ app/Repositories/VendorRepository.php | 95 ++++ app/Transformers/CompanyTransformer.php | 16 + app/Transformers/ExpenseTransformer.php | 80 +++ app/Transformers/VendorContactTransformer.php | 47 ++ app/Transformers/VendorTransformer.php | 98 ++++ app/Utils/Traits/InvoiceEmailBuilder.php | 10 +- database/factories/ExpenseFactory.php | 22 + database/factories/VendorContactFactory.php | 24 + database/factories/VendorFactory.php | 23 + .../2014_10_13_000000_create_users_table.php | 116 ++++ routes/api.php | 10 +- tests/Feature/InvoiceEmailTest.php | 1 - 41 files changed, 2693 insertions(+), 28 deletions(-) create mode 100644 app/Factory/ExpenseFactory.php create mode 100644 app/Factory/VendorContactFactory.php create mode 100644 app/Factory/VendorFactory.php create mode 100644 app/Filters/VendorFilters.php create mode 100644 app/Http/Controllers/ExpenseController.php create mode 100644 app/Http/Controllers/VendorController.php create mode 100644 app/Http/Requests/Vendor/BulkVendorRequest.php create mode 100644 app/Http/Requests/Vendor/CreateVendorRequest.php create mode 100644 app/Http/Requests/Vendor/DestroyVendorRequest.php create mode 100644 app/Http/Requests/Vendor/EditVendorRequest.php create mode 100644 app/Http/Requests/Vendor/ShowVendorRequest.php create mode 100644 app/Http/Requests/Vendor/StoreVendorRequest.php create mode 100644 app/Http/Requests/Vendor/UpdateVendorRequest.php create mode 100644 app/Models/ExpenseCategory.php create mode 100644 app/Models/Vendor.php create mode 100644 app/Models/VendorContact.php create mode 100644 app/Policies/ExpensePolicy.php create mode 100644 app/Policies/VendorPolicy.php create mode 100644 app/Repositories/VendorContactRepository.php create mode 100644 app/Repositories/VendorRepository.php create mode 100644 app/Transformers/ExpenseTransformer.php create mode 100644 app/Transformers/VendorContactTransformer.php create mode 100644 app/Transformers/VendorTransformer.php create mode 100644 database/factories/ExpenseFactory.php create mode 100644 database/factories/VendorContactFactory.php create mode 100644 database/factories/VendorFactory.php diff --git a/app/Console/Commands/CreateTestData.php b/app/Console/Commands/CreateTestData.php index dd1c2bf36222..7dffda4c6345 100644 --- a/app/Console/Commands/CreateTestData.php +++ b/app/Console/Commands/CreateTestData.php @@ -241,7 +241,6 @@ class CreateTestData extends Command 'company_id' => $company->id ]); - factory(\App\Models\ClientContact::class, 1)->create([ 'user_id' => $user->id, 'client_id' => $client->id, @@ -262,9 +261,53 @@ class CreateTestData extends Command for ($x=0; $x<$y; $x++) { $this->createInvoice($client); $this->createQuote($client); + $this->createExpense($client); + $this->createVendor($client); } } + private function createExpense($client) + { + + factory(\App\Models\Expense::class, rand(10, 50))->create([ + 'user_id' => $client->user->id, + 'client_id' => $client->id, + 'company_id' => $client->company->id + ]); + + } + + private function createVendor($client) + { + + $vendor = factory(\App\Models\Vendor::class)->create([ + 'user_id' => $client->user->id, + 'company_id' => $client->company->id + ]); + + + factory(\App\Models\VendorContact::class, 1)->create([ + 'user_id' => $client->user->id, + 'vendor_id' => $vendor->id, + 'company_id' => $client->company->id, + 'is_primary' => 1 + ]); + + factory(\App\Models\VendorContact::class, rand(1, 50))->create([ + 'user_id' => $client->user->id, + 'vendor_id' => $vendor->id, + 'company_id' => $client->company->id, + 'is_primary' => 0 + ]); + + factory(\App\Models\Vendor::class, rand(10, 50))->create([ + 'user_id' => $client->user->id, + 'company_id' => $client->company->id + ]); + + + } + private function createInvoice($client) { $faker = \Faker\Factory::create(); diff --git a/app/DataMapper/EmailTemplateDefaults.php b/app/DataMapper/EmailTemplateDefaults.php index 7588757118a1..1e830a3382fa 100644 --- a/app/DataMapper/EmailTemplateDefaults.php +++ b/app/DataMapper/EmailTemplateDefaults.php @@ -11,7 +11,7 @@ namespace App\DataMapper; -use Parsedown; +use League\CommonMark\CommonMarkConverter; class EmailTemplateDefaults { @@ -23,7 +23,14 @@ class EmailTemplateDefaults public static function emailInvoiceTemplate() { - return Parsedown::instance()->line(self::transformText('invoice_message')); + $converter = new CommonMarkConverter([ + 'html_input' => 'strip', + 'allow_unsafe_links' => false, + ]); + + return $converter->convertToHtml(self::transformText('invoice_message')); + + //return Parsedown::instance()->line(self::transformText('invoice_message')); } public static function emailQuoteSubject() @@ -35,7 +42,13 @@ class EmailTemplateDefaults public static function emailQuoteTemplate() { - return Parsedown::instance()->line(self::transformText('quote_message')); + $converter = new CommonMarkConverter([ + 'html_input' => 'strip', + 'allow_unsafe_links' => false, + ]); + + return $converter->convertToHtml(self::transformText('quote_message')); + //return Parsedown::instance()->line(self::transformText('quote_message')); } public static function emailPaymentSubject() @@ -56,7 +69,7 @@ class EmailTemplateDefaults public static function emailReminder1Template() { - return Parsedown::instance()->line('First Email Reminder Text'); + // return Parsedown::instance()->line('First Email Reminder Text'); } public static function emailReminder2Subject() @@ -67,7 +80,7 @@ class EmailTemplateDefaults public static function emailReminder2Template() { - return Parsedown::instance()->line('Second Email Reminder Text'); + // return Parsedown::instance()->line('Second Email Reminder Text'); } public static function emailReminder3Subject() @@ -78,7 +91,7 @@ class EmailTemplateDefaults public static function emailReminder3Template() { - return Parsedown::instance()->line('Third Email Reminder Text'); + // return Parsedown::instance()->line('Third Email Reminder Text'); } public static function emailReminderEndlessSubject() diff --git a/app/Factory/ExpenseFactory.php b/app/Factory/ExpenseFactory.php new file mode 100644 index 000000000000..99a8564130e6 --- /dev/null +++ b/app/Factory/ExpenseFactory.php @@ -0,0 +1,39 @@ +user_id = $user_id; + $expense->company_id = $company_id; + $expense->is_deleted = false; + $expense->should_be_invoiced = false; + $expense->tax_name1 = ''; + $expense->tax_rate1 = 0; + $expense->tax_name2 = ''; + $expense->tax_rate2 = 0; + $expense->tax_name3 = ''; + $expense->tax_rate3 = 0; + $expense->expense_date = null; + $expense->payment_date = null; + + return $expense; + } +} diff --git a/app/Factory/InvoiceFactory.php b/app/Factory/InvoiceFactory.php index aa289c3655f6..8e9c40e121db 100644 --- a/app/Factory/InvoiceFactory.php +++ b/app/Factory/InvoiceFactory.php @@ -40,6 +40,8 @@ class InvoiceFactory $invoice->tax_rate1 = 0; $invoice->tax_name2 = ''; $invoice->tax_rate2 = 0; + $invoice->tax_name3 = ''; + $invoice->tax_rate3 = 0; $invoice->custom_value1 = 0; $invoice->custom_value2 = 0; $invoice->custom_value3 = 0; diff --git a/app/Factory/VendorContactFactory.php b/app/Factory/VendorContactFactory.php new file mode 100644 index 000000000000..5ba73935eeb3 --- /dev/null +++ b/app/Factory/VendorContactFactory.php @@ -0,0 +1,28 @@ +first_name = ""; + $vendor_contact->user_id = $user_id; + $vendor_contact->company_id = $company_id; + $vendor_contact->id = 0; + + return $vendor_contact; + } +} diff --git a/app/Factory/VendorFactory.php b/app/Factory/VendorFactory.php new file mode 100644 index 000000000000..bc5fb5e56d01 --- /dev/null +++ b/app/Factory/VendorFactory.php @@ -0,0 +1,37 @@ +company_id = $company_id; + $vendor->user_id = $user_id; + $vendor->name = ''; + $vendor->website = ''; + $vendor->private_notes = ''; + $vendor->balance = 0; + $vendor->paid_to_date = 0; + $vendor->country_id = 4; + $vendor->is_deleted = 0; + + $vendor_contact = VendorContactFactory::create($company_id, $user_id); + $vendor->contacts->add($vendor_contact); + + return $vendor; + } +} diff --git a/app/Filters/VendorFilters.php b/app/Filters/VendorFilters.php new file mode 100644 index 000000000000..183232a1ec0a --- /dev/null +++ b/app/Filters/VendorFilters.php @@ -0,0 +1,196 @@ +split($balance); + + return $this->builder->where('balance', $parts->operator, $parts->value); + } + + /** + * Filter between balances + * + * @param string balance + * @return Illuminate\Database\Query\Builder + */ + public function between_balance(string $balance): Builder + { + $parts = explode(":", $balance); + + return $this->builder->whereBetween('balance', [$parts[0], $parts[1]]); + } + + /** + * Filter based on search text + * + * @param string query filter + * @return Illuminate\Database\Query\Builder + * @deprecated + * + */ + public function filter(string $filter = '') : Builder + { + if (strlen($filter) == 0) { + return $this->builder; + } + + return $this->builder->where(function ($query) use ($filter) { + $query->where('vendors.name', 'like', '%'.$filter.'%') + ->orWhere('vendors.id_number', 'like', '%'.$filter.'%') + ->orWhere('vendor_contacts.first_name', 'like', '%'.$filter.'%') + ->orWhere('vendor_contacts.last_name', 'like', '%'.$filter.'%') + ->orWhere('vendor_contacts.email', 'like', '%'.$filter.'%') + ->orWhere('vendors.custom_value1', 'like', '%'.$filter.'%') + ->orWhere('vendors.custom_value2', 'like', '%'.$filter.'%') + ->orWhere('vendors.custom_value3', 'like', '%'.$filter.'%') + ->orWhere('vendors.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 = 'vendors'; + $filters = explode(',', $filter); + + return $this->builder->where(function ($query) use ($filters, $table) { + $query->whereNull($table . '.id'); + + if (in_array(parent::STATUS_ACTIVE, $filters)) { + $query->orWhereNull($table . '.deleted_at'); + } + + if (in_array(parent::STATUS_ARCHIVED, $filters)) { + $query->orWhere(function ($query) use ($table) { + $query->whereNotNull($table . '.deleted_at'); + + if (! in_array($table, ['users'])) { + $query->where($table . '.is_deleted', '=', 0); + } + }); + } + + if (in_array(parent::STATUS_DELETED, $filters)) { + $query->orWhere($table . '.is_deleted', '=', 1); + } + }); + } + + /** + * Sorts the list based on $sort + * + * @param string sort formatted as column|asc + * @return Illuminate\Database\Query\Builder + */ + public function sort(string $sort) : Builder + { + $sort_col = explode("|", $sort); + return $this->builder->orderBy($sort_col[0], $sort_col[1]); + } + + /** + * Returns the base query + * + * @param int company_id + * @return Illuminate\Database\Query\Builder + * @deprecated + */ + public function baseQuery(int $company_id, User $user) : Builder + { + $query = DB::table('vendors') + ->join('companies', 'companies.id', '=', 'vendors.company_id') + ->join('vendor_contacts', 'vendor_contacts.vendor_id', '=', 'vendors.id') + ->where('vendors.company_id', '=', $company_id) + ->where('vendor_contacts.is_primary', '=', true) + ->where('vendor_contacts.deleted_at', '=', null) + //->whereRaw('(vendors.name != "" or contacts.first_name != "" or contacts.last_name != "" or contacts.email != "")') // filter out buy now invoices + ->select( + // DB::raw('COALESCE(vendors.currency_id, companies.currency_id) currency_id'), + DB::raw('COALESCE(vendors.country_id, companies.country_id) country_id'), + DB::raw("CONCAT(COALESCE(vendor_contacts.first_name, ''), ' ', COALESCE(vendor_contacts.last_name, '')) contact"), + 'vendors.id', + 'vendors.name', + 'vendors.private_notes', + 'vendor_contacts.first_name', + 'vendor_contacts.last_name', + 'vendors.custom_value1', + 'vendors.custom_value2', + 'vendors.custom_value3', + 'vendors.custom_value4', + 'vendors.balance', + 'vendors.last_login', + 'vendors.created_at', + 'vendors.created_at as vendor_created_at', + 'vendor_contacts.phone', + 'vendor_contacts.email', + 'vendors.deleted_at', + 'vendors.is_deleted', + 'vendors.user_id', + 'vendors.id_number', + 'vendors.settings' + ); + + /** + * 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', Vendor::class)) { + $query->where('vendors.user_id', '=', $user->id); + } + + + return $query; + } + + /** + * Filters the query by the users company ID + * + * @param $company_id The company Id + * @return Illuminate\Database\Query\Builder + */ + public function entityFilter() + { + + //return $this->builder->whereCompanyId(auth()->user()->company()->id); + return $this->builder->company(); + } +} diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php index 51c7414522db..c677d053e042 100644 --- a/app/Http/Controllers/BaseController.php +++ b/app/Http/Controllers/BaseController.php @@ -86,6 +86,8 @@ class BaseController extends Controller //'company.payments', 'company.payments.paymentables', 'company.quotes', + 'company.vendors', + 'company.expenses', ]; } else { $include = [ diff --git a/app/Http/Controllers/ExpenseController.php b/app/Http/Controllers/ExpenseController.php new file mode 100644 index 000000000000..b762128f6d58 --- /dev/null +++ b/app/Http/Controllers/ExpenseController.php @@ -0,0 +1,510 @@ +vendor_repo = $vendor_repo; + } + + /** + * @OA\Get( + * path="/api/v1/vendors", + * operationId="getVendors", + * tags={"vendors"}, + * summary="Gets a list of vendors", + * description="Lists vendors, search and filters allow fine grained lists to be generated. + + Query parameters can be added to performed more fine grained filtering of the vendors, these are handled by the VendorFilters 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 vendors", + * @OA\Header(header="X-API-Version", ref="#/components/headers/X-API-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/Vendor"), + * ), + * @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(VendorFilters $filters) + { + $vendors = Vendor::filter($filters); + + return $this->listResponse($vendors); + } + + /** + * Display the specified resource. + * + * @param int $id + * @return \Illuminate\Http\Response + * + * + * @OA\Get( + * path="/api/v1/vendors/{id}", + * operationId="showVendor", + * tags={"vendors"}, + * 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 Vendor Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the vendor object", + * @OA\Header(header="X-API-Version", ref="#/components/headers/X-API-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/Vendor"), + * ), + * @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(ShowVendorRequest $request, Vendor $vendor) + { + return $this->itemResponse($vendor); + } + + /** + * Show the form for editing the specified resource. + * + * @param int $id + * @return \Illuminate\Http\Response + * + * + * @OA\Get( + * path="/api/v1/vendors/{id}/edit", + * operationId="editVendor", + * tags={"vendors"}, + * 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 Vendor 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-API-Version", ref="#/components/headers/X-API-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/Vendor"), + * ), + * @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(EditVendorRequest $request, Vendor $vendor) + { + return $this->itemResponse($vendor); + } + + /** + * Update the specified resource in storage. + * + * @param \Illuminate\Http\Request $request + * @param App\Models\Vendor $vendor + * @return \Illuminate\Http\Response + * + * + * + * @OA\Put( + * path="/api/v1/vendors/{id}", + * operationId="updateVendor", + * tags={"vendors"}, + * 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 Vendor 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-API-Version", ref="#/components/headers/X-API-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/Vendor"), + * ), + * @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(UpdateVendorRequest $request, Vendor $vendor) + { + if($request->entityIsDeleted($vendor)) + return $request->disallowUpdate(); + + $vendor = $this->client_repo->save($request->all(), $vendor); + + $this->uploadLogo($request->file('company_logo'), $vendor->company, $vendor); + + return $this->itemResponse($vendor->fresh()); + } + + /** + * Show the form for creating a new resource. + * + * @return \Illuminate\Http\Response + * + * + * + * @OA\Get( + * path="/api/v1/vendors/create", + * operationId="getVendorsCreate", + * tags={"vendors"}, + * 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-API-Version", ref="#/components/headers/X-API-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/Vendor"), + * ), + * @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(CreateVendorRequest $request) + { + $vendor = VendorFactory::create(auth()->user()->company()->id, auth()->user()->id); + + return $this->itemResponse($vendor); + } + + /** + * Store a newly created resource in storage. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\Response + * + * + * + * @OA\Post( + * path="/api/v1/vendors", + * operationId="storeVendor", + * tags={"vendors"}, + * 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-API-Version", ref="#/components/headers/X-API-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/Vendor"), + * ), + * @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(StoreVendorRequest $request) + { + $vendor = $this->client_repo->save($request->all(), VendorFactory::create(auth()->user()->company()->id, auth()->user()->id)); + + $vendor->load('contacts', 'primary_contact'); + + $this->uploadLogo($request->file('company_logo'), $vendor->company, $vendor); + + return $this->itemResponse($vendor); + } + + /** + * Remove the specified resource from storage. + * + * @param int $id + * @return \Illuminate\Http\Response + * + * + * @OA\Delete( + * path="/api/v1/vendors/{id}", + * operationId="deleteVendor", + * tags={"vendors"}, + * 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 Vendor 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-API-Version", ref="#/components/headers/X-API-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(DestroyVendorRequest $request, Vendor $vendor) + { + //may not need these destroy routes as we are using actions to 'archive/delete' + $vendor->delete(); + + return response()->json([], 200); + } + + /** + * Perform bulk actions on the list view + * + * @param BulkVendorRequest $request + * @return \Illuminate\Http\Response + * + * + * @OA\Post( + * path="/api/v1/vendors/bulk", + * operationId="bulkVendors", + * tags={"vendors"}, + * summary="Performs bulk actions on an array of vendors", + * 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 Vendor User response", + * @OA\Header(header="X-API-Version", ref="#/components/headers/X-API-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/Vendor"), + * ), + * @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'); + $vendors = Vendor::withTrashed()->find($this->transformKeys($ids)); + + $vendors->each(function ($vendor, $key) use ($action) { + if (auth()->user()->can('edit', $vendor)) { + $this->client_repo->{$action}($vendor); + } + }); + + return $this->listResponse(Vendor::withTrashed()->whereIn('id', $this->transformKeys($ids))); + } + + /** + * Returns a client statement + * + * @return [type] [description] + */ + public function statement() + { + //todo + } +} diff --git a/app/Http/Controllers/Support/Messages/SendingController.php b/app/Http/Controllers/Support/Messages/SendingController.php index a1afc2e5b6ae..65b395d02c6e 100644 --- a/app/Http/Controllers/Support/Messages/SendingController.php +++ b/app/Http/Controllers/Support/Messages/SendingController.php @@ -71,7 +71,7 @@ class SendingController extends Controller $send_logs = false; - if($request->has('send_logs')); + if($request->has('send_logs')) $send_logs = $request->input('send_logs'); Mail::to(config('ninja.contact.ninja_official_contact')) diff --git a/app/Http/Controllers/TemplateController.php b/app/Http/Controllers/TemplateController.php index e2c7dba9bc1e..65690d36d1c8 100644 --- a/app/Http/Controllers/TemplateController.php +++ b/app/Http/Controllers/TemplateController.php @@ -11,7 +11,7 @@ namespace App\Http\Controllers; -use Parsedown; +use League\CommonMark\CommonMarkConverter; class TemplateController extends BaseController { @@ -106,9 +106,14 @@ class TemplateController extends BaseController $subject = request()->input('subject'); $body = request()->input('body'); + $converter = new CommonMarkConverter([ + 'html_input' => 'strip', + 'allow_unsafe_links' => false, + ]); + $data = [ 'subject' => request()->input('subject'), - 'body' => Parsedown::instance()->text(request()->input('body')), + 'body' => $converter->convertToHtml(request()->input('body')), ]; return response()->json($data, 200); diff --git a/app/Http/Controllers/VendorController.php b/app/Http/Controllers/VendorController.php new file mode 100644 index 000000000000..3206ea74e8c6 --- /dev/null +++ b/app/Http/Controllers/VendorController.php @@ -0,0 +1,509 @@ +vendor_repo = $vendor_repo; + } + + /** + * @OA\Get( + * path="/api/v1/vendors", + * operationId="getVendors", + * tags={"vendors"}, + * summary="Gets a list of vendors", + * description="Lists vendors, search and filters allow fine grained lists to be generated. + + Query parameters can be added to performed more fine grained filtering of the vendors, these are handled by the VendorFilters 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 vendors", + * @OA\Header(header="X-API-Version", ref="#/components/headers/X-API-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/Vendor"), + * ), + * @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(VendorFilters $filters) + { + $vendors = Vendor::filter($filters); + + return $this->listResponse($vendors); + } + + /** + * Display the specified resource. + * + * @param int $id + * @return \Illuminate\Http\Response + * + * + * @OA\Get( + * path="/api/v1/vendors/{id}", + * operationId="showVendor", + * tags={"vendors"}, + * 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 Vendor Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the vendor object", + * @OA\Header(header="X-API-Version", ref="#/components/headers/X-API-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/Vendor"), + * ), + * @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(ShowVendorRequest $request, Vendor $vendor) + { + return $this->itemResponse($vendor); + } + + /** + * Show the form for editing the specified resource. + * + * @param int $id + * @return \Illuminate\Http\Response + * + * + * @OA\Get( + * path="/api/v1/vendors/{id}/edit", + * operationId="editVendor", + * tags={"vendors"}, + * 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 Vendor 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-API-Version", ref="#/components/headers/X-API-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/Vendor"), + * ), + * @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(EditVendorRequest $request, Vendor $vendor) + { + return $this->itemResponse($vendor); + } + + /** + * Update the specified resource in storage. + * + * @param \Illuminate\Http\Request $request + * @param App\Models\Vendor $vendor + * @return \Illuminate\Http\Response + * + * + * + * @OA\Put( + * path="/api/v1/vendors/{id}", + * operationId="updateVendor", + * tags={"vendors"}, + * 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 Vendor 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-API-Version", ref="#/components/headers/X-API-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/Vendor"), + * ), + * @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(UpdateVendorRequest $request, Vendor $vendor) + { + if($request->entityIsDeleted($vendor)) + return $request->disallowUpdate(); + + $vendor = $this->client_repo->save($request->all(), $vendor); + + $this->uploadLogo($request->file('company_logo'), $vendor->company, $vendor); + + return $this->itemResponse($vendor->fresh()); + } + + /** + * Show the form for creating a new resource. + * + * @return \Illuminate\Http\Response + * + * + * + * @OA\Get( + * path="/api/v1/vendors/create", + * operationId="getVendorsCreate", + * tags={"vendors"}, + * 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-API-Version", ref="#/components/headers/X-API-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/Vendor"), + * ), + * @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(CreateVendorRequest $request) + { + $vendor = VendorFactory::create(auth()->user()->company()->id, auth()->user()->id); + + return $this->itemResponse($vendor); + } + + /** + * Store a newly created resource in storage. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\Response + * + * + * + * @OA\Post( + * path="/api/v1/vendors", + * operationId="storeVendor", + * tags={"vendors"}, + * 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-API-Version", ref="#/components/headers/X-API-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/Vendor"), + * ), + * @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(StoreVendorRequest $request) + { + $vendor = $this->client_repo->save($request->all(), VendorFactory::create(auth()->user()->company()->id, auth()->user()->id)); + + $vendor->load('contacts', 'primary_contact'); + + $this->uploadLogo($request->file('company_logo'), $vendor->company, $vendor); + + return $this->itemResponse($vendor); + } + + /** + * Remove the specified resource from storage. + * + * @param int $id + * @return \Illuminate\Http\Response + * + * + * @OA\Delete( + * path="/api/v1/vendors/{id}", + * operationId="deleteVendor", + * tags={"vendors"}, + * 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 Vendor 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-API-Version", ref="#/components/headers/X-API-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(DestroyVendorRequest $request, Vendor $vendor) + { + //may not need these destroy routes as we are using actions to 'archive/delete' + $vendor->delete(); + + return response()->json([], 200); + } + + /** + * Perform bulk actions on the list view + * + * @param BulkVendorRequest $request + * @return \Illuminate\Http\Response + * + * + * @OA\Post( + * path="/api/v1/vendors/bulk", + * operationId="bulkVendors", + * tags={"vendors"}, + * summary="Performs bulk actions on an array of vendors", + * 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 Vendor User response", + * @OA\Header(header="X-API-Version", ref="#/components/headers/X-API-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/Vendor"), + * ), + * @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'); + $vendors = Vendor::withTrashed()->find($this->transformKeys($ids)); + + $vendors->each(function ($vendor, $key) use ($action) { + if (auth()->user()->can('edit', $vendor)) { + $this->client_repo->{$action}($vendor); + } + }); + + return $this->listResponse(Vendor::withTrashed()->whereIn('id', $this->transformKeys($ids))); + } + + /** + * Returns a client statement + * + * @return [type] [description] + */ + public function statement() + { + //todo + } +} diff --git a/app/Http/Requests/Vendor/BulkVendorRequest.php b/app/Http/Requests/Vendor/BulkVendorRequest.php new file mode 100644 index 000000000000..b71d6216089c --- /dev/null +++ b/app/Http/Requests/Vendor/BulkVendorRequest.php @@ -0,0 +1,47 @@ +has('action')) { + return false; + } + + if (!in_array($this->action, $this->getBulkOptions(), true)) { + return false; + } + + return auth()->user()->can(auth()->user()->isAdmin(), Vendor::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/Vendor/CreateVendorRequest.php b/app/Http/Requests/Vendor/CreateVendorRequest.php new file mode 100644 index 000000000000..67aaad1555f4 --- /dev/null +++ b/app/Http/Requests/Vendor/CreateVendorRequest.php @@ -0,0 +1,29 @@ +user()->can('create', Vendor::class); + } +} diff --git a/app/Http/Requests/Vendor/DestroyVendorRequest.php b/app/Http/Requests/Vendor/DestroyVendorRequest.php new file mode 100644 index 000000000000..7ae60f72f66b --- /dev/null +++ b/app/Http/Requests/Vendor/DestroyVendorRequest.php @@ -0,0 +1,29 @@ +user()->can('edit', $this->vendor); + } +} diff --git a/app/Http/Requests/Vendor/EditVendorRequest.php b/app/Http/Requests/Vendor/EditVendorRequest.php new file mode 100644 index 000000000000..3acf0fd9440d --- /dev/null +++ b/app/Http/Requests/Vendor/EditVendorRequest.php @@ -0,0 +1,39 @@ +user()->can('edit', $this->vendor); + } + + // public function prepareForValidation() + // { + // $input = $this->all(); + + // //$input['id'] = $this->encodePrimaryKey($input['id']); + + // $this->replace($input); + + // } +} diff --git a/app/Http/Requests/Vendor/ShowVendorRequest.php b/app/Http/Requests/Vendor/ShowVendorRequest.php new file mode 100644 index 000000000000..85382e611678 --- /dev/null +++ b/app/Http/Requests/Vendor/ShowVendorRequest.php @@ -0,0 +1,29 @@ +user()->can('view', $this->vendor); + } +} diff --git a/app/Http/Requests/Vendor/StoreVendorRequest.php b/app/Http/Requests/Vendor/StoreVendorRequest.php new file mode 100644 index 000000000000..8dc87c66a089 --- /dev/null +++ b/app/Http/Requests/Vendor/StoreVendorRequest.php @@ -0,0 +1,79 @@ +user()->can('create', Vendor::class); + } + + public function rules() + { + + /* Ensure we have a client name, and that all emails are unique*/ + //$rules['name'] = 'required|min:1'; + $rules['id_number'] = 'unique:vendors,id_number,' . $this->id . ',id,company_id,' . $this->company_id; + //$rules['settings'] = new ValidVendorGroupSettingsRule(); + $rules['contacts.*.email'] = 'nullable|distinct'; + + $contacts = request('contacts'); + + if (is_array($contacts)) { + for ($i = 0; $i < count($contacts); $i++) { + + //$rules['contacts.' . $i . '.email'] = 'nullable|email|distinct'; + } + } + + return $rules; + } + + + protected function prepareForValidation() + { + $input = $this->all(); + + if (!isset($input['settings'])) { + $input['settings'] = VendorSettings::defaults(); + } + + + $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/Vendor/UpdateVendorRequest.php b/app/Http/Requests/Vendor/UpdateVendorRequest.php new file mode 100644 index 000000000000..20e1209a326e --- /dev/null +++ b/app/Http/Requests/Vendor/UpdateVendorRequest.php @@ -0,0 +1,73 @@ +user()->can('edit', $this->vendor); + } + + public function rules() + { + /* Ensure we have a client name, and that all emails are unique*/ + + $rules['country_id'] = 'integer|nullable'; + //$rules['id_number'] = 'unique:clients,id_number,,id,company_id,' . auth()->user()->company()->id; + $rules['id_number'] = 'unique:clients,id_number,' . $this->id . ',id,company_id,' . $this->company_id; + $rules['contacts.*.email'] = 'nullable|distinct'; + + $contacts = request('contacts'); + + if (is_array($contacts)) { + // for ($i = 0; $i < count($contacts); $i++) { + // // $rules['contacts.' . $i . '.email'] = 'nullable|email|unique:client_contacts,email,' . isset($contacts[$i]['id'].',company_id,'.$this->company_id); + // //$rules['contacts.' . $i . '.email'] = 'nullable|email'; + // } + } + 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(); + + $this->replace($input); + } +} diff --git a/app/Models/Company.php b/app/Models/Company.php index f86b2d991a02..756ebd93b4ad 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -114,6 +114,14 @@ class Company extends BaseModel return $this->hasMany(Client::class)->withTrashed(); } + /** + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function vendors() + { + return $this->hasMany(Vendor::class)->withTrashed(); + } + public function activities() { return $this->hasMany(Activity::class); diff --git a/app/Models/Expense.php b/app/Models/Expense.php index a445969e26c0..4b66310ce245 100644 --- a/app/Models/Expense.php +++ b/app/Models/Expense.php @@ -13,26 +13,51 @@ namespace App\Models; use App\Utils\Traits\MakesHash; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\SoftDeletes; class Expense extends BaseModel { - use MakesHash; + use SoftDeletes; - protected $guarded = [ - 'id', + protected $fillable = [ + 'client_id', + 'vendor_id', + 'expense_currency_id', + 'expense_date', + 'invoice_currency_id', + 'amount', + 'foreign_amount', + 'exchange_rate', + 'private_notes', + 'public_notes', + 'bank_id', + 'transaction_id', + 'expense_category_id', + 'tax_rate1', + 'tax_name1', + 'tax_rate2', + 'tax_name2', + 'tax_rate3', + 'tax_name3', + 'payment_date', + 'payment_type_id', + 'transaction_reference', + 'invoice_documents', + 'should_be_invoiced', + 'custom_value1', + 'custom_value2', + 'custom_value3', + 'custom_value4', ]; - protected $appends = ['expense_id']; - public function getRouteKeyName() - { - return 'expense_id'; - } + protected $casts = [ + 'is_deleted' => 'boolean', + 'updated_at' => 'timestamp', + 'created_at' => 'timestamp', + 'deleted_at' => 'timestamp', + ]; - public function getExpenseIdAttribute() - { - return $this->encodePrimaryKey($this->id); - } public function documents() { diff --git a/app/Models/ExpenseCategory.php b/app/Models/ExpenseCategory.php new file mode 100644 index 000000000000..a880b453dc8b --- /dev/null +++ b/app/Models/ExpenseCategory.php @@ -0,0 +1,34 @@ +belongsTo('App\Models\Expense'); + } +} diff --git a/app/Models/Vendor.php b/app/Models/Vendor.php new file mode 100644 index 000000000000..2a41ea6ad862 --- /dev/null +++ b/app/Models/Vendor.php @@ -0,0 +1,73 @@ + 'string', + 'currency_id' => 'string', + 'is_deleted' => 'boolean', + 'updated_at' => 'timestamp', + 'created_at' => 'timestamp', + 'deleted_at' => 'timestamp', + ]; + + public function documents() + { + return $this->morphMany(Document::class, 'documentable'); + } + + public function assigned_user() + { + return $this->belongsTo(User::class, 'assigned_user_id', 'id'); + } + + public function contacts() + { + return $this->hasMany(VendorContact::class)->orderBy('is_primary', 'desc'); + } + + public function activities() + { + return $this->hasMany(Activity::class); + } +} + diff --git a/app/Models/VendorContact.php b/app/Models/VendorContact.php new file mode 100644 index 000000000000..58145fc9ec13 --- /dev/null +++ b/app/Models/VendorContact.php @@ -0,0 +1,135 @@ + 'timestamp', + 'created_at' => 'timestamp', + 'deleted_at' => 'timestamp', + ]; + + protected $fillable = [ + 'first_name', + 'last_name', + 'phone', + 'custom_value1', + 'custom_value2', + 'custom_value3', + 'custom_value4', + 'email', + 'is_primary', + ]; + + public function getHashedIdAttribute() + { + return $this->encodePrimaryKey($this->id); + } + + public function getContactIdAttribute() + { + return $this->encodePrimaryKey($this->id); + } + + public function vendor() + { + return $this->belongsTo(Vendor::class)->withTrashed(); + } + + public function primary_contact() + { + return $this->where('is_primary', true); + } + + public function company() + { + return $this->belongsTo(Company::class); + } + + public function user() + { + return $this->belongsTo(User::class)->withTrashed(); + } + + public function sendPasswordResetNotification($token) + { + $this->notify(new ClientContactResetPassword($token)); + } + + public function preferredLocale() + { + $languages = Cache::get('languages'); + + return $languages->filter(function ($item) { + return $item->id == $this->client->getSetting('language_id'); + })->first()->locale; + + //$lang = Language::find($this->client->getSetting('language_id')); + + //return $lang->locale; + } + + + /** + * Retrieve the model for a bound value. + * + * @param mixed $value + * @return \Illuminate\Database\Eloquent\Model|null + */ + public function resolveRouteBinding($value) + { + return $this + ->withTrashed() + ->where('id', $this->decodePrimaryKey($value))->firstOrFail(); + } +} diff --git a/app/Policies/ExpensePolicy.php b/app/Policies/ExpensePolicy.php new file mode 100644 index 000000000000..1b2ac5daf988 --- /dev/null +++ b/app/Policies/ExpensePolicy.php @@ -0,0 +1,33 @@ +isAdmin() || $user->hasPermission('create_expense') || $user->hasPermission('create_all'); + } +} diff --git a/app/Policies/VendorPolicy.php b/app/Policies/VendorPolicy.php new file mode 100644 index 000000000000..61410870243f --- /dev/null +++ b/app/Policies/VendorPolicy.php @@ -0,0 +1,33 @@ +isAdmin() || $user->hasPermission('create_vendor') || $user->hasPermission('create_all'); + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 33bd56f6837d..9f63ee91bb78 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -15,6 +15,7 @@ use App\Models\Activity; use App\Models\Client; use App\Models\Company; use App\Models\CompanyGateway; +use App\Models\Expense; use App\Models\GroupSetting; use App\Models\Invoice; use App\Models\Payment; @@ -24,10 +25,12 @@ use App\Models\RecurringInvoice; use App\Models\RecurringQuote; use App\Models\TaxRate; use App\Models\User; +use App\Models\Vendor; use App\Policies\ActivityPolicy; use App\Policies\ClientPolicy; use App\Policies\CompanyGatewayPolicy; use App\Policies\CompanyPolicy; +use App\Policies\ExpensePolicy; use App\Policies\GroupSettingPolicy; use App\Policies\InvoicePolicy; use App\Policies\PaymentPolicy; @@ -37,6 +40,7 @@ use App\Policies\RecurringInvoicePolicy; use App\Policies\RecurringQuotePolicy; use App\Policies\TaxRatePolicy; use App\Policies\UserPolicy; +use App\Policies\VendorPolicy; use Auth; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Illuminate\Support\Facades\Gate; @@ -62,6 +66,8 @@ class AuthServiceProvider extends ServiceProvider GroupSetting::class => GroupSettingPolicy::class, CompanyGateway::class => CompanyGatewayPolicy::class, TaxRate::class => TaxRatePolicy::class, + Vendor::class => VendorPolicy::class, + Expense::class => ExpensePolicy::class, ]; /** diff --git a/app/Repositories/InvoiceRepository.php b/app/Repositories/InvoiceRepository.php index ded950d80d9a..730a2fa3192e 100644 --- a/app/Repositories/InvoiceRepository.php +++ b/app/Repositories/InvoiceRepository.php @@ -72,7 +72,6 @@ class InvoiceRepository extends BaseRepository } } - if (isset($data['invitations'])) { $invitations = collect($data['invitations']); @@ -81,7 +80,6 @@ class InvoiceRepository extends BaseRepository InvoiceInvitation::destroy($invitation); }); - foreach ($data['invitations'] as $invitation) { $inv = false; @@ -96,6 +94,7 @@ class InvoiceRepository extends BaseRepository $new_invitation->invoice_id = $invoice->id; $new_invitation->client_contact_id = $this->decodePrimaryKey($invitation['client_contact_id']); $new_invitation->save(); + } } } diff --git a/app/Repositories/VendorContactRepository.php b/app/Repositories/VendorContactRepository.php new file mode 100644 index 000000000000..ff879576d560 --- /dev/null +++ b/app/Repositories/VendorContactRepository.php @@ -0,0 +1,76 @@ +contacts->pluck('id'))->diff($contacts->pluck('id'))->each(function ($contact) { + VendorContact::destroy($contact); + }); + + $this->is_primary = true; + /* Set first record to primary - always */ + $contacts = $contacts->sortByDesc('is_primary')->map(function ($contact) { + $contact['is_primary'] = $this->is_primary; + $this->is_primary = false; + return $contact; + }); + + //loop and update/create contacts + $contacts->each(function ($contact) use ($vendor) { + $update_contact = null; + + if (isset($contact['id'])) { + $update_contact = VendorContact::find($this->decodePrimaryKey($contact['id'])); + } + + if (!$update_contact) { + $update_contact = new VendorContact; + $update_contact->vendor_id = $vendor->id; + $update_contact->company_id = $vendor->company_id; + $update_contact->user_id = $vendor->user_id; + $update_contact->contact_key = Str::random(40); + } + + $update_contact->fill($contact); + + $update_contact->save(); + }); + + + + //always made sure we have one blank contact to maintain state + if ($contacts->count() == 0) { + $new_contact = new VendorContact; + $new_contact->vendor_id = $vendor->id; + $new_contact->company_id = $vendor->company_id; + $new_contact->user_id = $vendor->user_id; + $new_contact->contact_key = Str::random(40); + $new_contact->is_primary = true; + $new_contact->save(); + } + } +} diff --git a/app/Repositories/VendorRepository.php b/app/Repositories/VendorRepository.php new file mode 100644 index 000000000000..7b4a0def4ee1 --- /dev/null +++ b/app/Repositories/VendorRepository.php @@ -0,0 +1,95 @@ +contact_repo = $contact_repo; + } + + /** + * Gets the class name. + * + * @return string The class name. + */ + public function getClassName() + { + return Vendor::class; + } + + /** + * Saves the vendor and its contacts + * + * @param array $data The data + * @param \App\Models\vendor $vendor The vendor + * + * @return vendor|\App\Models\vendor|null vendor Object + */ + public function save(array $data, Vendor $vendor) : ?Vendor + { + $vendor->fill($data); + + $vendor->save(); + + if ($vendor->id_number == "" || !$vendor->id_number) { + $vendor->id_number = $this->getNextVendorNumber($vendor); + } //todo write tests for this and make sure that custom vendor numbers also works as expected from here + + $vendor->save(); + + if (isset($data['contacts'])) { + $contacts = $this->contact_repo->save($data['contacts'], $vendor); + } + + if (empty($data['name'])) { + $data['name'] = $vendor->present()->name(); + } + + + return $vendor; + } + + /** + * Store vendors in bulk. + * + * @param array $vendor + * @return vendor|null + */ + public function create($vendor): ?Vendor + { + return $this->save( + $vendor, + VendorFactory::create(auth()->user()->company()->id, auth()->user()->id) + ); + } +} diff --git a/app/Transformers/CompanyTransformer.php b/app/Transformers/CompanyTransformer.php index e264cd1f482b..1212e17f1405 100644 --- a/app/Transformers/CompanyTransformer.php +++ b/app/Transformers/CompanyTransformer.php @@ -17,6 +17,7 @@ use App\Models\Client; use App\Models\Company; use App\Models\CompanyGateway; use App\Models\CompanyUser; +use App\Models\Expense; use App\Models\GroupSetting; use App\Models\Payment; use App\Models\Product; @@ -53,6 +54,7 @@ class CompanyTransformer extends EntityTransformer 'timezone', 'language', 'expenses', + 'vendors', 'payments', 'company_user', 'groups', @@ -137,6 +139,20 @@ class CompanyTransformer extends EntityTransformer return $this->includeCollection($company->clients, $transformer, Client::class); } + public function includeExpenses(Company $company) + { + $transformer = new ExpenseTransformer($this->serializer); + + return $this->includeCollection($company->expenses, $transformer, Expense::class); + } + + public function includeVendors(Company $company) + { + $transformer = new VendorTransformer($this->serializer); + + return $this->includeCollection($company->vendors, $transformer, Vendor::class); + } + public function includeGroups(Company $company) { $transformer = new GroupSettingTransformer($this->serializer); diff --git a/app/Transformers/ExpenseTransformer.php b/app/Transformers/ExpenseTransformer.php new file mode 100644 index 000000000000..67097ebaa0fc --- /dev/null +++ b/app/Transformers/ExpenseTransformer.php @@ -0,0 +1,80 @@ + $this->encodePrimaryKey($expense->id), + 'user_id' => $this->encodePrimaryKey($expense->user_id), + 'assigned_user_id' => $this->encodePrimaryKey($expense->assigned_user_id), + 'vendor_id' => $this->encodePrimaryKey($expense->vendor_id), + 'invoice_id' => $this->encodePrimaryKey($expense->invoice_id), + 'client_id' => $this->encodePrimaryKey($expense->client_id), + '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 ?: '', + 'payment_type_id' => (string)$expense->payment_type_id ?: '', + 'recurring_expense_id' => (string)$expense->recurring_expense_id ?: '', + 'is_deleted' => (bool) $expense->is_deleted, + 'should_be_invoiced' => (bool) $expense->should_be_invoiced, + 'invoice_documents' => (bool) $expense->invoice_documents, + 'amount' => (float)$expense->amount ?: 0, + 'foreign_amount' => (float)$expense->foreign_amount ?: 0, + 'exchange_rate' => (float)$expense->exchange_rate ?: 0, + 'tax_name1' => $expense->tax_name1 ? $expense->tax_name1 : '', + 'tax_rate1' => (float) $expense->tax_rate1, + 'tax_name2' => $expense->tax_name2 ? $expense->tax_name2 : '', + 'tax_rate2' => (float) $expense->tax_rate2, + 'tax_name3' => $expense->tax_name3 ? $expense->tax_name3 : '', + 'tax_rate3' => (float) $expense->tax_rate3, + 'private_notes' => (string) $expense->private_notes ?: '', + 'public_notes' => (string) $expense->public_notes ?: '', + 'transaction_reference' => (string) $expense->transaction_reference ?: '', + 'transaction_id' => (string) $expense->transaction_id ?: '', + 'expense_date' => $expense->expense_date ?: '', + 'payment_date' => $expense->payment_date ?: '', + 'custom_value1' => $expense->custom_value1 ?: '', + 'custom_value2' => $expense->custom_value2 ?: '', + 'custom_value3' => $expense->custom_value3 ?: '', + 'custom_value4' => $expense->custom_value4 ?: '', + 'updated_at' => $expense->updated_at, + 'archived_at' => $expense->deleted_at, + ]; + } +} \ No newline at end of file diff --git a/app/Transformers/VendorContactTransformer.php b/app/Transformers/VendorContactTransformer.php new file mode 100644 index 000000000000..1b94ca3b8e21 --- /dev/null +++ b/app/Transformers/VendorContactTransformer.php @@ -0,0 +1,47 @@ + $this->encodePrimaryKey($vendor->id), + 'first_name' => $vendor->first_name ?: '', + 'last_name' => $vendor->last_name ?: '', + 'email' => $vendor->email ?: '', + 'updated_at' => $vendor->updated_at, + 'archived_at' => $vendor->deleted_at, + 'is_primary' => (bool) $vendor->is_primary, + 'phone' => $vendor->phone ?: '', + 'custom_value1' => $vendor->custom_value1 ?: '', + 'custom_value2' => $vendor->custom_value2 ?: '', + 'custom_value3' => $vendor->custom_value3 ?: '', + 'custom_value4' => $vendor->custom_value4 ?: '', + ]; + } +} diff --git a/app/Transformers/VendorTransformer.php b/app/Transformers/VendorTransformer.php new file mode 100644 index 000000000000..2be36ecaa321 --- /dev/null +++ b/app/Transformers/VendorTransformer.php @@ -0,0 +1,98 @@ +serializer); + + return $this->includeCollection($vendor->activities, $transformer, Activity::class); + } + + /** + * @param Vendor $vendor + * + * @return \League\Fractal\Resource\Collection + */ + public function includeContacts(Vendor $vendor) + { + $transformer = new VendorContactTransformer($this->serializer); + + return $this->includeCollection($vendor->contacts, $transformer, VendorContact::class); + } + + /** + * @param Vendor $vendor + * + * @return array + */ + public function transform(Vendor $vendor) + { + return [ + 'id' => $this->encodePrimaryKey($vendor->id), + 'user_id' => $this->encodePrimaryKey($vendor->user_id), + 'assigned_user_id' => $this->encodePrimaryKey($vendor->assigned_user_id), + 'name' => $vendor->name ?: '', + 'website' => $vendor->website ?: '', + 'private_notes' => $vendor->private_notes ?: '', + 'last_login' => (int)$vendor->last_login, + 'address1' => $vendor->address1 ?: '', + 'address2' => $vendor->address2 ?: '', + 'phone' => $vendor->phone ?: '', + 'city' => $vendor->city ?: '', + 'state' => $vendor->state ?: '', + 'postal_code' => $vendor->postal_code ?: '', + 'country_id' => (string)$vendor->country_id ?: '', + 'custom_value1' => $vendor->custom_value1 ?: '', + 'custom_value2' => $vendor->custom_value2 ?: '', + 'custom_value3' => $vendor->custom_value3 ?: '', + 'custom_value4' => $vendor->custom_value4 ?: '', + 'is_deleted' => (bool) $vendor->is_deleted, + 'vat_number' => $vendor->vat_number ?: '', + 'id_number' => $vendor->id_number ?: '', + 'updated_at' => $vendor->updated_at, + 'archived_at' => $vendor->deleted_at, + ]; + } +} diff --git a/app/Utils/Traits/InvoiceEmailBuilder.php b/app/Utils/Traits/InvoiceEmailBuilder.php index 0b4576acafc7..0204fcf04cca 100644 --- a/app/Utils/Traits/InvoiceEmailBuilder.php +++ b/app/Utils/Traits/InvoiceEmailBuilder.php @@ -14,6 +14,7 @@ namespace App\Utils\Traits; use App\Models\ClientContact; use App\Models\Invoice; use Illuminate\Support\Carbon; +use League\CommonMark\CommonMarkConverter; use Parsedown; /** @@ -84,7 +85,14 @@ trait InvoiceEmailBuilder //process markdown if ($is_markdown) { - $data = Parsedown::instance()->line($data); + //$data = Parsedown::instance()->line($data); + + $converter = new CommonMarkConverter([ + 'html_input' => 'strip', + 'allow_unsafe_links' => false, + ]); + + $data = $converter->convertToHtml($data); } return $data; diff --git a/database/factories/ExpenseFactory.php b/database/factories/ExpenseFactory.php new file mode 100644 index 000000000000..19cc77ccc88f --- /dev/null +++ b/database/factories/ExpenseFactory.php @@ -0,0 +1,22 @@ +define(App\Models\Expense::class, function (Faker $faker) { + return [ + 'amount' => $faker->numberBetween(1,10), + 'custom_value1' => $faker->text(10), + 'custom_value2' => $faker->text(10), + 'custom_value3' => $faker->text(10), + 'custom_value4' => $faker->text(10), + 'exchange_rate' => $faker->randomFloat(2,0,1), + 'expense_date' => $faker->date(), + 'is_deleted' => false, + 'public_notes' => $faker->text(50), + 'private_notes' => $faker->text(50), + 'transaction_reference' => $faker->text(5), + ]; +}); \ No newline at end of file diff --git a/database/factories/VendorContactFactory.php b/database/factories/VendorContactFactory.php new file mode 100644 index 000000000000..8f28c38c94d6 --- /dev/null +++ b/database/factories/VendorContactFactory.php @@ -0,0 +1,24 @@ +define(App\Models\VendorContact::class, function (Faker $faker) { + return [ + 'first_name' => $faker->firstName, + 'last_name' => $faker->lastName, + 'phone' => $faker->phoneNumber, + 'email' => $faker->unique()->safeEmail, + ]; + +}); diff --git a/database/factories/VendorFactory.php b/database/factories/VendorFactory.php new file mode 100644 index 000000000000..9f28f2999893 --- /dev/null +++ b/database/factories/VendorFactory.php @@ -0,0 +1,23 @@ +define(App\Models\Vendor::class, function (Faker $faker) { + return [ + 'name' => $faker->name(), + 'website' => $faker->url, + 'private_notes' => $faker->text(200), + 'vat_number' => $faker->text(25), + 'id_number' => $faker->text(20), + 'custom_value1' => $faker->text(20), + 'custom_value2' => $faker->text(20), + 'custom_value3' => $faker->text(20), + 'custom_value4' => $faker->text(20), + 'address1' => $faker->buildingNumber, + 'address2' => $faker->streetAddress, + 'city' => $faker->city, + 'state' => $faker->state, + 'postal_code' => $faker->postcode, + 'country_id' => 4, + ]; +}); 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 3579cec58720..95da14ca581b 100644 --- a/database/migrations/2014_10_13_000000_create_users_table.php +++ b/database/migrations/2014_10_13_000000_create_users_table.php @@ -1222,6 +1222,122 @@ class CreateUsersTable extends Migration $table->foreign('client_id')->references('id')->on('clients')->onDelete('cascade'); }); + + Schema::create('vendors', function (Blueprint $table) { + $table->increments('id'); + $table->timestamps(6); + $table->softDeletes(); + $table->unsignedInteger('user_id'); + $table->unsignedInteger('assigned_user_id'); + $table->unsignedInteger('company_id'); + $table->unsignedInteger('currency_id')->nullable(); + $table->string('name')->nullable(); + $table->string('address1'); + $table->string('address2'); + $table->string('city'); + $table->string('state'); + $table->string('postal_code'); + $table->unsignedInteger('country_id')->nullable(); + $table->string('work_phone'); + $table->text('private_notes'); + $table->string('website'); + $table->tinyInteger('is_deleted')->default(0); + $table->string('vat_number')->nullable(); + $table->string('transaction_name')->nullable(); + $table->string('id_number')->nullable(); + + $table->string('custom_value1')->nullable(); + $table->string('custom_value2')->nullable(); + $table->string('custom_value3')->nullable(); + $table->string('custom_value4')->nullable(); + + + $table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->foreign('country_id')->references('id')->on('countries'); + $table->foreign('currency_id')->references('id')->on('currencies'); + }); + + Schema::create('vendor_contacts', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('company_id'); + $table->unsignedInteger('user_id'); + $table->unsignedInteger('vendor_id')->index(); + $table->timestamps(6); + $table->softDeletes(); + + $table->boolean('is_primary')->default(0); + $table->string('first_name')->nullable(); + $table->string('last_name')->nullable(); + $table->string('email')->nullable(); + $table->string('phone')->nullable(); + + $table->string('custom_value1')->nullable(); + $table->string('custom_value2')->nullable(); + $table->string('custom_value3')->nullable(); + $table->string('custom_value4')->nullable(); + + $table->foreign('vendor_id')->references('id')->on('vendors')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade'); + + }); + + Schema::create('expense_categories', function ($table) { + $table->increments('id'); + $table->unsignedInteger('user_id'); + $table->unsignedInteger('company_id')->index(); + $table->timestamps(6); + $table->softDeletes(); + $table->string('name')->nullable(); + }); + + Schema::create('expenses', function (Blueprint $table) { + $table->increments('id'); + $table->timestamps(6); + $table->softDeletes(); + + $table->unsignedInteger('company_id')->index(); + $table->unsignedInteger('vendor_id')->nullable(); + $table->unsignedInteger('user_id'); + $table->unsignedInteger('assigned_user_id'); + $table->unsignedInteger('invoice_id')->nullable(); + $table->unsignedInteger('client_id')->nullable(); + $table->unsignedInteger('bank_id')->nullable(); + $table->unsignedInteger('invoice_currency_id')->nullable(false); + $table->unsignedInteger('expense_currency_id')->nullable(false); + $table->unsignedInteger('invoice_category_id')->nullable(); + $table->unsignedInteger('payment_type_id')->nullable(); + $table->unsignedInteger('recurring_expense_id')->nullable(); + $table->boolean('is_deleted')->default(false); + $table->decimal('amount', 13, 2); + $table->decimal('foreign_amount', 13, 2); + $table->decimal('exchange_rate', 13, 4); + $table->string('tax_name1')->nullable(); + $table->decimal('tax_rate1', 13, 3)->default(0); + $table->string('tax_name2')->nullable(); + $table->decimal('tax_rate2', 13, 3)->default(0); + $table->string('tax_name3')->nullable(); + $table->decimal('tax_rate3', 13, 3)->default(0); + $table->date('expense_date')->nullable(); + $table->date('payment_date')->nullable(); + $table->text('private_notes'); + $table->text('public_notes'); + $table->text('transaction_reference'); + $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(); + + // Relations + $table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + + }); } /** diff --git a/routes/api.php b/routes/api.php index fec3de34761c..a544d2daab72 100644 --- a/routes/api.php +++ b/routes/api.php @@ -62,6 +62,14 @@ Route::group(['middleware' => ['api_db','api_secret_check','token_auth','locale' Route::post('recurring_quotes/bulk', 'RecurringQuoteController@bulk')->name('recurring_quotes.bulk'); + Route::resource('expenses', 'ExpenseController'); // name = (expenses. index / create / show / update / destroy / edit + + Route::post('expenses/bulk', 'ExpenseController@bulk')->name('expenses.bulk'); + + Route::resource('vendors', 'VendorController'); // name = (vendors. index / create / show / update / destroy / edit + + Route::post('vendors/bulk', 'VendorController@bulk')->name('vendors.bulk'); + Route::resource('client_statement', 'ClientStatementController@statement'); // name = (client_statement. index / create / show / update / destroy / edit Route::resource('payments', 'PaymentController'); // name = (payments. index / create / show / update / destroy / edit @@ -103,9 +111,7 @@ Route::group(['middleware' => ['api_db','api_secret_check','token_auth','locale' Route::post('credits/bulk', 'CreditController@bulk')->name('credits.bulk'); - Route::resource('expenses', 'ExpenseController'); // name = (expenses. index / create / show / update / destroy / edit - Route::post('expenses/bulk', 'ExpenseController@bulk')->name('expenses.bulk'); Route::get('settings', 'SettingsController@index')->name('user.settings'); diff --git a/tests/Feature/InvoiceEmailTest.php b/tests/Feature/InvoiceEmailTest.php index c8640b517c73..a30f53f6576a 100644 --- a/tests/Feature/InvoiceEmailTest.php +++ b/tests/Feature/InvoiceEmailTest.php @@ -14,7 +14,6 @@ use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Session; -use Parsedown; use Tests\MockAccountData; use Tests\TestCase;