silverstripe / silverstripe-omnipay

Silverstripe integration with Omnipay PHP payments library.
BSD 3-Clause "New" or "Revised" License
39 stars 67 forks source link

PxPay Card Tokens #158

Open mattsagen opened 7 years ago

mattsagen commented 7 years ago

Hi there, thx for the awesome work. I am trying to use PaymentExpress_PxPay to store cards in offsite hosted gateway. I am not sure how to do this through the Silverstripe Wrapper as I can't find an example?

But before I spend time on it, I just want to be sure that after we have created a card through PxPay during a purchase, we can provide the amount, currency, and token via PxPay and not redirect the customer to the offsite gateway. It seems obvious that this is how it would work, otherwise tokens aren't much use (we are using for subscriptions and one-click purchases) but I want to make sure.

If this is correct use case, then could someone provide an example of create card call from within a SS Controller? I'm thinking that I might have to do away with the wrapper and just use the raw Omnipay classes like https://github.com/thephpleague/omnipay-paymentexpress/issues/1 but would rather have handy SS integration...

After succesfully doing normal test purchases I tried adding the EnableAddBillCard with a BillingID, then changing the transaction number, removing EnableAddBillCard, and trying again, but no luck.

$payment = Payment::create() ->init("PaymentExpress_PxPay", 100, "NZD") ->setSuccessUrl($this->Link()."/success") ->setFailureUrl($this->Link()."/cancelled"); // Save it to the database to generate an ID $payment->write(); $data = ['transactionId' => '125f8s3d','EnableAddBillCard' => 1,'BillingID' => 'xyz555']; $response = ServiceFactory::create() ->getService($payment, ServiceFactory::INTENT_PURCHASE) ->initiate($data); return $response->redirectOrRespond();

bummzack commented 7 years ago

It seems like you'd need to create a CC via PxPayGateway::createCard first. Then use the Token you get from this action for subsequent purchase requests. From what I can tell, you should use the PaymentExpress_PxPost Gateway for subsequent requests instead, as you don't want the user to be redirected anymore…

This is a gateway-specific method though and not part of the silverstripe-omnipay wrapper. You could implement this as a service, see https://github.com/silverstripe/silverstripe-omnipay#the-payment-services-and-service-factory

Custom service implementations can be registered via Config API, something like this:

ServiceFactory:
  services:
    createCard: 'Fullly\Qualified\ClassName\MyCreateCardService'

Then you can use: ServiceFactory::create()->getService($payment, 'createCard'); to get a Service instance.

I'm not familiar with the PxPay specification to give advice where to store the token though… Payment seems to be the wrong place. Maybe store them in a separate table?

TL/DR The createCard action of the PxPay Gateway isn't supported out of the box, but you can integrate it yourself and configure silverstripe-omnipay to seamlessly work with your code. PRs are welcome of course. Or you could start a separate Module that adds PxPay specific functionality to silverstripe-omnipay.

mattsagen commented 7 years ago

Thanks, Bummzack - So easy enough to set up a new service based on PurchaseService, and then to change the call to the gateway from purchase to createCard. Now a bit stuck on where to deal with the response to store the token from DPS - I thought I could just extend the onCaptured behaviour, but although I can see a payment status change to Captured, the event never fires... ` class ABCPayment extends DataExtension {

public function onCaptured($response) {

    $content = "Response: ";

    $fp = fopen($_SERVER['DOCUMENT_ROOT'] . "/text.txt","wb");

    fwrite($fp,$content);

    fclose($fp);

}

} `

I'm not using a Payable extension and hadn't really decided what datamodel to employ (considered just adding it to Payment, and wouldn't mind hearing your thoughts about why not to). Any idea why that never gets called, or any idea of a better place to do that from?

bummzack commented 7 years ago

I'd strongly suggest against using the purchase behavior, as Captured should be a status reserved to actual purchases. But I guess for initial testing it's fine… just consider moving to separate states later on (eg. PendingCardCreated and CardCreated or similar).

As previously stated: I'm not really familiar with the PxPay gateways. To me, it seems like you can just create a Card (eg. get a Token for a CC), without performing an actual purchase. Then you can use the token to authorize multiple Purchases. If that's the case, it might be better to store the token with the current Member? Otherwise you'd have to search for the most recent Payment for a Member that has a token to get the Token you need?

The onCaptured hook should fire (especially if the Payment status changed to Captured). Are you sure you applied the extension via config and did a dev/build?

mattsagen commented 7 years ago

Thanks again bummzack, I think maybe I overcomplicated my question.

I could have sworn I built after adding the extension, but I must not have, because it's firing thank goodness!

It makes total sense for us to use the purchase behaviour during the store card, which appears to be supported by DPS, because then the customer only ever sees an actual purchase for a real amount.

I can now see DpsBillingId coming back in the response, so I think all is well. Now I just need to get familiar with PxPost, because when I simply switched to the SS purchase service, and supplied the DpsBillingId, I got a "The number parameter is required" error... inching closer anyway :)

mattsagen commented 7 years ago

Ok, my custom create card service is working - last remaining issue is that I don't see any onCancelled() events firing - the onComplete($result) is sweet and is storing the card token and notifying our app (we run payments as a microservice) but I can't seem to get the onCancelled() which would be much preferred to trusting success/fail URL... it's on the same data extension as the working onComplete()... any known issues/gotchas?

bummzack commented 7 years ago

Wait… aren't you working with @cjsewell? He just implemented createCard, see #161.

The extension should be added to Payment and yes, onCancelled should work (when the payment was cancelled on the external payment form).

mattsagen commented 7 years ago

No, I didn't see that he was working on that at the same time... nearly identical ... but I have tried with his as well and I can't get the onCancelled() to ever be called - this is when clicking "Cancel" in the hosted payment page. It redirects to the failure URL, but no event. Our CustomPayment extension is correctly added to Payment, it extends DataExtension, and onComplete($response) works within the same class...

cjsewell commented 7 years ago

Hey @mattsagen

I had the same issue, this isnt a silverstripe-omnipay issue really. Its due to the fact that PXPay does not support a cancel url/event. And the main DPS Omnipay packge sets both the fail and success urls to the return url https://github.com/thephpleague/omnipay-paymentexpress/blob/master/src/Message/PxPayAuthorizeRequest.php#L196

It returns as complete, but with an error....

They only way to catch this as far as I can tell with PXPay is to use the updateServiceResponse extension hook and check the response yourself.

By testing, it appears when you cancel a payment, the response back from DPS sets the $cardHolderName to "User Cancelled" and the $responseCode to "RC"

SilverStripe\Omnipay\Service\PaymentService:
    extensions:
        - PaymentServiceExtension
class PaymentServiceExtension extends Extension
{

    function updateServiceResponse(SilverStripe\Omnipay\Service\ServiceResponse $response)
    {
        $dpsResponse = $response->getOmnipayResponse()->getData();
        if ($dpsResponse instanceof SimpleXMLElement) {
            $cardHolderName = (string) $dpsResponse->CardHolderName;
            $success        = $dpsResponse->Success == 1;
            $responseText   = (string) $dpsResponse->ResponseText;
            $responseCode   = (string) $dpsResponse->ReCo;
            if (!$success && $responseCode == 'RC' && $cardHolderName == 'User Cancelled') {
                // User canceled?
                Debug::dump('CardHolderName: '.$cardHolderName);
                Debug::dump('Success: '.($success ? 'true' : 'false'));
                Debug::dump('ResponseText: '.$responseText);
                Debug::dump('ReCo: '.$responseCode);
            }
        }
    }
}
bummzack commented 7 years ago

I'll look into this… is a "cancelled" payment really considered to be "successful" by the PxPay Gateway? It should at least be marked as erroneous?

Please note: All ServiceResponse objects are going through updateServiceResponse. When using updateServiceResponse always check: