Creating a Magento Payment Module – Part 3 – Model, Controllers and Events

There are many tutorials out there which show parts of implementing a payment module. None completely build the module. They all stop short of showing how they implemented their nameless payment method.

This is part 3 of the tutorial. Here's part 1 and part 2 in case you missed them.

This post will walk through the model, action controllers and how they interact with the mockpay api.

My/Mockpay/Model/Standard.php

This model extends Mage_Payment_Model_Method_Abstract and if you look at the code of the base model, you'll see it has a lot of protected boolean attributes. These attributes are probed by the various consumers of these payment methods to figure out which features of the payment API our module supports.

Most of the modules I looked at for examples do this by overriding the properties instead of just setting them in the constructor. Our model defines four properties:

  • $_canUseForMultiShipping which enables/disables this method to be used for orders split up and shipped to many addresses,
  • $_canUseInternal which enables/disables using this method from the admin order screen,
  • $_code which is the unique payment method code for this module. It should match the code defined in the config.xml file. Its just text.
  • $_isInitializeNeeded, which enables a call to the initialize method when payment is captured.

The model::initialize() method is called when the order is placed. (Last step of the checkout process when the "place order" button is clicked.)
Looking at other modules is a great way to figure out how this code should work and if you look at the paypal or authorize.net modules you'll find a bunch more options are configured. These modules do the full authorize/capture process. Authorize occurs when the payment method is selected and the capture method is called when you click the "place order" button is clicked.

My module is very simple, needed only one method to be called at the very end of the payment process.
I opted to use initialize() instead of capture/authorizenbsp;because it always occurs at the same point in the process and I observed that initialize is only called in one place. I didn't find that to be true of the other methods.

In my initialize method, I'm setting the order state to "pending payment". This provides a clue to the folks working with the order in the backend that no payment has been collected for this order yet. I'm also connecting to the mockpay api and sending the order details. Mockpay will respond by providing a session token which will be unique to this order. The token is used to associate the customer on our site to the order details on the mockpay site with the order info and amount we're requesting. This is also where we'll inform mockpay about the cancel, error and success urls for this order.

Once payment is completed or canceled. The customer will be redirected back to our site. At that point we have a little more work to do with the order to either make an invoice and change the order state if successful or cancel it if the order fails or is canceled.

We'll use action controllers to handle these requests.

These are in the controllers/StandardController.php file. This is also extending an existing magento resource called "Mage_Core_Controller_Front_Action".

The successAction() will handle the success condition of the api where payment was completed successfully.
We'll get a token back from the mockpay api and we'll be able to use the token to query the api to get more details about the payment.

If everything checks out, we can change the state of the order from STATE_PENDING_PAYMENT to STATE_NEW, so its ready for someone on the store to ship and complete. This is also where we'll convert the quote into an invoice and mark it as paid. I skipped this step in the first version and it means someone on the backend has to do the extra step of creating an invoice and marking it as paid in magento.

Once all these steps are completed, I can then redirect the customer to the order complete page.

The other possible conditions are cancel, the person doesn't want to pay or failure where payment can't be completed because of lack of funds. I have these two conditions routed to the same action, the cancelAction().

Cancel will make the quote inactive and call the order objects cancel method.

With these actions in place, we have a functional payment module integrated with the mock pay api.

I've tried to comment as much as possible in the code so hopefully it will be instructive as you attempt to create your own payment method.

The full code for this payment module can be found on github.

 

derak

 

24 thoughts on “Creating a Magento Payment Module – Part 3 – Model, Controllers and Events

  1. Thankyou for sharing this knowledge. I'd been searching for a resource that could get me started and it looks like I've found it…

  2. Hi thanks for the brilliant tutorial. However, I have installed the module but it wont redirect? 

    Could you be able to tell me why? I've been stuck for two whole days!

    1. The URL for mock pay probably needs to be warmed up. I’m hosting that part in Zend’s cloud service. It puts my VM to sleep when its idle for too long. Just click on the service URL in the blog post. It should come back with some XML once it wakes up.

      Aside of that, some liberal use of mage::log() may be needs to figure out where the redirect is breaking.

  3. Hi Derak,

    I've also installed the module, and everything would seem to be installed correctly on the front and back ends.  However, clicking on the 'Place Order' button produces no apparent effect.  Am I missing something here?  Can you make a suggestion or two to get me pointed in the right direction.  I'm stuck.

      1. Excellent tutorial. I got these in my log:

        DEBUG (7): Called My_Mockpay_Model_Standard::initialize with payment sale
        ERR (3): Warning: include(SoapClient.php) [<a href='function.include'>function.include</a>]: failed to open stream: No such file or directory in C:\wamp\www\magento\lib\Varien\Autoload.php on line 93
        ERR (3): Warning: include() [<a href='function.include'>function.include</a>]: Failed opening 'SoapClient.php' for inclusion (include_path='C:\wamp\www\magento\app\code\local;C:\wamp\www\magento\app\code\community;C:\wamp\www\magento\app\code\core;C:\wamp\www\magento\lib;.;C:\php5\pear') in C:\wamp\www\magento\lib\Varien\Autoload.php on line 93

        It wont redirect as Tom stated. Thanks

        1. Sounds like the soap client isn't enabled in php. Load a pup info page and make sure that a section shows up for soap client.

  4. Hello Derak,

    The redirect actually kept me on the checkout page, i clicked the link you posted but that didnt make ant difference. Any other suggestions?

    Thanks

    1. Make sure to go into the control panel, payment methods, mockpay:

      for “URL to Form”, use http://a1ikuznugm2rjfqr.my.phpcloud.com/mockpay/form/paynow.php

      for the “WSDL to SOAP Interface” use http://a1ikuznugm2rjfqr.my.phpcloud.com/mockpay/service/endpoint.php?wsdl

      Those are the only settings you have to fill in. I’m sorry this post didn’t meet your needs.

       

  5. Hi Derak,

    Great tutorial! I am able to make it work :)

    How about if I need to display a custom form, instead of redirecting to a different page, what files should I add/edit here? I know I'm supposed to create a Block and some .phtml but have no idea on how to go about it (structure, file location, filenames, classes to extend). Could you show me, or point me to some resources?

    Thanks!

      1. Glad you found what you needed. Just an fyi; The reason payment services choose the gateway model is because it puts all the PCI-DSS security requirements on the payment gateway instead of your application. If you’re site accepts the credit card info directly, you’ll need to do that stuff yourself.

  6. In your explanation for successAction(), you mention

    This is also where we'll convert the quote into an invoice and mark it as paid.

    However, I could not find which part of the code does it. It's supposed to be inside this if() statement, right?

    if($state == Mage_Sales_Model_Order::STATE_PENDING_PAYMENT){ … }

    I could only find the a) setting of the state to STATE_NEW and b) setting of quote to inactive:

    $order->setState(Mage_Sales_Model_Order::STATE_NEW ,true,$msg,false);

    $quote->setIsActive(false)->save();

    My question is, what should be the code here to "convert the quote into an invoice, and mark it as paid"?

    1. Its not really converting a quote to an invoice. They both exist seperately. Its really creating a new invoice based on data in your quote.
      In the example module, that step occurs in the controller when the gateway redirects your browser back to the store front.
      https://github.com/derak-kilgo/magento-mockpay/blob/master/My/Mockpay/controllers/StandardController.php
      Or its should be. How about that. I forgot to add that to my example code. Its fixed in github.

      Thanks for catching that.

      1. Wow, thanks for the quick response!

        I see that you added "$this->_createInvoice($order);" and a new function "_createInvoice($orderObj)".

        However, there is already an existing "_createInvoice()" function in line 43. Should that previous declaration be removed, since it doesn't seem to be used, doesn't accept any parameters, and it uses "$this->_order" which doesn't seem to exist?

  7. In your config.xml, payment_action is sale. I have read in another blog that payment_action must be any of the following: order, authorize, authorize_capture, as defined in Mage_Payment_Model_Method_Abstract.

    1. Why do we use sale and not one of those above?

    2. Is sale arbitrary, or is it defined somewhere else like the ones above are defined as constants in Mage_PaymentModel_Method_Abstract?

    3. What would be the reasons why anyone would want to separate the code for authorize, authorize_capture and order, instead of just doing it how you did it here – just using initialize(), which is much simpler?

    4. In your initialize(), 

    //Payment is also used for refund and other backend functions.
    //Verify this is a sale before continuing.
    if($paymentAction != 'sale'){
        return $this;
    }

    would I need to modify this code and provide additional if() statements if I want to handle "refund and other backend functions"? If so, what values of $paymentAction should I check for, for "refund" and the other "backend functions" you are referring to? (Where can I find a list of these?)

    I'm sorry for the many questions. I'm really learning so much from this tutorial and your code.

     

    1. Its arbitrary. I think I saw it used in a post on inchroo and it worked. Logicly, “authorize_capture” is what we want to do but I don’t think it worked when I was trying to use it.
      You’re right though. ‘sale’ isn’t a valid action. If you change it to something else, you’ll need to update the initialize() function in the model because its checking for it there.

      The payment module is accessed in more places than just checkout. When your working with an order in the backend it can also access the payment module. A payment gateway which just authorizes payment will go back and capture payment when the item is shipped or if it can refund, will do so if the order is canceled. The order knows what payment method was used so the payment method is tied to the order.

      The order,authorize,authorize_capture workflow follows the business rules that credit card companies want users of their systems to follow. https://www.chasepaymentech.com/the_basics.html

      They’re also more flexable with their payment gateway customers like paypal.

      I’m happy to answer questions as best I can. I found the moneybookers and authorize.net modules really helpful for examples.

  8. To give a context for my question, we are actually migrating to Magento from Interspire Shopping Cart. In Interspire, the payment form would be displayed after everything else, especially the Order Review/Summary. At this very last step of the checkout, we just display an iframe, which displays a payment form hosted on our payment gateway's server (since we're not PCI compliant). In Magento, however, the last part isn't the payment form, but the Order Review. 

    So my question is, would it be possible at all to create a payment method that uses an iframe (or convert your example into one that uses an iframe, instead of loading a whole new page). This iframe would display the payment form hosted on our payment gateway's server.

    I ask this because based on what I've read, and after playing with your code, it seems that it is not possible in Magento. Displaying this iframe, for example, on Step 4 (Payment Information) would mean that when the form is filled out and submitted by the user, the page would redirect to a success page. Step 5 (Order Review) is bypassed. That wouldn't work because it is in Step 5 (accdg to your example) where the success page is even defined. So after submitting the payment form (in an iframe) in Step 4, it wouldn't know what to do next.

    I also tried to research how to put this iframe in Step 5 instead of Step 4, but couldn't find anything.

    What are your thoughts on this?

    1. To do so requires more modification of the checkout process views and helpers.

      Acutally, magento enterprise edition is supposed to have that option on all of its payment methods. The whole payment area is actually hosted by magento and displayed as an iframe. So I’m sure its possible.

      Check out https://github.com/bitpay/magento-plugin

      At appears to be using an iframe.

      The trick is it has to hook in to the checkout process earlier.
      The module needs to do the authorize step in the iframe and provide some data about the transaction that can be used later, in the capture step to capture or verify capture of the funds when the user clicks the last button in the checkout workflow.

  9. In the above post, you mentioned:

    The model::initialize() method is called when the ["place order" button is clicked.]

    and also,

    Authorize occurs when the payment method is selected and the capture method is called when you click the "place order" button is clicked.

    Putting them together, it means that clicking the "Place Order" button will call both initialize() and capture(), correct? My question is, since I noticed initialize() is always called first, is capture() called automatically after initialize() or do we need to call it inside of our initilize() function?

    Also, I have a question above which you may have forgotten to read. It's dated AUGUST 27, 2013 – 2:23 AM. Would you be so kind to check it too?

    1. I think that behavior depends the payment_action set in the config.
      Set a different payment_action, those methods are called at different times I think.

      You’re diving into this problem deeper than I did, so I’m not sure how much more I can help.

      There isn’t a lot of good documentation on when stuff gets called. I wasn’t aware of the other connections until I put the module in QA and someone discovered the backend order screens were broken.

  10. Thanks for this, Derak. After diving into your code, I was able to modify and integrate it with a RES based API (instead of SOAP). It works like a charm!

    Now, I have a couple of requirements that I feel like I hit a dead end.

    First is to save the payment transaction on Magento including a transaction reference no. generated and returned by the third-party payment gateway after making a callback. I can already capture this transaction reference no. but have no idea on how to save it to payment transaction. I tried this code but it's no good:

    <code>

                        if($state == Mage_Sales_Model_Order::STATE_PENDING_PAYMENT){
                                $this->_createInvoice($order);
                                //sets the status to 'pending'.
                                $msg = 'Payment completed via UBiz.';
                                $order->setState(Mage_Sales_Model_Order::STATE_NEW ,true,$msg,false);
                                //$order->setData('state', Mage_Sales_Model_Order::STATE_COMPLETE);
                                
                                // Save Payment Transation that was captured from the Payment Gateway API
                                $order->setAdditionalInformation(Mage_Sales_Model_Order_Payment_Transaction::RAW_DETAILS);
                                
                                $order->save();
                                
                                // @var $quote Mage_Sales_Model_Quote 
                                $quote = Mage::getSingleton('checkout/session')->getQuote();
                                $quote->setData('order_reference_id',$order_reference_id);
                                $quote->setIsActive(false)->save();
                                
                                file_put_contents('php://stderr', print_r($msg, TRUE)); //debug variable to apache error log
                            }

    </code>

     

    Second, is to display the returned transaction reference no. on the success page. 

    Would appreciate your inputs on this as I've already scoured google for the past days and havent got any better answers. 

     

     

Leave a Reply

Your email address will not be published. Required fields are marked *