diff --git a/app/Console/Commands/CheckData.php b/app/Console/Commands/CheckData.php index 8b4ab1317551..eae52b925ee1 100644 --- a/app/Console/Commands/CheckData.php +++ b/app/Console/Commands/CheckData.php @@ -389,8 +389,8 @@ class CheckData extends Command $invoice_balance = $client->invoices->where('is_deleted', false)->where('status_id', '>', 1)->sum('balance'); $credit_balance = $client->credits->where('is_deleted', false)->sum('balance'); - if($client->balance != $invoice_balance) - $invoice_balance -= $credit_balance;//doesn't make sense to remove the credit amount + // if($client->balance != $invoice_balance) + // $invoice_balance -= $credit_balance;//doesn't make sense to remove the credit amount $ledger = CompanyLedger::where('client_id', $client->id)->orderBy('id', 'DESC')->first(); diff --git a/app/Http/Controllers/Auth/ContactForgotPasswordController.php b/app/Http/Controllers/Auth/ContactForgotPasswordController.php index 24481f476713..cb5e4212334d 100644 --- a/app/Http/Controllers/Auth/ContactForgotPasswordController.php +++ b/app/Http/Controllers/Auth/ContactForgotPasswordController.php @@ -12,8 +12,10 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; +use App\Libraries\MultiDB; use Illuminate\Contracts\View\Factory; use Illuminate\Foundation\Auth\SendsPasswordResetEmails; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Password; use Illuminate\View\View; @@ -65,4 +67,31 @@ class ContactForgotPasswordController extends Controller { return Password::broker('contacts'); } + + public function sendResetLinkEmail(Request $request) + { + //MultiDB::userFindAndSetDb($request->input('email')); + + $user = MultiDB::hasContact(['email' => $request->input('email')]); + + $this->validateEmail($request); + + // We will send the password reset link to this user. Once we have attempted + // to send the link, we will examine the response then see the message we + // need to show to the user. Finally, we'll send out a proper response. + $response = $this->broker()->sendResetLink( + $this->credentials($request) + ); + + if ($request->ajax()) { + return $response == Password::RESET_LINK_SENT + ? response()->json(['message' => 'Reset link sent to your email.', 'status' => true], 201) + : response()->json(['message' => 'Email not found', 'status' => false], 401); + } + + return $response == Password::RESET_LINK_SENT + ? $this->sendResetLinkResponse($request, $response) + : $this->sendResetLinkFailedResponse($request, $response); + } + } diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php index 7044ace379fe..4d81705ea3f6 100644 --- a/app/Http/Controllers/Auth/ForgotPasswordController.php +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -12,6 +12,7 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; +use App\Libraries\MultiDB; use Illuminate\Foundation\Auth\SendsPasswordResetEmails; use Illuminate\Http\Request; use Illuminate\Support\Facades\Password; @@ -103,6 +104,10 @@ class ForgotPasswordController extends Controller */ public function sendResetLinkEmail(Request $request) { + //MultiDB::userFindAndSetDb($request->input('email')); + + $user = MultiDB::hasUser(['email' => $request->input('email')]); + $this->validateEmail($request); // We will send the password reset link to this user. Once we have attempted diff --git a/app/Http/Controllers/ClientController.php b/app/Http/Controllers/ClientController.php index c52d6267aa39..c38aee312a8c 100644 --- a/app/Http/Controllers/ClientController.php +++ b/app/Http/Controllers/ClientController.php @@ -21,6 +21,7 @@ use App\Http\Requests\Client\EditClientRequest; use App\Http\Requests\Client\ShowClientRequest; use App\Http\Requests\Client\StoreClientRequest; use App\Http\Requests\Client\UpdateClientRequest; +use App\Http\Requests\Client\UploadClientRequest; use App\Jobs\Client\StoreClient; use App\Jobs\Client\UpdateClient; use App\Models\Client; @@ -29,6 +30,7 @@ use App\Transformers\ClientTransformer; use App\Utils\Ninja; use App\Utils\Traits\BulkOptions; use App\Utils\Traits\MakesHash; +use App\Utils\Traits\SavesDocuments; use App\Utils\Traits\Uploadable; use Illuminate\Http\Request; use Illuminate\Http\Response; @@ -42,6 +44,7 @@ class ClientController extends BaseController use MakesHash; use Uploadable; use BulkOptions; + use SavesDocuments; protected $entity_type = Client::class; @@ -269,6 +272,7 @@ class ClientController extends BaseController */ public function update(UpdateClientRequest $request, Client $client) { + if ($request->entityIsDeleted($client)) { return $request->disallowUpdate(); } @@ -515,4 +519,66 @@ class ClientController extends BaseController { //todo } + + /** + * Update the specified resource in storage. + * + * @param UploadClientRequest $request + * @param Client $client + * @return Response + * + * + * + * @OA\Put( + * path="/api/v1/clients/{id}/upload", + * operationId="uploadClient", + * tags={"clients"}, + * summary="Uploads a document to a client", + * description="Handles the uploading of a document to a client", + * @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 Client 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/Client"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function upload(UploadClientRequest $request, Client $client) + { + + if ($request->has('documents')) + $this->saveDocuments($request->file('documents'), $client); + + return $this->itemResponse($client->fresh()); + + } + } diff --git a/app/Http/Controllers/ClientPortal/RecurringInvoiceController.php b/app/Http/Controllers/ClientPortal/RecurringInvoiceController.php index ae7a6589cf60..e484dedeba06 100644 --- a/app/Http/Controllers/ClientPortal/RecurringInvoiceController.php +++ b/app/Http/Controllers/ClientPortal/RecurringInvoiceController.php @@ -13,10 +13,15 @@ namespace App\Http\Controllers\ClientPortal; use App\Http\Controllers\Controller; use App\Http\Requests\ClientPortal\ShowRecurringInvoiceRequest; +use App\Jobs\Mail\NinjaMailer; +use App\Jobs\Mail\NinjaMailerJob; +use App\Jobs\Mail\NinjaMailerObject; +use App\Mail\RecurringInvoice\ClientContactRequestCancellationObject; use App\Models\RecurringInvoice; use App\Notifications\ClientContactRequestCancellation; use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesHash; +use App\Utils\Traits\Notifications\UserNotifies; use Illuminate\Contracts\View\Factory; use Illuminate\Http\Request; use Illuminate\View\View; @@ -28,6 +33,7 @@ class RecurringInvoiceController extends Controller { use MakesHash; use MakesDates; + use UserNotifies; /** * Show the list of recurring invoices. @@ -57,7 +63,22 @@ class RecurringInvoiceController extends Controller { //todo double check the user is able to request a cancellation //can add locale specific by chaining ->locale(); - $recurring_invoice->user->notify(new ClientContactRequestCancellation($recurring_invoice, auth()->user())); + + $nmo = new NinjaMailerObject; + $nmo->mailable = (new NinjaMailer((new ClientContactRequestCancellationObject($recurring_invoice, auth()->user()))->build())); + $nmo->company = $recurring_invoice->company; + $nmo->settings = $recurring_invoice->company->settings; + + $notifiable_users = $this->filterUsersByPermissions($recurring_invoice->company->company_users, $recurring_invoice, ['recurring_cancellation']); + + $notifiable_users->each(function ($company_user) use($nmo){ + + $nmo->to_user = $company_user->user; + NinjaMailerJob::dispatch($nmo); + + }); + + //$recurring_invoice->user->notify(new ClientContactRequestCancellation($recurring_invoice, auth()->user())); return $this->render('recurring_invoices.cancellation.index', [ 'invoice' => $recurring_invoice, diff --git a/app/Http/Controllers/CompanyController.php b/app/Http/Controllers/CompanyController.php index 0b437b32b2b7..9bfffe27f6d8 100644 --- a/app/Http/Controllers/CompanyController.php +++ b/app/Http/Controllers/CompanyController.php @@ -20,6 +20,7 @@ use App\Http\Requests\Company\EditCompanyRequest; use App\Http\Requests\Company\ShowCompanyRequest; use App\Http\Requests\Company\StoreCompanyRequest; use App\Http\Requests\Company\UpdateCompanyRequest; +use App\Http\Requests\Company\UploadCompanyRequest; use App\Jobs\Company\CreateCompany; use App\Jobs\Company\CreateCompanyPaymentTerms; use App\Jobs\Company\CreateCompanyTaskStatuses; @@ -503,4 +504,65 @@ class CompanyController extends BaseController return response()->json(['message' => ctrans('texts.success')], 200); } + + /** + * Update the specified resource in storage. + * + * @param UploadCompanyRequest $request + * @param Company $client + * @return Response + * + * + * + * @OA\Put( + * path="/api/v1/companies/{id}/upload", + * operationId="uploadCompanies", + * tags={"companies"}, + * summary="Uploads a document to a company", + * description="Handles the uploading of a document 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\Parameter( + * name="id", + * in="path", + * description="The Company 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/Company"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function upload(UploadCompanyRequest $request, Company $company) + { + + if ($request->has('documents')) + $this->saveDocuments($request->file('documents'), $company); + + return $this->itemResponse($company->fresh()); + + } } diff --git a/app/Http/Controllers/CreditController.php b/app/Http/Controllers/CreditController.php index 346dda1ee391..4974cbb875fc 100644 --- a/app/Http/Controllers/CreditController.php +++ b/app/Http/Controllers/CreditController.php @@ -14,6 +14,7 @@ use App\Http\Requests\Credit\EditCreditRequest; use App\Http\Requests\Credit\ShowCreditRequest; use App\Http\Requests\Credit\StoreCreditRequest; use App\Http\Requests\Credit\UpdateCreditRequest; +use App\Http\Requests\Credit\UploadCreditRequest; use App\Jobs\Entity\EmailEntity; use App\Jobs\Invoice\EmailCredit; use App\Models\Client; @@ -24,6 +25,7 @@ use App\Transformers\CreditTransformer; use App\Utils\Ninja; use App\Utils\TempFile; use App\Utils\Traits\MakesHash; +use App\Utils\Traits\SavesDocuments; use Illuminate\Http\Response; /** @@ -32,7 +34,8 @@ use Illuminate\Http\Response; class CreditController extends BaseController { use MakesHash; - + use SavesDocuments; + protected $entity_type = Credit::class; protected $entity_transformer = CreditTransformer::class; @@ -56,7 +59,7 @@ class CreditController extends BaseController * @OA\Get( * path="/api/v1/credits", * operationId="getCredits", - * tags={"invoices"}, + * tags={"credits"}, * summary="Gets a list of credits", * description="Lists credits, search and filters allow fine grained lists to be generated. * @@ -576,4 +579,66 @@ class CreditController extends BaseController return response()->download($file_path); } + + /** + * Update the specified resource in storage. + * + * @param UploadCreditRequest $request + * @param Credit $client + * @return Response + * + * + * + * @OA\Put( + * path="/api/v1/credits/{id}/upload", + * operationId="uploadCredits", + * tags={"credits"}, + * summary="Uploads a document to a credit", + * description="Handles the uploading of a document to a credit", + * @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 Credit Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the Credit 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/Credit"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function upload(UploadCreditRequest $request, Credit $credit) + { + + if ($request->has('documents')) + $this->saveDocuments($request->file('documents'), $credit); + + return $this->itemResponse($credit->fresh()); + + } + } diff --git a/app/Http/Controllers/ExpenseController.php b/app/Http/Controllers/ExpenseController.php index c2c2fede38a8..9b7b74939a33 100644 --- a/app/Http/Controllers/ExpenseController.php +++ b/app/Http/Controllers/ExpenseController.php @@ -21,12 +21,14 @@ use App\Http\Requests\Expense\EditExpenseRequest; use App\Http\Requests\Expense\ShowExpenseRequest; use App\Http\Requests\Expense\StoreExpenseRequest; use App\Http\Requests\Expense\UpdateExpenseRequest; +use App\Http\Requests\Expense\UploadExpenseRequest; use App\Models\Expense; use App\Repositories\ExpenseRepository; use App\Transformers\ExpenseTransformer; use App\Utils\Ninja; use App\Utils\Traits\BulkOptions; use App\Utils\Traits\MakesHash; +use App\Utils\Traits\SavesDocuments; use App\Utils\Traits\Uploadable; use Illuminate\Http\Request; use Illuminate\Http\Response; @@ -40,7 +42,8 @@ class ExpenseController extends BaseController use MakesHash; use Uploadable; use BulkOptions; - + use SavesDocuments; + protected $entity_type = Expense::class; protected $entity_transformer = ExpenseTransformer::class; @@ -507,4 +510,65 @@ class ExpenseController extends BaseController { //todo } + + /** + * Update the specified resource in storage. + * + * @param UploadExpenseRequest $request + * @param Expense $expense + * @return Response + * + * + * + * @OA\Put( + * path="/api/v1/expenses/{id}/upload", + * operationId="uploadExpense", + * tags={"expense"}, + * summary="Uploads a document to a expense", + * description="Handles the uploading of a document to a expense", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Parameter( + * name="id", + * in="path", + * description="The Expense Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the Expense object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/Expense"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function upload(UploadExpenseRequest $request, Expense $expense) + { + + if ($request->has('documents')) + $this->saveDocuments($request->file('documents'), $expense); + + return $this->itemResponse($expense->fresh()); + + } } diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index d287ebb5ede0..460c018811e3 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -25,6 +25,7 @@ use App\Http\Requests\Invoice\EditInvoiceRequest; use App\Http\Requests\Invoice\ShowInvoiceRequest; use App\Http\Requests\Invoice\StoreInvoiceRequest; use App\Http\Requests\Invoice\UpdateInvoiceRequest; +use App\Http\Requests\Invoice\UploadInvoiceRequest; use App\Jobs\Entity\EmailEntity; use App\Jobs\Invoice\StoreInvoice; use App\Jobs\Invoice\ZipInvoices; @@ -38,6 +39,7 @@ use App\Transformers\QuoteTransformer; use App\Utils\Ninja; use App\Utils\TempFile; use App\Utils\Traits\MakesHash; +use App\Utils\Traits\SavesDocuments; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\File; @@ -49,7 +51,8 @@ use Illuminate\Support\Facades\Storage; class InvoiceController extends BaseController { use MakesHash; - + use SavesDocuments; + protected $entity_type = Invoice::class; protected $entity_transformer = InvoiceTransformer::class; @@ -393,8 +396,6 @@ class InvoiceController extends BaseController $invoice = $this->invoice_repo->save($request->all(), $invoice); - UnlinkFile::dispatchNow(config('filesystems.default'), $invoice->client->invoice_filepath().$invoice->number.'.pdf'); - event(new InvoiceWasUpdated($invoice, $invoice->company, Ninja::eventVars())); return $this->itemResponse($invoice); @@ -851,4 +852,65 @@ class InvoiceController extends BaseController return response(['message' => 'Oops, something went wrong. Make sure you have symlink to storage/ in public/ directory.'], 500); } } + + /** + * Update the specified resource in storage. + * + * @param UploadInvoiceRequest $request + * @param Invoice $invoice + * @return Response + * + * + * + * @OA\Put( + * path="/api/v1/invoices/{id}/upload", + * operationId="uploadInvoice", + * tags={"invoices"}, + * summary="Uploads a document to a invoice", + * description="Handles the uploading of a document to a invoice", + * @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 Invoice Hashed ID", + * example="D2J234DFA", + * required=true, + * @OA\Schema( + * type="string", + * format="string", + * ), + * ), + * @OA\Response( + * response=200, + * description="Returns the Invoice 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/Invoice"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function upload(UploadInvoiceRequest $request, Invoice $invoice) + { + + if ($request->has('documents')) + $this->saveDocuments($request->file('documents'), $invoice); + + return $this->itemResponse($invoice->fresh()); + + } } diff --git a/app/Http/Requests/Client/UploadClientRequest.php b/app/Http/Requests/Client/UploadClientRequest.php new file mode 100644 index 000000000000..4a7848472547 --- /dev/null +++ b/app/Http/Requests/Client/UploadClientRequest.php @@ -0,0 +1,39 @@ +user()->can('edit', $this->client); + } + + public function rules() + { + + $rules = []; + + if($this->input('documents')) + $rules['documents'] = 'file|mimes:html,csv,png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:2000000'; + + return $rules; + + } +} diff --git a/app/Http/Requests/Company/UploadCompanyRequest.php b/app/Http/Requests/Company/UploadCompanyRequest.php new file mode 100644 index 000000000000..7f9079d74799 --- /dev/null +++ b/app/Http/Requests/Company/UploadCompanyRequest.php @@ -0,0 +1,39 @@ +user()->can('edit', $this->company); + } + + public function rules() + { + + $rules = []; + + if($this->input('documents')) + $rules['documents'] = 'file|mimes:png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:20000'; + + return $rules; + + } +} diff --git a/app/Http/Requests/Credit/UploadCreditRequest.php b/app/Http/Requests/Credit/UploadCreditRequest.php new file mode 100644 index 000000000000..d50a8ddc70dc --- /dev/null +++ b/app/Http/Requests/Credit/UploadCreditRequest.php @@ -0,0 +1,39 @@ +user()->can('edit', $this->credit); + } + + public function rules() + { + + $rules = []; + + if($this->input('documents')) + $rules['documents'] = 'file|mimes:html,csv,png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:2000000'; + + return $rules; + + } +} diff --git a/app/Http/Requests/Expense/UploadExpenseRequest.php b/app/Http/Requests/Expense/UploadExpenseRequest.php new file mode 100644 index 000000000000..e52c13a8050d --- /dev/null +++ b/app/Http/Requests/Expense/UploadExpenseRequest.php @@ -0,0 +1,39 @@ +user()->can('edit', $this->expense); + } + + public function rules() + { + + $rules = []; + + if($this->input('documents')) + $rules['documents'] = 'file|mimes:html,csv,png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:2000000'; + + return $rules; + + } +} diff --git a/app/Http/Requests/Invoice/UploadInvoiceRequest.php b/app/Http/Requests/Invoice/UploadInvoiceRequest.php new file mode 100644 index 000000000000..5a3688c80145 --- /dev/null +++ b/app/Http/Requests/Invoice/UploadInvoiceRequest.php @@ -0,0 +1,39 @@ +user()->can('edit', $this->invoice); + } + + public function rules() + { + + $rules = []; + + if($this->input('documents')) + $rules['documents'] = 'file|mimes:html,csv,png,ai,svg,jpeg,tiff,pdf,gif,psd,txt,doc,xls,ppt,xlsx,docx,pptx|max:2000000'; + + return $rules; + + } +} diff --git a/app/Jobs/Account/CreateAccount.php b/app/Jobs/Account/CreateAccount.php index 6694505cf4ef..d145fdc8e2a1 100644 --- a/app/Jobs/Account/CreateAccount.php +++ b/app/Jobs/Account/CreateAccount.php @@ -17,8 +17,12 @@ use App\Jobs\Company\CreateCompany; use App\Jobs\Company\CreateCompanyPaymentTerms; use App\Jobs\Company\CreateCompanyTaskStatuses; use App\Jobs\Company\CreateCompanyToken; +use App\Jobs\Mail\NinjaMailer; +use App\Jobs\Mail\NinjaMailerJob; +use App\Jobs\Mail\NinjaMailerObject; use App\Jobs\User\CreateUser; use App\Jobs\Util\VersionCheck; +use App\Mail\Admin\AccountCreatedObject; use App\Models\Account; use App\Notifications\Ninja\NewAccountCreated; use App\Utils\Ninja; @@ -88,7 +92,16 @@ class CreateAccount $spaa9f78->fresh(); - $sp035a66->notification(new NewAccountCreated($spaa9f78, $sp035a66))->ninja(); + //todo implement SLACK notifications + //$sp035a66->notification(new NewAccountCreated($spaa9f78, $sp035a66))->ninja(); + + $nmo = new NinjaMailerObject; + $nmo->mailable = new NinjaMailer((new AccountCreatedObject($spaa9f78, $sp035a66))->build()); + $nmo->company = $sp035a66; + $nmo->to_user = $spaa9f78; + $nmo->settings = $sp035a66->settings; + + NinjaMailerJob::dispatchNow($nmo); VersionCheck::dispatchNow(); diff --git a/app/Jobs/Mail/BaseMailerJob.php b/app/Jobs/Mail/BaseMailerJob.php index 095839bd9bdb..fd34481b3582 100644 --- a/app/Jobs/Mail/BaseMailerJob.php +++ b/app/Jobs/Mail/BaseMailerJob.php @@ -29,7 +29,10 @@ use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Lang; use Turbo124\Beacon\Facades\LightLogs; -/*Multi Mailer implemented*/ +/* +Multi Mailer implemented +@Deprecated 14/02/2021 +*/ class BaseMailerJob implements ShouldQueue { diff --git a/app/Jobs/Mail/NinjaMailer.php b/app/Jobs/Mail/NinjaMailer.php new file mode 100644 index 000000000000..c8f697d13992 --- /dev/null +++ b/app/Jobs/Mail/NinjaMailer.php @@ -0,0 +1,46 @@ +mail_obj = $mail_obj; + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + + return $this->from(config('mail.from.address'), config('mail.from.name')) + ->subject($this->mail_obj->subject) + ->markdown($this->mail_obj->markdown, $this->mail_obj->data) + ->withSwiftMessage(function ($message) { + $message->getHeaders()->addTextHeader('Tag', $this->mail_obj->tag); + }); + + } +} diff --git a/app/Jobs/Mail/NinjaMailerJob.php b/app/Jobs/Mail/NinjaMailerJob.php new file mode 100644 index 000000000000..1d76befe4223 --- /dev/null +++ b/app/Jobs/Mail/NinjaMailerJob.php @@ -0,0 +1,155 @@ +nmo = $nmo; + + } + + public function handle() + { + /*If we are migrating data we don't want to fire any emails*/ + if ($this->nmo->company->is_disabled) + return true; + + MultiDB::setDb($this->nmo->company->db); + + //if we need to set an email driver do it now + $this->setMailDriver(); + + //send email + try { + nlog("trying to send"); + Mail::to($this->nmo->to_user->email) + ->send($this->nmo->mailable); + } catch (\Exception $e) { + //$this->failed($e); + nlog("error failed with {$e->getMessage()}"); + if ($this->nmo->to_user instanceof ClientContact) { + $this->logMailError($e->getMessage(), $this->nmo->to_user->client); + } + } + } + + private function setMailDriver() + { + /* Singletons need to be rebooted each time just in case our Locale is changing*/ + App::forgetInstance('translator'); + App::forgetInstance('mail.manager'); //singletons must be destroyed! + + /* Inject custom translations if any exist */ + Lang::replace(Ninja::transformTranslations($this->nmo->settings)); + + switch ($this->nmo->settings->email_sending_method) { + case 'default': + break; + case 'gmail': + $this->setGmailMailer(); + break; + default: + break; + } + } + + private function setGmailMailer() + { + $sending_user = $this->settings->gmail_sending_user_id; + + $user = User::find($this->decodePrimaryKey($sending_user)); + + $google = (new Google())->init(); + $google->getClient()->setAccessToken(json_encode($user->oauth_user_token)); + + if ($google->getClient()->isAccessTokenExpired()) { + $google->refreshToken($user); + } + + /* + * Now that our token is refreshed and valid we can boot the + * mail driver at runtime and also set the token which will persist + * just for this request. + */ + + config(['mail.driver' => 'gmail']); + config(['services.gmail.token' => $user->oauth_user_token->access_token]); + config(['mail.from.address' => $user->email]); + config(['mail.from.name' => $user->present()->name()]); + + //(new MailServiceProvider(app()))->register(); + + nlog("after registering mail service provider"); + nlog(config('services.gmail.token')); + } + + private function logMailError($errors, $recipient_object) + { + SystemLogger::dispatch( + $errors, + SystemLog::CATEGORY_MAIL, + SystemLog::EVENT_MAIL_SEND, + SystemLog::TYPE_FAILURE, + $recipient_object + ); + } + + private function failed($exception = null) + { + nlog('mailer job failed'); + nlog($exception->getMessage()); + + $job_failure = new EmailFailure(); + $job_failure->string_metric5 = get_parent_class($this); + $job_failure->string_metric6 = $exception->getMessage(); + + LightLogs::create($job_failure) + ->batch(); + } +} diff --git a/app/Jobs/Mail/NinjaMailerObject.php b/app/Jobs/Mail/NinjaMailerObject.php new file mode 100644 index 000000000000..55756083d190 --- /dev/null +++ b/app/Jobs/Mail/NinjaMailerObject.php @@ -0,0 +1,32 @@ +company->is_disabled) { - return true; - } - //Set DB MultiDB::setDb($this->company->db); - //if we need to set an email driver do it now - $this->setMailDriver(); - //iterate through company_users $this->company->company_users->each(function ($company_user) { @@ -93,16 +88,15 @@ class PaymentFailureMailer extends BaseMailerJob implements ShouldQueue unset($methods[$key]); $mail_obj = (new PaymentFailureObject($this->client, $this->error, $this->company, $this->payment_hash))->build(); - $mail_obj->from = [config('mail.from.address'), config('mail.from.name')]; - //send email - try { - Mail::to($company_user->user->email) - ->send(new EntityNotificationMailer($mail_obj)); - } catch (\Exception $e) { - //$this->failed($e); - $this->logMailError($e->getMessage(), $this->client); - } + $nmo = new NinjaMailerObject; + $nmo->mailable = new NinjaMailer($mail_obj); + $nmo->company = $this->company; + $nmo->to_user = $company_user->user; + $nmo->settings = $this->settings; + + NinjaMailerJob::dispatch($nmo); + } }); } diff --git a/app/Jobs/Util/Import.php b/app/Jobs/Util/Import.php index a5b6d619546b..08895dab6741 100644 --- a/app/Jobs/Util/Import.php +++ b/app/Jobs/Util/Import.php @@ -34,6 +34,7 @@ use App\Http\ValidationRules\ValidUserForCompany; use App\Jobs\Company\CreateCompanyToken; use App\Jobs\Ninja\CheckCompanyData; use App\Jobs\Ninja\CompanySizeCheck; +use App\Jobs\Util\VersionCheck; use App\Libraries\MultiDB; use App\Mail\MigrationCompleted; use App\Models\Activity; @@ -81,10 +82,10 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Http\UploadedFile; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str; use Turbo124\Beacon\Facades\LightLogs; +use Illuminate\Support\Facades\Mail; class Import implements ShouldQueue { @@ -226,15 +227,22 @@ class Import implements ShouldQueue private function setInitialCompanyLedgerBalances() { Client::cursor()->each(function ($client) { + + $invoice_balances = $client->invoices->where('is_deleted', false)->where('status_id', '>', 1)->sum('balance'); + $company_ledger = CompanyLedgerFactory::create($client->company_id, $client->user_id); $company_ledger->client_id = $client->id; - $company_ledger->adjustment = $client->balance; + $company_ledger->adjustment = $invoice_balances; $company_ledger->notes = 'Migrated Client Balance'; - $company_ledger->balance = $client->balance; + $company_ledger->balance = $invoice_balances; $company_ledger->activity_id = Activity::CREATE_CLIENT; $company_ledger->save(); $client->company_ledger()->save($company_ledger); + + $client->balance = $invoice_balances; + $client->save(); + }); } @@ -1029,10 +1037,36 @@ class Import implements ShouldQueue } if (array_key_exists('invoice_id', $resource) && $resource['invoice_id'] && array_key_exists('invoices', $this->ids)) { - $invoice_id = $this->transformId('invoices', $resource['invoice_id']); - $entity = Invoice::where('id', $invoice_id)->withTrashed()->first(); + + $try_quote = false; + $exception = false; + + try{ + $invoice_id = $this->transformId('invoices', $resource['invoice_id']); + $entity = Invoice::where('id', $invoice_id)->withTrashed()->first(); + } + catch(\Exception $e){ + nlog("i couldn't find the invoice document {$resource['invoice_id']}, perhaps it is a quote?"); + nlog($e->getMessage()); + + $try_quote = true; + } + + if($try_quote && array_key_exists('quotes', $this->ids) ) { + + $quote_id = $this->transformId('quotes', $resource['invoice_id']); + $entity = Quote::where('id', $quote_id)->withTrashed()->first(); + $exception = $e; + + } + + if(!$entity) + throw new Exception("Resource invoice/quote document not available."); + + } + if (array_key_exists('expense_id', $resource) && $resource['expense_id'] && array_key_exists('expenses', $this->ids)) { $expense_id = $this->transformId('expenses', $resource['expense_id']); $entity = Expense::where('id', $expense_id)->withTrashed()->first(); diff --git a/app/Jobs/Util/UploadFile.php b/app/Jobs/Util/UploadFile.php index 9930f403bf3b..a33fe8b55964 100644 --- a/app/Jobs/Util/UploadFile.php +++ b/app/Jobs/Util/UploadFile.php @@ -78,7 +78,7 @@ class UploadFile implements ShouldQueue $instance = Storage::disk($this->disk)->putFileAs( $path, $this->file, - $this->file->hashName() + $this->file->hashName() ); if (in_array($this->file->extension(), ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff', 'psd'])) { diff --git a/app/Libraries/MultiDB.php b/app/Libraries/MultiDB.php index 2fed0652a6b9..3240283af898 100644 --- a/app/Libraries/MultiDB.php +++ b/app/Libraries/MultiDB.php @@ -142,6 +142,31 @@ class MultiDB return null; } + /** + * @param array $data + * @return User|null + */ + public static function hasContact(array $data) : ?ClientContact + { + if (! config('ninja.db.multi_db_enabled')) { + return ClientContact::where($data)->withTrashed()->first(); + } + + foreach (self::$dbs as $db) { + self::setDB($db); + + $user = ClientContacts::where($data)->withTrashed()->first(); + + if ($user) { + return $user; + } + } + + self::setDefaultDatabase(); + + return null; + } + public static function contactFindAndSetDb($token) :bool { foreach (self::$dbs as $db) { @@ -160,7 +185,7 @@ class MultiDB public static function userFindAndSetDb($email) : bool { - //multi-db active + //multi-db active foreach (self::$dbs as $db) { if (User::on($db)->where(['email' => $email])->get()->count() >= 1) { // if user already exists, validation will fail return true; diff --git a/app/Listeners/Credit/CreditEmailedNotification.php b/app/Listeners/Credit/CreditEmailedNotification.php index 83d334d98a67..0b085e046ad4 100644 --- a/app/Listeners/Credit/CreditEmailedNotification.php +++ b/app/Listeners/Credit/CreditEmailedNotification.php @@ -12,7 +12,11 @@ namespace App\Listeners\Credit; use App\Jobs\Mail\EntitySentMailer; +use App\Jobs\Mail\NinjaMailer; +use App\Jobs\Mail\NinjaMailerJob; +use App\Jobs\Mail\NinjaMailerObject; use App\Libraries\MultiDB; +use App\Mail\Admin\EntitySentObject; use App\Notifications\Admin\EntitySentNotification; use App\Utils\Traits\Notifications\UserNotifies; use Illuminate\Contracts\Queue\ShouldQueue; @@ -41,6 +45,11 @@ class CreditEmailedNotification implements ShouldQueue $credit->last_sent_date = now(); $credit->save(); + $nmo = new NinjaMailerObject; + $nmo->mailable = new NinjaMailer( (new EntitySentObject($event->invitation, 'credit', $event->template))->build() ); + $nmo->company = $credit->company; + $nmo->settings = $credit->company->settings; + foreach ($event->invitation->company->company_users as $company_user) { $user = $company_user->user; @@ -51,7 +60,11 @@ class CreditEmailedNotification implements ShouldQueue if (($key = array_search('mail', $methods)) !== false && $first_notification_sent === true) { unset($methods[$key]); - EntitySentMailer::dispatch($event->invitation, 'credit', $user, $event->invitation->company, $event->template); + $nmo->to_user = $user; + + NinjaMailerJob::dispatch($nmo); + + //EntitySentMailer::dispatch($event->invitation, 'credit', $user, $event->invitation->company, $event->template); $first_notification_sent = false; } diff --git a/app/Listeners/SendVerificationNotification.php b/app/Listeners/SendVerificationNotification.php index 8bc5a287a825..058130a5947b 100644 --- a/app/Listeners/SendVerificationNotification.php +++ b/app/Listeners/SendVerificationNotification.php @@ -11,7 +11,11 @@ namespace App\Listeners; +use App\Jobs\Mail\NinjaMailer; +use App\Jobs\Mail\NinjaMailerJob; +use App\Jobs\Mail\NinjaMailerObject; use App\Libraries\MultiDB; +use App\Mail\Admin\VerifyUserObject; use App\Notifications\Ninja\VerifyUser; use App\Utils\Ninja; use Exception; @@ -45,7 +49,16 @@ class SendVerificationNotification implements ShouldQueue MultiDB::setDB($event->company->db); try { - $event->user->notify(new VerifyUser($event->user, $event->company)); + + $nmo = new NinjaMailerObject; + $nmo->mailable = new NinjaMailer((new VerifyUserObject($event->user, $event->company))->build()); + $nmo->company = $event->company; + $nmo->to_user = $event->user; + $nmo->settings = $event->company->settings; + + NinjaMailerJob::dispatch($nmo); + + // $event->user->notify(new VerifyUser($event->user, $event->company)); Ninja::registerNinjaUser($event->user); } catch (Exception $e) { diff --git a/app/Mail/Admin/AccountCreatedObject.php b/app/Mail/Admin/AccountCreatedObject.php new file mode 100644 index 000000000000..847b15bea549 --- /dev/null +++ b/app/Mail/Admin/AccountCreatedObject.php @@ -0,0 +1,52 @@ +user = $user; + $this->company = $company; + } + + public function build() + { + + $data = [ + 'title' => ctrans('texts.new_signup'), + 'message' => ctrans('texts.new_signup_text', ['user' => $this->user->present()->name(), 'email' => $this->user->email, 'ip' => $this->user->ip]), + 'url' => config('ninja.web_url'), + 'button' => ctrans('texts.account_login'), + 'signature' => $this->company->settings->email_signature, + 'settings' => $this->company->settings, + 'logo' => $this->company->present()->logo(), + ]; + + + $mail_obj = new \stdClass; + $mail_obj->subject = ctrans('texts.new_signup'); + $mail_obj->data = $data; + $mail_obj->markdown = 'email.admin.generic'; + $mail_obj->tag = $this->company->company_key; + + return $mail_obj; + } +} \ No newline at end of file diff --git a/app/Mail/Admin/ResetPasswordObject.php b/app/Mail/Admin/ResetPasswordObject.php new file mode 100644 index 000000000000..ec19c4cafd6c --- /dev/null +++ b/app/Mail/Admin/ResetPasswordObject.php @@ -0,0 +1,53 @@ +token = $token; + $this->user = $user; + $this->company = $company; + } + + public function build() + { + + $data = [ + 'title' => ctrans('texts.your_password_reset_link'), + 'message' => ctrans('texts.reset_password'), + 'url' => route('password.reset', ['token' => $this->token, 'email' => $this->user->email]), + 'button' => ctrans('texts.reset'), + 'signature' => $this->company->settings->email_signature, + 'settings' => $this->company->settings, + 'logo' => $this->company->present()->logo(), + ]; + + $mail_obj = new \stdClass; + $mail_obj->subject = ctrans('texts.your_password_reset_link'); + $mail_obj->data = $data; + $mail_obj->markdown = 'email.admin.generic'; + $mail_obj->tag = $this->company->company_key; + + return $mail_obj; + } +} \ No newline at end of file diff --git a/app/Mail/Admin/VerifyUserObject.php b/app/Mail/Admin/VerifyUserObject.php new file mode 100644 index 000000000000..f674f59ec46a --- /dev/null +++ b/app/Mail/Admin/VerifyUserObject.php @@ -0,0 +1,58 @@ +user = $user; + $this->company = $company; + } + + public function build() + { + $this->user->confirmation_code = $this->createDbHash($this->company->db); + $this->user->save(); + + $data = [ + 'title' => ctrans('texts.confirmation_subject'), + 'message' => ctrans('texts.confirmation_message'), + 'url' => url("/user/confirm/{$this->user->confirmation_code}"), + 'button' => ctrans('texts.button_confirmation_message'), + 'settings' => $this->company->settings, + 'logo' => $this->company->present()->logo(), + 'signature' => $this->company->settings->email_signature, + ]; + + + $mail_obj = new \stdClass; + $mail_obj->subject = ctrans('texts.confirmation_subject'); + $mail_obj->data = $data; + $mail_obj->markdown = 'email.admin.generic'; + $mail_obj->tag = $this->company->company_key; + + return $mail_obj; + } +} \ No newline at end of file diff --git a/app/Mail/ClientContact/ClientContactResetPasswordObject.php b/app/Mail/ClientContact/ClientContactResetPasswordObject.php new file mode 100644 index 000000000000..a36cba49ace3 --- /dev/null +++ b/app/Mail/ClientContact/ClientContactResetPasswordObject.php @@ -0,0 +1,54 @@ +token = $token; + $this->client_contact = $client_contact; + $this->company = $client_contact->company; + } + + public function build() + { + + $data = [ + 'title' => ctrans('texts.your_password_reset_link'), + 'message' => ctrans('texts.reset_password'), + 'url' => route('client.password.reset', ['token' => $this->token, 'email' => $this->client_contact->email]), + 'button' => ctrans('texts.reset'), + 'signature' => $this->company->settings->email_signature, + 'settings' => $this->company->settings, + 'logo' => $this->company->present()->logo(), + ]; + + + $mail_obj = new \stdClass; + $mail_obj->subject = ctrans('texts.your_password_reset_link'); + $mail_obj->data = $data; + $mail_obj->markdown = 'email.admin.generic'; + $mail_obj->tag = $this->company->company_key; + + return $mail_obj; + } +} \ No newline at end of file diff --git a/app/Mail/RecurringInvoice/ClientContactRequestCancellationObject.php b/app/Mail/RecurringInvoice/ClientContactRequestCancellationObject.php new file mode 100644 index 000000000000..ee533ef901ee --- /dev/null +++ b/app/Mail/RecurringInvoice/ClientContactRequestCancellationObject.php @@ -0,0 +1,55 @@ +recurring_invoice = $recurring_invoice; + $this->client_contact = $client_contact; + $this->company = $recurring_invoice->company; + } + + public function build() + { + + $data = [ + 'title' => ctrans('texts.recurring_cancellation_request', ['contact' => $this->client_contact->present()->name()]), + 'message' => ctrans('texts.recurring_cancellation_request_body', ['contact' => $this->client_contact->present()->name(), 'client' => $this->client_contact->client->present()->name(), 'invoice' => $this->recurring_invoice->number]), + 'url' => config('ninja.web_url'), + 'button' => ctrans('texts.account_login'), + 'signature' => $this->company->settings->email_signature, + 'settings' => $this->company->settings, + 'logo' => $this->company->present()->logo(), + ]; + + + $mail_obj = new \stdClass; + $mail_obj->subject = ctrans('texts.recurring_cancellation_request', ['contact' => $this->client_contact->present()->name()]); + $mail_obj->data = $data; + $mail_obj->markdown = 'email.admin.generic'; + $mail_obj->tag = $this->company->company_key; + + return $mail_obj; + } +} \ No newline at end of file diff --git a/app/Models/ClientContact.php b/app/Models/ClientContact.php index 115fb1e206b0..b158c3cadca0 100644 --- a/app/Models/ClientContact.php +++ b/app/Models/ClientContact.php @@ -11,6 +11,10 @@ namespace App\Models; +use App\Jobs\Mail\NinjaMailer; +use App\Jobs\Mail\NinjaMailerJob; +use App\Jobs\Mail\NinjaMailerObject; +use App\Mail\ClientContact\ClientContactResetPasswordObject; use App\Models\Presenters\ClientContactPresenter; use App\Notifications\ClientContactResetPassword; use App\Utils\Traits\MakesHash; @@ -151,7 +155,15 @@ class ClientContact extends Authenticatable implements HasLocalePreference public function sendPasswordResetNotification($token) { - $this->notify(new ClientContactResetPassword($token)); + $nmo = new NinjaMailerObject; + $nmo->mailable = new NinjaMailer((new ClientContactResetPasswordObject($token, $this))->build()); + $nmo->to_user = $this; + $nmo->company = $this->company; + $nmo->settings = $this->company->settings; + + NinjaMailerJob::dispatch($nmo); + + //$this->notify(new ClientContactResetPassword($token)); } public function preferredLocale() diff --git a/app/Models/User.php b/app/Models/User.php index 9e0133057188..f5e94281a502 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -11,6 +11,10 @@ namespace App\Models; +use App\Jobs\Mail\NinjaMailer; +use App\Jobs\Mail\NinjaMailerJob; +use App\Jobs\Mail\NinjaMailerObject; +use App\Mail\Admin\ResetPasswordObject; use App\Models\Presenters\UserPresenter; use App\Notifications\ResetPasswordNotification; use App\Utils\Traits\MakesHash; @@ -371,6 +375,15 @@ class User extends Authenticatable implements MustVerifyEmail */ public function sendPasswordResetNotification($token) { - $this->notify(new ResetPasswordNotification($token)); + + $nmo = new NinjaMailerObject; + $nmo->mailable = new NinjaMailer( (new ResetPasswordObject($token, $this, $this->account->default_company))->build()); + $nmo->to_user = $this; + $nmo->settings = $this->account->default_company->settings; + $nmo->company = $this->account->default_company; + + NinjaMailerJob::dispatch($nmo); + + //$this->notify(new ResetPasswordNotification($token)); } } diff --git a/app/Notifications/Admin/EntitySentNotification.php b/app/Notifications/Admin/EntitySentNotification.php index 53dc0e75d73a..b499d8313699 100644 --- a/app/Notifications/Admin/EntitySentNotification.php +++ b/app/Notifications/Admin/EntitySentNotification.php @@ -21,6 +21,7 @@ use Illuminate\Notifications\Notification; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +//@deprecated class EntitySentNotification extends Notification implements ShouldQueue { //use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -77,39 +78,6 @@ class EntitySentNotification extends Notification implements ShouldQueue */ public function toMail($notifiable) { - //@TODO THESE ARE @DEPRECATED NOW we are now using app/Mail/Admin/* - $amount = Number::formatMoney($this->entity->amount, $this->entity->client); - $subject = ctrans( - "texts.notification_{$this->entity_name}_sent_subject", - [ - 'client' => $this->contact->present()->name(), - 'invoice' => $this->entity->number, - ] - ); - - $data = [ - 'title' => $subject, - 'message' => ctrans( - "texts.notification_{$this->entity_name}_sent", - [ - 'amount' => $amount, - 'client' => $this->contact->present()->name(), - 'invoice' => $this->entity->number, - ] - ), - 'url' => $this->invitation->getAdminLink(), - 'button' => ctrans("texts.view_{$this->entity_name}"), - 'signature' => $this->settings->email_signature, - 'logo' => $this->company->present()->logo(), - 'settings' => $this->settings, - ]; - - return (new MailMessage) - ->subject($subject) - ->markdown('email.admin.generic', $data) - ->withSwiftMessage(function ($message) { - $message->getHeaders()->addTextHeader('Tag', $this->company->company_key); - }); } /** @@ -120,9 +88,7 @@ class EntitySentNotification extends Notification implements ShouldQueue */ public function toArray($notifiable) { - return [ - // - ]; + return []; } public function toSlack($notifiable) diff --git a/app/Notifications/ClientContactRequestCancellation.php b/app/Notifications/ClientContactRequestCancellation.php index ee69fae9c691..1b7530c42c2c 100644 --- a/app/Notifications/ClientContactRequestCancellation.php +++ b/app/Notifications/ClientContactRequestCancellation.php @@ -22,6 +22,7 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Mail; +//@deprecated for mail class ClientContactRequestCancellation extends Notification { // use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -56,7 +57,7 @@ class ClientContactRequestCancellation extends Notification */ public function via($notifiable) { - return ['mail', 'slack']; + return ['slack']; } /** @@ -67,19 +68,6 @@ class ClientContactRequestCancellation extends Notification */ public function toMail($notifiable) { - if (static::$toMailCallback) { - return call_user_func(static::$toMailCallback, $notifiable, $this->client_contact); - } - - $client_contact_name = $this->client_contact->present()->name(); - $client_name = $this->client_contact->client->present()->name(); - $recurring_invoice_number = $this->recurring_invoice->number; - - return (new MailMessage) - ->subject('Request for recurring invoice cancellation from '.$client_contact_name) - ->markdown('email.support.cancellation', [ - 'message' => "Contact [{$client_contact_name}] from Client [{$client_name}] requested to cancel Recurring Invoice [#{$recurring_invoice_number}]", - ]); } /** diff --git a/app/Notifications/ClientContactResetPassword.php b/app/Notifications/ClientContactResetPassword.php index 4766410778ab..ff9cd764efb9 100644 --- a/app/Notifications/ClientContactResetPassword.php +++ b/app/Notifications/ClientContactResetPassword.php @@ -19,6 +19,7 @@ use Illuminate\Notifications\Notification; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +//@deprecated class ClientContactResetPassword extends Notification { // use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -55,7 +56,7 @@ class ClientContactResetPassword extends Notification */ public function via($notifiable) { - return ['mail']; + return []; } /** diff --git a/app/Notifications/GmailTestNotification.php b/app/Notifications/GmailTestNotification.php deleted file mode 100644 index c815830389c5..000000000000 --- a/app/Notifications/GmailTestNotification.php +++ /dev/null @@ -1,73 +0,0 @@ -line('The introduction to the notification.') - ->action('Notification Action', url('/')) - ->line('Thank you for using our application!'); - } - - /** - * Get the array representation of the notification. - * - * @param mixed $notifiable - * @return array - */ - public function toArray($notifiable) - { - return [ - // - ]; - } -} diff --git a/app/Notifications/NewAccountCreated.php b/app/Notifications/NewAccountCreated.php index 3f692c009a59..d21fc554bb7a 100644 --- a/app/Notifications/NewAccountCreated.php +++ b/app/Notifications/NewAccountCreated.php @@ -20,6 +20,7 @@ use Illuminate\Notifications\Notification; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +//@deprecated class NewAccountCreated extends Notification { //use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -50,7 +51,7 @@ class NewAccountCreated extends Notification */ public function via($notifiable) { - return ['slack', 'mail']; + return ['slack']; } /** @@ -61,26 +62,6 @@ class NewAccountCreated extends Notification */ public function toMail($notifiable) { - $user_name = $this->user->first_name.' '.$this->user->last_name; - $email = $this->user->email; - $ip = $this->user->ip; - - $data = [ - 'title' => ctrans('texts.new_signup'), - 'message' => ctrans('texts.new_signup_text', ['user' => $user_name, 'email' => $email, 'ip' => $ip]), - 'url' => config('ninja.web_url'), - 'button' => ctrans('texts.account_login'), - 'signature' => $this->company->settings->email_signature, - 'logo' => $this->company->present()->logo(), - 'settings' => $this->company->settings, - ]; - - return (new MailMessage) - ->subject(ctrans('texts.new_signup')) - ->withSwiftMessage(function ($message) { - $message->getHeaders()->addTextHeader('Tag', $this->company->company_key); - }) - ->markdown('email.admin.generic', $data); } /** diff --git a/app/Notifications/Ninja/NewAccountCreated.php b/app/Notifications/Ninja/NewAccountCreated.php index 1367a2a5bac2..bc558ba7c6da 100644 --- a/app/Notifications/Ninja/NewAccountCreated.php +++ b/app/Notifications/Ninja/NewAccountCreated.php @@ -20,6 +20,8 @@ use Illuminate\Notifications\Notification; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; + +//@deprecated class NewAccountCreated extends Notification { // use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -50,7 +52,7 @@ class NewAccountCreated extends Notification */ public function via($notifiable) { - return ['slack', 'mail']; + return ['slack']; } /** @@ -61,26 +63,6 @@ class NewAccountCreated extends Notification */ public function toMail($notifiable) { - $user_name = $this->user->first_name.' '.$this->user->last_name; - $email = $this->user->email; - $ip = $this->user->ip; - - $data = [ - 'title' => ctrans('texts.new_signup'), - 'message' => ctrans('texts.new_signup_text', ['user' => $user_name, 'email' => $email, 'ip' => $ip]), - 'url' => config('ninja.web_url'), - 'button' => ctrans('texts.account_login'), - 'signature' => $this->company->settings->email_signature, - 'logo' => $this->company->present()->logo(), - 'settings' => $this->company->settings, - ]; - - return (new MailMessage) - ->subject(ctrans('texts.new_signup')) - ->markdown('email.admin.generic', $data) - ->withSwiftMessage(function ($message) { - $message->getHeaders()->addTextHeader('Tag', $this->company->company_key); - }); } /** diff --git a/app/Notifications/Ninja/VerifyUser.php b/app/Notifications/Ninja/VerifyUser.php index 6319cfcc8033..bfb79d70d402 100644 --- a/app/Notifications/Ninja/VerifyUser.php +++ b/app/Notifications/Ninja/VerifyUser.php @@ -19,6 +19,7 @@ use Illuminate\Notifications\Notification; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +//@deprecated class VerifyUser extends Notification { // use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; @@ -46,7 +47,7 @@ class VerifyUser extends Notification */ public function via($notifiable) { - return ['mail']; + return ['']; } /** @@ -57,19 +58,6 @@ class VerifyUser extends Notification */ public function toMail($notifiable) { - $data = [ - 'title' => ctrans('texts.confirmation_subject'), - 'message' => ctrans('texts.confirmation_message'), - 'url' => url("/user/confirm/{$this->user->confirmation_code}"), - 'button' => ctrans('texts.button_confirmation_message'), - 'signature' => '', - 'logo' => 'https://www.invoiceninja.com/wp-content/uploads/2019/01/InvoiceNinja-Logo-Round-300x300.png', - 'settings' => $this->company->settings, - ]; - - return (new MailMessage) - ->subject(ctrans('texts.confirmation_subject')) - ->markdown('email.admin.generic', $data); } /** diff --git a/app/Notifications/ResetPasswordNotification.php b/app/Notifications/ResetPasswordNotification.php index ff1162fa2dd6..387e79441539 100644 --- a/app/Notifications/ResetPasswordNotification.php +++ b/app/Notifications/ResetPasswordNotification.php @@ -6,6 +6,7 @@ use Illuminate\Bus\Queueable; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; +//@deprecated class ResetPasswordNotification extends Notification { // use Queueable; @@ -30,7 +31,7 @@ class ResetPasswordNotification extends Notification */ public function via($notifiable) { - return ['mail']; + return []; } /** diff --git a/app/Notifications/SendGenericNotification.php b/app/Notifications/SendGenericNotification.php deleted file mode 100644 index eff2a8d1e334..000000000000 --- a/app/Notifications/SendGenericNotification.php +++ /dev/null @@ -1,124 +0,0 @@ -entity = $invitation->{$entity_string}; - $this->contact = $invitation->contact; - $this->settings = $this->entity->client->getMergedSettings(); - $this->subject = $subject; - $this->body = $body; - $this->invitation = $invitation; - $this->entity_string = $entity_string; - } - - /** - * Get the notification's delivery channels. - * - * @param mixed $notifiable - * @return array - */ - public function via($notifiable) - { - return ['mail']; - } - - /** - * Get the mail representation of the notification. - * - * @param mixed $notifiable - * @return MailMessage - */ - public function toMail($notifiable) - { - $mail_message = (new MailMessage) - ->withSwiftMessage(function ($message) { - $message->getHeaders()->addTextHeader('Tag', $this->invitation->company->company_key); - })->markdown($this->getTemplateView(), $this->buildMailMessageData()); - //})->markdown('email.template.plain', $this->buildMailMessageData()); - - $mail_message = $this->buildMailMessageSettings($mail_message); - - return $mail_message; - } - - /** - * Get the array representation of the notification. - * - * @param mixed $notifiable - * @return array - */ - public function toArray($notifiable) - { - return [ - // - ]; - } - - public function toSlack($notifiable) - { - return ''; - // $logo = $this->company->present()->logo(); - // $amount = Number::formatMoney($this->invoice->amount, $this->invoice->client); - - // return (new SlackMessage) - // ->success() - // ->from(ctrans('texts.notification_bot')) - // ->image($logo) - // ->content(ctrans( - // 'texts.notification_invoice_viewed', - // [ - // 'amount' => $amount, - // 'client' => $this->contact->present()->name(), - // 'invoice' => $this->invoice->number - // ] - // )); - } -} diff --git a/app/Observers/CreditObserver.php b/app/Observers/CreditObserver.php new file mode 100644 index 000000000000..9969f7f02c60 --- /dev/null +++ b/app/Observers/CreditObserver.php @@ -0,0 +1,75 @@ +client->credit_filepath() . $credit->number.'.pdf'); + } + + /** + * Handle the client "deleted" event. + * + * @param Credit $credit + * @return void + */ + public function deleted(Credit $credit) + { + + } + + /** + * Handle the client "restored" event. + * + * @param Credit $credit + * @return void + */ + public function restored(Credit $credit) + { + // + } + + /** + * Handle the client "force deleted" event. + * + * @param Credit $credit + * @return void + */ + public function forceDeleted(Credit $credit) + { + // + } +} diff --git a/app/Observers/InvoiceObserver.php b/app/Observers/InvoiceObserver.php index cee12d293a29..14f5aa9c68de 100644 --- a/app/Observers/InvoiceObserver.php +++ b/app/Observers/InvoiceObserver.php @@ -11,6 +11,7 @@ namespace App\Observers; +use App\Jobs\Util\UnlinkFile; use App\Jobs\Util\WebhookHandler; use App\Models\Client; use App\Models\Invoice; @@ -50,6 +51,9 @@ class InvoiceObserver if ($subscriptions) { WebhookHandler::dispatch(Webhook::EVENT_UPDATE_INVOICE, $invoice, $invoice->company); } + + UnlinkFile::dispatchNow(config('filesystems.default'), $invoice->client->invoice_filepath() . $invoice->number.'.pdf'); + } /** diff --git a/app/Observers/QuoteObserver.php b/app/Observers/QuoteObserver.php index 89ca20f81624..f2a9471111af 100644 --- a/app/Observers/QuoteObserver.php +++ b/app/Observers/QuoteObserver.php @@ -11,6 +11,7 @@ namespace App\Observers; +use App\Jobs\Util\UnlinkFile; use App\Jobs\Util\WebhookHandler; use App\Models\Quote; use App\Models\Webhook; @@ -49,6 +50,9 @@ class QuoteObserver if ($subscriptions) { WebhookHandler::dispatch(Webhook::EVENT_UPDATE_QUOTE, $quote, $quote->company); } + + UnlinkFile::dispatchNow(config('filesystems.default'), $quote->client->quote_filepath() . $quote->number.'.pdf'); + } /** diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 9937060e098a..3d96377fa950 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -16,6 +16,7 @@ use App\Models\Client; use App\Models\Company; use App\Models\CompanyGateway; use App\Models\CompanyToken; +use App\Models\Credit; use App\Models\Expense; use App\Models\Invoice; use App\Models\Payment; @@ -29,6 +30,7 @@ use App\Observers\ClientObserver; use App\Observers\CompanyGatewayObserver; use App\Observers\CompanyObserver; use App\Observers\CompanyTokenObserver; +use App\Observers\CreditObserver; use App\Observers\ExpenseObserver; use App\Observers\InvoiceObserver; use App\Observers\PaymentObserver; @@ -79,6 +81,7 @@ class AppServiceProvider extends ServiceProvider Company::observe(CompanyObserver::class); CompanyGateway::observe(CompanyGatewayObserver::class); CompanyToken::observe(CompanyTokenObserver::class); + Credit::observe(CreditObserver::class); Expense::observe(ExpenseObserver::class); Invoice::observe(InvoiceObserver::class); Payment::observe(PaymentObserver::class); diff --git a/app/Services/Credit/ApplyPayment.php b/app/Services/Credit/ApplyPayment.php index e97e38c3854b..40a6652e1e71 100644 --- a/app/Services/Credit/ApplyPayment.php +++ b/app/Services/Credit/ApplyPayment.php @@ -147,7 +147,7 @@ class ApplyPayment event(new InvoiceWasUpdated($this->invoice, $this->invoice->company, Ninja::eventVars())); if ((int)$this->invoice->balance == 0) { - $this->invoice->service()->deletePdf(); + // $this->invoice->service()->deletePdf(); event(new InvoiceWasPaid($this->invoice, $payment, $this->payment->company, Ninja::eventVars())); } } diff --git a/app/Services/Credit/CreditService.php b/app/Services/Credit/CreditService.php index 94a511395ef4..85ab8fddc64c 100644 --- a/app/Services/Credit/CreditService.php +++ b/app/Services/Credit/CreditService.php @@ -104,6 +104,13 @@ class CreditService return $this; } + public function updateBalance($adjustment) + { + $this->credit->balance -= $adjustment; + + return $this; + } + public function fillDefaults() { $settings = $this->credit->client->getMergedSettings(); diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index b45e5eb19b6e..ccaa2e929954 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -266,7 +266,7 @@ class InvoiceService //$this->invoice = $this->invoice->calc()->getInvoice(); - $this->deletePdf(); + // $this->deletePdf(); return $this; } diff --git a/app/Services/Payment/DeletePayment.php b/app/Services/Payment/DeletePayment.php index 4c58a11b8c19..b0fd8aeab657 100644 --- a/app/Services/Payment/DeletePayment.php +++ b/app/Services/Payment/DeletePayment.php @@ -69,7 +69,7 @@ class DeletePayment private function updateClient() { - $this->payment->client->service()->updatePaidToDate(-1 * $this->payment->amount)->save(); + //$this->payment->client->service()->updatePaidToDate(-1 * $this->payment->amount)->save(); return $this; } @@ -92,6 +92,7 @@ class DeletePayment $paymentable_invoice->client ->service() ->updateBalance($paymentable_invoice->pivot->amount) + ->updatePaidToDate($paymentable_invoice->pivot->amount * -1) ->save(); if ($paymentable_invoice->balance == $paymentable_invoice->amount) { diff --git a/app/Utils/Traits/Notifications/UserNotifies.php b/app/Utils/Traits/Notifications/UserNotifies.php index 8b38541c800d..477a4dd36567 100644 --- a/app/Utils/Traits/Notifications/UserNotifies.php +++ b/app/Utils/Traits/Notifications/UserNotifies.php @@ -13,6 +13,10 @@ namespace App\Utils\Traits\Notifications; /** * Class UserNotifies. + * + * I think the term $required_permissions is confusing here, what + * we are actually defining is the notifications available on the + * user itself. */ trait UserNotifies { @@ -74,10 +78,41 @@ trait UserNotifies $notifiable_methods = []; $notifications = $company_user->notifications; + //conditional to define whether the company user has the required notification for the MAIL notification TYPE if (count(array_intersect($required_permissions, $notifications->email)) >= 1 || count(array_intersect($required_permissions, ['all_user_notifications'])) >= 1 || count(array_intersect($required_permissions, ['all_notifications'])) >= 1) { array_push($notifiable_methods, 'mail'); } return $notifiable_methods; } + + /* + * Returns a filtered collection of users with the + * required notification - NOTE this is only implemented for + * EMAIL notification types - we'll need to chain + * additional types at a later stage. + */ + public function filterUsersByPermissions($company_users, $entity, array $required_notification) + { + + return $company_users->filter(function($company_user) use($required_notification, $entity){ + + return $this->checkNotificationExists($company_user, $entity, $required_notification); + + }); + + } + + private function checkNotificationExists($company_user, $entity, $required_notification) + { + /* Always make sure we push the `all_notificaitons` into the mix */ + array_push($required_notification, 'all_notifications'); + + /* Selectively add the all_user if the user is associated with the entity */ + if ($entity->user_id == $company_user->_user_id || $entity->assigned_user_id == $company_user->user_id) + array_push($required_notification, 'all_user_notifications'); + + + return count(array_intersect($required_notification, $company_user->notifications->email)) >= 1; + } } diff --git a/config/trustedproxy.php b/config/trustedproxy.php new file mode 100644 index 000000000000..e618ae2475a1 --- /dev/null +++ b/config/trustedproxy.php @@ -0,0 +1,50 @@ + null, // [,], '*', ',' + + /* + * To trust one or more specific proxies that connect + * directly to your server, use an array or a string separated by comma of IP addresses: + */ + // 'proxies' => ['192.168.1.1'], + // 'proxies' => '192.168.1.1, 192.168.1.2', + + /* + * Or, to trust all proxies that connect + * directly to your server, use a "*" + */ + // 'proxies' => '*', + + /* + * Which headers to use to detect proxy related data (For, Host, Proto, Port) + * + * Options include: + * + * - Illuminate\Http\Request::HEADER_X_FORWARDED_ALL (use all x-forwarded-* headers to establish trust) + * - Illuminate\Http\Request::HEADER_FORWARDED (use the FORWARDED header to establish trust) + * - Illuminate\Http\Request::HEADER_X_FORWARDED_AWS_ELB (If you are using AWS Elastic Load Balancer) + * + * - 'HEADER_X_FORWARDED_ALL' (use all x-forwarded-* headers to establish trust) + * - 'HEADER_FORWARDED' (use the FORWARDED header to establish trust) + * - 'HEADER_X_FORWARDED_AWS_ELB' (If you are using AWS Elastic Load Balancer) + * + * @link https://symfony.com/doc/current/deployment/proxies.html + */ + 'headers' => Illuminate\Http\Request::HEADER_X_FORWARDED_ALL, + +]; diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 6f6df8a58ed8..e4fb00157301 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -4138,6 +4138,9 @@ $LANG = array( ///////////////////////////////////////////////// 'start_migration' => 'Start Migration', + 'recurring_cancellation_request' => 'Request for recurring invoice cancellation from :contact', + 'recurring_cancellation_request_body' => ':contact from Client :client requested to cancel Recurring Invoice :invoice', + ); return $LANG; diff --git a/resources/views/email/auth/password-reset.blade.php b/resources/views/email/auth/password-reset.blade.php index 2f366cf1f224..ac5ebb54e274 100644 --- a/resources/views/email/auth/password-reset.blade.php +++ b/resources/views/email/auth/password-reset.blade.php @@ -1,19 +1,14 @@ @component('email.template.master', ['design' => 'light', 'whitelabel' => false]) @slot('header') - @include('email.components.header', ['logo' => 'https://www.invoiceninja.com/wp-content/uploads/2015/10/logo-white-horizontal-1.png']) + @include('email.components.header', ['logo' => $logo]) @endslot -

You are receiving this email because we received a password reset request for your account.

+

{{ ctrans('texts.reset_password') }}

- Reset Password + {{ ctrans('texts.reset') }} -

- If you’re having trouble clicking the "Reset Password" button, copy and paste the URL below into your web - browser: -

- {{ $link }} @endcomponent diff --git a/routes/api.php b/routes/api.php index dd5509523ea7..653244096ed3 100644 --- a/routes/api.php +++ b/routes/api.php @@ -32,6 +32,7 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::get('activities/download_entity/{activity}', 'ActivityController@downloadHistoricalEntity'); Route::resource('clients', 'ClientController'); // name = (clients. index / create / show / update / destroy / edit + Route::put('clients/{client}/upload', 'ClientController@upload')->name('clients.upload'); Route::post('clients/bulk', 'ClientController@bulk')->name('clients.bulk'); @@ -40,12 +41,14 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::get('invoices/{invoice}/delivery_note', 'InvoiceController@deliveryNote')->name('invoices.delivery_note'); Route::get('invoices/{invoice}/{action}', 'InvoiceController@action')->name('invoices.action'); + Route::put('invoices/{invoice}/upload', 'InvoiceController@upload')->name('invoices.upload'); Route::get('invoice/{invitation_key}/download', 'InvoiceController@downloadPdf')->name('invoices.downloadPdf'); Route::post('invoices/bulk', 'InvoiceController@bulk')->name('invoices.bulk'); Route::resource('credits', 'CreditController'); // name = (credits. index / create / show / update / destroy / edit + Route::put('credits/{credit}/upload', 'CreditController@upload')->name('credits.upload'); Route::get('credits/{credit}/{action}', 'CreditController@action')->name('credits.action'); @@ -70,6 +73,7 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::post('recurring_quotes/bulk', 'RecurringQuoteController@bulk')->name('recurring_quotes.bulk'); Route::resource('expenses', 'ExpenseController'); // name = (expenses. index / create / show / update / destroy / edit + Route::put('expenses/{expense}/upload', 'ExpenseController@upload'); Route::post('expenses/bulk', 'ExpenseController@bulk')->name('expenses.bulk'); @@ -128,6 +132,7 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::post('migration/start', 'MigrationController@startMigration'); Route::resource('companies', 'CompanyController'); // name = (companies. index / create / show / update / destroy / edit + Route::put('companies/{company}/upload', 'CompanyController@upload'); Route::resource('tokens', 'TokenController')->middleware('password_protected'); // name = (tokens. index / create / show / update / destroy / edit Route::post('tokens/bulk', 'TokenController@bulk')->name('tokens.bulk')->middleware('password_protected');