Modifying Phoenix_Worldpay For Testing in Magento

Worldpay offers an easy way to switch from live to test mode, however if your server is unreachable by them then after completing the test transaction you won’t be redirected back to your Magento store.

We can make the following temporary adaptions to the module which will allow us to manually pass the required data via a GET string:

Phoenix_Worldpay_ProcessingController

   protected function _checkReturnedPost()
    {
//        // check request type
//        if (!$this->getRequest()->isPost()) {
//            Mage::throwException('Wrong request type.');
//        }
//
//        // validate request ip coming from WorldPay/RBS subnet
//        $helper = Mage::helper('core/http');
//        if (method_exists($helper, 'getRemoteAddr')) {
//            $remoteAddr = $helper->getRemoteAddr();
//        } else {
//            $request = $this->getRequest()->getServer();
//            $remoteAddr = $request['REMOTE_ADDR'];
//        }
//        if (!preg_match('/\.worldpay\.com$/', gethostbyaddr($remoteAddr))) {
//            Mage::throwException('Domain can\'t be validated as WorldPay-Domain.');
//        }

        // get request variables
        $request = $this->getRequest()->getParams();
        if (empty($request)) {
            Mage::throwException('Request doesn\'t contain POST elements.');
        }

        // check order id
        if (empty($request['MC_orderid']) || strlen($request['MC_orderid']) > 50) {
            Mage::throwException('Missing or invalid order ID');
        }

        // load order for further validation
        $this->_order = Mage::getModel('sales/order')->loadByIncrementId($request['MC_orderid']);
        if (!$this->_order->getId()) {
            Mage::throwException('Order not found');
        }

        $this->_paymentInst = $this->_order->getPayment()->getMethodInstance();

        // check transaction password
        if ($this->_paymentInst->getConfigData('transaction_password') != $request['callbackPW']) {
            Mage::throwException('Transaction password wrong');
        }

        return $request;
    }

– We remove the remote address checks at the start of the method
– We change the getPost to a getParams, allowing us to provide GET parameters

Phoenix_Worldpay_Model_Cc

public function capture(Varien_Object $payment, $amount)
	{
        $payment->getOrder()->addStatusToHistory($payment->getOrder()->getStatus(), $this->_getHelper()->__('Worldpay transaction has been captured.'));
        return true;
        if (!$this->canCapture()) {
            return $this;
        }

        if (Mage::app()->getRequest()->getParam('transId')) {
            // Capture is called from response action
            $payment->setStatus(self::STATUS_APPROVED);
            return $this;
        }
        $transactionId = $payment->getLastTransId();
        $params = $this->_prepareAdminRequestParams();
        $params['transId']  = $transactionId;
        $params['authMode'] = '0';
        $params['op']       = 'postAuth-full';

        $responseBody = $this->processAdminRequest($params);
        $response = explode(',', $responseBody);

        if (count($response) <= 0 || $response[0] != 'A' || $response[1] != $transactionId) {
            $message = $this->_getHelper()->__('Error during capture online. Server response: %s', $responseBody);
            $this->_debug($message);
            Mage::throwException($message);
        } else {
            $payment->getOrder()->addStatusToHistory($payment->getOrder()->getStatus(), $this->_getHelper()->__('Worldpay transaction has been captured.'));
        }
    }

– If capturing is required, then this will need further modification, however for our needs we only needed to return true from this method.

It should now be possible to provide a GET string with all of the required parameters, E.g.

http://mydomain.local/worldpay/processing/response?MC_orderid=100012592&transStatus=Y&authAmount=158.00&authCurrency=GBP&authMode=A

Alphabetic Option Value Ordering in Magento’s Layered Navigation

Magento will by default order layered navigation options by position. To use alphabetic values first and foremost, do the following:

Override the Eav_Entity_Attribute_Source_Table model

<global>
    <models>
        <eav>
            <rewrite>
        	    <entity_attribute_source_table>Namespace_Module_Model_Eav_Entity_Attribute_Source_Table</entity_attribute_source_table>
        	</rewrite>
        </eav>
    </models>
</global>

PHP

Override the getAllOptions method as follows:

<?php
class Namespace_Module_Model_Eav_Entity_Attribute_Source_Table extends Mage_Eav_Model_Entity_Attribute_Source_Table
{
    public function getAllOptions($withEmpty = true, $defaultValues = false)
    {
        $storeId = $this->getAttribute()->getStoreId();
        if (!is_array($this->_options)) {
            $this->_options = array();
        }
        if (!is_array($this->_optionsDefault)) {
            $this->_optionsDefault = array();
        }
        if (!isset($this->_options[$storeId])) {
            $collection = Mage::getResourceModel('eav/entity_attribute_option_collection')
                ->setAttributeFilter($this->getAttribute()->getId())
                ->setStoreFilter($this->getAttribute()->getStoreId());

            $collection->getSelect()->order(['main_table.sort_order asc', 'value asc']);
            $collection->load();

            $this->_options[$storeId]        = $collection->toOptionArray();
            $this->_optionsDefault[$storeId] = $collection->toOptionArray('default_value');
        }



        $options = ($defaultValues ? $this->_optionsDefault[$storeId] : $this->_options[$storeId]);
        if ($withEmpty) {
            array_unshift($options, array('label' => '', 'value' => ''));
        }

        return $options;
    }
}

This uses sort order first, then uses value to order the attribute options.

Magento – Programatic Quote, Order, Invoices

<?php
 
require_once 'app/Mage.php';
 
Mage::app('default');
 
$store = Mage::app()->getStore('default');
 
$customer = Mage::getModel('customer/customer');
$customer->setStore($store);
$customer->loadByEmail('email_address@gmail.com');
 
$quote = Mage::getModel('sales/quote');
$quote->setStore($store);
$quote->assignCustomer($customer);
 
$product1 = Mage::getModel('catalog/product')->load(166); /* HTC Touch Diamond */
$buyInfo1 = array('qty' => 1);
 
$product2 = Mage::getModel('catalog/product')->load(18); /* Sony Ericsson W810i */
$buyInfo2 = array('qty' => 3);
 
$quote->addProduct($product1, new Varien_Object($buyInfo1));
$quote->addProduct($product2, new Varien_Object($buyInfo2));
 
$billingAddress = $quote->getBillingAddress()->addData($customer->getPrimaryBillingAddress());
$shippingAddress = $quote->getShippingAddress()->addData($customer->getPrimaryShippingAddress());
 
$shippingAddress->setCollectShippingRates(true)->collectShippingRates()
                ->setShippingMethod('flatrate_flatrate')
                ->setPaymentMethod('checkmo');
 
$quote->getPayment()->importData(array('method' => 'checkmo'));
 
$quote->collectTotals()->save();
 
$service = Mage::getModel('sales/service_quote', $quote);
$service->submitAll();
$order = $service->getOrder();
 
$invoice = Mage::getModel('sales/service_order', $order)->prepareInvoice();
$invoice->setRequestedCaptureCase(Mage_Sales_Model_Order_Invoice::CAPTURE_ONLINE);
$invoice->register();
 
$transaction = Mage::getModel('core/resource_transaction')
                    ->addObject($invoice)
                    ->addObject($invoice->getOrder());
 
$transaction->save();
  • The $invoice->register() calls pay() in the Mage_Sales_Model_Order_Invoice class. This fires the event sales_order_invoice_pay which is useful for hooking into after an invoice has been paid for; E.g. automatically sending the invoice email upon creation:
   public function sendEmail(Varien_Event_Observer $observer)
    {
        $event = $observer->getEvent();
        /** @var Mage_Sales_Model_Order_Invoice $invoice */
        $invoice = $event->getInvoice();
        if(!$invoice->getEmailSent()){
            $invoice->sendEmail(true);
        }
    }

Fix Logging In on Old Magento 1 Installs

On certain Magento instance installs, a valid customer login will seem to fail and redirect to the login page. We’ve seen this on plenty of Magento 1.6s thus far, however newer versions may also be affected.

The problem lies in the login method of the Mage_Customer_Model_Session class. The fix, in this instance is to remove the renewSession call within that method:

 public function login($username, $password)
{
    /** @var $customer Mage_Customer_Model_Customer */
    $customer = Mage::getModel('customer/customer')
        ->setWebsiteId(Mage::app()->getStore()->getWebsiteId());

    if ($customer->authenticate($username, $password)) {
        $this->setCustomerAsLoggedIn($customer);
        // This breaks certain setups
        //$this->renewSession();
        return true;
    }
    return false;
}

public function setCustomerAsLoggedIn($customer)
{
	$this->setCustomer($customer);
	// This breaks certain setups
//        $this->renewSession();
	Mage::getSingleton('core/session')->renewFormKey();
	Mage::dispatchEvent('customer_login', array('customer'=>$customer));
	return $this;
}

This has been rolled into a module here: https://github.com/llapgoch/fix-old-magento-login

Programatically Modifying Products’ Stock Item Objects

Recently we had a project to make all products within a number of different categories available as backorder products. The client did not want to go through and change all of the products individually, so we embarked on attempting to do this programatically.

Our first idea was to do this using observers, so hypothetically whenever a product or product collection loaded we would be able to modify the accompanying stock item object if the product was in one of the categories in question.

The observers we identified to perform this were:

catalog_product_is_salable_after
We would use the above event to detect whether the product was in a particular category and set isSalable on the product object to true if so. This would make the product appear purchasable from the listing and detail pages instead of displaying the Out of Stock message.

catalog_product_collection_load_after
catalog_product_load_after
sales_quote_item_collection_products_after_load
We used the above three events to modify the following values:

  $stockItem->setIsInStock(true)
    ->setData('backorders', Mage_CatalogInventory_Model_Stock::BACKORDERS_YES_NONOTIFY)
    ->setUseConfigBackorders(false)
    ->setData('is_in_stock', true);

This method worked to a certain extent, however the process fell down when on the One Page Checkout; the Mage_CatalogInventory_Model_Stock object’s registerProductsSale creates a stock item object and populates it manually without firing any observers to hook into. This means that the final check in this method will always fail, and the user will be denied purchasing the product in question.

An attempted workaround was to override the Mage_CatalogInventory_Model_Stock class’ registerProductsSale method and perform our category check there. This, however caused another issue in that after registering the sale, the stock object gets saved in order to decrement the stock available; this also saves the values we’ve set meaning that if the client ever wanted to disable this functionality they would be left with any products which had been ordered in a backorder state.

Attempt 2 – Overriding

Against our better judgement, we opted instead to override the *Mage_CatalogInventory_Model_Stock_Item** class rather than changing its values with observers; this got around the issue of the stock item being saved and the values within it being crystallised. Here’s the class in question:

<?php

class Namespace_Module_Model_CatalogInventory_Stock_Item extends Mage_CatalogInventory_Model_Stock_Item
{
    public function getIsInStock()
    {
        if($this->isPurchaseableViaCategory()){
            return true;
        }

        return parent::getIsInStock(); 
    }

    public function getBackorders()
    {
        if($this->isPurchaseableViaCategory()){
            return Mage_CatalogInventory_Model_Stock::BACKORDERS_YES_NONOTIFY;
        }

        return parent::getBackorders();
    }

    public function isPurchaseableViaCategory()
    {
        $product = $this->getProduct();
        if(!$product && $this->getProductId()){
            $product = Mage::getModel('catalog/product')->load($this->getProductId());
        }

        if(!$product){
            return false;
        }

        return Mage::helper('module')->isPurchaseableViaCategory($product);
    }
}

And in our helper, we do the work of checking whether the product is in one of our selected categories (or a subcategory):

class Namespace_Module_Helper_Data extends Mage_Core_Helper_Abstract{
    protected $_allowBackOrderCategories = array("gold");

    public function isPurchaseableViaCategory(Mage_Catalog_Model_Product $product){
        $keys = array();

        $cats = $product->getCategoryCollection()
            ->addAttributeToSelect('url_key');

        foreach($cats as $cat){
            $this->getParentUrlKeys($cat, $keys);
        }

        foreach($this->_allowBackOrderCategories as $cat){
            if(in_array($cat, $keys)){
                return true;
            }
        }
    }

    public function getParentUrlKeys(Mage_Catalog_Model_Category $cat, &$urlKeys) {
        if(!in_array($cat->getUrlKey(), $urlKeys)) {
            $urlKeys[] = $cat->getUrlKey();
        }

        if($cat->getParentCategory()->getId()){
            return $this->getParentUrlKeys($cat->getParentCategory(), $urlKeys);
        }
    }
}

We were still able to complete our saleable check (for the display on the product listing and detail page) using an observer:

<catalog_product_is_salable_after>
    <observers>
        <modify_saleable>
            <type>singleton</type>
            <class>module/observer</class>
            <method>checkIsSaleable</method>
        </modify_saleable>
    </observers>
</catalog_product_is_salable_after>
public function checkIsSaleable($observer) {
    $product = $observer->getProduct();

    if($product->getIgnoreSaleableCategory()){
        return;
    }

    if(Mage::helper('module')->isPurchaseableViaCategory($product)){
        $observer->getSalable()->setIsSalable(true);
    }
}

For completeness, we added a property to the product object to be able to identify whether this product exists in a category and should have been listed as out of stock. We can then identify these products and display a different message (Such as delivery times) on the listing and detail pages. Here’s where it’s used in our Helper:

public function isOutOfStockButPurchasable(Mage_Catalog_Model_Product $product){
    if(!$product){
        return false;
    }

    $salableCheckOne = $product->isSalable();
    $product->setIgnoreSaleableCategory(true);

    $salableCheckTwo = $product->isSalable();
    $product->setIgnoreSaleableCategory(false);

    return $salableCheckOne !== $salableCheckTwo;
}

Our modified products will then be identifiable because the value from the two checks will not match.

Caveats

The only problem from this method is that if the client saves one of the out of stock products within one of these categories, then the values for backorder and is_in_stock will be saved to the database, meaning that if the code is disabled those saved products will still be available via backorder.

Fixing Issues With Magento’s Massaction PDF Tax Values

There appears to be an issue in Magento’s Massaction Invoice PDF. The issue presents itself when outputting the total tax for the invoice, which will be completely different to the amount displayed when the PDF is created from the view invoice page.

The issue exists in the Mage_Tax_Helper_Data class’ getCalculatedTaxes method. The first few lines decide what the variable $current will be;

if ($this->_getFromRegistry('current_invoice')) {
    $current = $this->_getFromRegistry('current_invoice');
} elseif ($this->_getFromRegistry('current_creditmemo')) {
    $current = $this->_getFromRegistry('current_creditmemo');
} else {
    $current = $source;
}

In the single version of PDF creation, the current_invoice registry key is set and is used going forward. This is omitted from the massaction update, so the method falls back to the $source parameter, which happens to be an instance of an order object.

The fix is to override Mage_Sales_Model_Order_Pdf_Invoice and create a new getPdf method, with just a two line addition inside of the foreach:

public function getPdf($invoices = array())
{
    $this->_beforeGetPdf();
    $this->_initRenderer('invoice');

    $pdf = new Zend_Pdf();
    $this->_setPdf($pdf);
    $style = new Zend_Pdf_Style();
    $this->_setFontBold($style, 10);

    foreach ($invoices as $invoice) {
        // Set the invoice here, as not doing this (E.g. default Magento) causes the wrong value to be calculated
        // when using the massaction pdf action. Unregister first, otherwise only the first invoice will be set
        Mage::unregister('current_invoice');
        Mage::register('current_invoice', $invoice);
        
        if ($invoice->getStoreId()) {
            Mage::app()->getLocale()->emulate($invoice->getStoreId());
            Mage::app()->setCurrentStore($invoice->getStoreId());
        }
        $page  = $this->newPage();
        $order = $invoice->getOrder();
        /* Add image */
        $this->insertLogo($page, $invoice->getStore());
        /* Add address */
        $this->insertAddress($page, $invoice->getStore());
        /* Add head */
        $this->insertOrder(
            $page,
            $order,
            Mage::getStoreConfigFlag(self::XML_PATH_SALES_PDF_INVOICE_PUT_ORDER_ID, $order->getStoreId())
        );
        /* Add document text and number */
        $this->insertDocumentNumber(
            $page,
            Mage::helper('sales')->__('Invoice # ') . $invoice->getIncrementId()
        );
        /* Add table */
        $this->_drawHeader($page);
        /* Add body */
        foreach ($invoice->getAllItems() as $item){
            if ($item->getOrderItem()->getParentItem()) {
                continue;
            }
            /* Draw item */
            $this->_drawItem($item, $page, $order);
            $page = end($pdf->pages);
        }
        /* Add totals */
        $this->insertTotals($page, $invoice);
        if ($invoice->getStoreId()) {
            Mage::app()->getLocale()->revert();
        }
    }
    $this->_afterGetPdf();
    return $pdf;
}

And massaction PDFs will once again have the correct tax amount.

Programatically add a Layout Update File in Magento

We sometimes need logic to decide whether an update file should be loaded or not, and need to do this outside of a module’s config.xml area/layout/update node. Instead, we can use an observer and programatically decide whether to add the layout file or not.

config.xml

<?xml version="1.0"?>
<config>
    <modules>
        <Namespace_Module>
            <version>1.0.0</version>
        </Namespace_Module>
    </modules>
    <global>
        <models>
            <mymodule>
                <class>Loewenstark_Layout_Model</class>
            </mymodule>
        </models>
        <events>
            <core_layout_update_updates_get_after>
                <observers>
                    <mymodule_add_layout>
                        <type>singleton</type>
                        <class>mymodule/observer</class>
                        <method>addLayoutXml</method>
                    </mymodule_add_layout>
                </observers>
            </core_layout_update_updates_get_after>     
        </events>
    </global>
</config>

The Observer

Because the updates object extends a SimpleXML Object, we can add our file to it here

<?php
class Namespace_Module_Model_Observer
{
    public function loadXML(Varien_Event_Observer $observer)
    {
        // Conditional to decide whether to load the XML file
        if(Mage::helper('mymodule')->isEnabled()) {
            $xml = $observer->getUpdates()
                ->addChild('mymodule');
            /* @var $xml SimpleXMLElement */
            $xml->addAttribute('module', 'Namespace_Module');
            $xml->addChild('file', 'mymodule/layoutfile.xml');
        }
    }
}