mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-09 03:14:30 -04:00
Tokenization for paypal advanced cards
This commit is contained in:
parent
45f378eb9d
commit
8d5b8e2319
@ -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],
|
||||
|
@ -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 ' ';
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
*/
|
@ -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()
|
||||
]
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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> -->
|
||||
<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
|
Loading…
x
Reference in New Issue
Block a user