1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-09 20:52:56 +01:00

Merge branch 'powerboard' into v5-develop

This commit is contained in:
David Bomba 2024-09-17 10:09:53 +10:00
commit b1d92d8354
23 changed files with 2294 additions and 8 deletions

28
app/Helpers/Sanitizer.php Normal file
View File

@ -0,0 +1,28 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Helpers;
class Sanitizer
{
public static function removeBlanks($input): array
{
foreach ($input as &$value) {
if (is_array($value)) {
// Recursively apply the filter to nested arrays
$value = self::removeBlanks($value);
}
}
// Use array_filter to remove empty or null values
return array_filter($input);
}
}

View File

@ -50,7 +50,7 @@ class InvoiceSummary extends Component
public function downloadDocument($invoice_hashed_id)
{
nlog("here");
$contact = $this->getContext()['contact'];
$_invoices = $this->getContext()['invoices'];
$i = $_invoices->first(function ($i) use($invoice_hashed_id){
@ -61,11 +61,6 @@ class InvoiceSummary extends Component
$file = (new \App\Jobs\Entity\CreateRawPdf($i->invitations()->where('client_contact_id', $contact->id)->first()))->handle();
nlog("here");
nlog($file);
$headers = ['Content-Type' => 'application/pdf'];
return response()->streamDownload(function () use ($file) {

View File

@ -28,6 +28,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property bool $update_details
* @property bool $is_deleted
* @property string $config
* @property object $settings
* @property mixed $fees_and_limits
* @property string|null $custom_value1
* @property string|null $custom_value2
@ -74,6 +75,7 @@ class CompanyGateway extends BaseModel
protected $casts = [
'fees_and_limits' => 'object',
'settings' => 'object',
'updated_at' => 'timestamp',
'created_at' => 'timestamp',
'deleted_at' => 'timestamp',
@ -156,6 +158,7 @@ class CompanyGateway extends BaseModel
'80af24a6a691230bbec33e930ab40666' => 323,
'vpyfbmdrkqcicpkjqdusgjfluebftuva' => 324, //BTPay
'91be24c7b792230bced33e930ac61676' => 325,
'b67581d804dbad1743b61c57285142ad' => 326, //Powerboard
];
protected $touches = [];
@ -483,6 +486,18 @@ class CompanyGateway extends BaseModel
return $fee;
}
public function getSettings()
{
// return $this->settings;
return $this->settings ?? new \stdClass;
}
public function setSettings($settings)
{
$this->settings = $settings;
$this->save();
}
public function webhookUrl()
{
return route('payment_webhook', ['company_key' => $this->company->company_key, 'company_gateway_id' => $this->hashed_id]);

View File

@ -235,6 +235,10 @@ class Gateway extends StaticModel
],
GatewayType::ACSS => ['refund' => false, 'token_billing' => true, 'webhooks' => []]
]; // Rotessa
case 64: //b67581d804dbad1743b61c57285142ad - powerboard
return [
GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true],
];
default:
return [];
}

View File

@ -156,6 +156,8 @@ class SystemLog extends Model
public const TYPE_ROTESSA = 325;
public const TYPE_POWERBOARD = 326;
public const TYPE_QUOTA_EXCEEDED = 400;
public const TYPE_UPSTREAM_FAILURE = 401;

View File

@ -410,6 +410,7 @@ class BaseDriver extends AbstractPaymentDriver
if($invoice && $fee_count == 0){
nlog("apparently no fee, so injecting here!");
if(!$invoice->uses_inclusive_taxes){ //must account for taxes! ? line item taxes also
@ -771,6 +772,18 @@ class BaseDriver extends AbstractPaymentDriver
);
}
public function logUnsuccessfulGatewayResponse($response, $gateway_const)
{
SystemLogger::dispatch(
$response,
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
$gateway_const,
$this->client,
$this->client->company,
);
}
public function genericWebhookUrl()
{
return route('payment_notification_webhook', [

View File

@ -0,0 +1,465 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\PaymentDrivers\CBAPowerBoard;
use App\Models\Payment;
use App\Models\SystemLog;
use App\Models\GatewayType;
use App\Models\PaymentHash;
use App\Models\PaymentType;
use App\Jobs\Util\SystemLogger;
use App\Exceptions\PaymentFailed;
use Illuminate\Http\Client\RequestException;
use App\PaymentDrivers\CBAPowerBoardPaymentDriver;
use App\PaymentDrivers\CBAPowerBoard\Models\Charge;
use App\PaymentDrivers\Common\LivewireMethodInterface;
use App\PaymentDrivers\CBAPowerBoard\Models\PaymentSource;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\PaymentDrivers\CBAPowerBoard\Models\Gateway;
class CreditCard implements LivewireMethodInterface
{
private Gateway $cba_gateway;
public function __construct(public CBAPowerBoardPaymentDriver $powerboard)
{
$this->cba_gateway = $this->powerboard->settings()->getPaymentGatewayConfiguration(GatewayType::CREDIT_CARD);
}
public function authorizeView(array $data)
{
$data['payment_method_id'] = GatewayType::CREDIT_CARD;
$view = $this->powerboard->company_gateway->getConfigField('threeds') ? 'gateways.powerboard.credit_card.authorize' : 'gateways.powerboard.credit_card.authorize_no_3ds';
return render($view, $this->paymentData($data));
}
public function authorizeResponse($request)
{
if($request->browser_details)
{
$payment_source = $this->storePaymentSource($request);
nlog($payment_source);
$browser_details = json_decode($request->browser_details, true);
$payload = [
"capture" => false,
"amount" => 1,
"currency" => $this->powerboard->client->currency()->code,
"description" => "Card authorization",
"customer" => [
"payment_source" => [
"vault_token" => $payment_source->vault_token,
"gateway_id" => $this->powerboard->settings()->getGatewayId(GatewayType::CREDIT_CARD),
],
],
"_3ds" => [
"browser_details" => $browser_details,
],
];
nlog($payload);
$r = $this->powerboard->gatewayRequest('/v1/charges/3ds', (\App\Enum\HttpVerb::POST)->value, $payload, []);
if ($r->failed()) {
return $this->processUnsuccessfulPayment($r);
}
$charge = $r->json();
nlog($charge['resource']['data']);
return response()->json($charge['resource']['data'], 200);
}
elseif($request->charge) {
$charge_request = json_decode($request->charge, true);
nlog("we have the charge request");
nlog($charge_request);
$payload = [
'_3ds' => [
'id' => $charge_request['charge_3ds_id'],
],
"capture" => false,
"authorization" => true,
"amount"=> 1,
"currency"=> $this->powerboard->client->currency()->code,
"store_cvv"=> true,
];
nlog($payload);
$r = $this->powerboard->gatewayRequest("/v1/charges", (\App\Enum\HttpVerb::POST)->value, $payload, []);
if($r->failed()){
$error_payload = $this->getErrorFromResponse($r);
throw new PaymentFailed($error_payload[0], $error_payload[1]);
}
$charge = (new \App\PaymentDrivers\CBAPowerBoard\Models\Parse())->encode(Charge::class, $r->object()->resource->data) ?? $r->throw();
nlog($charge);
if ($charge->status == 'complete') {
$this->powerboard->logSuccessfulGatewayResponse(['response' => $charge, 'data' => $this->powerboard->payment_hash], SystemLog::TYPE_POWERBOARD);
$vt = $charge->customer->payment_source->vault_token;
$data = [
"payment_source" => [
"vault_token" => $vt,
],
];
$customer = $this->powerboard->customer()->findOrCreateCustomer($data);
$cgt = $this->powerboard->customer()->storePaymentMethod($charge->customer->payment_source, $charge->customer);
return redirect()->route('client.payment_methods.show', ['payment_method' => $cgt->hashed_id]);
}
}
elseif($request->charge_no3d){
nlog($request->all());
$payment_source = $this->storePaymentSource($request);
nlog($payment_source);
$data = [
"payment_source" => [
"vault_token" => $payment_source->vault_token,
],
];
$customer = $this->powerboard->customer()->findOrCreateCustomer($data);
$cgt = $this->powerboard->customer()->storePaymentMethod($payment_source, $customer);
$cgt->gateway_customer_reference = $this->powerboard->settings()->getGatewayId(GatewayType::CREDIT_CARD);
$cgt->save();
return redirect()->route('client.payment_methods.show', ['payment_method' => $cgt->hashed_id]);
}
return redirect()->route('client.payment_methods.index');
}
private function getCustomer(): array
{
$data = [
'first_name' => $this->powerboard->client->present()->first_name(),
'last_name' => $this->powerboard->client->present()->first_name(),
'email' => $this->powerboard->client->present()->email(),
// 'phone' => $this->powerboard->client->present()->phone(),
// 'type' => 'card',
'address_line1' => $this->powerboard->client->address1 ?? '',
'address_line2' => $this->powerboard->client->address2 ?? '',
'address_state' => $this->powerboard->client->state ?? '',
'address_country' => $this->powerboard->client->country->iso_3166_3 ?? '',
'address_city' => $this->powerboard->client->city ?? '',
'address_postcode' => $this->powerboard->client->postal_code ?? '',
];
return \App\Helpers\Sanitizer::removeBlanks($data);
}
private function storePaymentSource($request)
{
$this->powerboard->init();
$payment_source = $request->gateway_response;
$payload = array_merge($this->getCustomer(), [
'token' => $payment_source,
"vault_type" => "permanent",
'store_ccv' => true,
]);
nlog($payload);
$r = $this->powerboard->gatewayRequest('/v1/vault/payment_sources', (\App\Enum\HttpVerb::POST)->value, $payload, []);
if($r->failed())
return $this->powerboard->processInternallyFailedPayment($this->powerboard, $r->throw());
nlog($r->object());
$source = (new \App\PaymentDrivers\CBAPowerBoard\Models\Parse())->encode(PaymentSource ::class, $r->object()->resource->data);
return $source;
}
public function paymentData(array $data): array
{
if($this->cba_gateway->verification_status != "completed")
throw new PaymentFailed("This payment method is not configured as yet. Reference Powerboard portal for further information", 400);
$merge = [
'public_key' => $this->powerboard->company_gateway->getConfigField('publicKey'),
'widget_endpoint' => $this->powerboard->widget_endpoint,
'gateway' => $this->powerboard,
'environment' => $this->powerboard->environment,
'gateway_id' => $this->cba_gateway->_id,
];
return array_merge($data, $merge);
}
public function paymentView(array $data)
{
$data = $this->paymentData($data);
return render('gateways.powerboard.credit_card.pay', $data);
}
public function livewirePaymentView(array $data): string
{
return 'gateways.powerboard.credit_card.pay_livewire';
}
public function tokenBilling($request, $cgt, $client_present = false)
{
$payload = [
"amount" => $this->powerboard->payment_hash->data->amount_with_fee,
"currency" => $this->powerboard->client->currency()->code,
"customer" => [
"payment_source" => [
"vault_token" => $cgt->token,
"gateway_id" => $cgt->gateway_customer_reference
]
]
];
$r = $this->powerboard->gatewayRequest('/v1/charges', (\App\Enum\HttpVerb::POST)->value, $payload, []);
nlog($r->body());
if($r->failed()){
$error_payload = $this->getErrorFromResponse($r);
throw new PaymentFailed($error_payload[0], $error_payload[1]);
}
$charge = (new \App\PaymentDrivers\CBAPowerBoard\Models\Parse())->encode(Charge::class, $r->object()->resource->data) ?? $r->throw();
nlog($charge);
$this->powerboard->logSuccessfulGatewayResponse(['response' => $charge, 'data' => $this->powerboard->payment_hash], SystemLog::TYPE_POWERBOARD);
return $this->processSuccessfulPayment($charge);
}
private function get3dsToken(PaymentSource $source, $request)
{
$payment_hash = PaymentHash::query()->where('hash', $request->payment_hash)->first();
$browser_details = json_decode($request->browser_details,true);
$payload = [
"amount" => $payment_hash->data->amount_with_fee,
"currency" => $this->powerboard->client->currency()->code,
"description" => $this->powerboard->getDescription(),
"customer" => [
"payment_source" => [
"vault_token" => $source->vault_token,
"gateway_id" => $this->powerboard->settings()->getGatewayId(GatewayType::CREDIT_CARD),
],
],
"_3ds" => [
"browser_details" => $browser_details,
],
];
nlog($payload);
$r = $this->powerboard->gatewayRequest('/v1/charges/3ds', (\App\Enum\HttpVerb::POST)->value, $payload, []);
if ($r->failed()) {
return $this->processUnsuccessfulPayment($r);
}
$charge = $r->json();
return response()->json($charge['resource']['data'], 200);
}
public function paymentResponse(PaymentResponseRequest $request)
{
nlog($request->all());
$this->powerboard->payment_hash->data = array_merge((array) $this->powerboard->payment_hash->data, ['response' => $request->all()]);
$this->powerboard->payment_hash->save();
$payload = [];
/** Token Payment */
if($request->input('token', false))
{
$cgt = $this->powerboard
->client
->gateway_tokens()
->where('company_gateway_id', $this->powerboard->company_gateway->id)
->where('token', $request->token)
->first();
return $this->tokenBilling($request, $cgt, true);
}
elseif($request->browser_details)
{
$payment_source = $this->storePaymentSource($request);
nlog($payment_source);
return $this->get3dsToken($payment_source, $request);
}
elseif($request->charge) {
$charge_request = json_decode($request->charge, true);
nlog($charge_request);
$payload = [
'_3ds' => [
'id' => $charge_request['charge_3ds_id'],
],
"amount"=> $this->powerboard->payment_hash->data->amount_with_fee, //@phpstan-ignore-line
"currency"=> $this->powerboard->client->currency()->code,
"store_cvv"=> true,
];
$r = $this->powerboard->gatewayRequest("/v1/charges", (\App\Enum\HttpVerb::POST)->value, $payload, []);
if($r->failed())
return $this->processUnsuccessfulPayment($r);
$charge = (new \App\PaymentDrivers\CBAPowerBoard\Models\Parse())->encode(Charge::class, $r->object()->resource->data) ?? $r->throw();
nlog($charge);
if ($charge->status == 'complete') {
$this->powerboard->logSuccessfulGatewayResponse(['response' => $charge, 'data' => $this->powerboard->payment_hash], SystemLog::TYPE_POWERBOARD);
$vt = $charge->customer->payment_source->vault_token;
if($request->store_card){
$data = [
"payment_source" => [
"vault_token" => $vt,
],
];
$customer = $this->powerboard->customer()->findOrCreateCustomer($data);
$cgt = $this->powerboard->customer()->storePaymentMethod($charge->customer->payment_source, $charge->customer);
}
return $this->processSuccessfulPayment($charge);
}
elseif($charge->error){
$this->powerboard->logUnsuccessfulGatewayResponse($charge, SystemLog::TYPE_POWERBOARD);
throw new PaymentFailed($charge->error->message, $charge->status);
}
}
}
public function processSuccessfulPayment(Charge $charge)
{
$data = [
'payment_type' => PaymentType::CREDIT_CARD_OTHER,
'amount' => $this->powerboard->payment_hash->data->amount_with_fee,
'transaction_reference' => $charge->_id,
'gateway_type_id' => GatewayType::CREDIT_CARD,
];
$payment = $this->powerboard->createPayment($data, Payment::STATUS_COMPLETED);
SystemLogger::dispatch(
['response' => $charge, 'data' => $data],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_POWERBOARD,
$this->powerboard->client,
$this->powerboard->client->company,
);
if ($payment->invoices()->whereHas('subscription')->exists()) {
$subscription = $payment->invoices()->first()->subscription;
if ($subscription && array_key_exists('return_url', $subscription->webhook_configuration) && strlen($subscription->webhook_configuration['return_url']) >= 1) {
return redirect($subscription->webhook_configuration['return_url']);
}
}
return redirect()->route('client.payments.show', ['payment' => $payment->hashed_id]);
}
private function getErrorFromResponse($response)
{
try {
$response->throw();
} catch (RequestException $exception) {
$error_object = $exception->response->object();
$this->powerboard->logUnsuccessfulGatewayResponse($error_object, SystemLog::TYPE_POWERBOARD);
$error_message = "Unknown error";
match($error_object->error->code) {
"GatewayError" => $error_message = $error_object->error->message,
"UnfulfilledCondition" => $error_message = $error_object->error->message,
"transaction_declined" => $error_message = $error_object->error->details[0]->status_code_description,
default => $error_message = $error_object->error->message ?? "Unknown error",
};
return [$error_message, $exception->getCode()];
}
}
public function processUnsuccessfulPayment($response)
{
$error_payload = $this->getErrorFromResponse($response);
return response()->json(['message' => $error_payload[0], 'code' => $error_payload[1]], $error_payload[1]);
}
}

View File

@ -0,0 +1,186 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\PaymentDrivers\CBAPowerBoard;
use App\Helpers\Sanitizer;
use App\Models\ClientGatewayToken;
use App\PaymentDrivers\CBAPowerBoard\Models\Customer as ModelsCustomer;
use App\PaymentDrivers\CBAPowerBoard\Models\PaymentSource;
use App\PaymentDrivers\CBAPowerBoardPaymentDriver;
class Customer
{
public function __construct(public CBAPowerBoardPaymentDriver $powerboard)
{
}
public function findOrCreateCustomer(array $customer_data): mixed
{
$token = $this->powerboard
->client
->gateway_tokens()
->whereNotNull('gateway_customer_reference')
->where('company_gateway_id', $this->powerboard->company_gateway->id)
->first();
if($token && $customer = $this->getCustomer($token->gateway_customer_reference)){
return (new \App\PaymentDrivers\CBAPowerBoard\Models\Parse())->encode(ModelsCustomer::class, $customer->resource->data);
}
if($customer = $this->findCustomer())
return (new \App\PaymentDrivers\CBAPowerBoard\Models\Parse())->encode(ModelsCustomer::class, $customer);
return $this->createCustomer($customer_data);
}
public function getCustomer(string $id): mixed
{
$uri = "/v1/customers/{$id}";
$r = $this->powerboard->gatewayRequest($uri, (\App\Enum\HttpVerb::GET)->value, [], []);
nlog($r->json());
if($r->successful())
return $r->object();
return false;
}
public function findCustomer(): mixed
{
$uri = '/v1/customers';
$query = [
'reference' => $this->powerboard->client->client_hash,
];
$r = $this->powerboard->gatewayRequest($uri, (\App\Enum\HttpVerb::GET)->value, $query, []);
$search_results = $r->object();
nlog($search_results);
$customers = $search_results->resource->data;
return reset($customers); // returns first element or false
}
public function createCustomer(array $data = []): object
{
$payload = [
'company_name' => $this->powerboard->client->present()->name(),
'first_name' => $this->powerboard->client->present()->first_name(),
'last_name' => $this->powerboard->client->present()->first_name(),
'email' => $this->powerboard->client->present()->email(),
'reference' => $this->powerboard->client->client_hash,
// 'phone' => $this->powerboard->client->present()->phone(),
];
$payload = array_merge($payload, $data);
$payload = Sanitizer::removeBlanks($payload);
nlog($payload);
$uri = "/v1/customers";
$r = $this->powerboard->gatewayRequest($uri, (\App\Enum\HttpVerb::POST)->value, $payload, []);
if($r->failed())
$r->throw();
return (new \App\PaymentDrivers\CBAPowerBoard\Models\Parse())->encode(ModelsCustomer::class, $r->object()->resource->data) ?? $r->throw();
}
public function storePaymentMethod(?PaymentSource $payment_source = null, ?ModelsCustomer $customer = null): ClientGatewayToken
{
/** @var PaymentSource $source */
$source = $payment_source ? $payment_source : end($customer->payment_sources);
$payment_meta = new \stdClass();
$payment_meta->exp_month = (string) $source->expire_month;
$payment_meta->exp_year = (string) $source->expire_year;
$payment_meta->brand = (string) $source->card_scheme;
$payment_meta->last4 = (string) $source->card_number_last4;
$payment_meta->gateway_id = $source->gateway_id ?? null;
$payment_meta->type = \App\Models\GatewayType::CREDIT_CARD;
$data = [
'payment_meta' => $payment_meta,
'token' => $source->vault_token,
'payment_method_id' => \App\Models\GatewayType::CREDIT_CARD,
];
$cgt = $this->powerboard->storeGatewayToken($data, ['gateway_customer_reference' => $source->gateway_id]);
return $cgt;
}
public function addTokenToCustomer(string $token, ModelsCustomer $customer): mixed
{
nlog("add token to customer");
$uri = "/v1/customers/{$customer->_id}";
$payload = [
'payment_source' => [
'vault_token' => $token,
]
];
$r = $this->powerboard->gatewayRequest($uri, (\App\Enum\HttpVerb::POST)->value, $payload, []);
if($r->failed()){
nlog($r->body());
return $r->throw();
}
nlog($r->object());
$customer = (new \App\PaymentDrivers\CBAPowerBoard\Models\Parse())->encode(ModelsCustomer::class, $r->object()->resource->data);
$source = collect($customer->payment_sources)->first(function (PaymentSource $source) use ($token){
return $token == $source->vault_token;
});
nlog("i found the source");
nlog($source);
$cgt = $this->powerboard
->client
->gateway_tokens()
->where('token', $token)
->first();
nlog($cgt->id);
$meta = $cgt->meta;
$meta->gateway_id = $source->gateway_id;
$cgt->meta = $meta;
$cgt->save();
return $r->object();
}
}

View File

@ -0,0 +1,84 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\PaymentDrivers\CBAPowerBoard\Models;
class Charge
{
/** @var ?string */
public ?string $external_id;
/** @var ?string */
public ?string $_id;
/** @var ?string */
public ?string $created_at;
/** @var ?string */
public ?string $updated_at;
/** @var ?string */
public ?string $remittance_date;
/** @var ?string */
public ?string $company_id;
/** @var float */
public float $amount;
/** @var ?string */
public ?string $currency;
/** @var ?int */
public ?int $__v;
/** @var Transaction[] */
public array $transactions;
/** @var ?bool */
public ?bool $one_off;
/** @var ?bool */
public ?bool $archived;
/** @var Customer */
public Customer $customer;
/** @var ?bool */
public ?bool $capture;
/** @var ?string */
public? string $status;
/** @var ?array */
public ?array $items;
public function __construct(
?string $external_id,
?string $_id,
?string $created_at,
?string $updated_at,
?string $remittance_date,
?string $company_id,
float $amount,
?string $currency,
?int $__v,
array $transactions,
?bool $one_off,
?bool $archived,
Customer $customer,
?bool $capture,
?string $status,
?array $items,
) {
$this->external_id = $external_id;
$this->_id = $_id;
$this->created_at = $created_at;
$this->updated_at = $updated_at;
$this->remittance_date = $remittance_date;
$this->company_id = $company_id;
$this->amount = $amount;
$this->currency = $currency;
$this->__v = $__v;
$this->transactions = $transactions;
$this->one_off = $one_off;
$this->archived = $archived;
$this->customer = $customer;
$this->capture = $capture;
$this->status = $status;
$this->items = $items;
}
}

View File

@ -0,0 +1,85 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\PaymentDrivers\CBAPowerBoard\Models;
class Customer
{
/** @var ?string */
public ?string $_id;
/** @var ?string */
public ?string $_source_ip_address;
/** @var ?string */
public ?string $first_name;
/** @var ?string */
public ?string $last_name;
/** @var ?string */
public ?string $email;
/** @var ?string */
public ?string $reference;
/** @var ?string */
public ?string $default_source;
/** @var ?string */
public ?string $status;
/** @var ?bool */
public ?bool $archived;
/** @var ?string */
public ?string $created_at;
/** @var ?string */
public ?string $updated_at;
/** @var ?bool */
public ?bool $_check_expire_date;
/** @var ?PaymentSource[] */
public ?array $payment_sources;
/** @var ?PaymentSource */
public ?PaymentSource $payment_source;
/** @var ?array */
public ?array $payment_destinations;
/** @var ?string */
public ?string $company_id;
public function __construct(
?string $_id,
?string $_source_ip_address,
?string $first_name,
?string $last_name,
?string $email,
?string $reference,
?string $default_source,
?string $status,
?bool $archived,
?string $created_at,
?string $updated_at,
?bool $_check_expire_date,
?array $payment_sources,
?array $payment_destinations,
?string $company_id,
?PaymentSource $payment_source
) {
$this->_id = $_id;
$this->_source_ip_address = $_source_ip_address;
$this->first_name = $first_name;
$this->last_name = $last_name;
$this->email = $email;
$this->reference = $reference;
$this->default_source = $default_source;
$this->status = $status;
$this->archived = $archived;
$this->created_at = $created_at;
$this->updated_at = $updated_at;
$this->_check_expire_date = $_check_expire_date;
$this->payment_sources = $payment_sources;
$this->payment_destinations = $payment_destinations;
$this->company_id = $company_id;
$this->payment_source = $payment_source;
}
}

View File

@ -0,0 +1,56 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\PaymentDrivers\CBAPowerBoard\Models;
class Gateway
{
/** @var string */
public string $_id;
/** @var string */
public string $name;
/** @var string */
public string $type;
/** @var string */
public string $mode;
/** @var string */
public string $created_at;
/** @var string */
public string $updated_at;
/** @var bool */
public bool $archived;
/** @var bool */
public bool $default;
/** @var string */
public string $verification_status;
public function __construct(
string $_id,
string $name,
string $type,
string $mode,
string $created_at,
string $updated_at,
bool $archived,
bool $default,
string $verification_status
) {
$this->_id = $_id;
$this->name = $name;
$this->type = $type;
$this->mode = $mode;
$this->created_at = $created_at;
$this->updated_at = $updated_at;
$this->archived = $archived;
$this->default = $default;
$this->verification_status = $verification_status;
}
}

View File

@ -0,0 +1,70 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\PaymentDrivers\CBAPowerBoard\Models;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter;
class Parse
{
public function __construct()
{
}
public function encode($object_type, $document)
{
$phpDocExtractor = new PhpDocExtractor();
$reflectionExtractor = new ReflectionExtractor();
// list of PropertyTypeExtractorInterface (any iterable)
$typeExtractors = [$reflectionExtractor,$phpDocExtractor];
// list of PropertyDescriptionExtractorInterface (any iterable)
$descriptionExtractors = [$phpDocExtractor];
// list of PropertyInitializableExtractorInterface (any iterable)
$propertyInitializableExtractors = [$reflectionExtractor];
$propertyInfo = new PropertyInfoExtractor(
$propertyInitializableExtractors,
$descriptionExtractors,
$typeExtractors,
);
$classMetadataFactory = new ClassMetadataFactory(new AttributeLoader());
$metadataAwareNameConverter = new MetadataAwareNameConverter($classMetadataFactory);
$normalizer = new ObjectNormalizer($classMetadataFactory, $metadataAwareNameConverter, null, $propertyInfo);
$normalizers = [new DateTimeNormalizer(), $normalizer, new ArrayDenormalizer()];
$encoders = [new JsonEncoder()];
$serializer = new Serializer($normalizers, $encoders);
$data = $serializer->deserialize(json_encode($document), $object_type, 'json', [\Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer::SKIP_NULL_VALUES => true]);
return $data;
}
}

View File

@ -0,0 +1,96 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\PaymentDrivers\CBAPowerBoard\Models;
class PaymentSource
{
/** @var ?string */
public ?string $_id;
/** @var string */
public string $type;
/** @var string */
public string $vault_token;
/** @var string */
public string $card_name;
/** @var string */
public string $card_number_bin;
/** @var string */
public string $card_number_last4;
/** @var string */
public string $card_scheme;
/** @var string|null */
public ?string $address_line1;
/** @var string|null */
public ?string $address_line2;
/** @var string|null */
public ?string $address_city;
/** @var string|null */
public ?string $address_country;
/** @var string|null */
public ?string $address_state;
/** @var int */
public int $expire_month;
/** @var int */
public int $expire_year;
/** @var ?string */
public ?string $status;
/** @var ?string */
public ?string $created_at;
/** @var ?string */
public ?string $updated_at;
/** @var ?string */
public ?string $vault_type;
/** @var ?string */
public ?string $gateway_id;
public function __construct(
?string $_id,
string $type,
string $vault_token,
string $card_name,
string $card_number_bin,
string $card_number_last4,
string $card_scheme,
?string $address_line1,
?string $address_line2,
?string $address_city,
?string $address_country,
?string $address_state,
int $expire_month,
int $expire_year,
?string $status,
?string $created_at,
?string $updated_at,
?string $vault_type,
?string $gateway_id
) {
$this->_id = $_id;
$this->type = $type;
$this->vault_token = $vault_token;
$this->card_name = $card_name;
$this->card_number_bin = $card_number_bin;
$this->card_number_last4 = $card_number_last4;
$this->card_scheme = $card_scheme;
$this->address_line1 = $address_line1;
$this->address_line2 = $address_line2;
$this->address_city = $address_city;
$this->address_country = $address_country;
$this->address_state = $address_state;
$this->expire_month = $expire_month;
$this->expire_year = $expire_year;
$this->status = $status;
$this->created_at = $created_at;
$this->updated_at = $updated_at;
$this->vault_type = $vault_type;
$this->gateway_id = $gateway_id;
}
}

View File

@ -0,0 +1,18 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\PaymentDrivers\CBAPowerBoard\Models;
class PaymentSources
{
/** @var \App\PaymentDrivers\CBAPowerBoard\Models\PaymentSources[] */
public array $payment_sources;
}

View File

@ -0,0 +1,79 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\PaymentDrivers\CBAPowerBoard\Models;
class Threeds
{
public function __construct(public ?string $token){}
}
class Transaction
{
public ?Threeds $_3ds;
public ?string $gateway_specific_code;
public ?string $gateway_specific_description;
public ?string $error_message;
public ?string $error_code;
public ?string $status_code;
public ?string $status_code_description;
public ?string $type;
public ?string $status;
public float $amount;
public ?string $currency;
public ?string $_id;
public ?string $created_at;
public ?string $updated_at;
public ?string $processed_at;
public ?string $external_id;
public ?string $external_reference;
public ?string $authorization_code;
public function __construct(
?Threeds $_3ds,
?string $gateway_specific_code,
?string $gateway_specific_description,
?string $error_message,
?string $error_code,
?string $status_code,
?string $status_code_description,
?string $type,
?string $status,
float $amount,
?string $currency,
?string $_id,
?string $created_at,
?string $updated_at,
?string $processed_at,
?string $external_id,
?string $external_reference,
?string $authorization_code
) {
$this->_3ds = $_3ds;
$this->gateway_specific_code = $gateway_specific_code;
$this->gateway_specific_description = $gateway_specific_description;
$this->error_message = $error_message;
$this->error_code = $error_code;
$this->status_code = $status_code;
$this->status_code_description = $status_code_description;
$this->type = $type;
$this->status = $status;
$this->amount = $amount;
$this->currency = $currency;
$this->_id = $_id;
$this->created_at = $created_at;
$this->updated_at = $updated_at;
$this->processed_at = $processed_at;
$this->external_id = $external_id;
$this->external_reference = $external_reference;
$this->authorization_code = $authorization_code;
}
}

View File

@ -0,0 +1,96 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\PaymentDrivers\CBAPowerBoard;
use App\PaymentDrivers\CBAPowerBoard\Models\Gateways;
use App\PaymentDrivers\CBAPowerBoard\Models\Gateway;
use App\PaymentDrivers\CBAPowerBoardPaymentDriver;
class Settings
{
protected const GATEWAY_CBA = 'MasterCard';
protected const GATEWAY_AFTERPAY = 'Afterpay';
protected const GATEWAY_PAYPAL = 'Paypal';
protected const GATEWAY_ZIP = 'Zipmoney';
public function __construct(public CBAPowerBoardPaymentDriver $powerboard)
{
}
public function getGateways()
{
$r = $this->powerboard->gatewayRequest('/v1/gateways', (\App\Enum\HttpVerb::GET)->value, [], []);
if($r->failed())
$r->throw();
nlog($r->object());
return (new \App\PaymentDrivers\CBAPowerBoard\Models\Parse())->encode(Gateway::class."[]", $r->object()->resource->data);
}
/** We will need to have a process that updates this at intervals */
public function updateSettings():self
{
$gateways = $this->getGateways();
$settings = $this->powerboard->company_gateway->getSettings();
$settings->gateways = $gateways;
$this->powerboard->company_gateway->setSettings($settings);
return $this;
}
public function getSettings(): mixed
{
return $this->powerboard->company_gateway->getSettings();
}
public function getPaymentGatewayConfiguration(int $gateway_type_id): mixed
{
$type = self::GATEWAY_CBA;
match($gateway_type_id){
\App\Models\GatewayType::CREDIT_CARD => $type = self::GATEWAY_CBA,
default => $type = self::GATEWAY_CBA,
};
return $this->getGatewayByType($type);
}
private function getGatewayByType(string $gateway_type_const): mixed
{
$settings = $this->getSettings();
if(!property_exists($settings,'gateways')){
$this->updateSettings();
$settings = $this->getSettings();
}
$gateways = (new \App\PaymentDrivers\CBAPowerBoard\Models\Parse())->encode(Gateway::class."[]", $settings->gateways);
return collect($gateways)->first(function (Gateway $gateway) use ($gateway_type_const){
return $gateway->type == $gateway_type_const;
});
}
public function getGatewayId(int $gateway_type_id): string
{
$gateway = $this->getPaymentGatewayConfiguration($gateway_type_id);
return $gateway->_id;
}
}

View File

@ -0,0 +1,222 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\PaymentDrivers;
use App\Models\Invoice;
use App\Models\Payment;
use App\Models\SystemLog;
use App\Utils\HtmlEngine;
use App\Models\GatewayType;
use App\Models\PaymentHash;
use App\Models\PaymentType;
use App\Jobs\Util\SystemLogger;
use App\Utils\Traits\MakesHash;
use App\Models\ClientGatewayToken;
use Illuminate\Support\Facades\Http;
use App\PaymentDrivers\CBAPowerBoard\CreditCard;
use App\PaymentDrivers\CBAPowerBoard\Customer;
use App\PaymentDrivers\CBAPowerBoard\Settings;
/**
* Class CBAPowerBoardPaymentDriver.
*/
class CBAPowerBoardPaymentDriver extends BaseDriver
{
use MakesHash;
public $token_billing = true;
public $can_authorise_credit_card = false;
public $refundable = true;
public string $api_endpoint = 'https://api.powerboard.commbank.com.au';
public string $widget_endpoint = 'https://widget.powerboard.commbank.com.au/sdk/latest/widget.umd.min.js';
public string $environment = 'production_cba';
public const SYSTEM_LOG_TYPE = SystemLog::TYPE_POWERBOARD;
public static $methods = [
GatewayType::CREDIT_CARD => CreditCard::class,
];
/**
* Returns the gateway types.
*/
public function gatewayTypes(): array
{
$types = [];
if ($this->client
&& isset($this->client->country)
&& in_array($this->client->country->iso_3166_3, ['AUS'])
&& in_array($this->client->currency()->code, ['AUD'])
) {
$types[] = GatewayType::CREDIT_CARD;
}
return $types;
}
public function init(): self
{
if($this->company_gateway->getConfigField('testMode')) {
$this->widget_endpoint = 'https://widget.preproduction.powerboard.commbank.com.au/sdk/latest/widget.umd.min.js';
$this->api_endpoint = 'https://api.preproduction.powerboard.commbank.com.au';
$this->environment = 'preproduction_cba';
}
return $this;
}
public function setPaymentMethod($payment_method_id)
{
$class = self::$methods[$payment_method_id];
$this->payment_method = new $class($this);
return $this;
}
/**
* Proxy method to pass the data into payment method authorizeView().
*
* @param array $data
* @return \Illuminate\Http\RedirectResponse|mixed
*/
public function authorizeView(array $data)
{
$this->init();
return $this->payment_method->authorizeView($data);
}
/**
* Processes the gateway response for credit card authorization.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\RedirectResponse|mixed
*/
public function authorizeResponse($request)
{
return $this->payment_method->authorizeResponse($request);
}
/**
* View for displaying custom content of the driver.
*
* @param array $data
* @return mixed
*/
public function processPaymentView($data)
{
$this->init();
return $this->payment_method->paymentView($data);
}
/**
* Processing method for payment. Should never be reached with this driver.
*
* @return mixed
*/
public function processPaymentResponse($request)
{
return $this->payment_method->paymentResponse($request);
}
/**
* Detach payment method from custom payment driver.
*
* @param ClientGatewayToken $token
* @return bool
*/
public function detach(ClientGatewayToken $token): bool
{
// Driver doesn't support this feature.
return true;
}
public function refund(Payment $payment, $amount, $return_client_response = false)
{
}
public function processWebhookRequest($request)
{
}
public function getClientRequiredFields(): array
{
return [];
}
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
{
}
public function importCustomers()
{
}
public function auth(): bool
{
$this->init();
return true;
// try {
// $this->verifyConnect();
// return true;
// } catch(\Exception $e) {
// }
// return false;
}
public function gatewayRequest(string $uri, string $verb, array $payload, array $headers = [])
{
$this->init();
$r = Http::withHeaders($this->getHeaders($headers))
->{$verb}($this->api_endpoint.$uri, $payload);
nlog($r->body());
return $r;
}
public function getHeaders(array $headers = []): array
{
return array_merge([
'x-user-secret-key' => $this->company_gateway->getConfigField('secretKey'),
'Content-Type' => 'application/json',
],
$headers);
}
public function customer(): Customer
{
return new Customer($this);
}
public function settings(): Settings
{
return new Settings($this);
}
}

View File

@ -0,0 +1,50 @@
<?php
use App\Models\Gateway;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Model::unguard();
$fields = new \stdClass();
$fields->publicKey = '';
$fields->secretKey = '';
$fields->testMode = false;
$fields->threeds = false;
$powerboard = new Gateway();
$powerboard->id = 64;
$powerboard->name = 'CBA PowerBoard';
$powerboard->provider = 'CBAPowerBoard';
$powerboard->key = 'b67581d804dbad1743b61c57285142ad';
$powerboard->sort_order = 4543;
$powerboard->is_offsite = false;
$powerboard->visible = true;
$powerboard->fields = json_encode($fields);
$powerboard->save();
Schema::table("company_gateways", function (\Illuminate\Database\Schema\Blueprint $table){
$table->text('settings')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
//
}
};

View File

@ -88,7 +88,8 @@ class PaymentLibrariesSeeder extends Seeder
['id' => 60, 'name' => 'PayPal REST', 'provider' => 'PayPal_Rest', 'key' => '80af24a6a691230bbec33e930ab40665', 'fields' => '{"clientId":"","secret":"","signature":"","testMode":false}'],
['id' => 61, 'name' => 'PayPal Platform', 'provider' => 'PayPal_PPCP', 'key' => '80af24a6a691230bbec33e930ab40666', 'fields' => '{"testMode":false}'],
['id' => 62, 'name' => 'BTCPay', 'provider' => 'BTCPay', 'key' => 'vpyfbmdrkqcicpkjqdusgjfluebftuva', 'fields' => '{"btcpayUrl":"", "apiKey":"", "storeId":"", "webhookSecret":""}'],
['id' => 63, 'name' => 'Rotessa', 'is_offsite' => false, 'sort_order' => 22, 'provider' => 'Rotessa', 'key' => '91be24c7b792230bced33e930ac61676', 'fields' => '{"apiKey":"", "testMode":""}'],
['id' => 63, 'name' => 'Rotessa', 'is_offsite' => false, 'sort_order' => 22, 'provider' => 'Rotessa', 'key' => '91be24c7b792230bced33e930ac61676', 'fields' => '{"apiKey":"", "testMode":false}'],
['id' => 64, 'name' => 'CBA PowerBoard', 'is_offsite' => false, 'sort_order' => 26, 'provider' => 'CBAPowerBoard', 'key' => 'b67581d804dbad1743b61c57285142ad', 'fields' => '{"publicKey":"", "secretKey":"", "testMode":false, "Threeds":true}'],
];
foreach ($gateways as $gateway) {

View File

@ -93,7 +93,7 @@ inset: 6px;
document.getElementById("gateway_response").value =JSON.stringify( data );
formData = JSON.stringify(Object.fromEntries(new FormData(document.getElementById("server_response")))),
fetch('{{ route('client.payments.response') }}', {
method: 'POST',
headers: {

View File

@ -0,0 +1,244 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'Credit card', 'card_title' => 'Credit card'])
@section('gateway_head')
<meta name="instant-payment" content="yes" />
@endsection
@section('gateway_content')
<form action="{{ route('client.payment_methods.store', ['method' => App\Models\GatewayType::CREDIT_CARD]) }}" method="post" id="server-response">
@csrf
<input type="hidden" name="gateway_response">
<input type="hidden" name="company_gateway_id" value="{{ $gateway->getCompanyGatewayId() }}">
<input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}">
<input type="hidden" name="browser_details">
<input type="hidden" name="charge">
<button type="submit" class="hidden" id="stub">Submit</button>
</form>
<div class="alert alert-failure mb-4" hidden id="errors"></div>
<div id="powerboard-payment-container" class="w-full">
<div id="widget" style="block"></div>
<div id="widget-3dsecure"></div>
</div>
@component('portal.ninja2020.gateways.includes.pay_now', ['id' => 'authorize-card'])
{{ ctrans('texts.add_payment_method') }}
@endcomponent
@endsection
@section('gateway_footer')
<style>
iframe {
border: 0;
width: 100%;
height: 400px;
}
</style>
<script src="{{ $widget_endpoint }}"></script>
<script>
var widget = new cba.HtmlWidget('#widget', '{{ $public_key }}', '{{ $gateway_id }}');
widget.setEnv("{{ $environment }}");
widget.useAutoResize();
// widget.interceptSubmitForm('#server-response');
widget.onFinishInsert('input[name="gateway_response"]', "payment_source");
widget.load();
widget.trigger('tab', function (data){
console.log("tab Response", data);
console.log(widget.isValidForm());
let payNow = document.getElementById('pay-now');
payNow.disabled = widget.isInvalidForm();
});
widget.trigger('submit_form',function (data){
console.log("submit_form Response", data);
console.log(widget.isValidForm());
let payNow = document.getElementById('pay-now');
payNow.disabled = widget.isInvalidForm();
});
widget.trigger('tab',function (data){
console.log("tab Response", data);
console.log(widget.isValidForm());
let payNow = document.getElementById('pay-now');
payNow.disabled = widget.isInvalidForm();
});
widget.on("systemError", function(data) {
console.log("systemError Response", data);
});
widget.on("validationError", function(data) {
console.log("validationError", data);
});
widget.on("finish", async function(data) {
document.getElementById('errors').hidden = true;
console.log("finish", data);
try {
const resource = await get3dsToken();
console.log("3DS Token:", resource);
console.log("pre canvas");
console.log(resource._3ds.token);
var canvas = new cba.Canvas3ds('#widget-3dsecure', resource._3ds.token);
canvas.load();
let widget = document.getElementById('widget');
widget.classList.add('hidden');
} catch (error) {
console.error("Error fetching 3DS Token:", error);
}
canvas.on("chargeAuthSuccess", function(data) {
console.log(data);
document.querySelector(
'input[name="browser_details"]'
).value = null;
document.querySelector(
'input[name="charge"]'
).value = JSON.stringify(data);
document.getElementById('server-response').submit();
});
canvas.on("chargeAuthReject", function(data) {
console.log(data);
document.getElementById('errors').textContent = `Sorry, your transaction could not be processed...`;
document.getElementById('errors').hidden = false;
});
canvas.load();
});
widget.on("submit", async function (data){
console.log("submit");
console.log(data);
document.getElementById('errors').hidden = true;
})
widget.on('form_submit', function (data) {
console.log("form_submit", data);
console.log(data);
});
widget.on('submit', function (data) {
console.log("submit", data);
console.log(data);
});
widget.on('tab', function (data) {
console.log("tab", data);
console.log(data);
});
let payNow = document.getElementById('authorize-card');
payNow.addEventListener('click', () => {
payNow.disabled = true;
payNow.querySelector('svg').classList.remove('hidden');
payNow.querySelector('span').classList.add('hidden');
document.getElementById('server-response').submit();
});
async function get3dsToken() {
const browserDetails = {
name: navigator.userAgent.substring(0, 100), // The full user agent string, which contains the browser name and version
java_enabled: navigator.javaEnabled() ? "true" : "false", // Indicates if Java is enabled in the browser
language: navigator.language || navigator.userLanguage, // The browser language
screen_height: window.screen.height.toString(), // Screen height in pixels
screen_width: window.screen.width.toString(), // Screen width in pixels
time_zone: (new Date().getTimezoneOffset() * -1).toString(), // Timezone offset in minutes (negative for behind UTC)
color_depth: window.screen.colorDepth.toString() // Color depth in bits per pixel
};
document.querySelector(
'input[name="browser_details"]'
).value = JSON.stringify(browserDetails);
const formData = JSON.stringify(Object.fromEntries(new FormData(document.getElementById("server-response"))));
try {
// Return the fetch promise to handle it externally
const response = await fetch('{{ route('client.payment_methods.store', ['method' => App\Models\GatewayType::CREDIT_CARD]) }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
"X-Requested-With": "XMLHttpRequest",
"Accept": 'application/json',
"X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content
},
body: formData
})
if (!response.ok) {
return await response.json().then(errorData => {
throw new Error(errorData.message ?? 'Unknown error.');
});
}
return await response.json()
}
catch(error) {
document.getElementById('errors').textContent = `Sorry, your card could not be authorized...\n\n${error.message}`;
document.getElementById('errors').hidden = false;
console.error('Fetch error:', error); // Log error for debugging
throw error; //
}
}
const first = document.querySelector('input[name="payment-type"]');
if (first) {
first.click();
}
</script>
@endsection

View File

@ -0,0 +1,140 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'Credit card', 'card_title' => 'Credit card'])
@section('gateway_head')
<meta name="instant-payment" content="yes" />
@endsection
@section('gateway_content')
<form action="{{ route('client.payment_methods.store', ['method' => App\Models\GatewayType::CREDIT_CARD]) }}" method="post" id="server-response">
@csrf
<input type="hidden" name="gateway_response">
<input type="hidden" name="company_gateway_id" value="{{ $gateway->getCompanyGatewayId() }}">
<input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}">
<input type="hidden" name="charge_no3d">
<button type="submit" class="hidden" id="stub">Submit</button>
</form>
<div class="alert alert-failure mb-4" hidden id="errors"></div>
<div id="powerboard-payment-container" class="w-full">
<div id="widget" style="block"></div>
</div>
@component('portal.ninja2020.gateways.includes.pay_now', ['id' => 'authorize-card'])
{{ ctrans('texts.add_payment_method') }}
@endcomponent
@endsection
@section('gateway_footer')
<style>
iframe {
border: 0;
width: 100%;
height: 400px;
}
</style>
<script src="{{ $widget_endpoint }}"></script>
<script>
var widget = new cba.HtmlWidget('#widget', '{{ $public_key }}', '{{ $gateway_id }}');
widget.setEnv("{{ $environment }}");
widget.useAutoResize();
widget.interceptSubmitForm('#server-response');
widget.onFinishInsert('input[name="gateway_response"]', "payment_source");
widget.load();
widget.trigger('tab', function (data){
console.log("tab Response", data);
console.log(widget.isValidForm());
let payNow = document.getElementById('pay-now');
payNow.disabled = widget.isInvalidForm();
});
widget.trigger('submit_form',function (data){
console.log("submit_form Response", data);
console.log(widget.isValidForm());
let payNow = document.getElementById('pay-now');
payNow.disabled = widget.isInvalidForm();
});
widget.trigger('tab',function (data){
console.log("tab Response", data);
console.log(widget.isValidForm());
let payNow = document.getElementById('pay-now');
payNow.disabled = widget.isInvalidForm();
});
widget.on("systemError", function(data) {
console.log("systemError Response", data);
});
widget.on("validationError", function(data) {
console.log("validationError", data);
});
widget.on("finish", async function(data) {
document.getElementById('errors').hidden = true;
console.log("finish", data);
});
widget.on("submit", async function (data){
console.log("submit");
console.log(data);
document.getElementById('errors').hidden = true;
})
widget.on('form_submit', function (data) {
console.log("form_submit", data);
console.log(data);
});
widget.on('submit', function (data) {
console.log("submit", data);
console.log(data);
});
widget.on('tab', function (data) {
console.log("tab", data);
console.log(data);
});
let payNow = document.getElementById('authorize-card');
payNow.addEventListener('click', () => {
document.querySelector(
'input[name="charge_no3d"]'
).value = true;
payNow.disabled = true;
payNow.querySelector('svg').classList.remove('hidden');
payNow.querySelector('span').classList.add('hidden');
document.getElementById('stub').click();
});
</script>
@endsection

View File

@ -0,0 +1,337 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'Credit card', 'card_title' => 'Credit card'])
@section('gateway_head')
<meta name="instant-payment" content="yes" />
@endsection
@section('gateway_content')
<form action="javascript:void(0);" id="stepone">
<input type="hidden" name="gateway_response">
<button type="submit" class="hidden" id="stepone_submit">Submit</button>
</form>
<form action="{{ route('client.payments.response') }}" method="post" id="server-response">
@csrf
<input type="hidden" name="gateway_response">
<input type="hidden" name="store_card" id="store_card"/>
<input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
<input type="hidden" name="company_gateway_id" value="{{ $gateway->getCompanyGatewayId() }}">
<input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}">
<input type="hidden" name="token">
<input type="hidden" name="browser_details">
<input type="hidden" name="charge">
<button type="submit" class="hidden" id="stub">Submit</button>
</form>
<div class="alert alert-failure mb-4" hidden id="errors"></div>
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.payment_type')])
{{ ctrans('texts.credit_card') }}
@endcomponent
@include('portal.ninja2020.gateways.includes.payment_details')
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')])
<ul class="list-none">
@if(count($tokens) > 0)
@foreach($tokens as $token)
<li class="py-2 cursor-pointer">
<label class="mr-4">
<input
type="radio"
data-token="{{ $token->token }}"
name="payment-type"
class="form-check-input text-indigo-600 rounded-full cursor-pointer toggle-payment-with-token toggle-payment-with-token"/>
<span class="ml-1 cursor-pointer">**** {{ $token->meta?->last4 }}</span>
</label>
</li>
@endforeach
@endisset
<li class="py-2 cursor-pointer">
<label>
<input
type="radio"
id="toggle-payment-with-credit-card"
class="form-check-input text-indigo-600 rounded-full cursor-pointer"
name="payment-type"
checked/>
<span class="ml-1 cursor-pointer">{{ __('texts.new_card') }}</span>
</label>
</li>
</ul>
@endcomponent
<div id="powerboard-payment-container" class="w-full">
<div id="widget" style="block" class="hidden"></div>
<div id="widget-3dsecure"></div>
</div>
@include('portal.ninja2020.gateways.includes.save_card')
@include('portal.ninja2020.gateways.includes.pay_now')
@endsection
@section('gateway_footer')
<style>
iframe {
border: 0;
width: 100%;
height: 400px;
}
</style>
<script src="{{ $widget_endpoint }}"></script>
<script>
/** Loads the widget, links the widget to a dummy form */
var widget = new cba.HtmlWidget('#widget', '{{ $public_key }}', '{{ $gateway_id }}');
widget.setEnv("{{ $environment }}");
widget.useAutoResize();
widget.interceptSubmitForm('#stepone');
widget.onFinishInsert('#server-response input[name="gateway_response"]', "payment_source");
widget.load();
widget.on("systemError", function(data) {
console.log("systemError Response", data);
});
widget.on("validationError", function(data) {
console.log("validationError", data);
});
/** Retrieves the payment_source token and passes this to the 3ds handler */
widget.on("finish", function(data) {
document.getElementById('errors').hidden = true;
console.log("finish");
console.log(data);
process3ds();
});
widget.on("submit", function (data){
console.log("submit");
console.log(data);
document.getElementById('errors').hidden = true;
})
widget.on('form_submit', function (data) {
console.log("form_submit", data);
console.log(data);
});
widget.on('tab', function (data) {
console.log("tab", data);
console.log(data);
});
let payNow = document.getElementById('pay-now');
payNow.addEventListener('click', () => {
const div = document.getElementById('widget');
widget.getValidationState();
if(!widget.isValidForm() && div.offsetParent !== null){
console.log("invalid");
payNow.disabled = false;
payNow.querySelector('svg').classList.add('hidden');
payNow.querySelector('span').classList.remove('hidden');
return;
}
payNow.disabled = true;
payNow.querySelector('svg').classList.remove('hidden');
payNow.querySelector('span').classList.add('hidden');
let storeCard = document.querySelector(
'input[name=token-billing-checkbox]:checked'
);
if (storeCard) {
document.getElementById('store_card').value = storeCard.value;
}
if(div.offsetParent !== null)
document.getElementById('stepone_submit').click();
else
document.getElementById('server-response').submit();
});
async function process3ds()
{
try {
const resource = await get3dsToken();
console.log("3DS Token:", resource);
console.log("pre canvas");
console.log(resource._3ds.token);
if (resource.status == "not_authenticated" || resource.status == "authentication_not_supported") { // if status = authentication_not_supported, there will be no resource._3ds.token, so we need to fall back to a NON 3ds payment flow from here
throw new Error('There was an issue authenticating this payment method.');
return;
}
var canvas = new cba.Canvas3ds('#widget-3dsecure', resource._3ds.token);
canvas.load();
let widget = document.getElementById('widget');
widget.classList.add('hidden');
} catch (error) {
console.error("Error fetching 3DS Token:", error);
document.getElementById('errors').textContent = `Sorry, your transaction could not be processed...\n\n${error}`;
document.getElementById('errors').hidden = false;
return;
}
canvas.on("chargeAuthSuccess", function(data) {
console.log(data);
document.querySelector(
'input[name="browser_details"]'
).value = null;
document.querySelector(
'input[name="charge"]'
).value = JSON.stringify(data);
let storeCard = document.querySelector(
'input[name=token-billing-checkbox]:checked'
);
if (storeCard) {
document.getElementById('store_card').value = storeCard.value;
}
document.getElementById('server-response').submit();
});
canvas.on("chargeAuthReject", function(data) {
console.log(data);
document.getElementById('errors').textContent = `Sorry, your transaction could not be processed...`;
document.getElementById('errors').hidden = false;
});
canvas.load();
}
async function get3dsToken() {
const browserDetails = {
name: navigator.userAgent.substring(0, 100), // The full user agent string, which contains the browser name and version
java_enabled: navigator.javaEnabled() ? "true" : "false", // Indicates if Java is enabled in the browser
language: navigator.language || navigator.userLanguage, // The browser language
screen_height: window.screen.height.toString(), // Screen height in pixels
screen_width: window.screen.width.toString(), // Screen width in pixels
time_zone: (new Date().getTimezoneOffset() * -1).toString(), // Timezone offset in minutes (negative for behind UTC)
color_depth: window.screen.colorDepth.toString() // Color depth in bits per pixel
};
document.querySelector(
'input[name="browser_details"]'
).value = JSON.stringify(browserDetails);
const formData = JSON.stringify(Object.fromEntries(new FormData(document.getElementById("server-response"))));
try {
// Return the fetch promise to handle it externally
const response = await fetch('{{ route('client.payments.response') }}', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
"X-Requested-With": "XMLHttpRequest",
"Accept": 'application/json',
"X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content
},
body: formData
})
if (!response.ok) {
return await response.json().then(errorData => {
throw new Error(errorData.message ?? 'Unknown error.');
});
// const text = await response.text();
// throw new Error(`Network response was not ok: ${response.statusText}. Response text: ${text}`);
}
return await response.json()
}
catch(error) {
document.getElementById('errors').textContent = `Sorry, your transaction could not be processed...\n\n${error.message}`;
document.getElementById('errors').hidden = false;
console.error('Fetch error:', error); // Log error for debugging
throw error; //
}
}
const first = document.querySelector('input[name="payment-type"]');
if (first) {
first.click();
}
document
.getElementById('toggle-payment-with-credit-card')
.addEventListener('click', (element) => {
let widget = document.getElementById('widget');
widget.classList.remove('hidden');
document.getElementById('save-card--container').style.display ='grid';
document.querySelector('input[name=token]').value = '';
});
Array.from(
document.getElementsByClassName('toggle-payment-with-token')
).forEach((element) =>
element.addEventListener('click', (element) => {
document
.getElementById('widget')
.classList.add('hidden');
document.getElementById(
'save-card--container'
).style.display = 'none';
document.querySelector('input[name=token]').value =
element.target.dataset.token;
})
);
</script>
@endsection