Cron checks for Rotessa Paymetns

This commit is contained in:
David Bomba 2024-08-03 12:38:48 +10:00
parent da9e587f67
commit 4a7a54a9ea
9 changed files with 229 additions and 42 deletions

View File

@ -567,9 +567,9 @@ class CompanyGatewayController extends BaseController
{
//Throttle here
if (Cache::has("throttle_polling:import_customers:{$company_gateway->company->company_key}:{$company_gateway->hashed_id}")) {
return response()->json(['message' => 'Please wait whilst your previous attempts complete.'], 200);
}
// if (Cache::has("throttle_polling:import_customers:{$company_gateway->company->company_key}:{$company_gateway->hashed_id}")) {
// return response()->json(['message' => 'Please wait whilst your previous attempts complete.'], 200);
// }
dispatch(function () use ($company_gateway) {
MultiDB::setDb($company_gateway->company->db);

View File

@ -27,7 +27,7 @@ class AccountComponent extends Component
'routing_number' => null,
'institution_number' => null,
'transit_number' => null,
'bank_name' => ' ',
'bank_name' => null,
'account_number' => null,
'country' => 'US',
"authorization_type" => 'Online'

View File

@ -155,6 +155,7 @@ class CompanyGateway extends BaseModel
'hxd6gwg3ekb9tb3v9lptgx1mqyg69zu9' => 322,
'80af24a6a691230bbec33e930ab40666' => 323,
'vpyfbmdrkqcicpkjqdusgjfluebftuva' => 324, //BTPay
'91be24c7b792230bced33e930ac61676' => 325,
];
protected $touches = [];

View File

@ -0,0 +1,154 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2023. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\PaymentDrivers\Rotessa\Jobs;
use App\Utils\Ninja;
use App\Models\Payment;
use App\Models\SystemLog;
use App\Libraries\MultiDB;
use App\Models\PaymentHash;
use Illuminate\Bus\Queueable;
use App\Models\CompanyGateway;
use App\Jobs\Util\SystemLogger;
use Illuminate\Support\Facades\App;
use App\Jobs\Mail\PaymentFailedMailer;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class TransactionReport implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;
public $tries = 1; //number of retries
public $deleteWhenMissingModels = true;
public function __construct()
{
}
public function handle()
{
set_time_limit(0);
foreach(MultiDB::$dbs as $db)
{
MultiDB::setDB($db);
CompanyGateway::where('gateway_key', '91be24c7b792230bced33e930ac61676')
->cursor()
->each(function ($cg){
$driver = $cg->driver()->init();
//Approved Transactions
$transactions = $driver->gatewayRequest("get", "transaction_report", ['page' => 1, 'status' => 'Approved', 'start_date' => now()->subMonths(2)->format('Y-m-d')]);
if($transactions->successful())
{
$transactions = $transactions->json();
nlog($transactions);
Payment::query()
->where('company_id', $cg->company_id)
->where('status_id', Payment::STATUS_PENDING)
->whereIn('transaction_reference', array_column($transactions, "transaction_schedule_id"))
->cursor()
->each(function ($payment) use ($transactions) {
$payment->status_id = Payment::STATUS_COMPLETED;
$payment->save();
SystemLogger::dispatch(
['response' => collect($transactions)->where('id', $payment->transaction_reference)->first()->toArray(), 'data' => []],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_ROTESSA,
$payment->client,
$payment->company,
);
});
}
//Declined / Charged Back Transactions
$declined_transactions = $driver->gatewayRequest("get", "transaction_report", ['page' => 1, 'status' => 'Declined', 'start_date' => now()->subMonths(2)->format('Y-m-d')]);
$chargeback_transactions = $driver->gatewayRequest("get", "transaction_report", ['page' => 1, 'status' => 'Chargeback', 'start_date' => now()->subMonths(2)->format('Y-m-d')]);
if($declined_transactions->successful() && $chargeback_transactions->successful()) {
$transactions = array_merge($declined_transactions->json(), $chargeback_transactions->json());
nlog($transactions);
Payment::query()
->where('company_id', $cg->company_id)
->where('status_id', Payment::STATUS_PENDING)
->whereIn('transaction_reference', array_column($transactions, "transaction_schedule_id"))
->cursor()
->each(function ($payment) use ($transactions){
$client = $payment->client;
$payment->service()->deletePayment();
$payment->status_id = Payment::STATUS_FAILED;
$payment->save();
$payment_hash = PaymentHash::query()->where('payment_id', $payment->id)->first();
if ($payment_hash) {
App::forgetInstance('translator');
$t = app('translator');
$t->replace(Ninja::transformTranslations($client->getMergedSettings()));
App::setLocale($client->locale());
$error = ctrans('texts.client_payment_failure_body', [
'invoice' => implode(',', $payment->invoices->pluck('number')->toArray()),
'amount' => array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total, ]);
} else {
$error = 'Payment for '.$payment->client->present()->name()." for {$payment->amount} failed";
}
PaymentFailedMailer::dispatch(
$payment_hash,
$client->company,
$client,
$error
);
SystemLogger::dispatch(
['response' => collect($transactions)->where('id', $payment->transaction_reference)->first()->toArray(), 'data' => []],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_ROTESSA,
$payment->client,
$payment->company,
);
});
}
});
}
}
}

View File

@ -81,7 +81,7 @@ class PaymentMethod implements MethodInterface
'country' => ['required'],
'name' => ['required'],
'address_1' => ['required'],
'address_2' => ['required'],
// 'address_2' => ['required'],
'city' => ['required'],
'email' => ['required','email:filter'],
'province_code' => ['required','size:2','alpha'],
@ -90,7 +90,7 @@ class PaymentMethod implements MethodInterface
'account_number' => ['required'],
'bank_name' => ['required'],
'phone' => ['required'],
'home_phone' => ['required'],
'home_phone' => ['required','size:10'],
'bank_account_type'=>['required_if:country,US'],
'routing_number'=>['required_if:country,US'],
'institution_number'=>['required_if:country,CA','numeric'],
@ -159,11 +159,14 @@ class PaymentMethod implements MethodInterface
$transaction = new Transaction($request->only('frequency' ,'installments','amount','process_date') + ['comment' => $this->rotessa->getDescription(false) ]);
$transaction->additional(['customer_id' => $customer->gateway_customer_reference]);
$transaction = array_filter( $transaction->resolve());
$response = $this->rotessa->gateway->capture($transaction)->send();
$response = $this->rotessa->gatewayRequest('post','transaction_schedules', $transaction);
if($response->failed())
$response->throw();
if(!$response->isSuccessful()) throw new \Exception($response->getMessage(), (int) $response->getCode());
return $this->processPendingPayment($response->getParameter('id'), (float) $response->getParameter('amount'), PaymentType::ACSS , $customer->token);
$response = $response->json();
nlog($response);
return $this->processPendingPayment($response['id'], (float) $response['amount'], PaymentType::ACSS , $customer->token);
} catch(\Throwable $e) {
$this->processUnsuccessfulPayment( new InvalidResponseException($e->getMessage(), (int) $e->getCode()) );
}

View File

@ -13,7 +13,7 @@ class PatchTransactionSchedulesId extends BaseRequest implements RequestInterfac
public function setId(int $value) {
$this->setParameter('id',$value);
}
public function setAmount(int $value) {
public function setAmount($value) {
$this->setParameter('amount',$value);
}
public function setComment(string $value) {

View File

@ -15,7 +15,7 @@ class PostTransactionSchedulesUpdateViaPost extends BaseRequest implements Reque
public function setId(int $value) {
$this->setParameter('id',$value);
}
public function setAmount(int $value) {
public function setAmount($value) {
$this->setParameter('amount',$value);
}
public function setComment(string $value) {

View File

@ -30,6 +30,7 @@ use Illuminate\Database\Eloquent\Builder;
use App\PaymentDrivers\Rotessa\Resources\Customer;
use App\PaymentDrivers\Rotessa\PaymentMethod as Acss;
use App\PaymentDrivers\Rotessa\PaymentMethod as BankTransfer;
use Illuminate\Support\Facades\Http;
class RotessaPaymentDriver extends BaseDriver
{
@ -54,11 +55,6 @@ class RotessaPaymentDriver extends BaseDriver
public function init(): self
{
$this->gateway = Omnipay::create(
$this->company_gateway->gateway->provider
);
$this->gateway->initialize((array) $this->company_gateway->getConfig());
return $this;
}
@ -117,30 +113,42 @@ class RotessaPaymentDriver extends BaseDriver
}
public function importCustomers() {
$this->init();
try {
if(!$result = Cache::has("rotessa-import_customers-{$this->company_gateway->company->company_key}")) {
$result = $this->gateway->getCustomers()->send();
if(!$result->isSuccessful()) throw new \Exception($result->getMessage(), (int) $result->getCode());
// cache results
Cache::put("rotessa-import_customers-{$this->company_gateway->company->company_key}", $result->getData(), 60 * 60 * 24);
}
$result = Cache::get("rotessa-import_customers-{$this->company_gateway->company->company_key}");
$customers = collect($result)->unique('email');
$result = $this->gatewayRequest('get','customers',[]);
if($result->failed())
$result->throw();
$customers = collect($result->json())->unique('email');
$client_emails = $customers->pluck('email')->all();
$company_id = $this->company_gateway->company->id;
// get existing customers
$client_contacts = ClientContact::where('company_id', $company_id)->whereIn('email', $client_emails )->whereNull('deleted_at')->get();
$client_contacts = ClientContact::where('company_id', $company_id)
->whereIn('email', $client_emails )
->whereHas('client', function ($q){
$q->where('is_deleted', false);
})
->whereNull('deleted_at')
->get();
$client_contacts = $client_contacts->map(function($item, $key) use ($customers) {
return array_merge([], (array) $customers->firstWhere("email", $item->email) , ['custom_identifier' => $item->client->number, 'identifier' => $item->client->number, 'client_id' => $item->client->id ]);
return array_merge($customers->firstWhere("email", $item->email),['custom_identifier' => $item->client->number, 'identifier' => $item->client->number, 'client_id' => $item->client->id ]);
} );
// create payment methods
$client_contacts->each(
function($contact) use ($customers) {
$result = $this->gateway->getCustomersId(['id' => ($contact = (object) $contact)->id])->send();
function($contact) {
// $result = $this->gateway->getCustomersId(['id' => ($contact = (object) $contact)->id])->send();
$contact = (object)$contact;
$result = $this->gatewayRequest("get","customers/{$contact->id}");
$result = $result->json();
$this->client = Client::find($contact->client_id);
$customer = (new Customer($result->getData()))->additional(['id' => $contact->id, 'custom_identifier' => $contact->custom_identifier ] );
$customer = (new Customer($result))->additional(['id' => $contact->id, 'custom_identifier' => $contact->custom_identifier ] );
$this->findOrCreateCustomer($customer->additional + $customer->jsonSerialize());
}
);
@ -150,8 +158,8 @@ class RotessaPaymentDriver extends BaseDriver
$client_contacts = $customers->filter(function ($value, $key) use ($client_emails) {
return !in_array(((object) $value)->email, $client_emails);
})->each( function($customer) use ($company_id) {
// create new client contact from rotess customer
$customer = (object) $this->gateway->getCustomersId(['id' => ($customer = (object) $customer)->id])->send()->getData();
$customer = $this->gatewayRequest("get", "customers/{$customer['id']}")->json();
/**
{
"account_number": "11111111"
@ -186,7 +194,7 @@ class RotessaPaymentDriver extends BaseDriver
*/
$settings = ClientSettings::defaults();
$settings->currency_id = $this->company_gateway->company->getSetting('currency_id');
$customer = (object)$customer;
$client = (\App\Factory\ClientFactory::create($this->company_gateway->company_id, $this->company_gateway->user_id))->fill(
[
'address1' => $customer->address['address_1'] ?? '',
@ -245,12 +253,18 @@ class RotessaPaymentDriver extends BaseDriver
->where('gateway_customer_reference', Arr::only($data,'id'));
})
->exists();
if ($existing) return true;
if ($existing)
return true;
else if(!Arr::has($data,'id')) {
$result = $this->gateway->authorize($data)->send();
if (!$result->isSuccessful()) throw new \Exception($result->getMessage(), (int) $result->getCode());
// $result = $this->gateway->authorize($data)->send();
// if (!$result->isSuccessful()) throw new \Exception($result->getMessage(), (int) $result->getCode());
$customer = new Customer($result->getData());
$result = $this->gatewayRequest('post', 'customers', $data);
if($result->failed())
$result->throw();
$customer = new Customer($result->json());
$data = array_filter($customer->resolve());
}
@ -268,7 +282,6 @@ class RotessaPaymentDriver extends BaseDriver
return $data['id'];
throw new \Exception($result->getMessage(), (int) $result->getCode());
} catch (\Throwable $th) {
$data = [
@ -276,7 +289,7 @@ class RotessaPaymentDriver extends BaseDriver
'transaction_response' => $th->getMessage(),
'success' => false,
'description' => $th->getMessage(),
'code' =>(int) $th->getCode()
'code' => 500
];
SystemLogger::dispatch(['server_response' => is_null($result) ? '' : $result->getMessage(), 'data' => $data], SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, 880 , $this->client, $this->company_gateway->company);
@ -284,4 +297,20 @@ class RotessaPaymentDriver extends BaseDriver
throw $th;
}
}
public function gatewayRequest($verb, $uri, $payload = [])
{
$r = Http::withToken($this->company_gateway->getConfigField('apiKey'))
->{$verb}($this->getUrl().$uri, $payload);
nlog($r->body());
return $r;
}
private function getUrl(): string
{
return $this->company_gateway->getConfigField('testMode') ? 'https://sandbox-api.rotessa.com/v1/' : 'https://api.rotessa.com/v1/';
}
}

View File

@ -22,7 +22,7 @@
{{ ctrans('texts.address2') }}
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<input class="input w-full" id="address_2" name="address_2" type="text" placeholder="Address Line 2" required value="{{ old('address_2', $address_2) }}">
<input class="input w-full" id="address_2" name="address_2" type="text" placeholder="Address Line 2" value="{{ old('address_2', $address_2) }}">
</dd>
</div>