On Jan 3, 8:45 am, Phil Cruz <
p...@philcruz.com> wrote:
> We're not talking about eliminating core code. We're talking about moving
> the "create" functions
> from the core.cfc to the base.cfc where they can be overridden as needed.
> There would be no changes to getStatus or any "get" functions.
I did some research this morning to see how ActiveMerchant (Ruby)
handles this. I based cfpayment largely on their system so we would
have something equally useful. They have about 80 gateways it seems so
I figured they must have run into this before too. They have one kind
of key difference which impacts how this per-gateway override needs to
work.
First, CreditCard objects are defined by ActiveMerchant::Billing which
is like getting it from the cfpayment core (and not the gateway). I
think this makes sense as we don't want to extend the credit card
object itself if we can avoid it so it can be passed to any gateway.
The gateway should handle any gateway-specific enhancements to the
base data. The example that prompted this was Shawn needing a
getCardType() routine that would return a specific formatting of a
long or short card brand name ("MasterCard" or "Master Card" for
example).
This however can be handled by folding in the getCardType() method to
geomerchant.cfc and have it take the account as an argument. It's a
helper method specific to sending data to the gateway so it belongs in
the gateway (IMHO). I have a handful in my Braintree gateway too,
like dateToBraintree() and braintreeToDate(). I think the idea of
keeping the generic objects clean and "top level" is important to the
overall goal of making it as easy to switch consuming code between
gateways with minimal changes so that your application code doesn't
need to know anything more than purchase(account, money, options).
Second, I looked at how ActiveMerchant handles the response because
the need to override the response looks to be far more likely to
handle output from different kinds of processing systems. AM has a
subtle difference to cfpayment that I think bears evaluation. So,
let's walk through how their gateways post and parse results.
ActiveMerchant also has the idea of a super.process() called
commit(). In that commit(), they also submit the HTTP request. It
looks something like this (in typical Ruby-ish minimalism):
response = parse(action, ssl_post(endpoint_url, request, options))
That response is not actually a Response object; the result of
ssl_post() is run through parse(), which takes whatever response the
gateway sends back (name-value pairs, XML, JSON, etc) and converts it
into a structure/hash. Each gateway is responsible instantiating the
Response object and populating it:
Response.new(:authorization => response["transactionid"],
:test => test,
:cvv_result => response["cvvresponse"],
:avs_result => response["avsresponse"] )
This is what our gateways do where we analyze the result from the
gateway, pick out the various parameters and normalize them in the
response object. The difference is that our super.process() is
creating the Response object in base.cfc where it can't be overridden
and passing it back. This was part of the problem with Phil's stripe
implementation which needed to act upon the HTTP status code; in this
approach, he would have no problem making adjustments based on the
HTTP status code. If we used the ActiveMerchant approach, it would
also be trivial to override the response object if it's really needed
(we ultimately decided that it wasn't in Shawn's GEM case).
ActiveMerchant seems to provide extended response objects primarily
for integration-style gateways like Paypal so it's not common in
practice.
In my own vacuum, where I'm leaning right now would be to follow
ActiveMerchant and refactor the base gateway so that an individual
gateway implementation (braintree, gem, stripe, etc) would look like
this in process():
<cffunction name="process">
// either use the newly inherited createResponse() from base.cfc or
createObject() to use a custom one
<cfset Response = createResponse() />
...
// return a structure of values including the gateway response, the
result status, and original request
<cfset res = super.process(payload = p) />
// normalize values inline or sub out gateway parsing to a helper
function
<cfset normalized_values = parse(res) />
<cfset Response.init(response = res
,TransactionID = normalized_values.TransactionID
,Authorization = normalized_values.Authorization
,AVSCode = normalized_values.AVSCode
...);
// OR
<cfset Response.setTransactionID(normalized_values.TransactionID) />
<cfset Response.setAuthorization(normalized_values.Authorization) />
<cfset Response.setAVSCode(normalized_values.AVSCode) />
<cfreturn Response />
</cffunction>
The base.cfc would be refactored so that process() returns a structure
rather than a Response object. The Response object would be made
smarter to know how to take the keys in that structure and populate
the majority of the parameters. The normalization would continue to
be handled on a per-gateway basis like it is now.
Net result... Inputs come from core.cfc and Outputs come from the
gateways:
createCreditCard, createEFT, createToken and createMoney methods stay
in Core.cfc (mirrors ActiveMerchant::Billing).
createResponse method is moved to base.cfc (the output of a gateway).
base.cfc's process() would return a structure of request/response data
gateway CFCs would only need to add one line of code basically to
createResponse() inline and init using the results of the
super.process().
response.cfc would know how to take that structure in init() and
populate the core values (status, requestdata, test mode, etc),
leaving the gateway implementation to populate the rest of the values
like they do today.
It would be optional to sub out the normalization to a parse() method
(although I like the idea of that as a recommendation).
If were making code changes, I think this sounds like a better long-
term strategy for 1.0. Thoughts?
Brian