diff --git a/app/Http/Controllers/PaymentTermController.php b/app/Http/Controllers/PaymentTermController.php index bfc4e9df1de4..8059ccae6481 100644 --- a/app/Http/Controllers/PaymentTermController.php +++ b/app/Http/Controllers/PaymentTermController.php @@ -9,6 +9,7 @@ use App\Http\Requests\PaymentTerm\ShowPaymentTermRequest; use App\Http\Requests\PaymentTerm\StorePaymentTermRequest; use App\Http\Requests\PaymentTerm\UpdatePaymentTermRequest; use App\Models\PaymentTerm; +use App\Repositories\PaymentTermRepository; use App\Transformers\PaymentTermTransformer; use App\Utils\Traits\MakesHash; use Illuminate\Http\Request; @@ -21,9 +22,22 @@ class PaymentTermController extends BaseController protected $entity_transformer = PaymentTermTransformer::class; - public function __construct() + /** + * @var PaymentRepository + */ + protected $payment_term_repo; + + + /** + * PaymentTermController constructor. + * + * @param \App\Repositories\PaymentTermRepository $payment_term_repo The payment term repo + */ + public function __construct(PaymentTermRepository $payment_term_repo) { parent::__construct(); + + $this->payment_term_repo = $payment_term_repo; } /** @@ -388,4 +402,76 @@ class PaymentTermController extends BaseController return response()->json([], 200); } + + /** + * Perform bulk actions on the list view + * + * @return Collection + * + * + * @OA\Post( + * path="/api/v1/payment_terms/bulk", + * operationId="bulkPaymentTerms", + * tags={"payment_terms"}, + * summary="Performs bulk actions on an array of payment terms", + * 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="Payment Ter,s", + * 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 Payment Terms 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/PaymentTerm"), + * ), + * @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'); + + $payment_terms = PaymentTerm::withTrashed()->company()->find($this->transformKeys($ids)); + + $payment_terms->each(function ($payment_term, $key) use ($action) { + if (auth()->user()->can('edit', $payment_term)) { + $this->payment_term_repo->{$action}($payment_term); + } + }); + + return $this->listResponse(PaymentTerm::withTrashed()->whereIn('id', $this->transformKeys($ids))); + } + + } diff --git a/app/Jobs/Mail/EntityPaidMailer.php b/app/Jobs/Mail/EntityPaidMailer.php new file mode 100644 index 000000000000..44bfe3021bcc --- /dev/null +++ b/app/Jobs/Mail/EntityPaidMailer.php @@ -0,0 +1,89 @@ +company = $company; + + $this->user = $user; + + $this->payment = $payment; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + info("entity paid mailer"); + //Set DB + MultiDB::setDb($this->company->db); + + //if we need to set an email driver do it now + $this->setMailDriver($this->payment->client->getSetting('email_sending_method')); + + $mail_obj = (new EntityPaidObject($this->payment))->build(); + $mail_obj->from = [$this->payment->user->email, $this->payment->user->present()->name()]; + + //send email + Mail::to($this->user->email) + ->send(new EntityNotificationMailer($mail_obj)); + + //catch errors + if (count(Mail::failures()) > 0) { + $this->logMailError(Mail::failures()); + } + + } + + private function logMailError($errors) + { + SystemLogger::dispatch( + $errors, + SystemLog::CATEGORY_MAIL, + SystemLog::EVENT_MAIL_SEND, + SystemLog::TYPE_FAILURE, + $this->payment->client + ); + } + + +} diff --git a/app/Jobs/Mail/EntitySentMailer.php b/app/Jobs/Mail/EntitySentMailer.php index 55b307155e72..2eee1d4d6022 100644 --- a/app/Jobs/Mail/EntitySentMailer.php +++ b/app/Jobs/Mail/EntitySentMailer.php @@ -84,7 +84,7 @@ class EntitySentMailer extends BaseMailerJob implements ShouldQueue SystemLog::CATEGORY_MAIL, SystemLog::EVENT_MAIL_SEND, SystemLog::TYPE_FAILURE, - $this->invoice->client + $this->entity->client ); } diff --git a/app/Jobs/Mail/PaymentFailureMailer.php b/app/Jobs/Mail/PaymentFailureMailer.php new file mode 100644 index 000000000000..fa823279d551 --- /dev/null +++ b/app/Jobs/Mail/PaymentFailureMailer.php @@ -0,0 +1,91 @@ +company = $company; + + $this->message = $message; + + $this->client = $client; + + $this->amount = $amount; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + info("entity payment failure mailer"); + //Set DB + MultiDB::setDb($this->company->db); + + //if we need to set an email driver do it now + $this->setMailDriver($this->client->getSetting('email_sending_method')); + + $mail_obj = (new PaymentFailureObject($this->client, $this->message, $this->amount, $this->company))->build(); + $mail_obj->from = [$this->company->owner()->email, $this->company->owner()->present()->name()]; + + //send email + Mail::to($this->user->email) + ->send(new EntityNotificationMailer($mail_obj)); + + //catch errors + if (count(Mail::failures()) > 0) { + $this->logMailError(Mail::failures()); + } + + } + + private function logMailError($errors) + { + SystemLogger::dispatch( + $errors, + SystemLog::CATEGORY_MAIL, + SystemLog::EVENT_MAIL_SEND, + SystemLog::TYPE_FAILURE, + $this->client + ); + } + + +} diff --git a/app/Listeners/Payment/PaymentNotification.php b/app/Listeners/Payment/PaymentNotification.php index d798008baa1c..6c664faa66ce 100644 --- a/app/Listeners/Payment/PaymentNotification.php +++ b/app/Listeners/Payment/PaymentNotification.php @@ -11,6 +11,7 @@ namespace App\Listeners\Payment; +use App\Jobs\Mail\EntityPaidMailer; use App\Models\Activity; use App\Models\Invoice; use App\Models\Payment; @@ -46,10 +47,23 @@ class PaymentNotification implements ShouldQueue /*User notifications*/ foreach ($payment->company->company_users as $company_user) { + $user = $company_user->user; + $methods = $this->findUserEntityNotificationType($payment, $company_user, ['all_notifications']); + + if (($key = array_search('mail', $methods)) !== false) { + unset($methods[$key]); + + //Fire mail notification here!!! + //This allows us better control of how we + //handle the mailer + + EntityPaidMailer::dispatch($payment, $user, $payment->company); + } + $notification = new NewPaymentNotification($payment, $payment->company); - $notification->method = $this->findUserEntityNotificationType($payment, $company_user, ['all_notifications']); + $notification->method = $methods; if ($user) { $user->notify($notification); diff --git a/app/Mail/Admin/PaymentFailureObject.php b/app/Mail/Admin/PaymentFailureObject.php new file mode 100644 index 000000000000..1da8a8fe5e25 --- /dev/null +++ b/app/Mail/Admin/PaymentFailureObject.php @@ -0,0 +1,83 @@ +client = $client; + $this->message = $message; + $this->amount = $amount; + $this->company = $company; + } + + public function build() + { + $mail_obj = new \stdClass; + $mail_obj->amount = $this->getAmount(); + $mail_obj->subject = $this->getSubject(); + $mail_obj->data = $this->getData(); + $mail_obj->markdown = 'email.admin.generic'; + $mail_obj->tag = $this->company->company_key; + + return $mail_obj; + } + + private function getAmount() + { + return Number::formatMoney($this->amount, $this->client); + } + + private function getSubject() + { + return + ctrans( + 'texts.payment_failed_subject', + ['client' => $this->payment->client->present()->name()] + ); + } + + private function getData() + { + $signature = $this->client->getSetting('email_signature'); + + $data = [ + 'title' => ctrans( + 'texts.payment_failed_subject', + ['client' => $this->client->present()->name()] + ), + 'message' => ctrans( + 'texts.notification_payment_paid', + ['amount' => $this->getAmount(), + 'client' => $this->client->present()->name(), + 'message' => $this->message, + ] + ), + 'signature' => $signature, + 'logo' => $this->company->present()->logo(), + ]; + + return $data; + } +} \ No newline at end of file diff --git a/app/Notifications/Admin/EntitySentNotification.php b/app/Notifications/Admin/EntitySentNotification.php index 039affe86acd..72922715c017 100644 --- a/app/Notifications/Admin/EntitySentNotification.php +++ b/app/Notifications/Admin/EntitySentNotification.php @@ -69,6 +69,7 @@ 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", diff --git a/app/Notifications/Admin/EntityViewedNotification.php b/app/Notifications/Admin/EntityViewedNotification.php index e61e64057b4c..9dfe340727c8 100644 --- a/app/Notifications/Admin/EntityViewedNotification.php +++ b/app/Notifications/Admin/EntityViewedNotification.php @@ -68,6 +68,8 @@ class EntityViewedNotification extends Notification implements ShouldQueue */ public function toMail($notifiable) { + //@TODO THESE ARE @DEPRECATED NOW we are now using app/Mail/Admin/* + $data = $this->buildDataArray(); $subject = $this->buildSubject(); diff --git a/app/Notifications/Admin/InvoiceSentNotification.php b/app/Notifications/Admin/InvoiceSentNotification.php index fa2e9cdcacc6..245d4f6d852a 100644 --- a/app/Notifications/Admin/InvoiceSentNotification.php +++ b/app/Notifications/Admin/InvoiceSentNotification.php @@ -63,6 +63,9 @@ class InvoiceSentNotification extends Notification implements ShouldQueue */ public function toMail($notifiable) { + //@TODO THESE ARE @DEPRECATED NOW we are now using app/Mail/Admin/* + + $amount = Number::formatMoney($this->invoice->amount, $this->invoice->client); $subject = ctrans( 'texts.notification_invoice_sent_subject', diff --git a/app/Notifications/Admin/InvoiceViewedNotification.php b/app/Notifications/Admin/InvoiceViewedNotification.php index 75ac346da48b..5bc49983f311 100644 --- a/app/Notifications/Admin/InvoiceViewedNotification.php +++ b/app/Notifications/Admin/InvoiceViewedNotification.php @@ -62,6 +62,10 @@ class InvoiceViewedNotification extends Notification implements ShouldQueue */ public function toMail($notifiable) { + + //@TODO THESE ARE @DEPRECATED NOW we are now using app/Mail/Admin/* + + $amount = Number::formatMoney($this->invoice->amount, $this->invoice->client); $subject = ctrans( 'texts.notification_invoice_viewed_subject', diff --git a/app/Notifications/Admin/NewPartialPaymentNotification.php b/app/Notifications/Admin/NewPartialPaymentNotification.php index b088267ef0b8..5298eb5ce921 100644 --- a/app/Notifications/Admin/NewPartialPaymentNotification.php +++ b/app/Notifications/Admin/NewPartialPaymentNotification.php @@ -58,6 +58,9 @@ class NewPartialPaymentNotification extends Notification implements ShouldQueue */ public function toMail($notifiable) { + //@TODO THESE ARE @DEPRECATED NOW we are now using app/Mail/Admin/* + + $amount = Number::formatMoney($this->payment->amount, $this->payment->client); $invoice_texts = ctrans('texts.invoice_number_short'); diff --git a/app/Notifications/Admin/NewPaymentNotification.php b/app/Notifications/Admin/NewPaymentNotification.php index 6069e7cafca7..ca0c77f368fa 100644 --- a/app/Notifications/Admin/NewPaymentNotification.php +++ b/app/Notifications/Admin/NewPaymentNotification.php @@ -61,6 +61,9 @@ class NewPaymentNotification extends Notification implements ShouldQueue */ public function toMail($notifiable) { + //@TODO THESE ARE @DEPRECATED NOW we are now using app/Mail/Admin/* + + $amount = Number::formatMoney($this->payment->amount, $this->payment->client); $invoice_texts = ctrans('texts.invoice_number_short'); diff --git a/app/PaymentDrivers/PayPalExpressPaymentDriver.php b/app/PaymentDrivers/PayPalExpressPaymentDriver.php index 82b7ea5d6786..7bc3273f171d 100644 --- a/app/PaymentDrivers/PayPalExpressPaymentDriver.php +++ b/app/PaymentDrivers/PayPalExpressPaymentDriver.php @@ -13,7 +13,7 @@ namespace App\PaymentDrivers; use App\Events\Payment\PaymentWasCreated; -//use App\Jobs\Invoice\UpdateInvoicePayment; +use App\Jobs\Mail\PaymentFailureMailer; use App\Jobs\Util\SystemLogger; use App\Models\ClientGatewayToken; use App\Models\GatewayType; @@ -140,6 +140,9 @@ class PayPalExpressPaymentDriver extends BasePaymentDriver $this->client ); } elseif (!$response->isSuccessful()) { + + PaymentFailureMailer::dispatch($this->client, $response->getMessage, $this->client->company, $response['PAYMENTINFO_0_AMT']); + SystemLogger::dispatch( [ 'data' => $request->all(), @@ -271,12 +274,12 @@ class PayPalExpressPaymentDriver extends BasePaymentDriver return $payment; } - public function refund(Payment $payment, $amount = null) + public function refund(Payment $payment, $amount) { $this->gateway(); $response = $this->gateway - ->refund(['transactionReference' => $payment->transaction_reference, 'amount' => $amount ?? $payment->amount]) + ->refund(['transactionReference' => $payment->transaction_reference, 'amount' => $amount]) ->send(); if ($response->isSuccessful()) { @@ -305,6 +308,7 @@ class PayPalExpressPaymentDriver extends BasePaymentDriver $this->client ); + return false; } } diff --git a/app/Policies/PaymentTermPolicy.php b/app/Policies/PaymentTermPolicy.php new file mode 100644 index 000000000000..8e9fc0c230d1 --- /dev/null +++ b/app/Policies/PaymentTermPolicy.php @@ -0,0 +1,33 @@ +isAdmin() || $user->hasPermission('create_all'); + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index bff130dd116c..256a3daa91c6 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -22,6 +22,7 @@ use App\Models\Expense; use App\Models\GroupSetting; use App\Models\Invoice; use App\Models\Payment; +use App\Models\PaymentTerm; use App\Models\Product; use App\Models\Quote; use App\Models\RecurringInvoice; @@ -41,6 +42,7 @@ use App\Policies\ExpensePolicy; use App\Policies\GroupSettingPolicy; use App\Policies\InvoicePolicy; use App\Policies\PaymentPolicy; +use App\Policies\PaymentTermPolicy; use App\Policies\ProductPolicy; use App\Policies\QuotePolicy; use App\Policies\RecurringInvoicePolicy; @@ -72,6 +74,7 @@ class AuthServiceProvider extends ServiceProvider GroupSetting::class => GroupSettingPolicy::class, Invoice::class => InvoicePolicy::class, Payment::class => PaymentPolicy::class, + PaymentTerm::class => PaymentTermPolicy::class, Product::class => ProductPolicy::class, Quote::class => QuotePolicy::class, RecurringInvoice::class => RecurringInvoicePolicy::class, diff --git a/app/Repositories/PaymentTermRepository.php b/app/Repositories/PaymentTermRepository.php new file mode 100644 index 000000000000..e106b87d9392 --- /dev/null +++ b/app/Repositories/PaymentTermRepository.php @@ -0,0 +1,20 @@ + (string) $this->encodePrimaryKey($payment_term->id), 'num_days' => (int) $payment_term->num_days, 'name' => (string) ctrans('texts.payment_terms_net') . ' ' . $payment_term->getNumDays(), - 'created_at' => (int)$payment_term->created_at, - 'updated_at' => (int)$payment_term->updated_at, - 'archived_at' => (int)$payment_term->deleted_at, + 'is_deleted' => (bool) $payment_term->is_deleted, + 'created_at' => (int) $payment_term->created_at, + 'updated_at' => (int) $payment_term->updated_at, + 'archived_at' => (int) $payment_term->deleted_at, ]; } 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 ceaf2564c25e..a4e3aa9b5300 100644 --- a/database/migrations/2014_10_13_000000_create_users_table.php +++ b/database/migrations/2014_10_13_000000_create_users_table.php @@ -1028,6 +1028,7 @@ class CreateUsersTable extends Migration $table->string('name')->nullable(); $table->unsignedInteger('company_id')->nullable(); $table->unsignedInteger('user_id')->nullable(); + $table->boolean('is_deleted')->default(0); $table->timestamps(6); $table->softDeletes('deleted_at', 6); diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 01c1ee02b191..760d4022d916 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -3205,4 +3205,7 @@ return [ 'to_view_entity_password' => 'To view the :entity you need to enter password.', 'showing_x_of' => 'Showing :first to :last out of :total results', 'no_results' => 'No results found.', + + 'payment_failed_subject' => 'Payment failed for Client :client', + 'payment_failed_body' => 'A payment made by client :client failed with message :message', ]; diff --git a/routes/api.php b/routes/api.php index 2b38c22f3661..b88743d8309c 100644 --- a/routes/api.php +++ b/routes/api.php @@ -73,6 +73,8 @@ Route::group(['middleware' => ['api_db', 'token_auth', 'locale'], 'prefix' => 'a Route::resource('payment_terms', 'PaymentTermController');// name = (payments. index / create / show / update / destroy / edit + Route::post('payment_terms/bulk', 'PaymentTermController@bulk')->name('payment_terms.bulk'); + Route::resource('payments', 'PaymentController');// name = (payments. index / create / show / update / destroy / edit Route::post('payments/refund', 'PaymentController@refund')->name('payments.refund'); diff --git a/tests/Unit/CollectionMergingTest.php b/tests/Unit/CollectionMergingTest.php index ec29983786a0..e397b30936f7 100644 --- a/tests/Unit/CollectionMergingTest.php +++ b/tests/Unit/CollectionMergingTest.php @@ -20,63 +20,6 @@ class CollectionMergingTest extends TestCase Session::start(); - $this->setCurrentCompanyId(1); - - $this->terms = PaymentTerm::all(); - } - - public function testBlankCollectionReturned() - { - $this->assertEquals($this->terms->count(), 8); - } - - public function testMergingCollection() - { - $payment_terms = collect(config('ninja.payment_terms')); - - $new_terms = $this->terms->map(function ($term) { - return $term['num_days']; - }); - - $payment_terms->merge($new_terms); - - $this->assertEquals($payment_terms->count(), 8); - } - - public function testSortingCollection() - { - $payment_terms = collect(config('ninja.payment_terms')); - - $new_terms = $this->terms->map(function ($term) { - return $term['num_days']; - }); - - $payment_terms->merge($new_terms) - ->sortBy('num_days') - ->values() - ->all(); - - $term = $payment_terms->first(); - - $this->assertEquals($term['num_days'], 0); - } - - public function testSortingCollectionLast() - { - $payment_terms = collect(config('ninja.payment_terms')); - - $new_terms = $this->terms->map(function ($term) { - return $term['num_days']; - }); - - $payment_terms->merge($new_terms) - ->sortBy('num_days') - ->values() - ->all(); - - $term = $payment_terms->last(); - - $this->assertEquals($term['num_days'], 90); } public function testUniqueValues()