Okay! I've done a bit of successful work, and now need some serious input from the community to tidy it up.
I have created a branch of GetPaid that successfully mocks up one possible design for eliminating ZCML overrides for the Google Checkout payment processor. Anyone should be able to try it out: $ svn co https://getpaid.googlecode.com/svn/getpaid.buildout/branches/brandon-no-overrides $ cd brandon-no-overrides $ python2.4 bootstrap.py $ bin/buildout -c 316.cfg $ bin/instance fg Once the instance is up and running, use the ZMI to create a Plone site that includes "GetPaid", and visit the GetPaid Setup's "Payment Options" form. If you have named the Plone site "/Plone", then the options will be at this URL: http://localhost:8080/Plone/@@manage-getpaid-payment-options If you will create a buyable piece of content (I always just throw in a Page after marking Pages as purchasable) and add it to your cart, so that the Shopping Cart portlet appears, you will see a fake "Checkout" button that changes depending on which payment processor you have selected. The same button will appear on the main shopping cart view! In both cases, the fake button will be labeled either "[ Null Payment Processor Button ]" or "[Google Checkout Button]" depending on whether the "Testing" or "Google Checkout" payment processor is currently selected. And, it all works without requiring a single ZCML override! (If you check, you'll find that Google Checkout's "override.zcml" files are all gone from this branch of GetPaid.) So, getting rid of overrides turns out to be very simple. It's the design questions that this raises that are going to require the project to stretch across another few days. :-) Let's tackle these big questions one at a time. 1. How should we "look up" the active Payment Processor? --------------------------------------------------------- I was rather surprised when I saw the code that determines which payment processor is the currently selected or active processor for the site. One instance of the code lives in ``PloneGetPaid/browser/checkout.py``:: siteroot = getToolByName(self.context, "portal_url").getPortalObject() manage_options = IGetPaidManagementOptions(siteroot) processor_name = manage_options.payment_processor if not processor_name: raise RuntimeError( "No Payment Processor Specified" ) processor = component.getAdapter( siteroot, interfaces.IPaymentProcessor, processor_name ) There are three steps here. First, something called a "tool" is used to select the "siteroot" by using an ASCII name string. This is a bit odd to someone like me from the Grok world, where we find the current site root by calling "get_application(current_context)", but if it's the normal way of doing things in Plone then I'm cool with it. Second, the GetPaid "Manage Options" are pulled out of the site: these are all of the options controlled by the "Site Setup -> GetPaid" series of forms. I find it a bit odd that they're not available as a utility, so that you could just ask for "IGetPaidManagementOptions" and have the site lookup occur automatically as part of utility resolution. But, again, if this is the Normal Plone Way then it's fine. Finally, we pull out the value of the "payment_processor" option and then manually look up the IPaymentProcessor registered as an adapter of the siteroot under that name. This is where things are just weird to me. :-) Isn't the whole point of the site root, and of local utilities in Zope in general, that you can ask for a resource that - without your having to know - is customized within the current site, and you just magically get the right answer without knowing that? This stunt, of adapting the site root to an interface of a particular name that the user has chosen, just seems like a long-way-'round procedure to simulate what happens more naturally if you just create an ISelectedPaymentProcessor interface and have the payment processor selection form register the chosen processor as a site-wide utility uniquely providing that interface. Using a utility - if, indeed, they exist under Plone the way we're using Plone here; maybe utilities are a Zope 3 feature that's not fully supported under Plone yet, and that's the reason for these idioms? - would also make the ZCML registrations of each payment processor less odd. Currently, the Google Checkout processor, to take one example, says things like this in its ZCML:: <adapter for="Products.CMFCore.interfaces.ISiteRoot" provides="getpaid.core.interfaces.IPaymentProcessor" name="Google Checkout" factory=".googlecheckout.GoogleCheckoutProcessor" /> In what sense, we have to ask when reading this declaration, is a payment processor an adapter of an ISiteRoot? Normally an adapter takes a feature-rich object, and gives it new behaviors. But does a site root really provide behaviors which an IPaymentProcessor is improving upon so that they are able to process payments? It's a very odd way of looking at the problem. Declaring an IPaymentProcessor to be an adapter seems, again, to be a way to do manually what utilities do automatically; it seems to be a way to open a window to the site so that things like its URL can be grabbed. But surely a payment processor isn't really a more specific adaptation of a site root, just because it needs the site's URL? Anyway, all of this is quibbling. The whole edifice seems to work the way it's currently written. But I wanted to ask, because there's obviously some mismatch here between the way PloneGetPaid is written, and the techniques that I myself am familiar with from Zope 3 land, and I'd like to understand the differences better so that, as I write more GetPaid code in the coming days, it matches the philosophy of the existing code as closely as possible. Now, one to more meaty matters! 2. How should the "Checkout" button view be rendered? ----------------------------------------------------- If you tried out my branch before reading this far, you probably find it very lame that my "Checkout" buttons right now are just bold text with square brackets around them. I should explain why, so that you can help me towards a solution. PloneGetPaid provides two views that need a "checkout" button: 1. Products/PloneGetPaid/browser/templates/portlet-cart.pt This is the cart portlet, that appears on the right side of most pages of the site if you have at least one item in your cart. 2. Products/PloneGetPaid/browser/templates/cart-actions.pt This is the main shopping cart view, that comes up if you click one of the "Manage cart" links that PloneGetPaid makes available. The current production version of PloneGetPaid has both of these buttons hard-wired in these two views, because, as originally written, they couldn't possibly go anywhere than to PloneGetPaid's own checkout wizard. The problem is that Google Checkout needs both of them to send you to Google instead! That's why the current production version of Google Checkout has those nasty ZCML overrides: because it needs to fully replace *both* of those views with new copies that provide specialized "Checkout" buttons. Yes, that's right: it replaces two entire views just to replace a single button in each of them! My new version of PloneGetPaid replaces the hard-coded "Checkout" button in each view with code that tries to pull in dynamically-generated button HTML instead, through an expression that currently looks like this:: <a tal:replace="structure renderer/checkout_link">Checkout</a> This required me to place a tiny method named `checkout_link()` in each renderer, both in ``Products/PloneGetPaid/browser/portlets/cart.py`` and in ``Products/PloneGetPaid/browser/cart.py``, that needs to go and figure out how the currently-active payment processor would like its checkout button rendered. A quick note: why do I allow the whole button to be controlled by the payment processor like this, instead of just the link? The reason is that the Google Checkout button needs to be constructed quite differently than the normal PloneGetPaid button. The way the views are currently coded, normal buttons in these views are *always* type="submit" HTML form inputs whose value="" is their action name; the normal Plone form machinery wakes up when they are submitted and calls the right `[email protected](...)`` method on the view. But a Google Checkout submit button is completely different! It points at a completely different URL - one that lives over on Google's site - and needs to submit several form parameters when it gets there. Because of this, it can't actually be part of the same ``<form>`` as all of the other buttons in the cart portlet or cart management page! It can't follow any of the normal rules for a portlet button. Instead, the new templates first have to say ``</form>`` to end the "normal" PloneGetPaid form whose buttons are used for editing the shopping cart, then start a whole new form that just wraps the one button intended for Google. There are also cosmetic changes: the Google checkout button uses a little Google logo inside. For all these reasons, it doesn't seem reasonable for payment processors to provide some phalanx of options that the cart views interpret in order to build the right kind of button for things like Google checkout. Instead, it seems more reasonable for each payment processor to be given wholesale control over rendering the full HTML for its own "Checkout" button. That makes everything becomes a bit simpler. So, back to my hard-coded way of asking for a checkout button:: <a tal:replace="structure renderer/checkout_link">Checkout</a> This is a mess for at least two reasons. First, it's a mess because I have to add two ``checkout_link()`` methods, one to the cart portlet and one to the main shopping cart view. And the methods pretty much repeat the same code: they do the dance that I quoted up in section 1 for finding the currently selected payment processor. This is code which I'll have to factor out later into a single routine if we decided to keep it. Second, it's a mess because instead of properly asking for, say, a view to be rendered, the ``checkout_link()`` methods just ask the payment processor's method named ``checkout_link()`` (whoops, looks like I was inconsistent in my naming when writing up this quick prototype; I'll fix it later to make it match the name of ``checkout_button()`` if we keep any of this code) to get back the HTML that it should insert where the button goes. Right now, these are just the two static strings:: u'<b>[Google Checkout Button]</b>' u'<b>[ Null Payment Processor Button ]</b>' Why, you'll ask, didn't I go all the way and make these actual templates attached to views, so that I could go ahead and put the real buttons in? (Since, after all, they *must* be template-driven views, since they have variable portions where their submit URLs have to be specified?) The reason is that, by the time I found myself down in each ``checkout_link()`` function, I found myself knowing almost nothing about my environment: neither the context I'm being rendered for, nor the root of the site I live in, or anything. Now, this might just be my fault, for not passing enough variables around; it would certainly be easy to fix. But before beefing up the functions, I wanted to ask whether there isn't some easier way that I could just use a plain view instead, and eliminate both the new methods on the cart views and the new methods on the payment processors. (Oh, by the way: I've only added these new ``checkout_link()`` methods on the "Testing" and "Google Checkout" payment processors. If you try any other processors yet under my new branch they'll break. It's just a proof-of-concept, after all.) What I would really like to do, each of the two places I need a checkout button rendered, is to just go like this:: <a tal:replace="structure context/@@getpaid_checkout_button"/>Checkout</a> The question is: how should the view multiplex so that it winds up consulting the currently active payment processor to determine what the button should look like? There are at least two options, and maybe more that you'll think of and share with me: 1. Have a single ``@@getpaid_checkout_button`` view defined by PloneGetPaid. When called, it does the current-payment-processor dance, gets from the payment processor the path to the template it should use (or, better yet, a class to use as the view), and then returns the rendered result. 2. Have several ``@@getpaid_checkout_button`` views, one for each payment processor, and have the site owner's selection of payment processor automatically turn on its particular view. I know that this sort of thing is possible in Zope 3, by having the submission of the "Payment Processor" form trigger the un-registering of the old processor's ``@@getpaid_checkout_button`` view and the new registering of the new processor's view. But, given the look of the current-payment-processor dance quoted way up above (how long is this email so far, anyway?), it looks like maybe the GetPaid team doesn't like registering and un-registering lots of things - or even a few things - when a new change processor is selected. I guess that's dangerous because registrations would always have to be persisted across site upgrades and everything? Which would make it preferable, each time we need the current payment processor, to start with the site root and look it up afresh? If so, then approach #1, above, is fine; I just wanted to ask in case a view-registration-driven approach hadn't yet been thought about and wound up having advantages. Not that I can see any. :-) So: my primitive methods-returning-strings mechanism needs to be replaced with something fancier like a view. I like option #1, I think, but wanted to ask since Plone programming is still new territory for me. All of the above are small issues compared to this last one. Here goes! 3. Is Google Checkout a Payment Processor? ------------------------------------------ I would like to argue that Google Checkout is *not* a payment processor. Yes, that's right. I humbly submit that it's a conceptual mistake that Google Checkout tries to shoehorn itself in as an IPaymentProcessor. To see this clearly, check out the definition of IPaymentProcessor itself:: class IPaymentProcessor( Interface ): def authorize( order, payment_information ): ... def capture( order, amount ): ... def refund( order, amount ): ... This fact was, helpfully, first pointed out to me by my friend Derek Richardson. He's right! Look closely at those three operations, and consider the fact that not a *single one of those functions* is implemented by Google Checkout! It doesn't do any of the functions of a payment processor at all. Well, then, what is it? The answer is that Google Checkout is a *wizard*, not a *processor*. Let me suggest that the structure of GetPaid (maybe I should draw a real diagram for this next bit and post it on my blog?) looks like this: 1. The Shopping Cart accumulates objects you want to buy. 2. When your Shopping Cart is full, you want to visit a Checkout Wizard. You are sent to the Checkout Wizard by the "Checkout" button at the bottom of any of the Shopping Cart views. 3. The Wizard asks you lots of questions, and gives you lots of chances to get the answers right as you traverse its forms and make mistakes and correct them. 4. Once the Wizard has the customer's shipping and credit card information, it needs to verify the credit card. So it goes and looks for the current Payment Processor and uses its ``authorize()`` method, and so forth. The problem with the whole structure of GetPaid at the moment is that it is *only* built to support its own, built-in checkout wizard. That's why the links described in #2, above, are hard-wired: GetPaid is not built to let any custom actions intrude until its own checkout wizard has completed and step #4 has sprang into action. Google Checkout, in other words, does *not* replace the functionality of, say, Authorize.Net; instead, it replaces the functionality of the whole, internal checkout wizard! Google Checkout is the peer, and the alternative, to all of the forms that GetPaid presents the user with; it is not a peer, in any functional sense, to normal payment processors that just handle a simple ``authorize()`` call. So, even though this first demonstration I've made of ZCML-override-less Google Checkout magic does so by adding yet another method to the GoogleCheckoutProcessor, I actually think that's the wrong place for it! I think it would be a confusion for me to go into ICheckoutProcessor and add a fourth method, ``get_checkout_button_view()`` or whatever, next to ``authorize()`` and it's cousins because it would be putting apples next to oranges. Think of how odd the resulting collection of change processors would look! Some would have a checkout-button method, but, necessarily, then provide no ``authorize()`` method (since by the time authorization takes place on an offsite checkout wizard, control has long since left the hands of Plone); and others would have a checkout-button method that just dumbly echoed the URL of the normal, internal GetPaid checkout wizard, and that only differed by what they did when ``authorize()`` was called. We should therefore have two entirely separate classes of object. I. Checkout wizards. A. Google Checkout. B. On-site GetPaid Checkout, which needs a Payment Processor. II. Payment Processors. A. Authorize.Net B. PayPal C. And so forth... A Checkout Wizard offers, at the very least, to render a button, if asked, that will send the user to its first page when clicked. A Payment Processor takes a credit card number and processes it. Simple! :-) But there's one last question raised, which deserves its own little section of this design argument. 4. Should the UI reveal that Checkout Wizards are different? ------------------------------------------------------------ Given the above argument about GetPaid's internal structure - which I *think* is sound, but, obviously, which I'm not certain about because I've felt the need to describe at very great length since this is my first foray into a GetPaid redesign - how should we present the difference between a Wizard and a Processor to the user? There seem at least two options. 1. Conflate them. This would let us keep the GetPaid "Payment Options" site setup page just like it is now, with a single pull-down for the Payment Processor. That single pull-down would list both on-site payment processors and off-site wizards in a single list. If the user selected something on-site, then the "Checkout" links in his Plone instance will point at the built-in wizard. If instead he selects something like "Google Checkout", then his users will fly off-site when the select "Checkout" instead. But only we programmers will know that the service which Google Checkout provides inside of our site is quite different than the service that, say, Authorize.Net offers; the user will see the choice as a simple one. 2. Separate them. Here, there would be two boxes: Checkout Wizard: ___On-site_Checkout___ Payment Processor: ___Authorize.Net______ As long as "On-site checkout" was the selected wizard, the second box would provide a range of options. But if they changed the wizard to an off-site wizard, like Google Checkout, then the second box would need to either gray itself out, or restrict itself to something like "Offsite Google processing" as long as they have Google Checkout selected as their wizard. How shall we choose between these two options? Let me provide four possible decision-making criteria. * Option #1 is simpler, and may therefore be preferable if it is possible. * Option #2 communicates more about the difference between on-site and off-site processing. This might be a semantic UI advantage. Also, if the on-site wizard grew lots of options in the future that governed its behavior, they too would need to be grayed out when an off-site processor was selected (hmm, but I guess that could happen just as easily under #1, so maybe never mind about that point). * Option #2 would allow for off-site wizards that actually offered several payment processor back-ends behind them. I don't know if such things actually exist, but we'd automatically have conceptual room for them if they did with the two-box form design. * I have heard rumors that there is a multiple-payment-processor project in the works. This would, presumably, at least allow the site owner to highlight *several* payment processors and let each user choose which one to use at checkout time. Or, am I wrong? Maybe the effort is really not about the payment processors sitting behind the default on-site checkout wizard, but is going to be designed to let people, at check-out, select one among several wizards to go visit to finish their transaction? I suppose the latter is more likely, since giving the user the option between Authorize.Net and one of their competitors is pretty useless - since the user would see the same on-site wizard in either case - compared to letting them use Google Checkout or Paypal, where they might already have an account, for their check-out instead. Anyway, I have a vague idea that option #2 will be more friendly to the multiple-payment-processor effort than option #1, because it opens the way for more meaningful multiple-choice selections if the day comes where, instead of a pull-down box, you can add several payment processors. Think about it; it might, once the site manager is done working, look like: Selected Payment Processors: ! On-site Wizard ! Authorize.Net ! PayFlowPro ! Google Checkout ( Google Checkout payment processor ) Okay, I think that exhausts the ideas and confusions that I've got in my head at the moment. In summary, I think that refactoring to support off-site payment processors is simple, but that we have to be intelligent in choosing which of the several possible simple mechanisms we choose, because one choice might make things easy for our users, while another might box us in and make things difficult to configure orthogonally going forward. Let me know where I'm right, where I'm wrong, and weigh in on the direction I should take with this branch. Once we've chosen a direction, I'll update all of the other payment processors to match, write lots of tests, and let everyone try out the branch before I ask to merge it. Thanks for any feedback! -- Brandon Craig Rhodes [email protected] http://rhodesmill.org/brandon --~--~---------~--~----~------------~-------~--~----~ You received this message because you are subscribed to the Google Groups "getpaid-dev" group. To post to this group, send email to [email protected] To unsubscribe from this group, send email to [email protected] For more options, visit this group at http://groups.google.com/group/getpaid-dev?hl=en -~----------~----~----~----~------~----~------~--~---
