15

I have got a problem with Magento single coupon code that is marked as having been used at the time the customer clicks on the Place Order button. If the Paypal payment fails or the client leaves the page before the order is complete, he won't able to go back and re-order with this coupon which is set to be only used once, and has been marked already been used.

I have found a piece of code that decreases the number of times the coupons has been used by the user and allows him to reuse the coupon. Unfortunately, he gets an error when trying to connect the Paypal page when clicking the place order button. In order to be able to use the coupon another time and access the Paypal page, I have to delete the lines in SQL database in tables salesrule_coupon_usage and salesrule_customer with this customer's ID.

Here is the code I need to change to automatically delete coupon usage information for a customer ID:

public function cancel($observer)
{
    $order = $observer->getEvent()->getPayment()->getOrder();
    if ($order->canCancel()) {
        if ($code = $order->getCouponCode()) {
            $coupon = Mage::getModel('salesrule/coupon')->load($code, 'code');
            if ($coupon->getTimesUsed() > 0) {
                $coupon->setTimesUsed($coupon->getTimesUsed() - 1);
                $coupon->save();
            }

            $rule = Mage::getModel('salesrule/rule')->load($coupon->getRuleId());
            error_log("\nrule times used=" . $rule->getTimesUsed(), 3, "var/log/debug.log");
            if ($rule->getTimesUsed() > 0) {
                $rule->setTimesUsed($rule->getTimesUsed()-1);
                $rule->save();
            }

            if ($customerId = $order->getCustomerId()) {
                if ($customerCoupon = Mage::getModel('salesrule/rule_customer')->loadByCustomerRule($customerId, $rule->getId())) {
                    $couponUsage = new Varien_Object();
                    Mage::getResourceModel('salesrule/coupon_usage')->loadByCustomerCoupon($couponUsage, $customerId, $coupon->getId());

                    if ($couponUsage->getTimesUsed() > 0) {
                        /* I can't find any #@$!@$ interface to do anything but increment a coupon_usage record */
                        $resource = Mage::getSingleton('core/resource');
                        $writeConnection = $resource->getConnection('core_write');
                        $tableName = $resource->getTableName('salesrule_coupon_usage');

                        $query = "UPDATE {$tableName} SET times_used = times_used-1 "
                            .  "WHERE coupon_id = {$coupon->getId()} AND customer_id = {$customerId} AND times_used > 0";

                        $writeConnection->query($query);
                    }

                    if ($customerCoupon->getTimesUsed() > 0) {
                        $customerCoupon->setTimesUsed($customerCoupon->getTimesUsed()-1);
                        $customerCoupon->save();
                    }
                }
            }
        }
    }
}
Harshal Doshi Jain
  • 2,567
  • 1
  • 20
  • 16
user1805921
  • 151
  • 1
  • 4
  • Your need to better format your code. At the moment it is unreadable as a single line. – GenericJon Nov 07 '12 at 11:53
  • code is now well formated. Thx – user1805921 Nov 07 '12 at 14:12
  • Which version of Magento are you using? – Gaffe May 14 '13 at 15:31
  • Please, clarify: do you have working code and don't know where to place it? – vsushkov Dec 11 '13 at 08:19
  • 3
    That doesn't seem like the correct place to put the fix at all. You don't want to cancel the `Order`, you want to stop it from being created in the first place, or rather; you need to stop the `Coupon` from being saved prematurely. I would either wrap the whole process in a transaction or use an events system to trigger the coupon decrement. – Mathew Jan 06 '14 at 12:25
  • 1
    If you truely want to fix this issue, you should probably use paypal express checkout. It allows the customer to go to paypal and doesnt set the order until after the customer comes back to site to approve the order. That way the coupon isnt created at the checkout , but instead when the user returns to the site – numerical25 Jan 17 '14 at 21:56
  • 1
    other then that, a easier way to get this right is find out where the initial $coup->save is at, ignore it and trigger it when the order status changes instead. It encapsulates all the headache, instead of you going through tables and reverting things that were done. – numerical25 Jan 17 '14 at 22:00

1 Answers1

1

I believe this was an old bug from around 1.4 to maybe 1.6. But whether you've got an old version or not, this can fairly easily be fixed in place if you know your way around Magento.

The problem is, you have code that is updating the salesrule_coupon_usage table right when they click the pay button. This isn't really what you want at all. You want this to be wrapped into the payment transaction. I don't know if this bug is happening because you have custom code or are using a older version of Magento, but I'll tell you how I would fix the problem. Then I'll give you a fix similar to what you proposed:

Magento already has an abstraction called a "transaction". A transaction is used to put and group of objects together that need to either all succeed or all fail, no half-measures. Magento will go through the transaction and attempt to save each of the objects you have placed in it. If any of them fail (for example, payment doesn't go through), all the events that have been already saved are "rolled back".

Luckily, Magento already creates a transaction object to handle payment for the various things that need to update together on successful payment. You can tap into that transaction and use it to update the coupon usage correctly.

Here is the 10,000 foot view of what you need to do.

  • Find whatever is updating the salesrule_coupon_usage table too early and kill it. We are going to add in our own transaction-safe version, so we don't want it being saved anywhere else. Easiest way to do this is to figure out what model connects to that table and search for the creation of that model. For 1.7 and 1.8, that is the rule/customer model.
  • Create an observer to catch the start of a payment transaction. In most modern versions of magento this event is called sales_order_payment_place_start and can be witnessed in app/code/core/Mage/Sales/Model/Order/Payment.php
  • Pull the order out of the event and pull the coupon code out of the event.
  • Pull the actual model you want to update. It looks like someone couldn't find it in your code, but there should be some model that uses the salesrule_coupon_usage table hiding somewhere. Search though the .xml files for "salesrule_coupon_usage" and see which model using that table. Again, for me, on 1.7, that is the rule/customer model.
  • Load that model up, with the customer change the value where your coupon code is concerned to indicate that the customer has used the coupon, but don't save yet.
  • Get the transaction out of the event and register your updated coupon object with the addObject method.

And your done. The transaction will automatically try to save all of the objects that have been added to it. If any part fails (including a failed payment), it'll rollback the whole process and your coupon won't be used. Yay!

Now, in my opinion, the above is the best way to handle the issue, but if you are having issues for some reason, here is an alternative based on catching an unsuccessful payment and then using your code.

Again, here is the 10,000 foot view:

  • Create an observer to catch the failed payment event. In most versions the event you want is sales_order_payment_cancel.
  • Run the code you've got... it should do it. But to clarify for others:
    • Pull the order out of the event, pull the coupon code and customer id out of that.
    • Update the customer, rule and salesrule_coupon_usage table. (Although there really should be a model for that, I'm sure you can find it)

Now when a sale fails, you go back through and unwind everything manually. This isn't as clean as my first solution, but might be easier for you depending on your familiarity with Magento.

I'm fairly sure that new, clean versions of Magento don't have this issue, so let me offer a fairly obvious suggestion as a third solution.

  • Update Magento
  • If Magento is up to date, test disabling any custom modules, because something broke it. I've noticed that Amasty coupon plugins are particularly buggy.
  • If you've made custom changes to Magento core... good luck with that.

Good luck!

Damien Black
  • 5,579
  • 18
  • 24