Tokenization for paypal advanced cards

This commit is contained in:
David Bomba 2024-04-22 14:38:29 +10:00
parent 45f378eb9d
commit 8d5b8e2319
6 changed files with 259 additions and 45 deletions

View File

@ -194,6 +194,7 @@ class Gateway extends StaticModel
GatewayType::PAYPAL => ['refund' => false, 'token_billing' => false],
GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => false],
GatewayType::VENMO => ['refund' => false, 'token_billing' => false],
GatewayType::PAYPAL_ADVANCED_CARDS => ['refund' => false, 'token_billing' => true],
// GatewayType::SEPA => ['refund' => false, 'token_billing' => false],
// GatewayType::BANCONTACT => ['refund' => false, 'token_billing' => false],
// GatewayType::EPS => ['refund' => false, 'token_billing' => false],
@ -207,6 +208,7 @@ class Gateway extends StaticModel
GatewayType::PAYPAL => ['refund' => false, 'token_billing' => false],
GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => false],
GatewayType::VENMO => ['refund' => false, 'token_billing' => false],
GatewayType::PAYPAL_ADVANCED_CARDS => ['refund' => false, 'token_billing' => true],
// GatewayType::SEPA => ['refund' => false, 'token_billing' => false],
// GatewayType::BANCONTACT => ['refund' => false, 'token_billing' => false],
// GatewayType::EPS => ['refund' => false, 'token_billing' => false],

View File

@ -91,6 +91,8 @@ class GatewayType extends StaticModel
public const PAYLATER = 28;
public const PAYPAL_ADVANCED_CARDS = 29;
public function gateway()
{
return $this->belongsTo(Gateway::class);
@ -158,6 +160,9 @@ class GatewayType extends StaticModel
return ctrans('texts.mybank');
case self::PAYLATER:
return ctrans('texts.paypal_paylater');
case self::PAYPAL_ADVANCED_CARDS:
return ctrans('texts.credit_card');
default:
return ' ';
}

View File

@ -399,3 +399,64 @@ class PayPalWebhook implements ShouldQueue
}
}
*/
/** token created
* {
"id":"WH-1KN88282901968003-82E75604WM969463F",
"event_version":"1.0",
"create_time":"2022-08-15T14:13:48.978Z",
"resource_type":"payment_token",
"resource_version":"3.0",
"event_type":"VAULT.PAYMENT-TOKEN.CREATED",
"summary":"A payment token has been created.",
"resource":{
"time_created":"2022-08-15T07:13:48.964PDT",
"links":[
{
"href":"https://api-m.sandbox.paypal.com/v3/vault/payment-tokens/9n6724m",
"rel":"self",
"method":"GET",
"encType":"application/json"
},
{
"href":"https://api-m.sandbox.paypal.com/v3/vault/payment-tokens/9n6724m",
"rel":"delete",
"method":"DELETE",
"encType":"application/json"
}
],
"id":"9n6724m",
"payment_source":{
"card":{
"last_digits":"1111",
"brand":"VISA",
"expiry":"2027-02",
"billing_address":{
"address_line_1":"2211 N First Street",
"address_line_2":"17.3.160",
"admin_area_2":"San Jose",
"admin_area_1":"CA",
"postal_code":"95131",
"country_code":"US"
}
}
},
"customer":{
"id":"695922590"
}
},
"links":[
{
"href":"https://api-m.sandbox.paypal.com/v1/notifications/webhooks-events/WH-1KN88282901968003-82E75604WM969463F",
"rel":"self",
"method":"GET"
},
{
"href":"https://api-m.sandbox.paypal.com/v1/notifications/webhooks-events/WH-1KN88282901968003-82E75604WM969463F/resend",
"rel":"resend",
"method":"POST"
}
]
}
*/

View File

@ -52,6 +52,7 @@ class PayPalRestPaymentDriver extends BaseDriver
3 => 'paypal',
1 => 'card',
25 => 'venmo',
29 => 'paypal_advanced_cards',
// 9 => 'sepa',
// 12 => 'bancontact',
// 17 => 'eps',
@ -117,6 +118,7 @@ class PayPalRestPaymentDriver extends BaseDriver
"3" => $method = PaymentType::PAYPAL,
"25" => $method = PaymentType::VENMO,
"28" => $method = PaymentType::PAY_LATER,
"29" => $method = PaymentType::CREDIT_CARD_OTHER,
};
return $method;
@ -208,6 +210,10 @@ return render('gateways.paypal.pay', $data);
}
public function processTokenPayment(array $response) {
}
public function processPaymentResponse($request)
{
@ -216,6 +222,9 @@ return render('gateways.paypal.pay', $data);
$request['gateway_response'] = str_replace("Error: ", "", $request['gateway_response']);
$response = json_decode($request['gateway_response'], true);
if(isset($response['gateway_response']['token']) && strlen($response['gateway_response']['token']) > 2)
return $this->processTokenPayment($response);
// nlog($response);
//capture
$orderID = $response['orderID'];
@ -272,6 +281,8 @@ return render('gateways.paypal.pay', $data);
if(isset($response['status']) && $response['status'] == 'COMPLETED' && isset($response['purchase_units'])) {
nlog($response->json());
$data = [
'payment_type' => $this->getPaymentMethod($request->gateway_type_id),
'amount' => $response['purchase_units'][0]['payments']['captures'][0]['amount']['value'],
@ -281,8 +292,40 @@ return render('gateways.paypal.pay', $data);
$payment = $this->createPayment($data, \App\Models\Payment::STATUS_COMPLETED);
if ($request->has('store_card') && $request->input('store_card') === true) {
$payment_source = $response->json()['payment_source'];
if(isset($payment_source['card']) && ($payment_source['card']['attributes']['vault']['status'] ?? false) && $payment_source['card']['attributes']['vault']['status'] == 'VAULTED'){
$last4 = $payment_source['card']['last_digits'];
$expiry = $payment_source['card']['expiry']; //'2025-01'
$expiry_meta = explode('-', $expiry);
$brand = $payment_source['card']['brand'];
$payment_meta = new \stdClass();
$payment_meta->exp_month = $expiry_meta[1] ?? '';
$payment_meta->exp_year = $expiry_meta[0] ?? $expiry;
$payment_meta->brand = $brand;
$payment_meta->last4 = $last4;
$payment_meta->type = GatewayType::CREDIT_CARD;
$token = $payment_source['card']['attributes']['vault']['id']; // 09f28652d01257021
$gateway_customer_reference = $payment_source['card']['attributes']['vault']['customer']['id']; //rbTHnLsZqE;
$data['token'] = $token;
$data['payment_method_id'] = GatewayType::PAYPAL_ADVANCED_CARDS;
$data['payment_meta'] = $payment_meta;
$data['payment_method_id'] = GatewayType::CREDIT_CARD;
$additional['gateway_customer_reference'] = $gateway_customer_reference;
$this->storeGatewayToken($data, $additional);
}
}
SystemLogger::dispatch(
['response' => $response, 'data' => $data],
['response' => $response->json(), 'data' => $data],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_PAYPAL,
@ -329,32 +372,40 @@ return render('gateways.paypal.pay', $data);
private function getPaymentSource(): array
{
if($this->gateway_type_id == 1) {
//@todo - roll back here for advanced payments vs hosted card fields.
if($this->gateway_type_id == GatewayType::PAYPAL_ADVANCED_CARDS) {
return [
"card" => [
"attributes" => [
"verification" => [
"method" => "SCA_WHEN_REQUIRED", //SCA_ALWAYS
// "method" => "SCA_ALWAYS", //SCA_ALWAYS
],
"vault" => [
"store_in_vault" => "ON_SUCCESS", //must listen to this webhook - VAULT.PAYMENT-TOKEN.CREATED webhook.
],
],
"name" => $this->client->present()->primary_contact_name(),
"email_address" => $this->client->present()->email(),
"address" => [
"address_line_1" => $this->client->address1,
"address_line_2" => $this->client->address2,
"admin_area_2" => $this->client->city,
"admin_area_1" => $this->client->state,
"postal_code" => $this->client->postal_code,
"country_code" => $this->client->country->iso_3166_2,
],
"experience_context" => [
"user_action" => "PAY_NOW"
],
"experience_context" => [
"shipping_preference" => "SET_PROVIDED_ADDRESS"
],
// "name" => $this->client->present()->primary_contact_name(),
// "email_address" => $this->client->present()->email(),
// "address" => [
// "address_line_1" => $this->client->address1,
// "address_line_2" => $this->client->address2,
// "admin_area_2" => $this->client->city,
// "admin_area_1" => $this->client->state,
// "postal_code" => $this->client->postal_code,
// "country_code" => $this->client->country->iso_3166_2,
// ],
// "experience_context" => [
// "user_action" => "PAY_NOW"
// ],
"stored_credential" => [
"payment_initiator" => "MERCHANT", //"CUSTOMER" who initiated the transaction?
"payment_type" => "UNSCHEDULED",
// "payment_initiator" => "MERCHANT", //"CUSTOMER" who initiated the transaction?
"payment_initiator" => "CUSTOMER", //"" who initiated the transaction?
"payment_type" => "UNSCHEDULED", //UNSCHEDULED
"usage"=> "DERIVED",
],
],
@ -406,9 +457,9 @@ return render('gateways.paypal.pay', $data);
"custom_id" => $this->payment_hash->hash,
"description" => ctrans('texts.invoice_number') . '# ' . $invoice->number,
"invoice_id" => $invoice->number,
"payment_instruction" => [
"disbursement_mode" => "INSTANT",
],
// "payment_instruction" => [
// "disbursement_mode" => "INSTANT",
// ],
$this->getShippingAddress(),
"amount" => [
"value" => (string) $data['amount_with_fee'],
@ -440,6 +491,8 @@ return render('gateways.paypal.pay', $data);
$order['purchase_units'][0]["shipping"] = $shipping;
}
nlog($order);
$r = $this->gatewayRequest('/v2/checkout/orders', 'post', $order);
// nlog($r->json());
@ -463,7 +516,11 @@ return render('gateways.paypal.pay', $data);
],
]
: null;
: [
"name" => [
"full_name" => $this->client->present()->name()
]
];
}

View File

@ -5296,6 +5296,7 @@ $lang = array(
'rappen_rounding' => 'Rappen Rounding',
'rappen_rounding_help' => 'Round amount to 5 cents',
'assign_group' => 'Assign group',
'paypal_advanced_cards' => 'Advanced Card Payments',
);
return $lang;

View File

@ -12,26 +12,48 @@
<input type="hidden" name="gateway_type_id" id="gateway_type_id" value="{{ $gateway_type_id }}">
<input type="hidden" name="gateway_response" id="gateway_response">
<input type="hidden" name="amount_with_fee" id="amount_with_fee" value="{{ $total['amount_with_fee'] }}"/>
<input type="hidden" name="store_card" id="store_card">
<input type="hidden" name="token" value="" id="token">
</form>
@include('portal.ninja2020.gateways.includes.payment_details')
<div class="alert alert-failure mb-4" hidden id="errors"></div>
<div id="paypal-button-container" class="paypal-button-container"></div>
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')])
@if (count($tokens) > 0)
@foreach ($tokens as $token)
<label class="mr-4">
<input type="radio" data-token="{{ $token->token }}" name="payment-type"
class="form-radio cursor-pointer toggle-payment-with-token" />
<span class="ml-1 cursor-pointer">**** {{ $token->meta?->last4 }}</span>
</label>
@endforeach
@endisset
<label>
<input type="radio" id="toggle-payment-with-credit-card" class="form-radio cursor-pointer" name="payment-type"
checked />
<span class="ml-1 cursor-pointer">{{ __('texts.new_card') }}</span>
</label>
@endcomponent
<div id="checkout-form">
<!-- Containers for Card Fields hosted by PayPal -->
<!-- <div id="card-name-field-container"></div> -->
<!-- Containers for Card Fields hosted by PayPal -->
<div id="card-number-field-container"></div>
<div class="expcvv" style="display:flex;">
<div id="card-expiry-field-container" style="width:50%"></div>
<div id="card-cvv-field-container" style="width:50%"></div>
</div>
<!-- <button id="card-field-submit-button" type="button">
{{ ctrans('texts.pay_now') }}
</button> -->
@include('portal.ninja2020.gateways.includes.pay_now')
@include('portal.ninja2020.gateways.includes.save_card')
</div>
@include('portal.ninja2020.gateways.includes.pay_now')
@endsection
@section('gateway_footer')
@ -70,14 +92,32 @@
},
onApprove: function(data, actions) {
const { liabilityShift, orderID } = data;
if(liabilityShift) {
/* Handle liability shift. More information in 3D Secure response parameters */
// console.log("inside liability shift")
// console.log(liabilityShift)
// console.log(orderID);
// console.log(data);
//doesn't really do anything as failure is linked in SUBMIT. We only hit here after a successful return
//and where SCA is optional?
}
var errorDetail = Array.isArray(data.details) && data.details[0];
if (errorDetail && ['INSTRUMENT_DECLINED', 'PAYER_ACTION_REQUIRED'].includes(errorDetail.issue)) {
return actions.restart();
}
console.log("on approve");
console.log(data);
console.log(actions);
// console.log("on approve");
// console.log(data);
// console.log(actions);
let storeCard = document.querySelector('input[name=token-billing-checkbox]:checked');
if (storeCard) {
document.getElementById("store_card").value = storeCard.value;
}
document.getElementById("gateway_response").value = JSON.stringify( data );
document.getElementById("server_response").submit();
@ -88,16 +128,15 @@
window.location.href = "/client/invoices/";
},
// onError: function(error) {
onError: function(error) {
// console.log("on error")
// console.log(error);
document.getElementById('errors').textContent = `Sorry, your transaction could not be processed...\n\n${error.message}`;
document.getElementById('errors').hidden = false;
// document.getElementById('errors').textContent = `Sorry, your transaction could not be processed...\n\n${error.message}`;
// document.getElementById('errors').hidden = false;
// // document.getElementById("gateway_response").value = error;
// // document.getElementById("server_response").submit();
// },
},
onClick: function (){
}
@ -110,7 +149,7 @@
const numberField = cardField.NumberField({
inputEvents: {
onChange: (event)=> {
console.log("returns a stateObject", event);
// console.log("returns a stateObject", event);
}
},
});
@ -119,7 +158,7 @@
const cvvField = cardField.CVVField({
inputEvents: {
onChange: (event)=> {
console.log("returns a stateObject", event);
// console.log("returns a stateObject", event);
}
},
});
@ -128,7 +167,7 @@
const expiryField = cardField.ExpiryField({
inputEvents: {
onChange: (event)=> {
console.log("returns a stateObject", event);
// console.log("returns a stateObject", event);
}
},
});
@ -142,16 +181,20 @@
document.getElementById('pay-now').disabled = true;
document.querySelector('#pay-now > svg').classList.remove('hidden');
document.querySelector('#pay-now > svg').classList.add('justify-center');
document.querySelector('#pay-now > svg').classList.add('mx-auto');
document.querySelector('#pay-now > svg').classList.add('item-center');
document.querySelector('#pay-now > span').classList.add('hidden');
cardField.submit().then((response) => {
console.log("then");
console.log(response);
// lets goooo
// console.log("then");
// console.log(response);
}).catch((error) => {
console.log(error);
// console.log("catch error")
// console.log(error);
document.getElementById('pay-now').disabled = false;
document.querySelector('#pay-now > svg').classList.add('hidden');
@ -180,4 +223,49 @@
</script>
<script>
Array
.from(document.getElementsByClassName('toggle-payment-with-token'))
.forEach((element) => element.addEventListener('click', (e) => {
document
.getElementById('save-card--container').style.display = 'none';
document
.getElementById('checkout-form').style.display = 'none';
document
.getElementById('token').value = e.target.dataset.token;
}));
let payWithCreditCardToggle = document.getElementById('toggle-payment-with-credit-card');
if (payWithCreditCardToggle) {
payWithCreditCardToggle
.addEventListener('click', () => {
document
.getElementById('save-card--container').style.display = 'grid';
document
.getElementById('token').value = null;
});
}
let payNowButton = document.getElementById('pay-now');
if (payNowButton) {
payNowButton
.addEventListener('click', (e) => {
if (token) {
document.getElementById("token").value = token.value;
}
document.getElementById("gateway_response").value = JSON.stringify( data );
document.getElementById("server_response").submit();
});
}
</script>
@endpush