1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-05 18:52:44 +01:00

Payment Events

This commit is contained in:
David Bomba 2019-10-01 11:56:48 +10:00
parent 3721e2b7f1
commit 2d5d9b816b
13 changed files with 335 additions and 18 deletions

View File

@ -16,6 +16,7 @@ use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException as ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\Exceptions\ThrottleRequestsException;
use Illuminate\Support\Arr;
use Symfony\Component\Debug\Exception\FatalThrowableError;
class Handler extends ExceptionHandler
@ -99,7 +100,7 @@ class Handler extends ExceptionHandler
return response()->json(['error' => 'Unauthenticated.'], 401);
}
$guard = array_get($exception->guards(), 0);
$guard = Arr::get($exception->guards(), 0);
switch ($guard) {
case 'contact':

View File

@ -0,0 +1,123 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2019. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Listeners\Invoice;
use App\Jobs\Company\UpdateCompanyLedgerWithInvoice;
use App\Models\SystemLog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class UpdateInvoicePayment implements ShouldQueue
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle($event)
{
$payment = $event->payment;
$invoices = $payment->invoices();
$invoices_total = $invoices->sum('balance');
/* Simplest scenario*/
if($invoices_total == $payment->amount)
{
$invoices->each(function ($invoice){
$invoice->updateBalance($invoice->balance*-1);
UpdateCompanyLedgerWithInvoice::dispatchNow($invoice, ($invoice->balance*-1));
});
}
else {
$total = 0;
foreach($invoice as $invoice)
{
if($invoice->isPartial())
$total += $invoice->partial;
else
$total += $invoice->balance;
}
/* test if there is a batch of partial invoices that have been paid */
if($payment->amount == $total)
{
//process invoices and update balance depending on
//whether the invoice balance or partial amount was
//paid
}
else {
$data = [
'payment' => $payment,
'invoices' => $invoices,
'invoices_total' => $invoices_total,
'payment_amount' => $payment->amount,
'partial_check_amount' => $total,
];
$sl = [
'client_id' => $payment->client_id,
'user_id' => $payment->user_id,
'company_id' => $payment->company_id,
'log' => $data,
'category_id' => SystemLog::PAYMENT_RESPONSE,
'event_id' => SystemLog::PAYMENT_RECONCILIATION_FAILURE,
]
SystemLog::create($sl);
throw new Exception('payment amount does not match invoice totals');
}
}
}
}
/*
$payment = $event->payment;
$invoice = $payment->invoice;
$adjustment = $payment->amount * -1;
$partial = max(0, $invoice->partial - $payment->amount);
$invoice->updateBalances($adjustment, $partial);
$invoice->updatePaidStatus(true);
// store a backup of the invoice
$activity = Activity::wherePaymentId($payment->id)
->whereActivityTypeId(ACTIVITY_TYPE_CREATE_PAYMENT)
->first();
$activity->json_backup = $invoice->hidePrivateFields()->toJSON();
$activity->save();
if ($invoice->balance == 0 && $payment->account->auto_archive_invoice) {
$invoiceRepo = app('App\Ninja\Repositories\InvoiceRepository');
$invoiceRepo->archive($invoice);
}
*/

View File

@ -257,6 +257,7 @@ class Invoice extends BaseModel
}
/** TODO// DOCUMENT THIS FUNCTIONALITY */
public function pdf_url()
{
$public_path = 'storage/' . $this->client->client_hash . '/invoices/'. $this->invoice_number . '.pdf';
@ -269,4 +270,56 @@ class Invoice extends BaseModel
return $public_path;
}
/**
* @param bool $save
*/
public function updatePaidStatus($paid = false, $save = true) : bool
{
$status_id = false;
if ($paid && $this->balance == 0) {
$status_id = self::STATUS_PAID;
} elseif ($paid && $this->balance > 0 && $this->balance < $this->amount) {
$status_id = self::STATUS_PARTIAL;
} elseif ($this->isPartial() && $this->balance > 0) {
$status_id = ($this->balance == $this->amount ? self::STATUS_SENT : self::STATUS_PARTIAL);
}
if ($status_id && $status_id != $this->status_id) {
$this->status_id = $status_id;
if ($save) {
$this->save();
}
}
}
/**
* @return bool
*/
public function isPartial() : bool
{
return ($this->partial && $this->partial > 0) === true;
//return $this->status_id >= self::STATUS_PARTIAL;
}
/**
* @param float $balance_adjustment
*/
public function updateBalance($balance_adjustment)
{
if ($this->is_deleted)
return;
$balance_adjustment = floatval($balance_adjustment);
$this->balance = $this->balance + $balance_adjustment;
if($this->balance == 0)
$this->status_id = self::STATUS_PAID;
$this->save();
}
}

30
app/Models/SystemLog.php Normal file
View File

@ -0,0 +1,30 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com)
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2019. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://opensource.org/licenses/AAL
*/
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SystemLog extends Model
{
/* Category IDs */
const PAYMENT_RESPONSE = 1;
const GATEWAY_RESPONSE = 2;
/* Event IDs*/
const PAYMENT_RECONCILIATION_FAILURE = 10;
const PAYMENT_RECONCILIATION_SUCCESS = 11;
const GATEWAY_SUCCESS = 21;
const GATEWAY_FAILURE = 22;
const GATEWAY_ERROR = 23;
}

View File

@ -11,6 +11,7 @@
namespace App\Observers;
use App\Events\Payment\PaymentWasCreated;
use App\Models\Payment;
class PaymentObserver
@ -23,7 +24,7 @@ class PaymentObserver
*/
public function created(Payment $payment)
{
event(new PaymentWasCreated($payment));
}
/**

View File

@ -16,7 +16,9 @@ use App\Models\Client;
use App\Models\ClientContact;
use App\Models\CompanyGateway;
use App\Models\GatewayType;
use App\Models\Invoice;
use App\Models\Payment;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Omnipay\Omnipay;
@ -241,4 +243,17 @@ class BasePaymentDriver
return $payment;
}
public function attachInvoices(Payment $payment, $hashed_ids)
{
$invoices = Invoice::whereIn('id', $this->transformKeys(explode(",",$hashed_ids)))
->whereClientId($this->client->id)
->get();
$payment->invoices()->sync($invoices);
$payment->save();
return $payment;
}
}

View File

@ -13,10 +13,45 @@ namespace App\PaymentDrivers;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\Models\PaymentType;
use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request;
use Omnipay\Common\Item;
/**
* Response array
* (
'TOKEN' => 'EC-50V302605X606694D',
'SUCCESSPAGEREDIRECTREQUESTED' => 'false',
'TIMESTAMP' => '2019-09-30T22:21:21Z',
'CORRELATIONID' => '9e0da63193090',
'ACK' => 'SuccessWithWarning',
'VERSION' => '119.0',
'BUILD' => '53688488',
'L_ERRORCODE0' => '11607',
'L_SHORTMESSAGE0' => 'Duplicate Request',
'L_LONGMESSAGE0' => 'A successful transaction has already been completed for this token.',
'L_SEVERITYCODE0' => 'Warning',
'INSURANCEOPTIONSELECTED' => 'false',
'SHIPPINGOPTIONISDEFAULT' => 'false',
'PAYMENTINFO_0_TRANSACTIONID' => '5JE20141KL116573G',
'PAYMENTINFO_0_TRANSACTIONTYPE' => 'expresscheckout',
'PAYMENTINFO_0_PAYMENTTYPE' => 'instant',
'PAYMENTINFO_0_ORDERTIME' => '2019-09-30T22:20:57Z',
'PAYMENTINFO_0_AMT' => '31260.37',
'PAYMENTINFO_0_TAXAMT' => '0.00',
'PAYMENTINFO_0_CURRENCYCODE' => 'USD',
'PAYMENTINFO_0_EXCHANGERATE' => '0.692213615971749',
'PAYMENTINFO_0_PAYMENTSTATUS' => 'Pending',
'PAYMENTINFO_0_PENDINGREASON' => 'unilateral',
'PAYMENTINFO_0_REASONCODE' => 'None',
'PAYMENTINFO_0_PROTECTIONELIGIBILITY' => 'Ineligible',
'PAYMENTINFO_0_PROTECTIONELIGIBILITYTYPE' => 'None',
'PAYMENTINFO_0_ERRORCODE' => '0',
'PAYMENTINFO_0_ACK' => 'Success',
)
*/
class PayPalExpressPaymentDriver extends BasePaymentDriver
{
@ -61,20 +96,21 @@ class PayPalExpressPaymentDriver extends BasePaymentDriver
{
$response = $this->completePurchase($request->all());
\Log::error($request->all());
$transaction_reference = $response->getTransactionReference() ?: $request->input('token');
if ($response->isCancelled()) {
return false;
return redirect()->route('client.invoices.index')->with('warning',ctrans('texts.status_voided'));
} elseif (! $response->isSuccessful()) {
throw new Exception($response->getMessage());
}
//\Log::error(print_r($response,1));
//\Log::error(print_r($response->getData()));
\Log::error($response->getData());
//dd($response);
$payment = $this->createPayment($response->getData());
$this->attachInvoices($payment, $request->input('hashed_ids'));
return redirect()->route('client.payments.show', ['payment'=>$this->encodePrimaryKey($payment->id)]);
}
@ -175,12 +211,19 @@ class PayPalExpressPaymentDriver extends BasePaymentDriver
public function createPayment($data)
{
$payment = parent::createPayment($data);
$payment->amount = $this->convertFromStripeAmount($server_response->amount, $this->client->currency->precision);
$client_contact = $this->getContact();
$client_contact_id = $client_contact ? $client_contact->id : null;
$payment->amount = $data['PAYMENTINFO_0_AMT'];
$payment->payment_type_id = PaymentType::PAYPAL;
$payment->transaction_reference = $payment_method;
$payment->client_contact_id = $this->getContact();
$payment->transaction_reference = $data['PAYMENTINFO_0_TRANSACTIONID'];
$payment->client_contact_id = $client_contact_id;
$payment->save();
return $payment;
}
}

View File

@ -356,8 +356,7 @@ class StripePaymentDriver extends BasePaymentDriver
$payment->transaction_reference = $payment_method;
$payment->save();
$payment->invoices()->sync($invoices);
$payment->save();
$this->attachInvoices($payment, $hashed_ids);
/**

View File

@ -26,6 +26,7 @@ use App\Listeners\Invoice\CreateInvoiceActivity;
use App\Listeners\Invoice\CreateInvoiceInvitation;
use App\Listeners\Invoice\CreateInvoicePdf;
use App\Listeners\Invoice\UpdateInvoiceActivity;
use App\Listeners\Invoice\UpdateInvoicePayment;
use App\Listeners\SendVerificationNotification;
use App\Listeners\User\UpdateUserLastLogin;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
@ -54,6 +55,7 @@ class EventServiceProvider extends ServiceProvider
],
PaymentWasCreated::class => [
PaymentCreatedActivity::class,
UpdateInvoicePayment::class,
],
'App\Events\ClientWasArchived' => [
'App\Listeners\ActivityListener@archivedClient',

View File

@ -693,10 +693,7 @@ class CreateUsersTable extends Migration
Schema::create('payments', function ($t) {
$t->increments('id');
//$t->unsignedInteger('invoice_id')->nullable()->index(); //todo handle payments where there is no invoice OR we are paying MULTIPLE invoices
// *** this is handled by the use of the paymentables table. in here we store the
// entities which have been registered payments against
$t->increments('id');
$t->unsignedInteger('company_id')->index();
$t->unsignedInteger('client_id')->index();
$t->unsignedInteger('user_id')->nullable();
@ -713,7 +710,6 @@ class CreateUsersTable extends Migration
$t->softDeletes();
$t->boolean('is_deleted')->default(false);
//$t->foreign('invoice_id')->references('id')->on('invoices')->onDelete('cascade');
$t->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');
$t->foreign('client_id')->references('id')->on('clients')->onDelete('cascade');
$t->foreign('client_contact_id')->references('id')->on('client_contacts')->onDelete('cascade');
@ -946,6 +942,21 @@ class CreateUsersTable extends Migration
$table->string('format_moment');
$table->string('format_dart');
});
Schema::create('system_log', function ($table){
$table->increments('id');
$table->unsignedInteger('company_id');
$table->unsignedInteger('user_id')->nullable();
$table->unsignedInteger('client_id')->nullable();
$table->unsignedInteger('category_id')->nullable();
$table->unsignedInteger('event_id')->nullable();
$table->text('log');
$table->timestamps(6);
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade');
$table->foreign('client_id')->references('id')->on('clients')->onDelete('cascade');
});
}
/**

View File

@ -9,7 +9,7 @@
"/vendors/js/perfect-scrollbar.min.js": "/vendors/js/perfect-scrollbar.min.js?id=4a10bcfa0a9c9fa9d503",
"/vendors/js/jSignature.min.js": "/vendors/js/jSignature.min.js?id=4dc38fc88461b30ab711",
"/vendors/js/flashcanvas.min.js": "/vendors/js/flashcanvas.min.js?id=50f6e0a09e8a939c1da9",
"/vendors/js/flashcanvas.swf": "/vendors/js/flashcanvas.swf?id=d1a52ac12da100808048",
"/vendors/js/flashcanvas.swf": "/vendors/js/flashcanvas.swf?id=ed2a06bb83b57f2639b1",
"/vendors/css/select2.min.css": "/vendors/css/select2.min.css?id=8e44c39add2364bdb469",
"/vendors/js/select2.min.js": "/vendors/js/select2.min.js?id=0a96cf2d3a193019a91b",
"/vendors/css/select2-bootstrap4.css": "/vendors/css/select2-bootstrap4.css?id=85167d868d2bf2dc5603",

View File

@ -0,0 +1,38 @@
@if ($message = Session::get('success'))
<div class="alert alert-success alert-block">
<button type="button" class="close" data-dismiss="alert">×</button>
<strong>{{ $message }}</strong>
</div>
@endif
@if ($message = Session::get('error'))
<div class="alert alert-danger alert-block">
<button type="button" class="close" data-dismiss="alert">×</button>
<strong>{{ $message }}</strong>
</div>
@endif
@if ($message = Session::get('warning'))
<div class="alert alert-warning alert-block">
<button type="button" class="close" data-dismiss="alert">×</button>
<strong>{{ $message }}</strong>
</div>
@endif
@if ($message = Session::get('info'))
<div class="alert alert-info alert-block">
<button type="button" class="close" data-dismiss="alert">×</button>
<strong>{{ $message }}</strong>
</div>
@endif
@if ($errors->any())
<div class="alert alert-danger">
<button type="button" class="close" data-dismiss="alert">×</button>
Please check the form below for errors
</div>
@endif

View File

@ -51,6 +51,7 @@
</head>
@include('portal.default.header')
@yield('header')
@include('portal.default.flash-message')
@include('portal.default.sidebar')
@yield('sidebar')
@section('body')