Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New API available - Accept.js #41

Open
judgej opened this issue Jun 20, 2016 · 49 comments
Open

New API available - Accept.js #41

judgej opened this issue Jun 20, 2016 · 49 comments

Comments

@judgej
Copy link
Member

judgej commented Jun 20, 2016

This new API makes use of JavaScript on the front end to build forms and (presumably) to avoid the need to send credit card numbers to your own server. It is an extension to the DPM API.

An example application is available here:

https://github.com/AuthorizeNet/accept-sample-app

I'm not sure what documentation is available apart from that. Authorize.Net are keen to help us incorporate this API into the gateway, so I'm opening this issue to log any discussions on what that may involve or any ideas or issues that anyone may have.

OmniPay has traditionally not got involved with the front end part of the UI, but most gateways are moving towards a very front-end centric approach (e.g. with "drop-in" forms generated through JavaScript, and card tokenisation APIs that can be used by the front end to avoid card numbers going anywhere near your server). How this fits into OmniPay 2.x or whether it should be something to expand OmniPay 3.0 with, is up for question. I personally don't have an answer for that at this time.

@judgej judgej changed the title New API available - Accept.JS New API available - Accept.js Jun 21, 2016
@judgej
Copy link
Member Author

judgej commented Jun 21, 2016

As an aside, AIM is now called "Payment Transactions". AIM looks to be a name being slowly phased out. I'm wondering if the old AIM should have been left untouched in this gateway driver, and a separate new gateway created for the XML upgrade. JSON is available in beta as an alternative to XML - would have been nice to have had the JSON first, as it is much easier to handle in PHP. All done now, I guess.

@lukeholder
Copy link

+1 to adding it as a new gateway driver and not replacing the previous AIM gateway with any new features.

@judgej
Copy link
Member Author

judgej commented Aug 17, 2016

Some details here:

http://developer.authorize.net/api/reference/features/acceptjs.html

If I understand it correctly, the AcceptJS interface sits in the web client only and just tokenises card details using AJAX. Those tokens can then be used in place of card numbers in the normal AIM gateway, which may need some small changes to accept a cut-down card detail (just a token, instead of all the card number/dates/CVV details).

I've done something similar for PAYONE - there is a back end server-to-server direct payments gateway (called "Server") that can take either full card details (with PCI compliance needed) or a card token. The card token is generated on the web client with the help of a separate gateway (called "Client") that helps to generate the tokenization form.

@delatbabel
Copy link
Contributor

There are a lot of gateways that are moving towards having JS interfaces that sit on the client side and do the card tokenisation there. It's a really bad idea for a whole bunch of reasons but it does make some sense from a PCI compliance viewpoint (although base PCI compliance for card processing without local storage is not overly complicated and something that every eCommerce store should be doing IMHO). Anyway, I won't debate the politics of it.

I agree with @lukeholder -- +1 to making anything that uses AcceptJS a separate gateway and not replacing the existing working code.

@barryvdh
Copy link
Member

Agreed

@Mark-H
Copy link
Contributor

Mark-H commented Feb 13, 2017

Assuming someone has set up their frontend forms and js to generate the "opaqueData", does anyone know where exactly to pass this into the gateways? I'm finding the Authorize.net docs rather vague, and so far I've only found this code in the example app, but I haven't found the related code in the omnipay driver... from what I've read, it sounds like it might tie in with the cardReference on the AIM gateway requests, but that does not seem to be translated into similar XML anywhere that I can find.

There's been talk about a new driver specific for accept.js in this issue, but it looks like that hasn't happened.

Does anyone have any pointers on where to go from here? It is supposed to be possible but just not documented, or is support for it unavailable?

@judgej
Copy link
Member Author

judgej commented Feb 13, 2017

Hi @Mark-H we'll get some example code together and get you working. I use the DPM API mostly, which POSTs direct to the gateway, and has a "nofify" redirect to feed the result back to your site. Is it SIM or AIM you are using, or DPM?

The first step is probably to raise a new issue specifically on getting a working example into the documentation. We can follow it up there. If it is Accept.JS you need, then that would be a bit more involved if we need to write a new gateway driver for it, but I guess it's on the wishlist, so maybe worth it if you don't have and extreme deadline. When I first created this issue, Accept.JS was in beta and had little documentation. I haven't checked it since, so hopefully it is more stable now.

@Mark-H
Copy link
Contributor

Mark-H commented Feb 13, 2017

Hi @judgej thanks for the reply :)

This is for a new ecommerce addon for a CMS. There's no pressing deadline, but I was hoping to integrate Auth.net into it within the next few weeks as it's a popular request. I've used SIM in the past, and I'm trying to avoid AIM as I don't want to force PCI compliance on users. DPM is a bit tricky to implement, as the app doesn't create a transaction record until a payment method is selected and posted back to the server, so I can't track the transaction status the same way other gateways are handled.

Accept.js seems like the most user-friendly offering from Auth.net at the moment (no off-site payment form, no raw credit card info on server) that fits the structure, so I've set my sights on that ;)

The Accept.js documentation is probably a lot better than it was back in June, but still somewhat basic, especially the final part which seems to require prior knowledge of Auth.net's APIs that I'm missing.

I'd be happy to contribute where I can, just a little lost in the auth.net woods. I'll see if I can figure out how it should work, and how to build an omnipay gateway in the next few days, but if that doesn't work out I'll gladly offer a bounty for you Jason or someone else to build support for Accept.js.

@judgej
Copy link
Member Author

judgej commented Feb 15, 2017

I'm hoping that once we work out the front end, you can then just use AIM for the back end, but using the tokenised card rather than the original card details. I'm trying to get an Accept.JS working to try it out, but the examples are not quite there yet (e.g. the submit button does not wait for the response to the card tokenising before submitting the form to the server, so there are some things to be worked out there). It seems that the merchant site needs to create its own JS functions for submitting the card details and then putting the result back into the form to be submitted. Other gateways tend to include that type of functionality, with some options to change IDs of form items etc. This is lower level, but still workable.

When I can get that working, we can see if the OmnaPay AIM gateway driver needs any tweaks to be able to accept a tokenised card.

@Mark-H
Copy link
Contributor

Mark-H commented Feb 15, 2017

Okay so I've managed to get the client-side done (I can try to clean it up a bit by getting rid of app-specific code, and submit it for some documentation) and also managed to get the transaction to go through with the following tweaks to Message/AIMAuthorizeRequest.php, in the method addPayment:

    protected function addPayment(\SimpleXMLElement $data)
    {
        if ($this->getDataDescriptor() && $this->getDataValue()) {
            $data->transactionRequest->payment->opaqueData->dataDescriptor = $this->getDataDescriptor();
            $data->transactionRequest->payment->opaqueData->dataValue = $this->getDataValue();
        }
        else {
            $this->validate('card');
            /** @var CreditCard $card */
            $card = $this->getCard();
            $card->validate();
            $data->transactionRequest->payment->creditCard->cardNumber = $card->getNumber();
            $data->transactionRequest->payment->creditCard->expirationDate = $card->getExpiryDate('my');
            $data->transactionRequest->payment->creditCard->cardCode = $card->getCvv();
        }
    }

and adding these methods (I added them to the AIMAuthorizeRequest, but could also go on AIMAbstractRequest):

    public function getDataDescriptor()
    {
        return $this->getParameter('dataDescriptor') ? $this->getParameter('dataDescriptor') : $this->httpRequest->request->get('dataDescriptor');
    }

    public function getDataValue()
    {
        return $this->getParameter('dataValue') ? $this->getParameter('dataValue') : $this->httpRequest->request->get('dataValue');
    }

This allows implementations to pass the dataDescriptor and dataValue in the authorize()/purchase() methods, or it gets them from POST values with the name dataDescriptor/dataValue directly. If neither are available, it uses AIM like it did before.

@judgej
Copy link
Member Author

judgej commented Feb 15, 2017

Looks great :-)

This is where I wish we had different payment types - card, tokenisedCard, PayPal, blank account, ApplePay, saved card etc. that could handle all these things invisibly. Something for a future OmnPay...

This dataDescriptor/getDataValue pair seem very low level, but I'm not sure how else it could be done at this time. The known dataDescriptor values at least should go into the AIMAbstractRequest as constants for easy and consistent use.

Another approach could be to support set/get cardReference methods and have that set both the dataDescriptor and getDataValue pairs. Separate methods to set/get applyPayToken and payPalHash (or whatever it is) would do similar things. Not sure, what do you think? Trying to get as close as possible to a "standard" approach that other gateways would take. The cardReference is certainly used to input a tokenised credit card in other gateway drivers, so that could make sense to use, but we may need to be able to distinguish between saved card tokens and the nonce we have here at some point.

@Mark-H
Copy link
Contributor

Mark-H commented Feb 15, 2017

It sure would be nice if all things were standardised, but I personally think sticking to the names provided by the gateway is best until that day comes.

  • Stripe expects a token parameter from its client-side tokenisation
  • PayPal Express expects a token parameter, or gets it directly from the query string
  • Paymill is similar to stripe, also expects the token to be provided as parameter.
  • Mollie looks for a transactionReference parameter, or gets the id POSTed to its notification url
  • Braintree seems to have both a paymentMethodToken + paymentMethodNone and a token parameter

Those are the gateways I've worked with recently. In the case of Accept.js it's kinda vague what the names should be, so I went with the "suggestion" in the last example:

    // This is where you would set the data descriptor & data value to be posted back to your server
    console.log(responseData.dataDescriptor);
    console.log(responseData.dataValue);

except they're written to a hidden input instead of logged in the console :)

The suggested implementation in my previous post does need the setters (ugh, and now I realise that's why I needed to get it from the request, because it lacked setters), but I'd go with setDataDescriptor and setDataValue instead of trying to make it too clever. People using the library will still need to build their front-end, so if they can use (where available, cough) official examples in doing so, that sounds like the most straight forward approach.

@Mark-H
Copy link
Contributor

Mark-H commented Feb 15, 2017

Here's a simplified version of my javascript, run it after the right js file is loaded and on dom ready. Note: I haven't tested it in this form, I just edited out the bits and pieces specific to my app.

var form = document.getElementById('my-payment-form'),  // Change ID to fit your form
    btns = form.getElementsByTagName('button');  // Maybe change to class based selector if you don't use semantic <button>s
    
form.addEventListener('submit', function(e) {
    e.preventDefault();
    // Disable the submit button to prevent repeated clicks
    for (var j = 0; j < btns.length; j++) {
        btns[j].setAttribute('disabled', true);
    }
    var secureData = {}, authData = {}, cardData = {};

    cardData.cardNumber = document.getElementById('cc-number').value; // Change ID to fit your form
    cardData.month = document.getElementById('cc-exp-month').value; // Change ID to fit your form
    cardData.year = document.getElementById('cc-exp-year').value; // Change ID to fit your form
    secureData.cardData = cardData;

    authData.clientKey = 'YOUR CLIENT KEY HERE';
    authData.apiLoginID = 'YOUR API LOGIN ID HERE';
    secureData.authData = authData;

    Accept.dispatchData(secureData, 'responseHandler');
    return false;
});

function responseHandler(response) {
    if (response.messages.resultCode === 'Error') {
        var msgs = [];
        for (var i = 0; i < response.messages.message.length; i++) {
            msgs.push(response.messages.message[i].code + ': ' + response.messages.message[i].text);
        }
        msgs = msgs.join('<br>');
        var errorContainer = form.querySelector('.errors');
        if (errorContainer.textContent !== undefined) {
            errorContainer.textContent = msgs;
        }
        else {
            errorContainer.innerText = msgs;
        }
        return;
    }

    // Add the hitten inputs with the dataDescriptor and dataValue
    var input = document.createElement('input');
    input.setAttribute('type', 'hidden');
    input.setAttribute('name', 'dataDescriptor');
    input.setAttribute('value', response.opaqueData.dataDescriptor);
    form.appendChild(input);

    var input2 = document.createElement('input');
    input2.setAttribute('type', 'hidden');
    input2.setAttribute('name', 'dataValue');
    input2.setAttribute('value', response.opaqueData.dataValue);
    form.appendChild(input2);

   // Submit the form
    form.submit();
};

@judgej
Copy link
Member Author

judgej commented Feb 15, 2017

Okay, agreed setDataDescriptor and setDataValue as gateway-specific setters. That would be a starting point and offer flexibility for other payment sources that we don't cater for now.

There is a mention of cardReference in the main docs here though that does seem more to be geared towards saved, reusable card tokens. There is still a little ambiguity over whether a temporary nonce is a Token or a CardReference. I guess we can sort that out afterwards - it's just a layer on top.

@judgej
Copy link
Member Author

judgej commented Feb 15, 2017

I've been trying similar front end code to yours, but always get error:

E_WC_15:An error occurred during processing. Please try again.

The docs say that means:

Please provide valid CVV.

I've tried adding the CVV (cardCode) but get the same error. Not sure if you hit this and got around it? Sorry, this is a bit of a tangent.

@Mark-H
Copy link
Contributor

Mark-H commented Feb 15, 2017

Hmm no I didn't get that error, but only now notice that the cvv isn't being added in my request code. Will try it out tomorrow.

@judgej
Copy link
Member Author

judgej commented Feb 15, 2017

Silly me - I was using my account login ID instead of the API login ID. Works fine now after swapping that over. The error message was a little misleading. Just realised also the dataDescriptor (COMMON.ACCEPT.INAPP.PAYMENT) is provided by Accept.JS along with the card token, so it is not something that the merchant account needs to know. I was thinking before that the dataDescriptor would need to be coded in somewhere.

@judgej
Copy link
Member Author

judgej commented Feb 15, 2017

Something has always bothered me about these submit handlers. Here, the submit is caught, the card is tokenised, and the handler getting the token response puts the result into the form, then submits the form. That is great, and works, so long as you do not have any other form validation on the form. If you do, and it's JavaScript powered, then it all gets skipped since the submit() will submit the form immediately and at the lowest level. The new SagePay JS front end does that too, and that one is very particular about the validation of all the other details that will be submitted (the billing address must be submitted with the purchase transaction and everything must be valid, and you get three grace attempts to submit, except you only get one error returned at a time, so had better hope that you don't have three errors in your address fields).

I'm wondering if the approach is to catch the submit, then if the card has already been tokenised, just return true so the remainder of any validation on the form can run, and then the form can submit to the server naturally. But if the card has not yet been tokenised, then do the tokenisation, use the asynchronous result to fill out the hidden fields, but then instead of directly submitting the form at this stage, go back and re-trigger the form submit action, e.g. the submit button. That way all events linked to the form can get their chance to run. I just don't know if that is possible. Any idea?

Edit 2: been playing with this, an come up with this solution, which seems to work as I wanted. Take a look and see if it is any use. Try submitting with an error in the email field, then correct it and submit again. Going to see if I can apply this to Sage Pay now, because their JS approach has been driving me mad. They use the preventDefault() function on the form, which is a bloody nightmare to work around, because any script issuing that function is now saying , "this form is MINE".


On top of this, if the form does submit with the tokenised card, then if the form fails any server-side validation, it would be nice to be able to get the form back for correction with the token intact and the credit card fields hidden so they don't need to be entered again. So many shops get this wrong - I've lost count of how uncomfortable it is entering my CC details multiple times because, say, the server did not like my postcode without a space in it. I've entered it once, godamnit, stop asking me to enter it again! :-) Sorry, ranting.

@judgej
Copy link
Member Author

judgej commented Feb 20, 2017

Just a thought: the tokenised card consists of two strings, e.g.:

array(3) {
  ["dataDescriptor"]=>
  string(27) "COMMON.ACCEPT.INAPP.PAYMENT"
  ["dataValue"]=>
  string(22) "9487554895312816104603"
}

As well as accepting them as two separate values (no argument there) I wonder if the driver should also be able to accept them as a single string? For example "COMMON.ACCEPT.INAPP.PAYMENT 9487554895312816104603" (with whatever field separator) or as a JSON string.

@judgej
Copy link
Member Author

judgej commented Feb 22, 2017

@Mark-H If you would like to submit a PR for the set/get methods, preferably with a test, that would be very useful. If not, I'll do one, since it's a small change and opens up a new gateway API nicely. The front end bit we can work on as a section on the README.

@felixmaier1989
Copy link
Contributor

Hey guys, I'm currently facing the situation, therefore I created PR #77

I'm still wondering if we should pass opaque data via setters

$request->setOpaqueDataDescriptor($data_descriptor);
$request->setOpaqueDataValue($data_value);

or via the request params:

$request = $gateway->purchase(
    [
        'notifyUrl' => 'https://www.perdu.com',
        'amount' => $amount,
	'dataDescriptor' => $data_descriptor,
	'dataValue' => $data_value,
    ]
);

As far as I know, Omnipay Stripe does support a token parameter when creating the request.

@judgej
Copy link
Member Author

judgej commented Mar 2, 2017

The card details (which is what the "opaque data" is) belongs in the request object rather then the gateway object. The gateway configuration data is for invariant settings - switching modes, authentication details, system-wide callback/notification URLs etc. So the first option is definitely the way to do it.

@Mark-H
Copy link
Contributor

Mark-H commented Mar 2, 2017

I'm not sure if I agree @judgej - I would definitely expect the second example @felixmaier1989 posted over the first one, which is also how I have it working in my dev site. Internally that might rely on such a setter, but passing in the dataDescriptor/Value in with the purchase() method should most definitely work. They seem to be the same thing, after another look.

Sorry for not responding about doing a PR. Was trying to find the time/energy to spin up a fork and figure out how to add tests to omnipay but it's been a rough week productivity wise.

@judgej
Copy link
Member Author

judgej commented Mar 2, 2017

Ah, yes, sorry you are right. I misread that. I thought one of those examples was passing the data into the gateway object, but looking again, they are both creating the request object. What I said was technically right, but totally out of context here - with the setters in place, both those approaches will work (though the second one needs the "opaque" prefix to map onto the setters, i.e.

$request = $gateway->purchase(
    [
        'notifyUrl' => 'https://www.perdu.com',
        'amount' => $amount,
	'opaqueDataDescriptor' => $data_descriptor,
	'opaqueDataValue' => $data_value,
    ]
);

@judgej
Copy link
Member Author

judgej commented Mar 3, 2017

Thanks all - I'm just adding a few notes to the README about this then I'll merge. It's all hard work for everyone trying to fit all this sharing into a day job and life in general, and everyone understands that :-)

judgej added a commit to academe/omnipay-authorizenet that referenced this issue Mar 3, 2017
@judgej
Copy link
Member Author

judgej commented Mar 3, 2017

A PR has been merged which allows the AIM gateway to accept tokenized card details in place of the card details in the CreditCard object. It seems, for the back-end OmniPay at least, this is all that is needed to support Accept.JS. There is obviously front-end details to this, and examples will be welcome to add to the documentation, but that functionality at least is out of scope for this gateway driver.

The method of giving this driver the card details (TWO values and not just one) is specific to this gateway. We can wrap those up in a method shared with other gateways next, but this gets people moving forward in the meantime. One thought is that the cardReference is used, and the two tokenization strings are just concatenated, or maybe JSON encoded together (which could give a lot of extra flexibility, though is a bit more clumsy).

The resulting card reference will be massive long though. I suspect it contains the CC details encrypted rather than just an index for a what is cached on the gateway, but that's just a wild guess.

So, thoughts on a wrapper function to make it fall in line with other gateways?

@adavidw
Copy link

adavidw commented Mar 7, 2017

Sorry to jump in and hijack this thread, but I'm Aaron (Developer Evangelist from Authorize.Net) and I wanted to clear up something in this comment from Jun 21:

As an aside, AIM is now called "Payment Transactions". AIM looks to be a name being slowly phased out. I'm wondering if the old AIM should have been left untouched in this gateway driver, and a separate new gateway created for the XML upgrade. JSON is available in beta as an alternative to XML - would have been nice to have had the JSON first, as it is much easier to handle in PHP. All done now, I guess.

AIM is not now being called "Payment Transactions" nor is it the same as anything in our current API. That's our fault, because in the upgrade guide you link to, there used to be some wording along the lines of "AIM is now called Payment Transactions". That's never been true, and I think the wording was due to some internal miscommunication and misunderstanding.

It's more correct to say that AIM is and always will be a name for an older API of ours that was just for straight payment type of transactions based on doing an HTTP post with a bunch of name value pairs. We then had other similar APIs (CIM, ARB, DPM, SIM) to do things like hosted payment forms, card on file profiles, subscriptions, etc.

Instead of maintaining a separate API for each type of interaction, we've supplanted all of these other APIs with one current API that serves all of those functions, and is hierarchical and extensible for future use. This current API can be used by sending requests as XML or JSON, but the parameter names and the hierarchy are the same in each. These older APIs like AIM are in some cases officially deprecated or EOL'ed, but in all cases will not be receiving any feature improvements.

That's apparent with Accept.js, which returns a token that can only be used in a payment transaction request to our current API. There's no way to use this token with AIM.

As to how this relates to this project, if we had been clear about this strategy from the start, perhaps you would have put the existing AIM implementation into maintenance mode and done the work for current XML/JSON requests in a separate project. Seems like you went the other way, so I'm really sorry about that.

Please accept my deepest apologies, and please allow me to make it up to you by offering any possible assistance. Anything you need me for, let me know.

@adavidw
Copy link

adavidw commented Mar 7, 2017

This doesn't even get into the window of time a few years ago where we were trying to brand the new XML API as "AIM XML" and the old one as "AIM NVP". Don't get me started on that.

@felixmaier1989
Copy link
Contributor

So in other words, to be consistent we should not maintain AIMGateway but develop a newer Omnipay gateway AuthorizeNetGateway (let's call it so). AuthorizeNetGateway would contain the AIM methods + Opaque data handling.

Assuming I'm right, technically, what we have to do is:

  • copying AIMGateway to AuthorizeNetGateway
  • copying the unit tests as well
  • reverting the Opaque Data handling in AIMGateway
  • and AIMGateway should not be maintained anymore

@adavidw
Copy link

adavidw commented Mar 8, 2017

Well, maybe. It really depends on what your strategy is and how much backward compatibility you want maintain, things like that. Also, depends on how much XML you're already using in the AIM gateway vs the form posts with the name value pairs. I haven't been through the code yet to see what's XML and what's just form posts to the old endpoint.

Also, I haven't seen whether this driver attempts to support other old APIs other than the payment transactions in the AIM API (like SIM or CIM or ARB). If this gateway is only supporting the AIM transactions, and the actual transactions requests are being converted to XML or are already XML, there's no reason to fork the project.

The XML format for the AIM API is what was expanded into the current XML/JSON API. So, any "AIM" transactions that are using the XML format of AIM (what we used to call "AIM XML") are already structured correctly for the current API, and already going to the right endpoint. So, if you've already converted to XML, or were planning to anyway, then you don't really need to change course or fork the project. You just probably would want to drop "AIM" from the name since you're now including things like the opaque data from the current API. In that scenario, there's probably little value in keeping an old "AIM-only" version of the project since there's nothing that AIM did by itself that the new API can't do.

That's my general thinking, but I could have more specific recommendations depending on what you've already done or are planning to do.

@judgej
Copy link
Member Author

judgej commented Mar 8, 2017

Thanks @adavidw for clearing that up. I only read yesterday that the new Payment Transactions API is what used to be called "AIM". I think that may have been updated now, as the migration page now says the Payment Transactions API includes the functions that AIM supplied. That makes more sense.

So on the opaque data (using Accept.JS), if I understand correctly you are saying that the card nonce collected will not work with the AIM API? I'm desperately looking for the documentation that led me down believing it worked this way, but it seems to have vanished - or I am going mad. Is this something we can fix here by simply using a different endpoint, assuming the same XML structure has been carried over from AIM to the new API?

Last question: how stable is the JSON API? I ask because some of the XML structures do not translate directly into JSON structures (repeating elements inside a parent element likely need to be implemented as arrays) and I'm not sure the documentation has taken this on board yet.

Thanks again. I've been thinking about starting a separate gateway driver anyway, so this kind of reinforces a need for that.

Edit: just reread above - the AIM API here is now XML based. Does that mean it is now fully compatible with the new API, without any changes?

@judgej
Copy link
Member Author

judgej commented Mar 8, 2017

Okay, just to quell my panic, I've completed an end-to-end test script. Have a play with it here:

https://acadweb.co.uk/authorizenet/authorize-ajs.php

This uses the latest OmniPay Authorize.Net AIM driver (with XML) and Accept.JS I've just exposed the credit card form, and when you submit it, it will pay £10 to a test API, and the results displayed. And...it works. Have a play, use test credit cards, valid cards, invalid dates, mess with form validation, fiddle with the nonces. Knock yourself out :-)

@adavidw
Copy link

adavidw commented Mar 8, 2017

Hi @judgej,

Re: your edit - If what you're sending is all XML now, everything is probably just fine. You're probably fully compatible with what we are currently documenting as our Authorize.Net API.

The sequence of events goes roughly like this:

  1. We have an API called AIM that is just standard http form post with name/value pairs.
  2. We make an XML API that does all the same transaction types, but has different names for every parameter and is of course structured completely differently. We call this API "AIM XML".
  3. We expand the XML API to cover all of the other ways of interacting with the system. Since the AIM name/value pair API doesn't change, we remove the "AIM" name from this new expanded API. The expanded API still lives at the same endpoint, and eventually gets the ability to understand JSON formatted requests as well.
  4. We post confusing and misleading text to our upgrade guide saying stuff like "AIM is now called Payment Transactions". That's a statement that might have been true if we had been very specific in the text, saying "The transaction requests previously documented as AIM XML now comprise the 'Payment Transactions' portion of our new expanded XML API" or something like that.

So, if you're doing XML requests now, everything's probably fine. I say "probably", because there's a very small handful of API calls that have either changed or been superseded since we expanded the API. I don't know off the top of my head if any of those were the ones previously documented as AIM XML or if they were only in other parts of the system. However, if everything you're doing matches everything in our API reference, you have nothing to worry about. That API reference is the canonical truth of what's currently supported in our Authorize.Net API.

Now that you've started sending the opaque data for Accept.js with the transactions, you're beyond anything that's previously been documented in the AIM API, so speaking from a pure semantic perspective, you are no longer using the AIM API. You're formatting the request the same way and sending to the same endpoint, but an API is more than the endpoint, it's the whole collection of documented behaviors, and there's nothing documented as the AIM API that includes the Accept.js nonce.

It's far easier for us to say "sending the opaque data from an Accept.js call is not possible using AIM" instead of saying "sending the opaque data from an Accept.js call is not possible using the AIM name/value pair stuff, but if you take an AIM XML request and replace the card data with the opaque data that will work, despite not saying anything about that in any documentation labelled AIM". Just a lot easier to say "Don't use AIM for this. Here's the format for the request that you need to use" even though it sometimes results in head scratching and wondering, "wait, I thought this was AIM".

I probably sound rather loony going on like this, but when I see a conversation like the above where everyone's confused about our API and where it's going, it kind of hurts me. It means we did a poor job of messaging and communicating our strategy. So, I'm trying hard to make sure everyone understands things at least on the same level that we do. This is especially important in the future as we start to communicate about things being deprecated or going away. If at some point in the future we say "AIM is deprecated" or "AIM is going away" or anything like that, it's easier for your users if they know that the gateway driver is not connected to anything that we would be referring to there. Of course it's also easier if we are more specific in our communications and make clean breaks where we need to instead of applying the name of one thing to an rather unrelated thing.

Re: JSON - The JSON version of the API is as stable as the XML one. While we might make improvements to the way things are structured or parsed, we're committed to no breaking changes. So, if there's a structure that would work more logically as an array, and we can get our parser to recognize it as an array, we might change the way we document that structure, but only if we could still parse requests in the previous format correctly.

So, if everything's working fine (and it appears like it is), the only change you'd want to make on your end is to just drop AIM from the name.

@adavidw
Copy link

adavidw commented Mar 8, 2017

Re: our upgrade guide. You're right that the wording was updated recently, basically as soon as I saw it and said "Uh oh. That ain't right". I don't know when the wording actually got changed on the production web site, though. Between the time we create a piece of text and the time it goes live, there's a long gantlet of various legal review, corporate review, pre-processing, processing, post-processing, build, QA, and scheduling release windows that's all done by other portions of the Visa behemoth.

There's a future wording change in there that's even more specific, talking about the exact difference between AIM NVP and AIM XML and hopefully being even more clear, but that's still winding its way through the process.

@judgej
Copy link
Member Author

judgej commented Mar 8, 2017

So there is an "AIM [NVP]" API, and there is an "AIM XML" API, and they are two different APIs, despite what the name may suggest?

This probably means that in this OmniPay driver, rather than converting the AIM to use XML rather than NVP, we should have just created "AIM XML" alongside it, and people could use one or the other. It's becoming a little clearer now. It's a shame it wasn't worded like that a long time ago.

@adavidw
Copy link

adavidw commented Mar 8, 2017

Are the AIM NVP and AIM XML different APIs? In my view yes, because they have completely different formats and the parameter names are completely different. They both do the same things and effect the same transaction processes. So, we could get into a good discussion about when an API is different, but in my view, those are two completely different animals.

Related question: Is a superset like our current API a different API than the AIM XML API or just a new version?

@adavidw
Copy link

adavidw commented Mar 8, 2017

Note: everything I said in all these posts before is AIM specific. There are similar stories for other APIs that we've had (SIM, DPM, CIM), etc. Since you're implementing those as well, there are other considerations for which direction you take the project. Those APIs are all on their way out, but they're at different levels. Some of them have a superior replacement already, some don't.

Specifically addressing CIM, that's in a similar state as AIM. We have this thing called "CIM", we have an XML format for it going to the same endpoint as the other transactions, but along the way we move to packaging it as part of the consolidated API. One specific issue is that there's at least one call in the old CIM XML API that has a different name and format in the new API. createCustomerProfileTransactionRequest is superseded by new functionality available in the createTransactionRequest call. I think there might be other areas in CIM that are similar.

Here's a relevant thing I typed up about createCustomerProfileTransactionRequest.

@judgej
Copy link
Member Author

judgej commented Mar 8, 2017

I think, as has been suggested earlier, we create a brand new OmniPay driver for the latest JSON API to cover everything without any of the legacy stuff getting in the way (and vice-versa). People can carry on using the older APIs, or switch to the new APIs when ready, and both will be maintained as necessary. That's what I think we should do.

@adavidw
Copy link

adavidw commented Mar 8, 2017

Seems reasonable. I didn't want to suggest as much if it means doubling the work for you.

But, as we add new features, they'll only be available in the current API, so the new driver is the only place you'd have to put in support. The old driver would only need maintenance in the form of bug fixes or security fixes. Then the new driver is the thing people use if they want to use the new features.

Now, I don't want to complicate things further, but hypothetically speaking, what if we were to introduce a REST API somewhere down the road, like perhaps later this year? Not as a replacement for the current API, but just something additional where new features would get added to both and they both co-exist.

Would that change your plans any? Like, would you wait until that was available and then build the new driver around that? Or, would you make a new driver now, then change the new one to the REST API when available? Or, make a third driver when REST is available?

@judgej
Copy link
Member Author

judgej commented Mar 8, 2017

Hehe. Good question. I've been thinking about how the OmniPay drivers are generally structured for a while. They tend to operate on arrays of data that get passed around. They specifically do not have dependencies on SDKs or external libraries. I'm wondering whether that can be changed.

What I suspect (?) will be common between the current API and the REST API will be the objects and messages. I'm guessing the same objects - data structures - can be modelled the same way for both the REST and the non-REST APIs. So I would see these APIs as a bunch of shared objects with different ways to throw them around. It makes sense IMO to put those data structures into a [PHP] package of their own, with the OmniPay driver just being a wrapper to communicate with the gateway using these objects. SDKs tend to try to do far too much in one big lump, but if the data objects could be pulled out on their own, with no reference to how they are used, then that could make a great base for both a REST and a non-REST driver, with a minimum of rewriting. I guess it would be a bunch of value objects.

Not sure if that makes sense?

@Mark-H
Copy link
Contributor

Mark-H commented Mar 8, 2017

From this comment:

This probably means that in this OmniPay driver, rather than converting the AIM to use XML rather than NVP, we should have just created "AIM XML" alongside it, and people could use one or the other. It's becoming a little clearer now. It's a shame it wasn't worded like that a long time ago.

I think the AIM gateway already uses the XML approach? The AIMAbstractRequest is definitely generating XML. So no conversion is necessary for that to use the new endpoints. Hence the opaqueData working with the AIM gateway.

It also sounds to me like the JSON is the same API as the XML one, just with a different format. In which case my suggestion would be to just continue using XML. What would users gain from the different format?

@felixmaier1989's suggestion to copy the AIMGateway driver and its tests to a new AuthorizeNetGateway sounds like it makes the most sense, and also shouldn't be as much work as completely redoing everything.

@judgej
Copy link
Member Author

judgej commented Mar 8, 2017

@Mark-H yes, it uses XML now, but it used the NVP version before a PR converting it to XML late last year.

The XML there now is fine and can stay. If starting again with a new driver, I think going for JSON would be easier to handle. I think there are, at the moment, too many XML objects being thrown around the package. It should probably deal with value objects internally, with conversion to/from XML or JSON at the HTTP interface to the gateway.

@felixmaier1989
Copy link
Contributor

I agree with @Mark-H regarding XML over JSON. There does not seem any benefit using JSOn at this stage.

@judgej
Copy link
Member Author

judgej commented Mar 9, 2017

I'm constructing a set of value objects for the new API to play with as a kind of side-project, to what value they are, and whether I'm barking up the wrong tree. They will be able to produce the required JSON by simply using json_encode($transaction_object) after populating the transaction value object. There is no reason why it could not also support $transaction_object->toXml(). The idea is that internally these value objects have no JSON and no XML - they are just data. They are only converted to/from other serialized formats at the point those formats are needed. The main advantages I see is a structure of classes that are easier to maintain when reflecting changes in the API, can have built-in validation for all data you try to squeeze into it, and is a something that can be reused in other gateway frameworks. It's just an experiment for now, and I'm not proposing changing what currently works now in this OmniPay driver.

@zinith-zz
Copy link

I wan to capture multiple payments in a single form submit. Like capture initial payment now and create Recurring subscription for later date. But opaque token gets expired after first capture. How can I achieve that?

Thanks in advance.

@guiwoda
Copy link

guiwoda commented May 23, 2017

Hey @judgej, I just landed here and read status on the library.

If I understood this issue correctly, Omnipay/authorizenet does not yet implement a Gateway to the now called "Authorize.net API", but AIMGateway can be used as a workaround.

I have two projects currently using this library, with custom extensions for customer profiles and recurrent billing. Can I help out implementing the missing gateway?
I can try and sketch something out by myself this week or the next, or we can chat about it and define what needs to be done a bit more. Let me know what you think about it.

Cheers

@judgej
Copy link
Member Author

judgej commented May 23, 2017

Hi @guiwoda This may be of interest: https://github.com/judgej/authorizenet-objects I've been trying to create a series of value objects for the new API, working from the API documentation. Trying it out as I go along, I've found little quirks and mistakes in the documentation, but it's mostly fine. The idea was to use the value objects to construct the messages to send to the gateway, then write an Omnipay wrapper to do the sending and to accept and parse the result. One aim was to keep JSON and XML out of the value objects and just serialize to JSON right before sending (the README shows how easy it is to do that).

Whether this approach has wings or not, I don't yet know - it's just an experiment, and creating the value objects has taken me a lot longer than I expected (involves lots of cross-referencing between the new API docs, the demo API scripts and the older AIM/XML documentation). Maybe it's time to move to the Omnipay wrapper and to finish off the value objects later (it's the customer profile stuff I've been working on lately).

Or maybe I've got it all wrong and this isn't the way forward. What do you think?

One thing I will say, is that the Authorize.Net API is very rich - it does an awful lot with customers, customer profiles, payments, payment profiles, scheduled payments, etc. Omnipay is only interested in a small portion of that, but by building this up in layers, each layer can support what it wants to support today, hopefuly with the logic and knowledge in the right domains so they can be easily expanded tomorow, and parts reused in other projects.

@judgej
Copy link
Member Author

judgej commented May 24, 2017

@guiwoda Here, I've knocked together the start of a driver for this API, for Omnipay 3.x alpha (just dev-master at the moment). It will send a create authorise transaction, with lots of missing data, and get the decoded data containing the inevitable errors as a result. Have a play and see what you think. (I've probablt messed up the composer.json for it, but just install the latest dev-master of omnipay-common then clone this and point your autoloader at it.)

@guiwoda
Copy link

guiwoda commented May 24, 2017

(...) Or maybe I've got it all wrong and this isn't the way forward. What do you think?

Well, I've never liked but eventually understood why Omnipay decided to go for key/value pairs instead of DTOs. But I think each gateway implementation could benefit from a clearer object model. From a project dev perspective, key/value pairs are too vague on requirements and valid / invalid messaging, and don't allow portability between gateways.

I've knocked together the start of a driver for this API, for Omnipay 3.x alpha (...) Have a play and see what you think.

Great! I'll do that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants