diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index 7932cb3a56af..f862caf91f7a 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -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], diff --git a/app/Models/GatewayType.php b/app/Models/GatewayType.php index 473838f4e2d7..f30d5c97cb14 100644 --- a/app/Models/GatewayType.php +++ b/app/Models/GatewayType.php @@ -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 ' '; } diff --git a/app/PaymentDrivers/PayPal/PayPalWebhook.php b/app/PaymentDrivers/PayPal/PayPalWebhook.php index 0d800d826de6..fa08542af0e1 100644 --- a/app/PaymentDrivers/PayPal/PayPalWebhook.php +++ b/app/PaymentDrivers/PayPal/PayPalWebhook.php @@ -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" + } + ] + } + */ \ No newline at end of file diff --git a/app/PaymentDrivers/PayPalRestPaymentDriver.php b/app/PaymentDrivers/PayPalRestPaymentDriver.php index cdad6a880ea6..047f02df1578 100644 --- a/app/PaymentDrivers/PayPalRestPaymentDriver.php +++ b/app/PaymentDrivers/PayPalRestPaymentDriver.php @@ -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() + ] + ]; } diff --git a/lang/en/texts.php b/lang/en/texts.php index e542e667e9d4..970d5f6c832a 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -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; diff --git a/resources/views/portal/ninja2020/gateways/paypal/ppcp/card.blade.php b/resources/views/portal/ninja2020/gateways/paypal/ppcp/card.blade.php index aea037e36670..dc40aa436bd8 100644 --- a/resources/views/portal/ninja2020/gateways/paypal/ppcp/card.blade.php +++ b/resources/views/portal/ninja2020/gateways/paypal/ppcp/card.blade.php @@ -12,26 +12,48 @@ + + + @include('portal.ninja2020.gateways.includes.payment_details') +
+ @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')]) + @if (count($tokens) > 0) + @foreach ($tokens as $token) + + @endforeach + @endisset + + + + @endcomponent +
- - +
- - @include('portal.ninja2020.gateways.includes.pay_now') + + @include('portal.ninja2020.gateways.includes.save_card')
+ @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 @@ + + + @endpush \ No newline at end of file