From 2d5d9b816b4d22f78ae410aad6cd4d52d5edc9c4 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Tue, 1 Oct 2019 11:56:48 +1000 Subject: [PATCH] Payment Events --- app/Exceptions/Handler.php | 3 +- .../Invoice/UpdateInvoicePayment.php | 123 ++++++++++++++++++ app/Models/Invoice.php | 53 ++++++++ app/Models/SystemLog.php | 30 +++++ app/Observers/PaymentObserver.php | 3 +- app/PaymentDrivers/BasePaymentDriver.php | 15 +++ .../PayPalExpressPaymentDriver.php | 59 +++++++-- app/PaymentDrivers/StripePaymentDriver.php | 3 +- app/Providers/EventServiceProvider.php | 2 + .../2014_10_13_000000_create_users_table.php | 21 ++- public/mix-manifest.json | 2 +- .../portal/default/flash-message.blade.php | 38 ++++++ .../portal/default/layouts/master.blade.php | 1 + 13 files changed, 335 insertions(+), 18 deletions(-) create mode 100644 app/Listeners/Invoice/UpdateInvoicePayment.php create mode 100644 app/Models/SystemLog.php create mode 100644 resources/views/portal/default/flash-message.blade.php diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index be8e38075c..84c444a917 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -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': diff --git a/app/Listeners/Invoice/UpdateInvoicePayment.php b/app/Listeners/Invoice/UpdateInvoicePayment.php new file mode 100644 index 0000000000..f26597c80c --- /dev/null +++ b/app/Listeners/Invoice/UpdateInvoicePayment.php @@ -0,0 +1,123 @@ +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); + } +*/ \ No newline at end of file diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index c377db18fa..15cfbc874c 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -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(); + + + } } \ No newline at end of file diff --git a/app/Models/SystemLog.php b/app/Models/SystemLog.php new file mode 100644 index 0000000000..1e0c1a3e22 --- /dev/null +++ b/app/Models/SystemLog.php @@ -0,0 +1,30 @@ +transformKeys(explode(",",$hashed_ids))) + ->whereClientId($this->client->id) + ->get(); + + $payment->invoices()->sync($invoices); + $payment->save(); + + return $payment; + } } \ No newline at end of file diff --git a/app/PaymentDrivers/PayPalExpressPaymentDriver.php b/app/PaymentDrivers/PayPalExpressPaymentDriver.php index 010c7dbffe..b9f3ab1a63 100644 --- a/app/PaymentDrivers/PayPalExpressPaymentDriver.php +++ b/app/PaymentDrivers/PayPalExpressPaymentDriver.php @@ -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; } } \ No newline at end of file diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index b1498aaf05..9cc6c6312f 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -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); /** diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 024b25e709..c674a9ba10 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -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', 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 1624d0bc12..9b41ce1a6f 100644 --- a/database/migrations/2014_10_13_000000_create_users_table.php +++ b/database/migrations/2014_10_13_000000_create_users_table.php @@ -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'); + + }); } /** diff --git a/public/mix-manifest.json b/public/mix-manifest.json index 378ed869cd..a577a76b00 100644 --- a/public/mix-manifest.json +++ b/public/mix-manifest.json @@ -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", diff --git a/resources/views/portal/default/flash-message.blade.php b/resources/views/portal/default/flash-message.blade.php new file mode 100644 index 0000000000..66445001ec --- /dev/null +++ b/resources/views/portal/default/flash-message.blade.php @@ -0,0 +1,38 @@ +@if ($message = Session::get('success')) +
+ + {{ $message }} +
+@endif + + +@if ($message = Session::get('error')) +
+ + {{ $message }} +
+@endif + + +@if ($message = Session::get('warning')) +
+ + {{ $message }} +
+@endif + + +@if ($message = Session::get('info')) +
+ + {{ $message }} +
+@endif + + +@if ($errors->any()) +
+ + Please check the form below for errors +
+@endif \ No newline at end of file diff --git a/resources/views/portal/default/layouts/master.blade.php b/resources/views/portal/default/layouts/master.blade.php index 6e251e3a97..27c26c9a45 100644 --- a/resources/views/portal/default/layouts/master.blade.php +++ b/resources/views/portal/default/layouts/master.blade.php @@ -51,6 +51,7 @@ @include('portal.default.header') @yield('header') +@include('portal.default.flash-message') @include('portal.default.sidebar') @yield('sidebar') @section('body')