Working on partial invoice payments

This commit is contained in:
David Bomba 2019-10-02 08:44:13 +10:00
parent 78ae24df46
commit a57de08178
10 changed files with 189 additions and 73 deletions

View File

@ -46,8 +46,7 @@ class UpdateCompanyLedgerWithInvoice
*/ */
public function handle() public function handle()
{ {
\Log::error('in update company ledger with invoice');
$balance = 0; $balance = 0;
$ledger = CompanyLedger::whereClientId($this->invoice->client_id) $ledger = CompanyLedger::whereClientId($this->invoice->client_id)
@ -60,7 +59,6 @@ class UpdateCompanyLedgerWithInvoice
$adjustment = $balance + $this->adjustment; $adjustment = $balance + $this->adjustment;
\Log::error("adjusting balance {$balance} to {$adjustment}");
$company_ledger = CompanyLedgerFactory::create($this->invoice->company_id, $this->invoice->user_id); $company_ledger = CompanyLedgerFactory::create($this->invoice->company_id, $this->invoice->user_id);
$company_ledger->client_id = $this->invoice->client_id; $company_ledger->client_id = $this->invoice->client_id;

View File

@ -0,0 +1,54 @@
<?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 Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class UpdateInvoiceInvitations 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;
/**
* Move this into an event
*/
$invoices->each(function ($invoice) use($payment) {
$invoice->status_id = Invoice::STATUS_PAID;
$invoice->save();
$invoice->invitations()->update(['transaction_reference' => $payment->transaction_reference]);
});
}
}

View File

@ -14,11 +14,14 @@ namespace App\Listeners\Invoice;
use App\Jobs\Company\UpdateCompanyLedgerWithInvoice; use App\Jobs\Company\UpdateCompanyLedgerWithInvoice;
use App\Jobs\Company\UpdateCompanyLedgerWithPayment; use App\Jobs\Company\UpdateCompanyLedgerWithPayment;
use App\Models\SystemLog; use App\Models\SystemLog;
use App\Utils\Traits\SystemLogTrait;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
class UpdateInvoicePayment implements ShouldQueue class UpdateInvoicePayment implements ShouldQueue
{ {
use SystemLogTrait;
/** /**
* Create the event listener. * Create the event listener.
* *
@ -44,17 +47,17 @@ class UpdateInvoicePayment implements ShouldQueue
/* Simplest scenario*/ /* Simplest scenario*/
if($invoices_total == $payment->amount) if($invoices_total == $payment->amount)
{ {
\Log::error("invoice totals match payment amount");
$invoices->each(function ($invoice) use($payment){ $invoices->each(function ($invoice) use($payment){
//$invoice->updateBalance($invoice->balance*-1);
//UpdateCompanyLedgerWithInvoice::dispatchNow($invoice, ($invoice->balance*-1));
UpdateCompanyLedgerWithPayment::dispatchNow($payment, ($invoice->balance*-1)); UpdateCompanyLedgerWithPayment::dispatchNow($payment, ($invoice->balance*-1));
$invoice->clearPartial();
$invoice->updateBalance($invoice->balance*-1); $invoice->updateBalance($invoice->balance*-1);
}); });
} }
else { else {
\Log::error("invoice totals don't match, search for partials");
$total = 0; $total = 0;
foreach($invoice as $invoice) foreach($invoice as $invoice)
@ -71,12 +74,29 @@ class UpdateInvoicePayment implements ShouldQueue
/* test if there is a batch of partial invoices that have been paid */ /* test if there is a batch of partial invoices that have been paid */
if($payment->amount == $total) if($payment->amount == $total)
{ {
//process invoices and update balance depending on
//whether the invoice balance or partial amount was $invoices->each(function ($invoice) use($payment){
//paid
if($invoice->isPartial()) {
UpdateCompanyLedgerWithPayment::dispatchNow($payment, ($invoice->partial*-1));
$invoice->updateBalance($invoice->partial*-1);
$invoice->clearPartial();
$invoice->setDueDate();
//todo do we need to mark it as a partial?
}
else
{
UpdateCompanyLedgerWithPayment::dispatchNow($payment, ($invoice->balance*-1));
$invoice->clearPartial();
$invoice->updateBalance($invoice->balance*-1);
}
});
} }
else { else {
\Log::error("no matches, fail");
$data = [ $data = [
'payment' => $payment, 'payment' => $payment,
'invoices' => $invoices, 'invoices' => $invoices,
@ -85,16 +105,8 @@ class UpdateInvoicePayment implements ShouldQueue
'partial_check_amount' => $total, '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); $this->sysLog($data, SystemLog::GATEWAY_RESPONSE, SystemLog::PAYMENT_RECONCILIATION_FAILURE);
throw new Exception('payment amount does not match invoice totals'); throw new Exception('payment amount does not match invoice totals');
} }

View File

@ -15,6 +15,7 @@ use App\Events\Invoice\InvoiceWasUpdated;
use App\Helpers\Invoice\InvoiceCalc; use App\Helpers\Invoice\InvoiceCalc;
use App\Models\Currency; use App\Models\Currency;
use App\Models\Filterable; use App\Models\Filterable;
use App\Models\PaymentTerm;
use App\Utils\Number; use App\Utils\Number;
use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesDates;
use App\Utils\Traits\MakesInvoiceValues; use App\Utils\Traits\MakesInvoiceValues;
@ -302,6 +303,17 @@ class Invoice extends BaseModel
//return $this->status_id >= self::STATUS_PARTIAL; //return $this->status_id >= self::STATUS_PARTIAL;
} }
/**
* Clear partial fields
* @return void
*/
public function clearPartial() : void
{
$this->partial = null;
$this->partial_due_date = null;
$this->save();
}
/** /**
* @param float $balance_adjustment * @param float $balance_adjustment
*/ */
@ -322,4 +334,11 @@ class Invoice extends BaseModel
$this->save(); $this->save();
\Log::error('finished updatingoice balance'); \Log::error('finished updatingoice balance');
} }
public function setDueDate()
{
$this->due_date = Carbon::now()->addDays(PaymentTerm::find($this->company->settings->payment_terms_id)->num_days);
$this->save();
}
} }

View File

@ -16,8 +16,7 @@ use Illuminate\Database\Eloquent\Model;
class SystemLog extends Model class SystemLog extends Model
{ {
/* Category IDs */ /* Category IDs */
const PAYMENT_RESPONSE = 1; const GATEWAY_RESPONSE = 1;
const GATEWAY_RESPONSE = 2;
/* Event IDs*/ /* Event IDs*/
const PAYMENT_RECONCILIATION_FAILURE = 10; const PAYMENT_RECONCILIATION_FAILURE = 10;

View File

@ -64,9 +64,13 @@ class BasePaymentDriver
public function __construct(CompanyGateway $company_gateway, Client $client, $invitation = false) public function __construct(CompanyGateway $company_gateway, Client $client, $invitation = false)
{ {
$this->company_gateway = $company_gateway; $this->company_gateway = $company_gateway;
$this->invitation = $invitation; $this->invitation = $invitation;
$this->client = $client; $this->client = $client;
} }
/** /**
@ -175,27 +179,16 @@ class BasePaymentDriver
acceptNotification() - convert an incoming request from an off-site gateway to a generic notification object for further processing acceptNotification() - convert an incoming request from an off-site gateway to a generic notification object for further processing
*/ */
protected function paymentDetails($input) protected function paymentDetails($input) : array
{ {
// $gatewayTypeAlias = $this->gatewayType == GatewayType::TOKEN ? $this->gatewayType : GatewayType::getAliasFromId($this->gatewayType);
$data = [ $data = [
'currency' => $this->client->getCurrencyCode(), 'currency' => $this->client->getCurrencyCode(),
'transactionType' => 'Purchase', 'transactionType' => 'Purchase',
'clientIp' => request()->getClientIp(), 'clientIp' => request()->getClientIp(),
]; ];
/*
if ($paymentMethod) {
if ($this->customerReferenceParam) {
$data[$this->customerReferenceParam] = $paymentMethod->account_gateway_token->token;
}
$data[$this->sourceReferenceParam] = $paymentMethod->source_reference;
} elseif ($this->input) {
$data['card'] = new CreditCard($this->paymentDetailsFromInput($this->input));
} else {
$data['card'] = new CreditCard($this->paymentDetailsFromClient());
}
*/
return $data; return $data;
} }
@ -208,17 +201,7 @@ class BasePaymentDriver
->setItems($items) ->setItems($items)
->send(); ->send();
return $response;
if ($response->isRedirect()) {
// redirect to offsite payment gateway
$response->redirect();
} elseif ($response->isSuccessful()) {
// payment was successful: update database
print_r($response);
} else {
// payment failed: display message to customer
echo $response->getMessage();
}
/* /*
$this->purchaseResponse = (array)$response->getData();*/ $this->purchaseResponse = (array)$response->getData();*/
} }
@ -232,7 +215,7 @@ class BasePaymentDriver
->send(); ->send();
} }
public function createPayment($data) public function createPayment($data) : Payment
{ {
$payment = PaymentFactory::create($this->client->company->id, $this->client->user->id); $payment = PaymentFactory::create($this->client->company->id, $this->client->user->id);
@ -246,7 +229,7 @@ class BasePaymentDriver
} }
public function attachInvoices(Payment $payment, $hashed_ids) public function attachInvoices(Payment $payment, $hashed_ids) : Payment
{ {
$invoices = Invoice::whereIn('id', $this->transformKeys(explode(",",$hashed_ids))) $invoices = Invoice::whereIn('id', $this->transformKeys(explode(",",$hashed_ids)))
->whereClientId($this->client->id) ->whereClientId($this->client->id)

View File

@ -14,6 +14,7 @@ namespace App\PaymentDrivers;
use App\Events\Payment\PaymentWasCreated; use App\Events\Payment\PaymentWasCreated;
use App\Models\ClientGatewayToken; use App\Models\ClientGatewayToken;
use App\Models\GatewayType; use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentType; use App\Models\PaymentType;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -90,7 +91,28 @@ class PayPalExpressPaymentDriver extends BasePaymentDriver
*/ */
public function processPaymentView(array $data) public function processPaymentView(array $data)
{ {
$this->purchase($this->paymentDetails($data), $this->paymentItems($data)); $response = $this->purchase($this->paymentDetails($data), $this->paymentItems($data));
if ($response->isRedirect()) {
// redirect to offsite payment gateway
$response->redirect();
} elseif ($response->isSuccessful()) {
// payment was successful: update database
/* for this driver this method wont be hit*/
} else {
// payment failed: display message to customer
$log = [
'server_response' => $response->getData(),
'data' => $data
];
$this->sysLog($log);
throw new Exception("Error Processing Payment", 1);
}
} }
public function processPaymentResponse($request) public function processPaymentResponse($request)
@ -103,6 +125,14 @@ class PayPalExpressPaymentDriver extends BasePaymentDriver
if ($response->isCancelled()) { if ($response->isCancelled()) {
return redirect()->route('client.invoices.index')->with('warning',ctrans('texts.status_voided')); return redirect()->route('client.invoices.index')->with('warning',ctrans('texts.status_voided'));
} elseif (! $response->isSuccessful()) { } elseif (! $response->isSuccessful()) {
$data = [
'request' => $request->all(),
'server_response' => $response->getData()
];
$this->sysLog($data);
throw new Exception($response->getMessage()); throw new Exception($response->getMessage());
} }
@ -211,7 +241,7 @@ class PayPalExpressPaymentDriver extends BasePaymentDriver
return $items; return $items;
} }
public function createPayment($data) public function createPayment($data) : Payment
{ {
$payment = parent::createPayment($data); $payment = parent::createPayment($data);

View File

@ -18,7 +18,6 @@ use App\Models\GatewayType;
use App\Models\Invoice; use App\Models\Invoice;
use App\Models\Payment; use App\Models\Payment;
use App\Models\PaymentType; use App\Models\PaymentType;
use App\Models\SystemLog;
use App\Utils\Traits\MakesHash; use App\Utils\Traits\MakesHash;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
@ -231,6 +230,7 @@ class StripePaymentDriver extends BasePaymentDriver
$data['intent'] = $this->createPaymentIntent($payment_intent_data); $data['intent'] = $this->createPaymentIntent($payment_intent_data);
$data['gateway'] = $this; $data['gateway'] = $this;
return view($this->viewForType($data['payment_method_id']), $data); return view($this->viewForType($data['payment_method_id']), $data);
@ -362,17 +362,6 @@ class StripePaymentDriver extends BasePaymentDriver
event(new PaymentWasCreated($payment)); event(new PaymentWasCreated($payment));
/**
* Move this into an event
*/
$invoices->each(function ($invoice) use($payment) {
$invoice->status_id = Invoice::STATUS_PAID;
$invoice->save();
$invoice->invitations()->update(['transaction_reference' => $payment->transaction_reference]);
});
return redirect()->route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)]); return redirect()->route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)]);
@ -386,16 +375,7 @@ class StripePaymentDriver extends BasePaymentDriver
'invoices' => $invoices, 'invoices' => $invoices,
]; ];
$sl = [ $this->sysLog($log);
'company_id' => $this->client->company->id,
'client_id' => $this->client->id,
'user_id' => $this->client->user_id,
'log' => $log,
'category_id' => SystemLog::GATEWAY_RESPONSE,
'event_id' => SystemLog::GATEWAY_FAILURE,
];
SystemLog::create($sl);
throw new Exception("Failed to process payment", 1); throw new Exception("Failed to process payment", 1);

View File

@ -26,6 +26,7 @@ use App\Listeners\Invoice\CreateInvoiceActivity;
use App\Listeners\Invoice\CreateInvoiceInvitation; use App\Listeners\Invoice\CreateInvoiceInvitation;
use App\Listeners\Invoice\CreateInvoicePdf; use App\Listeners\Invoice\CreateInvoicePdf;
use App\Listeners\Invoice\UpdateInvoiceActivity; use App\Listeners\Invoice\UpdateInvoiceActivity;
use App\Listeners\Invoice\UpdateInvoiceInvitations;
use App\Listeners\Invoice\UpdateInvoicePayment; use App\Listeners\Invoice\UpdateInvoicePayment;
use App\Listeners\SendVerificationNotification; use App\Listeners\SendVerificationNotification;
use App\Listeners\User\UpdateUserLastLogin; use App\Listeners\User\UpdateUserLastLogin;
@ -56,6 +57,7 @@ class EventServiceProvider extends ServiceProvider
PaymentWasCreated::class => [ PaymentWasCreated::class => [
PaymentCreatedActivity::class, PaymentCreatedActivity::class,
UpdateInvoicePayment::class, UpdateInvoicePayment::class,
UpdateInvoiceInvitations::class,
], ],
'App\Events\ClientWasArchived' => [ 'App\Events\ClientWasArchived' => [
'App\Listeners\ActivityListener@archivedClient', 'App\Listeners\ActivityListener@archivedClient',

View File

@ -0,0 +1,39 @@
<?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\Utils\Traits;
use App\Models\SystemLog;
/**
* Class SystemLogTrait
* @package App\Utils\Traits
*/
trait SystemLogTrait
{
public function sysLog($log, $category_id = SystemLog::GATEWAY_RESPONSE, $event_id = SystemLog::GATEWAY_FAILURE)
{
$sl = [
'client_id' => $this->client->id,
'company_id' => $this->client->company->id,
'user_id' => $this->client->user_id,
'log' => $log,
'category_id' => $category_id,
'event_id' => $event_id,
];
SystemLog::create($sl);
}
}