we are using Magento mainly for transactions and have rewritten the frontend entirely through a custom application. I am having trouble applying a coupon code (shopping cart price rule) on a quote object. The coupon code seems to be rejected - setCouponCode does not return any error, but getCouponCode returns empty string.
I have verified that the coupon code is valid by making a transacting through the admin backend. Here is the code snippet below.
Can someone help me with getting the quote model object to accept and apply a coupon code?
function add_coupon($shoppingCartId, $couponcode) {
try {
$quoteObj = Mage::getModel('sales/quote')->load($shoppingCartId);
$quoteObj->getShippingAddress()->setCollectShippingRates(true);
$quoteObj->getShippingAddress()->setCouponCode($coupon)
->setTotalsCollectedFlag(true)
->collectTotals()
->save();
} catch (Exception $e) {
return array("status"=>"failed", "message"=>"Error applying coupon.");
}
if ($coupon) {
if (!$coupon == $quoteObj->getCouponCode()) {
return array("status"=>"failed", "message"=>"Coupon code is not valid.");
}
}
return array("status"=>"success");
}
I ran into the same issue, and discovered that I needed to call setCouponCode() before adding any items to my quote.
In your case, that would look like:
$quoteObj = Mage::getModel('sales/quote')->setCouponCode($coupon)->load($shoppingCartId);
All logic needed for dealing with coupons is in SalesRule module.
There is model Mage_SalesRule_Model_Coupon which is the coupon object, and it has a resource model Mage_SalesRule_Model_Mysql4_Coupon.
To create a new coupon you could instantiate the above coupon model, fill all fields and call save() method. It will write data to table salesrule_coupon. But if you'll look at this table you'll see that coupons depend on rule id, so you need to create some sales rule first.
Mage_SalesRule_Model_Rule cares about rules, it also has own resource model. I think it will be helpful for you to investigate how they are works
I lost a few hours to this tonight. Hopefully I can save someone the same.
Emily is correct for this example (where you load the shopping cart in to a quote).
This applies to Magento 1.7.0.2, I'm not sure if it holds for other versions.
If you are programmatically creating an order but you are not using the shopping cart to do so this may help you:
The underlying problem is that Magento is running collectTotals() each time you add/update an address on the quote - and here's the important part: it caches them. (In fact, after you save an address if you check the getTotalsCollectedFlag you'll see it's set to true!). Setting the coupon code after this, even if you run collectTotals(), doesn't apply the coupon code.
You could call setTotalsCollectedFlag(false) on the quote then apply the coupon, but this is a bad idea (it could cause some miscalculations to occur according to a Magento bug report that is no longer accessible), what you want to do is call setCouponCode before you set any addresses (or at the very least, before you set the last address).
Related
I am creating a payment module for our payment gateway...
So far I have 1) set up the back-end and 2) used the hookPayment() method to display a hidden form at part 5 of the checkout process (http://prestashop.dev/order). This will then redirect off to my gateway with all the required information. Good job.
The next part is the part I am struggling with. I don't understand what the return URIs are for payment-confirmation - just to give the customer some information on the status of the order (and maybe also update the back-office?).
For now, I have just a very simple method;
public function hookPaymentReturn()
{
// if (!$this->active) {
// return null;
// }
return $this->display(__FILE__, 'views/templates/front/confirmation.tpl');
}
in my main module file. I just want to get to this on the browser... I'll start worrying about POST'ed values after this. But for now I just don't know the URI. What will it be?? Do I need to register the route somehow?
Most payment modules confirmation.tpl get hooked into the order-confirmation.tpl of your theme which in turn gets called by the OrderConfirmationController.php.
This can be accessed by:
[YOUR_BASE_URL]/index.php?controller=order-confirmation&[some_stuff].
The [some_stuff] part is important though since the controller performs a series of validations and will redirect you somewhere else if these are not met
Laravel Cashier makes it pretty simple to swap plans:
$user->subscription('premium')
->swapAndInvoice();
This feature is also great because it invoices the user right away, HOWEVER, it also forces the user to pay right away rather than waiting an hour like it normally would. This seems to get in the way of my invoice.created webhook because once swapAndInvoice happens, I don't seem to be able to make further changes to the invoice.
If I just use ->swap(), it doesn't appear to create an invoice at all, but rather it just creates line items waiting to be added to the next invoice. Thoughts are appreciated.
Since posting this question, I have actually submitted a pull request to the Laravel Cashier repository to add a method within the StripeGateway.php file that can perform such functionality.
/**
* Invoice the billable entity outside of regular billing cycle without charging the user immediately.
*
* #return bool
*/
public function invoiceWithoutImmediateCharge()
{
try
{
$customer = $this->getStripeCustomer();
// Removed this because it immediately charges the user which doesn't allow the invoice to be modified by webhooks
//Stripe_Invoice::create(['customer' => $customer->id], $this->getStripeKey())->pay();
Stripe_Invoice::create(['customer' => $customer->id], $this->getStripeKey());
return true;
}
catch (\Stripe_InvalidRequestError $e)
{
return false;
}
}
According to the Stripe documentation, it appears as though you can actually apply coupons to a customer as a whole, but you can also apply coupons to their subscriptions. I am trying to apply a coupon to the customer's subscription after the subscription has already been created. I am using Laravel Cashier 4.2.
Here is what I have tried:
$company = Auth::user()->company;
$customer = $company->subscription()->getStripeCustomer(); // returns a StripeGateway object
$customer->subscription->applyCoupon($input['coupon_code']);
Here is the error message:
"Call to undefined method Stripe_Subscription::updateSubscription()"
I can use the applyCoupon() method to a customer as a whole, but not an actual subscription... Ideas appreciated.
The Stripe documentation only shows how to remove a discount from a subscription:
Discount Object. The only other information I've been able to find in their documentation is:
Coupons and discounts
If you want to provide discounts to certain customers, you can create coupon codes in the Dashboard. Coupons have a discount percentage and a duration, so you could create coupons like a 10% lifetime discount, or a one month 50% discount, etc. Coupons can also have expiration dates attached, after which they can't be used. Here's an example of adding a discount to a user's subscription. In this case, we've already created a coupon called 50OFF1MONTH:
curl https://api.stripe.com/v1/customers/cus_4fdAW5ftNQow1a \
-u sk_test_4LOIhlh19aUz8yFBLwMIxnBY: \
-d coupon=50OFF1MONTH
However, this isn't very helpful. Once again, Laravel's documentation is a little to elegant, and it's missing any info on the topic.
I wonder if I just need to recreate the subscription object entirely with the new coupon... But that's not ideal because you need a card token for that.
Update 1
I can confirm that this does in fact apply a coupon to the subscription itself
$user->subscription('monthly')
->withCoupon('code')
->create($creditCardToken);
However, once again the question is how can a coupon be added after the fact.
Starting with Cashier v11.* there are new convenience methods to update the underlying Stripe subscription object. To apply a coupon to the user's Subscription model instead of the Customer model, try this in the controller:
$user->subscription('default')->updateStripeSubscription(['coupon' => $couponCode]);
To remove the coupon from the subscription, send an update request and pass an empty coupon string. As described under another question, a coupon set to an empty string removes it, while passing null is ignored.
$user->subscription('default')->updateStripeSubscription(['coupon' => '']);
It doesn't look like what I'm trying to do is possible, so I found another way. All I do is swap the users existing plan to the current plan and use the withCoupon method. This does appear to apply the coupon to the subscription and doesn't seem to charge the user or change their billing period, but I could be wrong...
$company->subscription($company->stripe_plan)
->withCoupon($input['coupon_code'])
->swap();
Adding a Coupon to an Existing Subscription.
You can also use cashier method as below:
try {
$user->applyCoupon('coupon_code');
return redirect()->back()->withMessage('Coupon Applied');
} catch (Exception $e) {
return back()->withError($e->getMessage());
}
When applying a coupon, it's best to wrap this in a try / catch block because if the user enters an invalid coupon, Stripe will return an exception with the reason for the failure.
For everyone who can't find an easy and convenient way to swap the subscriptions with a coupon, here's what I came up with recently.
First of all, we'll have to create a custom Subscription eloquent model and extend Laravel\Cashier\Subscription class.
Now we're going to create the withCoupon() method there:
public function withCoupon($coupon)
{
$this->coupon = $coupon;
return $this;
}
The next thing will be extending the original swap() method and adding the coupon part. Just copy and paste the original method and add this piece of code somewhere before the $subscription->save(); line.
...
// Applying the discount coupon if such exists
if ($this->coupon) {
$subscription->coupon = $this->coupon;
}
...
$subscription->save();
The last thing you've got to do in order to make things work properly is to create a custom Billable trait and change subscriptions() method to use your custom Subscription model.
After that, you can apply your very own Billable trait to the billable model and apply coupons while swapping plans.
$user
->subscription($product)
->withCoupon($coupon)
->swap($plan);
Using Laravel 7.x with Cashier 10 you have the applyCoupon() method on a Billable (User) model.
Something like this in the controller:
/**
* Apply Stripe coupon
*/
public function applyCoupon(Request $request){
$user = $request->user();
if ($user->subscribedToPlan('plan_name', 'default')) {
try {
$user->applyCoupon($request->couponCode);
return redirect('subscription')->withMessage('Coupon Applied');
} catch (\Exception $e) {
return redirect('subscription')->withError($e->getMessage());
}
}
}
My objective is simple. I have a module called Quotes, I have managed to get Magento to create a new quote record each time a cart is created by changing the is_active column when a quote is 'checked out'. So I have a bunch of quotes each related to a customer and i have sales/order_item rows each related to a quote.
I have a page in the backend which displays a grid of all the quotes. When a quote is clicked, the edit page has two tabs, one with a Form.php showing the details of the quote. (customer name, date etc), then I have another tab which should contain a grid of all the items in that quote. It seems so simple:
$this->addTab("items_section", array(
"label" => Mage::helper("quote")->__("Quote Items"),
"title" => Mage::helper("quote")->__("Quote Items"),
"content" => $this->getLayout()->createBlock("quote/adminhtml_quotes_edit_tab_cart")->toHtml(),
));
Then in my cart block I have this:
protected function _prepareCollection()
{
$collection = Mage::getModel('sales/order_item')->getCollection();
print_r($collection);
$this->setCollection($collection);
return parent::_prepareCollection();
}
I'm not even interested in loading the correct collection (by order_id) because there is a problem ro be solved here first: The print_r statement reveals the collection I specified but passing it to $this->setCollection($collection) gives me 'No records found' rendered in the grid. In typical Magento fashion there are no errors etc. I understand that the model is supposed to query the database as needed but that doesn't seem to be happening. I suppose it's time to read Mage::core files but you can imagine my frustration at such a simple tasking being so complicated so I would appreciate it if anyone who knows what's going on here can help me out. Thanks in advance.
I may be wrong, but you can't setCollection() on a quote with sales order items. It has to be populated with sales/quote model items.
I don't know what the scope of $this is in _prepareCollection() but i'm assuming since its on the cart block, its dealing with a quote.
Just a hint, you might instead of print_r($collection), try doing this...
echo "<pre>";
foreach ($collection as $item) {
var_dump($item->debug());
}
It supplies pretty much just the important info instead of the database structure. You can also check your item type and make sure you are using the correct model for your setCollection method. You can also throw a break; in there if you want to just get the first item etc. Debugging magento objects can be tedious, and i've found that this helps.
Our head developer managed to help me get it working. We're still not sure why but this seemed to work
protected function _prepareCollection()
{
$quoteId = $this->getRequest()->getParam('id');
$quote = Mage::getModel('sales/quote')->getCollection()->addFieldToFilter('entity_id', $quoteId);
if ($quote->getFirstItem()->getId()) {
$collection = $quote->getFirstItem()->getItemsCollection(false);
}
$this->setCollection($collection);
return parent::_prepareCollection();
}
I'd like to implement a global order limit on certain products. The point of this is that I want to enable backorders on certain products and define several date periods where there are limits to how many of these individual products that may be ordered.
Currently my custom model is loaded with the relevant information for the chosen date period and attached to the product models when they are loaded as $product->setMyModel(...) on these events:
catalog_product_load_after
catalog_product_collection_load_after
sales_quote_item_collection_products_after_load
Accessing my model with data for a specific product is as simple as calling $product->getMyModel(), which I hence will refer to as simply my model.
This is what I want to do:
1. Whenever a product is added to a cart/quote, or placed in an order, I want to do something like this (pseudocode):
// Somehow get $product and $requestedQty (most likely from an event)
$myModel = $product->getMyModel();
if($myModel->applyOrderLimit()) {
// ($orderedQty + $requestedQty) <= $orderLimit
if($myModel->isRequestedQtyAvailable($requestedQty)) {
// Issue an error and prevent the item from being ordered
return;
}
// $orderedQty += $requestedQty
$myModel->addToQtyOrdered($requestedQty);
}
// Continue Magentos default behaviour
1.1. I suspect that Mage_CatalogInventory_Item::checkQuoteItemQty() should be overriden to capture the $requestedQty here.
2. Update $myModel::ordered_qty whenever an order is cancelled, refunded or such.
I guess the real question is where do I run this code, and is there anything more to implementing such an order limit and keeping track of the qty's than I have realized?
To me, this seem like quite a complex task. Which is why I need assistance from more experienced Magento developers!
Note: I couldnt figure out how to mix numbered lists and code blocks, but I hope its readable enough
You don't have to resort to rewriting the Mage_CatalogInventory_Model_Stock_Item:.checkQty() method in order to achieve your goal.
If you add an event observer to the event sales_quote_item_qty_set_after, your observer will be triggered in addition to the cataloginventory check.
public function salesQuoteItemQtySetAfter(Varien_Event_Observer $observer)
{
$quoteItem = $observer->getItem();
$qty = $quoteItem->getQty();
$myModel = $quoteItem->getProduct()->getMyModel()
// Your Logic
// If not salable set error for the quote item
$quoteItem->addErrorInfo(
'mymodule', // origin code
'currently_not_salable', // error code
'The Error Message'
);
}
The sales_quote_item_qty_set_after event is also used by the cataloginventory module to call checkQty(), so you can also examine Mage_CatalogInventory_Model_Observer::checkQuoteItemQty() for additional possibilities on what functionality is available.