mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-05 18:52:44 +01:00
Payment Events
This commit is contained in:
parent
3721e2b7f1
commit
2d5d9b816b
@ -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':
|
||||
|
123
app/Listeners/Invoice/UpdateInvoicePayment.php
Normal file
123
app/Listeners/Invoice/UpdateInvoicePayment.php
Normal 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);
|
||||
}
|
||||
*/
|
@ -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
30
app/Models/SystemLog.php
Normal 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;
|
||||
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
|
||||
/**
|
||||
|
@ -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',
|
||||
|
@ -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');
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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",
|
||||
|
38
resources/views/portal/default/flash-message.blade.php
Normal file
38
resources/views/portal/default/flash-message.blade.php
Normal 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
|
@ -51,6 +51,7 @@
|
||||
</head>
|
||||
@include('portal.default.header')
|
||||
@yield('header')
|
||||
@include('portal.default.flash-message')
|
||||
@include('portal.default.sidebar')
|
||||
@yield('sidebar')
|
||||
@section('body')
|
||||
|
Loading…
Reference in New Issue
Block a user