Is there any functionality in magento payment extention to stop the creation of an order before the success status from payment gateway is returned?
The Extension is designed as a payment redirect (with getOrderPlaceRedirectUrl) but really in the correct controller action, I do post from the action with params, to the gateway and return success if all OK, and return failure if not.
But the order is already created so I must cancel this order, but it should not create this order in the first place.
Maybe if I can design it as a gateway, I can use some payment method to do this?
I thought about the validate or prepareSave methods, but both of them are called twice - on accept payment method and on place order.
Also I thought about events - maybe I can use some event to do this post action and on failture just throw exception?
But I really think that in the payment methods, there has to be some standard functionality that I can use.
This is quite a common issue during Payment Module development.
Magento offers two hooks for payment method classes to provide redirect URL's, one before the order is created, one after.
If a payment method model implements getOrderPlaceRedirectUrl() the customer will be redirected after the confirmation step of the one page checkout, the order entity will be created.
If a payment method model implements the getCheckoutRedirectUrl() method, the customer will be redirected after the payment step of the one page checkout, and no order entity is created.
This is not ideal, but thats what Magento offers out of the box.
How about extending the _validate() method on Mage_Sales_Model_Service_Quote, and throw an error there so that it never gets to the "$transaction->save();" bit.
public function submitOrder()
{
$this->_deleteNominalItems();
// do some check here
$this->_validate();
// End checks
$quote = $this->_quote;
$isVirtual = $quote->isVirtual();
........
try {
$transaction->save();
$this->_inactivateQuote();
Mage::dispatchEvent('sales_model_service_quote_submit_success', array('order'=>$order, 'quote'=>$quote));
} catch (Exception $e) {
...........
}
...........
return $order;
}
Validate function looks like this:
protected function _validate()
{
$helper = Mage::helper('sales');
if (!$this->getQuote()->isVirtual()) {
$address = $this->getQuote()->getShippingAddress();
$addressValidation = $address->validate();
if ($addressValidation !== true) {
Mage::throwException(
$helper->__('Please check shipping address information. %s', implode(' ', $addressValidation))
);
}
$method= $address->getShippingMethod();
$rate = $address->getShippingRateByCode($method);
if (!$this->getQuote()->isVirtual() && (!$method || !$rate)) {
Mage::throwException($helper->__('Please specify a shipping method.'));
}
}
$addressValidation = $this->getQuote()->getBillingAddress()->validate();
if ($addressValidation !== true) {
Mage::throwException(
$helper->__('Please check billing address information. %s', implode(' ', $addressValidation))
);
}
if (!($this->getQuote()->getPayment()->getMethod())) {
Mage::throwException($helper->__('Please select a valid payment method.'));
}
return $this;
}
The extended function can look like this:
public function __construct(Mage_Sales_Model_Quote $quote)
{
$this->_quote = $quote;
parent::__construct($quote);
}
protected function _validate()
{
// Code to test comes here
Mage::throwException(Mage::helper('payment')->__('unsuccessfull.....'));
// Code ends, now call parent
return parent::_validate();
}
As I said - giving sample that I used for this solution at final.
I prefered to observe event to do post request. Really if you'll use method presented here
you will take the same effect, but I prefer to use event observer. So:
First add some data to config.xml to create event observer in frontend section
<events>
<sales_model_service_quote_submit_before>
<observers>
<lacpaycs>
<type>singleton</type>
<class>OS_LacPayCS_Model_Observer</class>
<method>lacpaycs_payment_send</method>
</lacpaycs>
</observers>
</sales_model_service_quote_submit_before>
</events>
then we must create Observer class in OS/LacPayCS/Mode/Observer.php:
class OS_LacPayCS_Model_Observer {
protected $_code = 'lacpaycs';
// Here some our additional functions
/**
* #param Varien_Object $observer
*/
public function lacpaycs_payment_send(Varien_Object $observer)
{
/**
* #var Mage_Sales_Model_Order $order
* #var Mage_Sales_Model_Quote $quote
*/
$order = $observer->getOrder();
$quote = $observer->getQuote();
$payment = $order->getPayment();
if ($payment->getMethodInstance()->getCode() != $this->_code) {
return;
}
$helper = Mage::helper('lacpaycs');
try {
// Here we prepare data and sending request to gateway, and getting response
if (!$this->_validateResponse($response)) {
Mage::throwException('Error '.$this->errorMsg);
}
} catch (Exception $e) {
Mage::throwException($e->getMessage());
}
}
}
So in two words what we doing here $_code is the same that in our payment model and with it we checking in observer if we catched event when customer using our payment method
All another code is simple, so I think it's no need to comment it
Related
After watching many laracasts, one statement is everywhere: keep the controller as light as possible.
Ok, I am trying to familiarize myself with laravel concepts and philosophy, with the Repository and the separation of concerns patterns and I have some questions that bother me, let's assume the following:
Route::resource('/item', 'ItemController');
class Item extends \Eloquent {}
the repo
class EloquentItemRepo implements ItemRepo {
public function all()
{
return Item::all();
}
public function find($id)
{
return Item::where('id', '=', $id);
}
}
and the controller:
class ItemController extends BaseController {
protected $item;
public function __construct(ItemRepo $item)
{
$this->item = $item;
}
public function index()
{
$items = $this->item->all();
return Response::json(compact('items'))
}
}
For now, everything is simple and clean (assume that the repo is loaded by providers etc.) the controller is really simple and does nothing except loading and returning the data (I used json but anything will do).
Please assume that I am using an auth filter that checks that the user
is logged in and exists, or return an error if it doesn't, so I don't
have to do any further check in the controller.
Now, what if I need to do more checks, for instance:
response_* methods are helpers that format a Json response
public function destroy($id)
{
try {
if ($this->item->destroy($id)) {
return Response::json(['success' => true]);
}
return response_failure(
Lang::get('errors.api.orders.delete'),
Config::get('status.error.forbidden')
);
} catch (Exception $e) {
return response_failure(
Lang::get('errors.api.orders.not_found'),
Config::get('status.error.notfound')
);
}
}
In this case I have to test many things:
The desctuction worked? (return true)
The destruction failed? (return false)
There was an error during deletion ? (ex.: the item wasn't found with firstOrFail)
I have methods where many more tests are done, and my impression is that the controller is growing bigger and bigger so I can handle any possible errors.
Is it the right way to manage this ? The controller should be full of checks or the tests should be moved elsewhere ?
In the provider I often use item->firstOrFail() and let the exception bubble up to the controller, is it good ?
If someone could point me to the right direction as all the laracasts or other tutorials always use the simpler case, where not many controls are needed.
Edits: Practical case
Ok so here a practical case of my questioning:
controller
/**
* Update an order.
* #param int $id Order id.
* #return \Illuminate\Http\JsonResponse
*/
public function update($id)
{
try {
$orderItem = $this->order->update($id, Input::all());
if (false === $orderItem) {
return response_failure(
Lang::get('errors.api.orders.update'),
Config::get('status.error.forbidden')
);
}
return response_success();
} catch (Exception $e) {
return response_failure(
Lang::get('errors.api.orders.not_found'),
Config::get('status.error.notfound')
);
}
}
repo
public function update($id, $input)
{
$itemId = $input['itemId'];
$quantity = $input['quantity'] ?: 1;
// cannot update without item id
if (!$itemId) {
return false;
}
$catalogItem = CatalogItem::where('hash', '=', $itemId)->firstOrFail();
$orderItem = OrderItem::fromCatalogItem($catalogItem);
// update quantity
$orderItem->quantity = $quantity;
return Order::findOrFail($id)->items()->save($orderItem);
}
In this case thare are 3 possible problems:
order not found
catalogItem not found
itemId not set in post data
In the way I have organized that, the problem is that the top level error message won't be clear, as it will alway state: "order not found" even if it's the catalog item that couldn't be found.
The only possibility that I see is to catch multiple exceptions codes in the controller and raise a different error message, but won't this overload the controller ?
I'm writing a custom module, where i've added a custom product type. How can i write an observer catalog_product_save_after only for that custom product type?
You cannot add an observer for that type of product, but you can check in the observer if the product is valid. If not then do nothing.
public function doSomething($observer){
$product = $observer->getEvent()->getProduct();
if ($product->getTypeId() != 'YOUR TYPE HERE'){
return $this;
}
//your magic here
}
The *_save_after events are fired from the Varien_Object class, and is dynamic depending on the class. So it is gonna be the same event for all product types.
You can still observe the catalog_product_save_after event and perform your actions depending on the product type :
public function yourObserverMethod($observer)
{
$product = $observer->getEvent()->getProduct();
if($product == Mage_Catalog_Model_Product_Type::TYPE_CONFIGURABLE) {
// Your stuff
}
}
Having issues setting the customers group id from an observer. The event is picking up on a new user creation via customer_register_success event. The event is passed to my observer, e.g.
public function registrationSuccess(Varien_Event_Observer $observer) {
// extract customer data from event
$customer = $observer->getCustomer()->getData();
Mage::log('COOKIES', json_encode($_COOKIE));
// a cookie should have been set with the membership id
if (isset($_COOKIE['membership_account_id'])) {
Mage::log('COOKIE SET, ASSOCIATING MEMBERSHIP');
// associate new account with membership, and upgrade user to membership status
$this->associateMembership($customer['entity_id'], $_COOKIE['membership_account_id']);
}
}
Which then calls the associateMembership method to update the group id, and set a custom customer attribute called rms_id:
public function associateMembership($customer_id, $account_id) {
// load customer model
$customer = Mage::getModel('customer/customer')->load($customer_id);
Mage::log('CUSTOMER DATA: ' . json_encode($customer->toArray()));
// upgrade customer to membership level, and set custom rms_id attribute
$customer
->setWebsiteId(Mage::app()->getWebsite()->getId())
->setGroupId(4)
->setRmsId($account_id);
// save
try {
$customer->save();
Mage::log('ACCOUNT ASSOCIATED: CUSTOMER ID = ' . $customer_id . ' ACCOUNT ID = ' . $account_id);
} catch (Exception $ex) {
Mage::log($ex);
}
}
For some reason, there's no error coming back. I'm getting the correct user id, and everything seems to be working. However, the group is not being set, nor is my custom id.
Should I be using another event that will allow the save to go through?
Try loading the website id before loading the customer
$customer = Mage::getModel('customer/customer')
$customer->setWebsiteId(Mage::app()->getWebsite()->getId());
$customer->load($customer_id);
customer_register_success will re-save the customer data after you save it in your custom observer
Also customer_register_success pass the customer data so you should not need to reload it.
see /app/code/core/Mage/Customer/controllers/AccountController.php
Mage::dispatchEvent('customer_register_success',
array('account_controller' => $this, 'customer' => $customer)
);
Try
public function registrationSuccess(Varien_Event_Observer $observer) {
// extract customer data from event
$customer = $observer->getCustomer();
Mage::log('COOKIES', json_encode($_COOKIE));
// a cookie should have been set with the membership id
if ($membership_account_id = Mage::getModel('core/cookie')->get('membership_account_id')) {
Mage::log('COOKIE SET, ASSOCIATING MEMBERSHIP');
$customer->setGroupId(4)
->setRmsId($membership_account_id);
}
return $this;
}
Try to set customer website id before load.
e.g.
$customer = Mage::getModel('customer/customer')
->setWebsiteId(Mage::app()->getWebsite()->getId())
->load($customer_id);
Also try to put die; after $customer->save(); while testing - in such case you will be sure that nothing else changes customers data after you (may be some other observer).
I have developed a custom module to meet my project requirements using Alan Storms tutorial for creating modules in magento.
I had the requirement of changing the price attribute dynamically on frontend based on a livefeed. Everysecond the feed is updated so every time the page refreshes a new price must be displayed for each product on the site.
I have override the product module and the price modules for this purpose. The issue is with tier pricing. When tier pricing comes into place I need to calculate the tier-price based on the live price.
For this also I managed to change using the price_type class override.
Now whenever an item is added to cart the tier-pricing was not working for that I wrote event_trigger ie an Observer which updates the tier_pricing on the event "checkout_cart_save_before" and here's my code
class My_Custom_Model_Observer extends Varien_Event_Observer
{
public function __construct()
{
}
public function updateCartBasedOnLiveFeed($observer)
{
foreach ($observer->getCart()->getQuote()->getAllVisibleItems() as $item /* #var $item Mage_Sales_Model_Quote_Item */)
{
$tierPrices = array();
$tierPrices = $item->getProduct()->getTierPrice();
$itemPrice = $item->getProduct()->getPrice();
$i=0;
foreach($tierPrices as $key => $tierPrice)
{
if(!is_numeric($key))
{
$updatedTierPrice = $itemPrice - ($itemPrice * ($tierPrice['price']/100));
$tierPrices[$key]['price'] = $updatedTierPrice;
$tierPrices[$key]['website_price'] = $updatedTierPrice;
}
else
{
if($tierPrice['price'] > 0)
{
$updatedTierPrice = $itemPrice - ($itemPrice * ($tierPrice['price']/100));
$tierPrice['price'] = $updatedTierPrice;
$tierPrice['website_price'] = $updatedTierPrice;
$tierPrices[$i] = $tierPrice;
$i++;
}
}
}
$item->getProduct()->setData('tier_price',$tierPrices);
}
}
}
The above code works excellently in cart page. But when it comes to checkout page. It works for a single item and when tier-pricing comes into play it does apply cart prices.
Please help me with this.
I also tried using other events along with the above event.
Event: sales_quote_save_before
public function updateQuoteLive($observer)
{
$tierPrices = array();
$quote_item = $observer->getEvent()->getQuote;
$itemPrice = $quote_item->getProduct()->getPrice();
$tierPrices = $quote_item->getProduct()->getTierPrice();
$tierPricesSize = sizeof($tierPrices);
for($i=0;$i<$tierPricesSize;$i++)
{
$updatedTierPrice = $itemPrice - ($itemPrice * ($tierPrices[$i]['price']/100));
$tierPrices[$i]['price'] = $updatedTierPrice;
$tierPrices[$i]['website_price'] = $updatedTierPrice;
}
$quote_item->getProduct()->setData('tier_price',$tierPrices);
}
When I tried to print the getQuote() function available in Quote.php I find that the tier prices there are not the ones which I updated using the first event. So I think I need to update the price before saving the quote. Please any one help me and show the correct direction.
Please help me with this I am missing some important step. Any help is greatly appreciated.
Thanks in advance.
It might be better off "saving" the new price in to the database when you update.
Try something along the lines of:
$product = $observer->getProduct();
$procuct->setPrice($updatedPrice);
This way when it comes to checkout it will be pulling in the correct price from the database (and avoids the headache of correcting it "mid-flight"
i realized such a project like you. I have no sales_quote_save_before Observer. I only use the checkout_cart_save_before. Based on the session the price will be setted.
I realized that like this way:
public function updatePrice( $observer )
{
try {
$cart = $observer->getCart();
$items = $cart->getItems();
foreach($items as $item)
{
$item->setCustomPrice($price);
$item->setOriginalCustomPrice($price);
}
} catch ( Exception $e )
{
Mage::log( "checkout_cart_save_before: " . $e->getMessage() );
}
}
I calcute the tierprices on the fly and with this Observer. All prices will be set up correct in the qoute.
Maybe you should try this way.
Regards boti
At last figured out the issue and got the solution.
The problem was that in cart page or checkout page when the getTierPrice() function is called, which is present in /app/code/core/Mage/Catalog/Product.php. It takes one parameter named $qty which is by default null. This function in turn calls the function getTierPrice which is present in /app/code/core/Mage/Type/Price.php file which takes two parameters $qty and $productObject. By default $qty is null and when it is null the function returns an array of tier_prices. But when the $qty value is passed then the function returns a single for that particular quantity.
So, I wrote my own custom function which calculates the tier prices based no my requirements like
I overridden both the core files with my custom module following Alan Storm's tutorials.
I've extended Mage_Catalog_Model_Product with My_CustomModule_Model_Product class and
then
Mage_Catalog_Model_Product_Type_Price with My_CustomModule_Model_Price
And then in /app/code/local/My/Custommodule/Model/Product.php
I added my custom code like
public function getTierPrice($qty=null)
{
if($qty)
{
return $this->getPriceModel()->getCustomTierPrice($qty, $this);
}
return $this->getPriceModel()->getTierPrice($qty, $this);
}
Then in /app/code/local/My/Custommodule/Model/Price.php
<?php
public function getCustomTierPrice($qty = null, $product)
{
$allGroups = Mage_Customer_Model_Group::CUST_GROUP_ALL;
$prices = $product->getData('tier_price');
if (is_null($prices)) {
$attribute = $product->getResource()->getAttribute('tier_price');
if ($attribute) {
$attribute->getBackend()->afterLoad($product);
$prices = $product->getData('tier_price');
}
}
foreach($prices as $key => $customPrices)
{
if($prices[$key]['price'] < 1)
{
$prices[$key]['price'] = abs($product->getPrice() - ($productPrice * ($customPrices['price']/100)));
$prices[$key]['website_price'] = $prices[$key]['price'];
}
}
}
which retured a customized value when $qty is passed and voila it worked.
I just posed this answer so that any one else who has similar requirement may get benefited with this.
?>
I am trying to implement my new payment method its working fine. But My requirement is little bit different. I need to redirect user to the payment gateway page. This is how I am trying to implement.
When user clicks on Place Order my Namespace_Bank_Model_Payment >> authorize method gets called. My gateway Says send an initial request, Based on details given gateway send a URL & Payment id. On this Url user must be redirected Where customer actually makes the payment. I have two actions in Controller success & error to handle the final response.
As, this code is getting called in an ajax request, I can't redirect user to another website. Can anybody guide me how to accomplish it?
Here is my code. I Have implemented getOrderPlaceRedirectUrl() method.
Here is my class::
<?php
class Namespace_Hdfc_Model_Payment extends Mage_Payment_Model_Method_Abstract
{
protected $_isGateway = true;
protected $_canAuthorize = true;
protected $_canUseCheckout = true;
protected $_code = "hdfc";
/**
* Order instance
*/
protected $_order;
protected $_config;
protected $_payment;
protected $_redirectUrl;
/**
* #return Mage_Checkout_Model_Session
*/
protected function _getCheckout()
{
return Mage::getSingleton('checkout/session');
}
/**
* Return order instance loaded by increment id'
*
* #return Mage_Sales_Model_Order
*/
protected function _getOrder()
{
return $this->_order;
}
/**
* Return HDFC config instance
*
*/
public function getConfig()
{
if(empty($this->_config))
$this->_config = Mage::getModel('hdfc/config');
return $this->_config;
}
public function authorize(Varien_Object $payment, $amount)
{
if (empty($this->_order))
$this->_order = $payment->getOrder();
if (empty($this->_payment))
$this->_payment = $payment;
$orderId = $payment->getOrder()->getIncrementId();
$order = $this->_getOrder();
$billingAddress = $order->getBillingAddress();
$tm = Mage::getModel('hdfc/hdfc');
$qstr = $this->getQueryString();
// adding amount
$qstr .= '&amt='.$amount;
//echo 'obj details:';
//print_r(get_class_methods(get_class($billingAddress)));
// adding UDFs
$qstr .= '&udf1='.$order->getCustomerEmail();
$qstr .= '&udf2='.str_replace(".", '', $billingAddress->getName() );
$qstr .= '&udf3='.str_replace("\n", ' ', $billingAddress->getStreetFull());
$qstr .= '&udf4='.$billingAddress->getCity();
$qstr .= '&udf5='.$billingAddress->getCountry();
$qstr .= '&trackid='.$orderId;
// saving transaction into database;
$tm->setOrderId($orderId);
$tm->setAction(1);
$tm->setAmount($amount);
$tm->setTransactionAt( now() );
$tm->setCustomerEmail($order->getCustomerEmail());
$tm->setCustomerName($billingAddress->getName());
$tm->setCustomerAddress($billingAddress->getStreetFull());
$tm->setCustomerCity($billingAddress->getCity());
$tm->setCustomerCountry($billingAddress->getCountry());
$tm->setTempStatus('INITIAL REQUEST SENT');
$tm->save();
Mage::Log("\n\n queryString = $qstr");
// posting to server
try{
$response = $this->_initiateRequest($qstr);
// if response has error;
if($er = strpos($response,"!ERROR!") )
{
$tm->setErrorDesc( $response );
$tm->setTempStatus('TRANSACTION FAILED WHILE INITIAL REQUEST RESPONSE');
$tm->save();
$this->_getCheckout()->addError( $response );
return false;
}
$i = strpos($response,":");
$paymentId = substr($response, 0, $i);
$paymentPage = substr( $response, $i + 1);
$tm->setPaymentId($paymentId);
$tm->setPaymentPage($paymentPage);
$tm->setTempStatus('REDIRECTING TO PAYMENT GATEWAY');
$tm->save();
// prepare url for redirection & redirect it to gateway
$rurl = $paymentPage . '?PaymentID=' . $paymentId;
Mage::Log("url to redicts:: $rurl");
$this->_redirectUrl = $rurl; // saving redirect rl in object
// header("Location: $rurl"); // this is where I am trying to redirect as it is an ajax call so it won't work
//exit;
}
catch (Exception $e)
{
Mage::throwException($e->getMessage());
}
}
public function getOrderPlaceRedirectUrl()
{
Mage::Log('returning redirect url:: ' . $this->_redirectUrl ); // not in log
return $this->_redirectUrl;
}
}
Now getOrderPlaceRedirectUrl() its getting called. I can see the Mage::log message. but the url is not there. I mean the value of $this->_redirectUrl is not there at the time of function call.
And one more thing, I am not planning to show customer any page like "You are being redirected".
Magento supports this type of payment gateway as standard and directly supports redirecting the user to a third party site for payment.
In your payment model, the one that extends Mage_Payment_Model_Method_Abstract, you'll need to implement the method:
function getOrderPlaceRedirectUrl() {
return 'http://www.where.should.we.pay.com/pay';
Typically you redirect the user to a page on your site, /mymodule/payment/redirect for example, and then handle the redirection logic in the action of the controller. This keeps your payment model clean and stateless, while allowing you to some some kind of "You are now being transferred to the gateway for payment" message.
Save everything you need to decide where to redirect to in a session variable, again typically Mage::getSingleton('checkout/session').
Magento have a pretty solid, if messy, implementation of this for Paypal standard. You can checkout how they do it in app/code/core/Mage/Paypal/{Model/Standard.php,controllers/StandardController.php}.
Hello guys here is solution.
In authorize function (see my code in above answer) change
$this->_redirectUrl = $rurl;
by Mage::getSingleton('customer/session')->setRedirectUrl($rurl);
& in function getOrderPlaceRedirectUrl() change it to like
public function getOrderPlaceRedirectUrl()
{
Mage::Log('returning redirect url:: ' . Mage::getSingleton('customer/session')->getRedirectUrl() );
return Mage::getSingleton('customer/session')->getRedirectUrl(); ;
}
after that code must be running & u'll be getting redirected to the third party gateway