Support refunds from the admin UI

This commit is contained in:
Joshua Dwire 2016-04-23 16:40:19 -04:00
parent f9c36fd761
commit b8170f0324
22 changed files with 457 additions and 20 deletions

View File

@ -0,0 +1,25 @@
<?php namespace App\Events;
use App\Events\Event;
use Illuminate\Queue\SerializesModels;
class PaymentWasRefunded extends Event {
use SerializesModels;
public $payment;
public $refundAmount;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($payment, $refundAmount)
{
$this->payment = $payment;
$this->refundAmount = $refundAmount;
}
}

View File

@ -182,14 +182,7 @@ class AccountController extends BaseController
if ($account->company->payment) { if ($account->company->payment) {
$payment = $account->company->payment; $payment = $account->company->payment;
$this->paymentService->refund($payment);
$gateway = $this->paymentService->createGateway($payment->account_gateway);
$refund = $gateway->refund(array(
'transactionReference' => $payment->transaction_reference,
'amount' => $payment->amount
));
$refund->send();
$payment->delete();
Session::flash('message', trans('texts.plan_refunded')); Session::flash('message', trans('texts.plan_refunded'));
\Log::info("Refunded Plan Payment: {$account->name} - {$user->email}"); \Log::info("Refunded Plan Payment: {$account->name} - {$user->email}");
} else { } else {

View File

@ -57,6 +57,7 @@ class PaymentController extends BaseController
'method', 'method',
'payment_amount', 'payment_amount',
'payment_date', 'payment_date',
'status',
'' ''
]), ]),
)); ));
@ -630,11 +631,12 @@ class PaymentController extends BaseController
public function bulk() public function bulk()
{ {
$action = Input::get('action'); $action = Input::get('action');
$amount = Input::get('amount');
$ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids'); $ids = Input::get('public_id') ? Input::get('public_id') : Input::get('ids');
$count = $this->paymentService->bulk($ids, $action); $count = $this->paymentService->bulk($ids, $action, array('amount'=>$amount));
if ($count > 0) { if ($count > 0) {
$message = Utils::pluralize($action.'d_payment', $count); $message = Utils::pluralize($action=='refund'?'refunded_payment':$action.'d_payment', $count);
Session::flash('message', $message); Session::flash('message', $message);
} }

View File

@ -252,6 +252,9 @@ class PublicClientController extends BaseController
'invoice' => trans('texts.invoice') . ' ' . $model->invoice, 'invoice' => trans('texts.invoice') . ' ' . $model->invoice,
'contact' => Utils::getClientDisplayName($model), 'contact' => Utils::getClientDisplayName($model),
'payment' => trans('texts.payment') . ($model->payment ? ' ' . $model->payment : ''), 'payment' => trans('texts.payment') . ($model->payment ? ' ' . $model->payment : ''),
'credit' => $model->payment_amount ? Utils::formatMoney($model->credit, $model->currency_id, $model->country_id) : '',
'payment_amount' => $model->payment_amount ? Utils::formatMoney($model->payment_amount, $model->currency_id, $model->country_id) : null,
'adjustment' => $model->adjustment ? Utils::formatMoney($model->adjustment, $model->currency_id, $model->country_id) : null,
]; ];
return trans("texts.activity_{$model->activity_type_id}", $data); return trans("texts.activity_{$model->activity_type_id}", $data);
@ -321,7 +324,7 @@ class PublicClientController extends BaseController
'clientFontUrl' => $account->getFontsUrl(), 'clientFontUrl' => $account->getFontsUrl(),
'entityType' => ENTITY_PAYMENT, 'entityType' => ENTITY_PAYMENT,
'title' => trans('texts.payments'), 'title' => trans('texts.payments'),
'columns' => Utils::trans(['invoice', 'transaction_reference', 'method', 'payment_amount', 'payment_date']) 'columns' => Utils::trans(['invoice', 'transaction_reference', 'method', 'payment_amount', 'payment_date', 'status'])
]; ];
return response()->view('public_list', $data); return response()->view('public_list', $data);
@ -340,8 +343,36 @@ class PublicClientController extends BaseController
->addColumn('payment_type', function ($model) { return $model->payment_type ? $model->payment_type : ($model->account_gateway_id ? '<i>Online payment</i>' : ''); }) ->addColumn('payment_type', function ($model) { return $model->payment_type ? $model->payment_type : ($model->account_gateway_id ? '<i>Online payment</i>' : ''); })
->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id); }) ->addColumn('amount', function ($model) { return Utils::formatMoney($model->amount, $model->currency_id, $model->country_id); })
->addColumn('payment_date', function ($model) { return Utils::dateToString($model->payment_date); }) ->addColumn('payment_date', function ($model) { return Utils::dateToString($model->payment_date); })
->addColumn('status', function ($model) { return $this->getPaymentStatusLabel($model); })
->make(); ->make();
} }
private function getPaymentStatusLabel($model)
{
$label = trans("texts.status_" . strtolower($model->payment_status_name));
$class = 'default';
switch ($model->payment_status_id) {
case PAYMENT_STATUS_PENDING:
$class = 'info';
break;
case PAYMENT_STATUS_COMPLETED:
$class = 'success';
break;
case PAYMENT_STATUS_FAILED:
$class = 'danger';
break;
case PAYMENT_STATUS_PARTIALLY_REFUNDED:
$label = trans('texts.status_partially_refunded_amount', [
'amount' => Utils::formatMoney($model->refunded, $model->currency_id, $model->country_id),
]);
$class = 'primary';
break;
case PAYMENT_STATUS_REFUNDED:
$class = 'default';
break;
}
return "<h4><div class=\"label label-{$class}\">$label</div></h4>";
}
public function quoteIndex() public function quoteIndex()
{ {

View File

@ -398,6 +398,7 @@ if (!defined('CONTACT_EMAIL')) {
//define('ACTIVITY_TYPE_UPDATE_PAYMENT', 11); //define('ACTIVITY_TYPE_UPDATE_PAYMENT', 11);
define('ACTIVITY_TYPE_ARCHIVE_PAYMENT', 12); define('ACTIVITY_TYPE_ARCHIVE_PAYMENT', 12);
define('ACTIVITY_TYPE_DELETE_PAYMENT', 13); define('ACTIVITY_TYPE_DELETE_PAYMENT', 13);
define('ACTIVITY_TYPE_REFUNDED_PAYMENT', 39);
define('ACTIVITY_TYPE_CREATE_CREDIT', 14); define('ACTIVITY_TYPE_CREATE_CREDIT', 14);
//define('ACTIVITY_TYPE_UPDATE_CREDIT', 15); //define('ACTIVITY_TYPE_UPDATE_CREDIT', 15);
@ -474,6 +475,12 @@ if (!defined('CONTACT_EMAIL')) {
define('INVOICE_STATUS_APPROVED', 4); define('INVOICE_STATUS_APPROVED', 4);
define('INVOICE_STATUS_PARTIAL', 5); define('INVOICE_STATUS_PARTIAL', 5);
define('INVOICE_STATUS_PAID', 6); define('INVOICE_STATUS_PAID', 6);
define('PAYMENT_STATUS_PENDING', 1);
define('PAYMENT_STATUS_FAILED', 2);
define('PAYMENT_STATUS_COMPLETED', 3);
define('PAYMENT_STATUS_PARTIALLY_REFUNDED', 4);
define('PAYMENT_STATUS_REFUNDED', 5);
define('PAYMENT_TYPE_CREDIT', 1); define('PAYMENT_TYPE_CREDIT', 1);
define('CUSTOM_DESIGN', 11); define('CUSTOM_DESIGN', 11);

View File

@ -22,6 +22,7 @@ use App\Events\QuoteInvitationWasViewed;
use App\Events\QuoteInvitationWasApproved; use App\Events\QuoteInvitationWasApproved;
use App\Events\PaymentWasCreated; use App\Events\PaymentWasCreated;
use App\Events\PaymentWasDeleted; use App\Events\PaymentWasDeleted;
use App\Events\PaymentWasRefunded;
use App\Events\PaymentWasArchived; use App\Events\PaymentWasArchived;
use App\Events\PaymentWasRestored; use App\Events\PaymentWasRestored;
use App\Events\CreditWasCreated; use App\Events\CreditWasCreated;
@ -309,6 +310,18 @@ class ActivityListener
); );
} }
public function refundedPayment(PaymentWasRefunded $event)
{
$payment = $event->payment;
$this->activityRepo->create(
$payment,
ACTIVITY_TYPE_REFUNDED_PAYMENT,
$event->refundAmount,
$event->refundAmount * -1
);
}
public function archivedPayment(PaymentWasArchived $event) public function archivedPayment(PaymentWasArchived $event)
{ {
if ($event->payment->is_deleted) { if ($event->payment->is_deleted) {

View File

@ -3,6 +3,7 @@
use Carbon; use Carbon;
use App\Models\Credit; use App\Models\Credit;
use App\Events\PaymentWasDeleted; use App\Events\PaymentWasDeleted;
use App\Events\PaymentWasRefunded;
use App\Ninja\Repositories\CreditRepository; use App\Ninja\Repositories\CreditRepository;
class CreditListener class CreditListener
@ -26,7 +27,24 @@ class CreditListener
$credit = Credit::createNew(); $credit = Credit::createNew();
$credit->client_id = $payment->client_id; $credit->client_id = $payment->client_id;
$credit->credit_date = Carbon::now()->toDateTimeString(); $credit->credit_date = Carbon::now()->toDateTimeString();
$credit->balance = $credit->amount = $payment->amount; $credit->balance = $credit->amount = $payment->amount - $payment->refunded;
$credit->private_notes = $payment->transaction_reference;
$credit->save();
}
public function refundedPayment(PaymentWasRefunded $event)
{
$payment = $event->payment;
// if the payment was from a credit we need to refund the credit
if ($payment->payment_type_id != PAYMENT_TYPE_CREDIT) {
return;
}
$credit = Credit::createNew();
$credit->client_id = $payment->client_id;
$credit->credit_date = Carbon::now()->toDateTimeString();
$credit->balance = $credit->amount = $event->refundAmount;
$credit->private_notes = $payment->transaction_reference; $credit->private_notes = $payment->transaction_reference;
$credit->save(); $credit->save();
} }

View File

@ -7,6 +7,7 @@ use App\Events\InvoiceWasUpdated;
use App\Events\InvoiceWasCreated; use App\Events\InvoiceWasCreated;
use App\Events\PaymentWasCreated; use App\Events\PaymentWasCreated;
use App\Events\PaymentWasDeleted; use App\Events\PaymentWasDeleted;
use App\Events\PaymentWasRefunded;
use App\Events\PaymentWasRestored; use App\Events\PaymentWasRestored;
use App\Events\InvoiceInvitationWasViewed; use App\Events\InvoiceInvitationWasViewed;
@ -58,7 +59,17 @@ class InvoiceListener
{ {
$payment = $event->payment; $payment = $event->payment;
$invoice = $payment->invoice; $invoice = $payment->invoice;
$adjustment = $payment->amount; $adjustment = $payment->amount - $payment->refunded;
$invoice->updateBalances($adjustment);
$invoice->updatePaidStatus();
}
public function refundedPayment(PaymentWasRefunded $event)
{
$payment = $event->payment;
$invoice = $payment->invoice;
$adjustment = $event->refundAmount;
$invoice->updateBalances($adjustment); $invoice->updateBalances($adjustment);
$invoice->updatePaidStatus(); $invoice->updatePaidStatus();
@ -72,7 +83,7 @@ class InvoiceListener
$payment = $event->payment; $payment = $event->payment;
$invoice = $payment->invoice; $invoice = $payment->invoice;
$adjustment = $payment->amount * -1; $adjustment = ($payment->amount - $payment->refunded) * -1;
$invoice->updateBalances($adjustment); $invoice->updateBalances($adjustment);
$invoice->updatePaidStatus(); $invoice->updatePaidStatus();

View File

@ -70,6 +70,8 @@ class Activity extends Eloquent
'quote' => $invoice ? link_to($invoice->getRoute(), $invoice->getDisplayName()) : null, 'quote' => $invoice ? link_to($invoice->getRoute(), $invoice->getDisplayName()) : null,
'contact' => $contactId ? $client->getDisplayName() : $user->getDisplayName(), 'contact' => $contactId ? $client->getDisplayName() : $user->getDisplayName(),
'payment' => $payment ? $payment->transaction_reference : null, 'payment' => $payment ? $payment->transaction_reference : null,
'payment_amount' => $payment ? $account->formatMoney($payment->amount, $payment) : null,
'adjustment' => $this->adjustment ? $account->formatMoney($this->adjustment, $this) : asdf,
'credit' => $credit ? $account->formatMoney($credit->amount, $client) : null, 'credit' => $credit ? $account->formatMoney($credit->amount, $client) : null,
]; ];

View File

@ -1,7 +1,9 @@
<?php namespace App\Models; <?php namespace App\Models;
use Event;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
use App\Events\PaymentWasCreated; use App\Events\PaymentWasCreated;
use App\Events\PaymentWasRefunded;
use Laracasts\Presenter\PresentableTrait; use Laracasts\Presenter\PresentableTrait;
class Payment extends EntityModel class Payment extends EntityModel
@ -50,6 +52,11 @@ class Payment extends EntityModel
public function payment_type() public function payment_type()
{ {
return $this->belongsTo('App\Models\PaymentType'); return $this->belongsTo('App\Models\PaymentType');
}
public function payment_status()
{
return $this->belongsTo('App\Models\PaymentStatus');
} }
public function getRoute() public function getRoute()
@ -69,6 +76,51 @@ class Payment extends EntityModel
return trim("payment {$this->transaction_reference}"); return trim("payment {$this->transaction_reference}");
} }
public function isPending()
{
return $this->payment_status_id = PAYMENT_STATUS_PENDING;
}
public function isFailed()
{
return $this->payment_status_id = PAYMENT_STATUS_FAILED;
}
public function isCompleted()
{
return $this->payment_status_id >= PAYMENT_STATUS_COMPLETED;
}
public function isPartiallyRefunded()
{
return $this->payment_status_id == PAYMENT_STATUS_PARTIALLY_REFUNDED;
}
public function isRefunded()
{
return $this->payment_status_id == PAYMENT_STATUS_REFUNDED;
}
public function recordRefund($amount = null)
{
if (!$this->isRefunded()) {
if (!$amount) {
$amount = $this->amount;
}
$new_refund = min($this->amount, $this->refunded + $amount);
$refund_change = $new_refund - $this->refunded;
if ($refund_change) {
$this->refunded = $new_refund;
$this->payment_status_id = $this->refunded == $this->amount ? PAYMENT_STATUS_REFUNDED : PAYMENT_STATUS_PARTIALLY_REFUNDED;
$this->save();
Event::fire(new PaymentWasRefunded($this, $refund_change));
}
}
}
public function getEntityType() public function getEntityType()
{ {
return ENTITY_PAYMENT; return ENTITY_PAYMENT;

View File

@ -0,0 +1,8 @@
<?php namespace App\Models;
use Eloquent;
class PaymentStatus extends Eloquent
{
public $timestamps = false;
}

View File

@ -97,6 +97,7 @@ class ActivityRepository
'contacts.last_name as last_name', 'contacts.last_name as last_name',
'contacts.email as email', 'contacts.email as email',
'payments.transaction_reference as payment', 'payments.transaction_reference as payment',
'payments.amount as payment_amount',
'credits.amount as credit' 'credits.amount as credit'
); );
} }

View File

@ -22,6 +22,7 @@ class PaymentRepository extends BaseRepository
->join('clients', 'clients.id', '=', 'payments.client_id') ->join('clients', 'clients.id', '=', 'payments.client_id')
->join('invoices', 'invoices.id', '=', 'payments.invoice_id') ->join('invoices', 'invoices.id', '=', 'payments.invoice_id')
->join('contacts', 'contacts.client_id', '=', 'clients.id') ->join('contacts', 'contacts.client_id', '=', 'clients.id')
->join('payment_statuses', 'payment_statuses.id', '=', 'payments.payment_status_id')
->leftJoin('payment_types', 'payment_types.id', '=', 'payments.payment_type_id') ->leftJoin('payment_types', 'payment_types.id', '=', 'payments.payment_type_id')
->leftJoin('account_gateways', 'account_gateways.id', '=', 'payments.account_gateway_id') ->leftJoin('account_gateways', 'account_gateways.id', '=', 'payments.account_gateway_id')
->leftJoin('gateways', 'gateways.id', '=', 'account_gateways.gateway_id') ->leftJoin('gateways', 'gateways.id', '=', 'account_gateways.gateway_id')
@ -39,6 +40,8 @@ class PaymentRepository extends BaseRepository
'clients.user_id as client_user_id', 'clients.user_id as client_user_id',
'payments.amount', 'payments.amount',
'payments.payment_date', 'payments.payment_date',
'payments.payment_status_id',
'payments.payment_type_id',
'invoices.public_id as invoice_public_id', 'invoices.public_id as invoice_public_id',
'invoices.user_id as invoice_user_id', 'invoices.user_id as invoice_user_id',
'invoices.invoice_number', 'invoices.invoice_number',
@ -50,8 +53,11 @@ class PaymentRepository extends BaseRepository
'payments.deleted_at', 'payments.deleted_at',
'payments.is_deleted', 'payments.is_deleted',
'payments.user_id', 'payments.user_id',
'payments.refunded',
'invoices.is_deleted as invoice_is_deleted', 'invoices.is_deleted as invoice_is_deleted',
'gateways.name as gateway_name' 'gateways.name as gateway_name',
'gateways.id as gateway_id',
'payment_statuses.name as payment_status_name'
); );
if (!\Session::get('show_trash:payment')) { if (!\Session::get('show_trash:payment')) {
@ -78,6 +84,7 @@ class PaymentRepository extends BaseRepository
->join('clients', 'clients.id', '=', 'payments.client_id') ->join('clients', 'clients.id', '=', 'payments.client_id')
->join('invoices', 'invoices.id', '=', 'payments.invoice_id') ->join('invoices', 'invoices.id', '=', 'payments.invoice_id')
->join('contacts', 'contacts.client_id', '=', 'clients.id') ->join('contacts', 'contacts.client_id', '=', 'clients.id')
->join('payment_statuses', 'payment_statuses.id', '=', 'payments.payment_status_id')
->leftJoin('invitations', function ($join) { ->leftJoin('invitations', function ($join) {
$join->on('invitations.invoice_id', '=', 'invoices.id') $join->on('invitations.invoice_id', '=', 'invoices.id')
->on('invitations.contact_id', '=', 'contacts.id'); ->on('invitations.contact_id', '=', 'contacts.id');
@ -104,7 +111,10 @@ class PaymentRepository extends BaseRepository
'contacts.last_name', 'contacts.last_name',
'contacts.email', 'contacts.email',
'payment_types.name as payment_type', 'payment_types.name as payment_type',
'payments.account_gateway_id' 'payments.account_gateway_id',
'payments.refunded',
'payments.payment_status_id',
'payment_statuses.name as payment_status_name'
); );
if ($filter) { if ($filter) {
@ -189,6 +199,4 @@ class PaymentRepository extends BaseRepository
parent::restore($payment); parent::restore($payment);
} }
} }

View File

@ -107,6 +107,11 @@ class EventServiceProvider extends ServiceProvider {
'App\Listeners\InvoiceListener@deletedPayment', 'App\Listeners\InvoiceListener@deletedPayment',
'App\Listeners\CreditListener@deletedPayment', 'App\Listeners\CreditListener@deletedPayment',
], ],
'App\Events\PaymentWasRefunded' => [
'App\Listeners\ActivityListener@refundedPayment',
'App\Listeners\InvoiceListener@refundedPayment',
'App\Listeners\CreditListener@refundedPayment',
],
'App\Events\PaymentWasRestored' => [ 'App\Events\PaymentWasRestored' => [
'App\Listeners\ActivityListener@restoredPayment', 'App\Listeners\ActivityListener@restoredPayment',
'App\Listeners\InvoiceListener@restoredPayment', 'App\Listeners\InvoiceListener@restoredPayment',

View File

@ -44,7 +44,9 @@ class ActivityService extends BaseService
'quote' => $model->invoice ? link_to('/quotes/' . $model->invoice_public_id, $model->invoice)->toHtml() : null, 'quote' => $model->invoice ? link_to('/quotes/' . $model->invoice_public_id, $model->invoice)->toHtml() : null,
'contact' => $model->contact_id ? link_to('/clients/' . $model->client_public_id, Utils::getClientDisplayName($model))->toHtml() : Utils::getPersonDisplayName($model->user_first_name, $model->user_last_name, $model->user_email), 'contact' => $model->contact_id ? link_to('/clients/' . $model->client_public_id, Utils::getClientDisplayName($model))->toHtml() : Utils::getPersonDisplayName($model->user_first_name, $model->user_last_name, $model->user_email),
'payment' => $model->payment ?: '', 'payment' => $model->payment ?: '',
'credit' => Utils::formatMoney($model->credit, $model->currency_id, $model->country_id) 'credit' => $model->payment_amount ? Utils::formatMoney($model->credit, $model->currency_id, $model->country_id) : '',
'payment_amount' => $model->payment_amount ? Utils::formatMoney($model->payment_amount, $model->currency_id, $model->country_id) : null,
'adjustment' => $model->adjustment ? Utils::formatMoney($model->adjustment, $model->currency_id, $model->country_id) : null
]; ];
return trans("texts.activity_{$model->activity_type_id}", $data); return trans("texts.activity_{$model->activity_type_id}", $data);

View File

@ -23,6 +23,10 @@ class PaymentService extends BaseService
{ {
public $lastError; public $lastError;
protected $datatableService; protected $datatableService;
protected static $refundableGateways = array(
GATEWAY_STRIPE
);
public function __construct(PaymentRepository $paymentRepo, AccountRepository $accountRepo, DatatableService $datatableService) public function __construct(PaymentRepository $paymentRepo, AccountRepository $accountRepo, DatatableService $datatableService)
{ {
@ -375,6 +379,12 @@ class PaymentService extends BaseService
function ($model) { function ($model) {
return Utils::dateToString($model->payment_date); return Utils::dateToString($model->payment_date);
} }
],
[
'payment_status_name',
function ($model) use ($entityType) {
return self::getStatusLabel($entityType, $model);
}
] ]
]; ];
} }
@ -390,9 +400,103 @@ class PaymentService extends BaseService
function ($model) { function ($model) {
return Payment::canEditItem($model); return Payment::canEditItem($model);
} }
],
[
trans('texts.refund_payment'),
function ($model) {
$max_refund = number_format($model->amount - $model->refunded, 2);
$formatted = Utils::formatMoney($max_refund, $model->currency_id, $model->country_id);
$symbol = Utils::getFromCache($model->currency_id, 'currencies')->symbol;
return "javascript:showRefundModal({$model->public_id}, '{$max_refund}', '{$formatted}', '{$symbol}')";
},
function ($model) {
return Payment::canEditItem($model) && (
($model->transaction_reference && in_array($model->gateway_id , static::$refundableGateways))
|| $model->payment_type_id == PAYMENT_TYPE_CREDIT
);
}
] ]
]; ];
} }
public function bulk($ids, $action, $params = array())
{
if ($action == 'refund') {
if ( ! $ids ) {
return 0;
}
$payments = $this->getRepo()->findByPublicIdsWithTrashed($ids);
foreach ($payments as $payment) {
if($payment->canEdit()){
if(!empty($params['amount'])) {
$this->refund($payment, floatval($params['amount']));
} else {
$this->refund($payment);
}
}
}
return count($payments);
} else {
return parent::bulk($ids, $action);
}
}
private function getStatusLabel($entityType, $model)
{
$label = trans("texts.status_" . strtolower($model->payment_status_name));
$class = 'default';
switch ($model->payment_status_id) {
case PAYMENT_STATUS_PENDING:
$class = 'info';
break;
case PAYMENT_STATUS_COMPLETED:
$class = 'success';
break;
case PAYMENT_STATUS_FAILED:
$class = 'danger';
break;
case PAYMENT_STATUS_PARTIALLY_REFUNDED:
$label = trans('texts.status_partially_refunded_amount', [
'amount' => Utils::formatMoney($model->refunded, $model->currency_id, $model->country_id),
]);
$class = 'primary';
break;
case PAYMENT_STATUS_REFUNDED:
$class = 'default';
break;
}
return "<h4><div class=\"label label-{$class}\">$label</div></h4>";
}
public function refund($payment, $amount = null) {
if (!$amount) {
$amount = $payment->amount;
}
$amount = min($amount, $payment->amount - $payment->refunded);
if (!$amount) {
return;
}
if ($payment->payment_type_id != PAYMENT_TYPE_CREDIT) {
$accountGateway = $this->createGateway($payment->account_gateway);
$refund = $accountGateway->refund(array(
'transactionReference' => $payment->transaction_reference,
'amount' => $amount,
));
$response = $refund->send();
if ($response->isSuccessful()) {
$payment->recordRefund($amount);
} else {
$this->error('Unknown', $response->getMessage(), $accountGateway);
}
} else {
$payment->recordRefund($amount);
}
}
} }

View File

@ -0,0 +1,49 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class PaymentsChanges extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::dropIfExists('payment_statuses');
Schema::create('payment_statuses', function($table)
{
$table->increments('id');
$table->string('name');
});
(new \PaymentStatusSeeder())->run();
Schema::table('payments', function($table)
{
$table->decimal('refunded', 13, 2);
$table->unsignedInteger('payment_status_id')->default(3);
$table->foreign('payment_status_id')->references('id')->on('payment_statuses');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('payments', function($table)
{
$table->dropColumn('refunded');
$table->dropForeign('payments_payment_status_id_foreign');
$table->dropColumn('payment_status_id');
});
Schema::dropIfExists('payment_statuses');
}
}

View File

@ -19,6 +19,7 @@ class DatabaseSeeder extends Seeder
$this->call('FontsSeeder'); $this->call('FontsSeeder');
$this->call('BanksSeeder'); $this->call('BanksSeeder');
$this->call('InvoiceStatusSeeder'); $this->call('InvoiceStatusSeeder');
$this->call('PaymentStatusSeeder');
$this->call('CurrenciesSeeder'); $this->call('CurrenciesSeeder');
$this->call('DateFormatsSeeder'); $this->call('DateFormatsSeeder');
$this->call('InvoiceDesignsSeeder'); $this->call('InvoiceDesignsSeeder');

View File

@ -0,0 +1,36 @@
<?php
use App\Models\PaymentStatus;
class PaymentStatusSeeder extends Seeder
{
public function run()
{
Eloquent::unguard();
$this->createPaymentStatuses();
Eloquent::reguard();
}
private function createPaymentStatuses()
{
$statuses = [
['id' => '1', 'name' => 'Pending'],
['id' => '2', 'name' => 'Failed'],
['id' => '3', 'name' => 'Completed'],
['id' => '4', 'name' => 'Partially Refunded'],
['id' => '5', 'name' => 'Refunded'],
];
foreach ($statuses as $status) {
$record = PaymentStatus::find($status['id']);
if ($record) {
$record->name = $status['name'];
$record->save();
} else {
PaymentStatus::create($status);
}
}
}
}

View File

@ -15,6 +15,7 @@ class UpdateSeeder extends Seeder
$this->call('FontsSeeder'); $this->call('FontsSeeder');
$this->call('BanksSeeder'); $this->call('BanksSeeder');
$this->call('InvoiceStatusSeeder'); $this->call('InvoiceStatusSeeder');
$this->call('PaymentStatusSeeder');
$this->call('CurrenciesSeeder'); $this->call('CurrenciesSeeder');
$this->call('DateFormatsSeeder'); $this->call('DateFormatsSeeder');
$this->call('InvoiceDesignsSeeder'); $this->call('InvoiceDesignsSeeder');

View File

@ -1176,6 +1176,20 @@ $LANG = array(
'page_size' => 'Page Size', 'page_size' => 'Page Size',
'live_preview_disabled' => 'Live preview has been disabled to support selected font', 'live_preview_disabled' => 'Live preview has been disabled to support selected font',
'invoice_number_padding' => 'Padding', 'invoice_number_padding' => 'Padding',
// Payment updates
'refund_payment' => 'Refund Payment',
'refund_max' => 'Max:',
'refund' => 'Refund',
'are_you_sure_refund' => 'Refund selected payments?',
'status_pending' => 'Pending',
'status_completed' => 'Completed',
'status_failed' => 'Failed',
'status_partially_refunded' => 'Partially Refunded',
'status_partially_refunded_amount' => ':amount Refunded',
'status_refunded' => 'Refunded',
'refunded_payment' => 'Refunded Payment',
'activity_39' => ':user refunded :adjustment of a :payment_amount payment (:payment)',
); );

View File

@ -53,7 +53,41 @@
->setOptions('sPaginationType', 'bootstrap') ->setOptions('sPaginationType', 'bootstrap')
->setOptions('aaSorting', [[isset($sortCol) ? $sortCol : '1', 'desc']]) ->setOptions('aaSorting', [[isset($sortCol) ? $sortCol : '1', 'desc']])
->render('datatable') !!} ->render('datatable') !!}
@if ($entityType == ENTITY_PAYMENT)
<div class="modal fade" id="paymentRefundModal" tabindex="-1" role="dialog" aria-labelledby="paymentRefundModalLabel" aria-hidden="true">
<div class="modal-dialog" style="min-width:150px">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title" id="paymentRefundModalLabel">{{ trans('texts.refund_payment') }}</h4>
</div>
<div class="modal-body">
<div class="form-horizontal">
<div class="form-group">
<label for="refundAmount" class="col-sm-offset-2 col-sm-2 control-label">{{ trans('texts.amount') }}</label>
<div class="col-sm-4">
<div class="input-group">
<span class="input-group-addon" id="refundCurrencySymbol"></span>
<input type="number" class="form-control" id="refundAmount" name="amount" step="0.01" min="0.01" placeholder="{{ trans('texts.amount') }}">
</div>
<div class="help-block">{{ trans('texts.refund_max') }} <span id="refundMax"></span></div>
</div>
</div>
</div>
</div>
<div class="modal-footer" style="margin-top: 0px">
<button type="button" class="btn btn-default" data-dismiss="modal">{{ trans('texts.cancel') }}</button>
<button type="button" class="btn btn-primary" id="completeRefundButton">{{ trans('texts.refund') }}</button>
</div>
</div>
</div>
</div>
@endif
{!! Former::close() !!} {!! Former::close() !!}
<script type="text/javascript"> <script type="text/javascript">
@ -102,6 +136,22 @@
$('#public_id').val(id); $('#public_id').val(id);
submitForm('invoice'); submitForm('invoice');
} }
@if ($entityType == ENTITY_PAYMENT)
var paymentId = null;
function showRefundModal(id, amount, formatted, symbol){
paymentId = id;
$('#refundCurrencySymbol').text(symbol);
$('#refundMax').text(formatted);
$('#refundAmount').val(amount).attr('max', amount);
$('#paymentRefundModal').modal('show');
}
function handleRefundClicked(){
$('#public_id').val(paymentId);
submitForm('refund');
}
@endif
function setTrashVisible() { function setTrashVisible() {
var checked = $('#trashed').is(':checked'); var checked = $('#trashed').is(':checked');
@ -156,6 +206,10 @@
actionListHandler(); actionListHandler();
} }
@if ($entityType == ENTITY_PAYMENT)
$('#completeRefundButton').click(handleRefundClicked)
@endif
$('.archive, .invoice').prop('disabled', true); $('.archive, .invoice').prop('disabled', true);
$('.archive:not(.dropdown-toggle)').click(function() { $('.archive:not(.dropdown-toggle)').click(function() {