4
votes

I'm working on a Magento store where the client has requested a few custom filters on orders so that they can be manually reviewed before being sent off to fulfillment. When these cases come up, the orders are marked with the built in Payment Review / Suspected Fraud state/status.

My problem is that, in the admin interface, you don't seem to be able to do much with an order in the Payment Review state. I added a custom 'Approve'-type button for manual approval of the order, but if it's reviewed and confirmed as fraud, what is the expected action to take? I would think to cancel or refund the order but that doesn't seem to be allowed. Checking canCancel or canCreditmemo on the order returns false. Would it be better to use a Hold state or something over Payment Review for a scenario like this?

4

4 Answers

8
votes

Rather than overriding the Mage_Sales_Model_Order object (not really ideal), I've discovered some existing hooks in the Magento toolkit that can enable administrator actions after an Order is flagged using the Suspected Fraud status. To enable these, the following steps are required:

In your payment method (inheriting from Mage_Payment_Model_Method_Abstract), add the following:

    protected $_canReviewPayment  = true;

    public function acceptPayment(Mage_Payment_Model_Info $payment) {
        parent::acceptPayment($payment);
        //perform gateway actions to remove Fraud flags. Capture should not occur here
        return true;
        //returning true will trigger a capture on any existing invoices, otherwise the admin can manually Invoice the order
    }

    public function denyPayment(Mage_Payment_Model_Info $payment) {
        parent::denyPayment($payment);
        //if your payment gateway supports it, you should probably void any pre-auth
        return true;  
    }

Magento's Order View block will check $order->canReviewPayment() which will look at the _canReviewPayment variable on the payment method, and if true, display two buttons on the Order View : "Accept Payment" and "Deny Payment". When clicked, the two new Payment Method functions we just added above will be called as appropriate.

If you have an Invoice associated with the Order already, that will be either be pay'd or cancel'd. Have a look at Mage_Sales_Model_Order_Payment::registerPaymentReviewAction for more detail.

3
votes

We have clients with the paygate issue regarding this Suspected Fraud or 'fraud' flag for the invoice payment, where the paygate did not notify Magento after the payment was manually approved. This seems to be a problem with authorize.net and certain paypal authorization and response configurations.

Here is the three part solution I developed to manipulate the order at least far enough to allow a credit memo to be created, and to clear the fraud status. This process also creates the order shipment and pushes the order forward to 'Processing' and then 'Complete.'

I made these three modifications in a local copy of the core code file /public_html/app/code/local/Mage/Adminhtml/Block/Sales/Order/View.php

(1) Commented line of code and replaced with one that permits the credit memo button to appear when an order is in Processing or Complete status even if $order->canCreditmemo() was not properly set to equal true.

// if ($this->_isAllowedAction('creditmemo') && $order->canCreditmemo()) {
if ($this->_isAllowedAction('creditmemo') && ($order->getState() == 'complete' || $order->getState() == 'processing')) {

(2) Created a button for clearing fraud status that calls the function referenced in #3

// 06/10/2014 Rand created button on Admin panel to clear fraud AFTER payment is authorized manually.
if ($order->getStatus() === 'fraud') {
$message = Mage::helper('sales')->__('*** CAUTION *** Payment must FIRST be authorized manually. Are you sure you want to clear this fraud status?');
$this->addButton('clear_fraud', array(
    'label'     => Mage::helper('sales')->__('Clear Fraud'),
    'onclick'   => 'setLocation(\'' . $this->clearFraud($order) . '\')',
));
}

(3) Created public function clearFraud($order) to accomplish the clearing of invoice to paid, the creation of shipment (if needed), and the clearing of $order state from 'fraud' to complete.

public function clearFraud($order)
{
// THIS FUNCTION IS CREATED BY RAND TO HANDLE CLEARING INCOMPLETED RECORDS
// CREATED BY AUTHORIZE.NET FRAUD PROTECTION PROCESS

// setState of order invoice(s) to one that will accept order completion, and save it.
if ($order->hasInvoices()) {
    foreach ($order->getInvoiceCollection() as $invoice) {
        $invoice->setState('2');
        $invoice->save();
    }
}

// Handle Shipment: Create it (if needed) and save the transaction.
if (!$order->hasShipments()) {
    $shipment = $order->prepareShipment();
    $shipment->register();
    $order->setIsInProcess(true);
    $transactionSave = Mage::getModel('core/resource_transaction')
        ->addObject($shipment)
        ->addObject($shipment->getOrder())
        ->save();
    $order->save();
}

// Set order to complete, and save the order
$order->setState(Mage_Sales_Model_Order::STATE_PROCESSING, true);
$order->save();
$order->setState(Mage_Sales_Model_Order::STATE_COMPLETE, true);
$order->save();

return $this->getUrl('*/sales_order');
}

That should provide some help in resolving how to make Magento behave the way you want.

2
votes

I think semantically it's important to differentiate between the Hold state and the Payment Review, so I would recommend that you keep the separate status/state and make it work within the client workflow.

You can override the canCancel() and canCreditmemo() functions so that they allow those actions when the order is in the Payment Review state. In your class that extends Mage_Sales_Model_Order, redefine those functions to check for your custom status/state.

HTH,
JD

0
votes

After searching for a solution for this issue, B.Sharp's option is the only fix I found. I mixed it with this https://www.hummingbirduk.com/suspected-fraud-transactions-magento/ and added $invoice->sendEmail(); to get the email sent to customer.

In our situation, the status "fraud suspected" is happening randomly, same customer with same basket and same paypal account might get it or not. Nothing to do with rounding, tax or currency.

class Mage_Adminhtml_Block_Sales_Order_View extends Mage_Adminhtml_Block_Widget_Form_Container

{

public function __construct()
{
    $this->_objectId    = 'order_id';
    $this->_controller  = 'sales_order';
    $this->_mode        = 'view';

    parent::__construct();

    $this->_removeButton('delete');
    $this->_removeButton('reset');
    $this->_removeButton('save');
    $this->setId('sales_order_view');
    $order = $this->getOrder();
    $coreHelper = Mage::helper('core');

    if ($this->_isAllowedAction('edit') && $order->canEdit()) {
        $confirmationMessage = $coreHelper->jsQuoteEscape(
            Mage::helper('sales')->__('Are you sure? This order will be canceled and a new one will be created instead')
        );
        $onclickJs = 'deleteConfirm(\'' . $confirmationMessage . '\', \'' . $this->getEditUrl() . '\');';
        $this->_addButton('order_edit', array(
            'label'    => Mage::helper('sales')->__('Edit'),
            'onclick'  => $onclickJs,
        ));
        // see if order has non-editable products as items
        $nonEditableTypes = array_keys($this->getOrder()->getResource()->aggregateProductsByTypes(
            $order->getId(),
            array_keys(Mage::getConfig()
                ->getNode('adminhtml/sales/order/create/available_product_types')
                ->asArray()
            ),
            false
        ));
        if ($nonEditableTypes) {
            $confirmationMessage = $coreHelper->jsQuoteEscape(
                Mage::helper('sales')
                    ->__('This order contains (%s) items and therefore cannot be edited through the admin interface at this time, if you wish to continue editing the (%s) items will be removed, the order will be canceled and a new order will be placed.',
                    implode(', ', $nonEditableTypes), implode(', ', $nonEditableTypes))
            );
            $this->_updateButton('order_edit', 'onclick',
                'if (!confirm(\'' . $confirmationMessage . '\')) return false;' . $onclickJs
            );
        }
    }

    if ($this->_isAllowedAction('cancel') && $order->canCancel()) {
        $confirmationMessage = $coreHelper->jsQuoteEscape(
            Mage::helper('sales')->__('Are you sure you want to cancel this order?')
        );
        $this->_addButton('order_cancel', array(
            'label'     => Mage::helper('sales')->__('Cancel'),
            'onclick'   => 'deleteConfirm(\'' . $confirmationMessage . '\', \'' . $this->getCancelUrl() . '\')',
        ));
    }

    if ($this->_isAllowedAction('emails') && !$order->isCanceled()) {
        $confirmationMessage = $coreHelper->jsQuoteEscape(
            Mage::helper('sales')->__('Are you sure you want to send order email to customer?')
        );
        $this->addButton('send_notification', array(
            'label'     => Mage::helper('sales')->__('Send Email'),
            'onclick'   => "confirmSetLocation('{$confirmationMessage}', '{$this->getEmailUrl()}')",
        ));
    }

    //if ($this->_isAllowedAction('creditmemo') && $order->canCreditmemo()) {
    if ($this->_isAllowedAction('creditmemo') && ($order->getState() == 'complete' || $order->getState() == 'processing')) {
        $confirmationMessage = $coreHelper->jsQuoteEscape(
            Mage::helper('sales')->__('This will create an offline refund. To create an online refund, open an invoice and create credit memo for it. Do you wish to proceed?')
        );
        $onClick = "setLocation('{$this->getCreditmemoUrl()}')";
        if ($order->getPayment()->getMethodInstance()->isGateway()) {
            $onClick = "confirmSetLocation('{$confirmationMessage}', '{$this->getCreditmemoUrl()}')";
        }
        $this->_addButton('order_creditmemo', array(
            'label'     => Mage::helper('sales')->__('Credit Memo'),
            'onclick'   => $onClick,
            'class'     => 'go'
        ));
    }

    // invoice action intentionally
    if ($this->_isAllowedAction('invoice') && $order->canVoidPayment()) {
        $confirmationMessage = $coreHelper->jsQuoteEscape(
            Mage::helper('sales')->__('Are you sure you want to void the payment?')
        );
        $this->addButton('void_payment', array(
            'label'     => Mage::helper('sales')->__('Void'),
            'onclick'   => "confirmSetLocation('{$confirmationMessage}', '{$this->getVoidPaymentUrl()}')",
        ));
    }

    if ($this->_isAllowedAction('hold') && $order->canHold()) {
        $this->_addButton('order_hold', array(
            'label'     => Mage::helper('sales')->__('Hold'),
            'onclick'   => 'setLocation(\'' . $this->getHoldUrl() . '\')',
        ));
    }

    if ($this->_isAllowedAction('unhold') && $order->canUnhold()) {
        $this->_addButton('order_unhold', array(
            'label'     => Mage::helper('sales')->__('Unhold'),
            'onclick'   => 'setLocation(\'' . $this->getUnholdUrl() . '\')',
        ));
    }

    if ($this->_isAllowedAction('review_payment')) {
        if ($order->canReviewPayment()) {
            $confirmationMessage = $coreHelper->jsQuoteEscape(
                Mage::helper('sales')->__('Are you sure you want to accept this payment?')
            );
            $onClick = "confirmSetLocation('{$confirmationMessage}', '{$this->getReviewPaymentUrl('accept')}')";
            $this->_addButton('accept_payment', array(
                'label'     => Mage::helper('sales')->__('Accept Payment'),
                'onclick'   => $onClick,
            ));
            $confirmationMessage = $coreHelper->jsQuoteEscape(
                Mage::helper('sales')->__('Are you sure you want to deny this payment?')
            );
            $onClick = "confirmSetLocation('{$confirmationMessage}', '{$this->getReviewPaymentUrl('deny')}')";
            $this->_addButton('deny_payment', array(
                'label'     => Mage::helper('sales')->__('Deny Payment'),
                'onclick'   => $onClick,
            ));
        }
        if ($order->canFetchPaymentReviewUpdate()) {
            $this->_addButton('get_review_payment_update', array(
                'label'     => Mage::helper('sales')->__('Get Payment Update'),
                'onclick'   => 'setLocation(\'' . $this->getReviewPaymentUrl('update') . '\')',
            ));
        }
    }

    if ($this->_isAllowedAction('invoice') && $order->canInvoice()) {
        $_label = $order->getForcedDoShipmentWithInvoice() ?
            Mage::helper('sales')->__('Invoice and Ship') :
            Mage::helper('sales')->__('Invoice');
        $this->_addButton('order_invoice', array(
            'label'     => $_label,
            'onclick'   => 'setLocation(\'' . $this->getInvoiceUrl() . '\')',
            'class'     => 'go'
        ));
    }

    if ($this->_isAllowedAction('ship') && $order->canShip()
        && !$order->getForcedDoShipmentWithInvoice()) {
        $this->_addButton('order_ship', array(
            'label'     => Mage::helper('sales')->__('Ship'),
            'onclick'   => 'setLocation(\'' . $this->getShipUrl() . '\')',
            'class'     => 'go'
        ));
    }

    if ($this->_isAllowedAction('reorder')
        && $this->helper('sales/reorder')->isAllowed($order->getStore())
        && $order->canReorderIgnoreSalable()
    ) {
        $this->_addButton('order_reorder', array(
            'label'     => Mage::helper('sales')->__('Reorder'),
            'onclick'   => 'setLocation(\'' . $this->getReorderUrl() . '\')',
            'class'     => 'go'
        ));
    }


    // 06/10/2014 Rand created button on Admin panel to clear fraud AFTER payment is authorized manually.
    if ($order->getStatus() === 'fraud') {
            $message = Mage::helper('sales')->__('*** CAUTION *** Payment must FIRST be authorized manually. Are you sure you want to clear this fraud status?');
                $this->addButton('clear_fraud', array(
                    'label'     => Mage::helper('sales')->__('Clear Fraud'),
                    'onclick'   => 'setLocation(\'' . $this->clearFraud($order) . '\')',
                    'class'     => 'go'
                ));
        }



}

/**
 * Retrieve order model object
 *
 * @return Mage_Sales_Model_Order
 */
public function getOrder()
{
    return Mage::registry('sales_order');
}

/**
 * Retrieve Order Identifier
 *
 * @return int
 */
public function getOrderId()
{
    return $this->getOrder()->getId();
}

public function getHeaderText()
{
    if ($_extOrderId = $this->getOrder()->getExtOrderId()) {
        $_extOrderId = '[' . $_extOrderId . '] ';
    } else {
        $_extOrderId = '';
    }
    return Mage::helper('sales')->__('Order # %s %s | %s', $this->getOrder()->getRealOrderId(), $_extOrderId, $this->formatDate($this->getOrder()->getCreatedAtDate(), 'medium', true));
}

public function getUrl($params='', $params2=array())
{
    $params2['order_id'] = $this->getOrderId();
    return parent::getUrl($params, $params2);
}

public function getEditUrl()
{
    return $this->getUrl('*/sales_order_edit/start');
}

public function getEmailUrl()
{
    return $this->getUrl('*/*/email');
}

public function getCancelUrl()
{
    return $this->getUrl('*/*/cancel');
}

public function getInvoiceUrl()
{
    return $this->getUrl('*/sales_order_invoice/start');
}

public function getCreditmemoUrl()
{
    return $this->getUrl('*/sales_order_creditmemo/start');
}

public function getHoldUrl()
{
    return $this->getUrl('*/*/hold');
}

public function getUnholdUrl()
{
    return $this->getUrl('*/*/unhold');
}

public function getShipUrl()
{
    return $this->getUrl('*/sales_order_shipment/start');
}

public function getCommentUrl()
{
    return $this->getUrl('*/*/comment');
}

public function getReorderUrl()
{
    return $this->getUrl('*/sales_order_create/reorder');
}

/**
 * Payment void URL getter
 */
public function getVoidPaymentUrl()
{
    return $this->getUrl('*/*/voidPayment');
}

protected function _isAllowedAction($action)
{
    return Mage::getSingleton('admin/session')->isAllowed('sales/order/actions/' . $action);
}

/**
 * Return back url for view grid
 *
 * @return string
 */
public function getBackUrl()
{
    if ($this->getOrder()->getBackUrl()) {
        return $this->getOrder()->getBackUrl();
    }

    return $this->getUrl('*/*/');
}

public function getReviewPaymentUrl($action)
{
    return $this->getUrl('*/*/reviewPayment', array('action' => $action));
}


public function clearFraud($order)
{


    $order->setState(Mage_Sales_Model_Order::STATE_PROCESSING, true);
    $order->setStatus('processing', false);
    $order->save();
    try {
        if(!$order->canInvoice()) {
            Mage::throwException(Mage::helper('core')->__('Cannot create an invoice.'));
        }
        $invoice = Mage::getModel('sales/service_order', $order)->prepareInvoice();
        if (!$invoice->getTotalQty()) {
            Mage::throwException(Mage::helper('core')->__('Cannot create an invoice without products.'));
        }
        $invoice->setRequestedCaptureCase(Mage_Sales_Model_Order_Invoice::CAPTURE_OFFLINE);
        $invoice->register();
        $invoice->sendEmail();
        $transactionSave = Mage::getModel('core/resource_transaction')->addObject($invoice)->addObject($invoice->getOrder());
        $transactionSave->save();
        } catch (Mage_Core_Exception $e) {
    }





    return $this->getUrl('*/sales_order');
}

}