Bulk actions for Payment Terms (#3752)

* Fixes for tests

* payment terms policies and repo

* Bulk actions for payment terms

* Fixes for documentation

* working on payment failure mailer
This commit is contained in:
David Bomba 2020-05-26 18:20:50 +10:00 committed by GitHub
parent 107342e9e6
commit c339c25d9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 455 additions and 66 deletions

View File

@ -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)));
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace App\Jobs\Mail;
use App\Jobs\Util\SystemLogger;
use App\Libraries\Google\Google;
use App\Libraries\MultiDB;
use App\Mail\Admin\EntityNotificationMailer;
use App\Mail\Admin\EntityPaidObject;
use App\Mail\Admin\EntitySentObject;
use App\Models\SystemLog;
use App\Models\User;
use App\Providers\MailServiceProvider;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Mail;
class EntityPaidMailer extends BaseMailerJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $company;
public $user;
public $payment;
public $entity_type;
public $entity;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($payment, $user, $company)
{
$this->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
);
}
}

View File

@ -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
);
}

View File

@ -0,0 +1,91 @@
<?php
namespace App\Jobs\Mail;
use App\Jobs\Util\SystemLogger;
use App\Libraries\Google\Google;
use App\Libraries\MultiDB;
use App\Mail\Admin\EntityNotificationMailer;
use App\Mail\Admin\EntityPaidObject;
use App\Mail\Admin\EntitySentObject;
use App\Mail\Admin\PaymentFailureObject;
use App\Models\SystemLog;
use App\Models\User;
use App\Providers\MailServiceProvider;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Mail;
class PaymentFailureMailer extends BaseMailerJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $client;
public $message;
public $company;
public $amount;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($client, $message, $company, $amount)
{
$this->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
);
}
}

View File

@ -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);

View File

@ -0,0 +1,83 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Mail\Admin;
use App\Models\User;
use App\Utils\Number;
class PaymentFailureObject
{
public $client;
public $message;
public $company;
public $amount;
public function __construct($client, $message, $amount, $company)
{
$this->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;
}
}

View File

@ -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",

View File

@ -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();

View File

@ -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',

View File

@ -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',

View File

@ -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');

View File

@ -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');

View File

@ -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;
}
}

View File

@ -0,0 +1,33 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Policies;
use App\Models\Payment;
use App\Models\User;
/**
* Class PaymentTermPolicy
* @package App\Policies
*/
class PaymentTermPolicy extends EntityPolicy
{
/**
* Checks if the user has create permissions
*
* @param User $user
* @return bool
*/
public function create(User $user) : bool
{
return $user->isAdmin() || $user->hasPermission('create_all');
}
}

View File

@ -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,

View File

@ -0,0 +1,20 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Repositories;
/**
* Class for payment term repository.
*/
class PaymentTermRepository extends BaseRepository
{
}

View File

@ -23,9 +23,10 @@ class PaymentTermTransformer extends EntityTransformer
'id' => (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,
];
}

View File

@ -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);

View File

@ -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',
];

View File

@ -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');

View File

@ -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()