cartridge and cartridge-payments : help with checkout step

1,053 views
Skip to first unread message

Luc Milland

unread,
Nov 13, 2013, 10:08:30 AM11/13/13
to mezzani...@googlegroups.com
Hello,
I am trying to make a shop with cartridge but I don't want to deal with
credit cards.
That's why I want to use cartridge-payment with Paypal express checkout
(payment part only, if I understand well).
Using sandbox paypals accounts and cartridge-payment readme, I can make
an order, pay it and be sent back to the shop at
shop/checkout/complete .
But there is my problem : the order is registred, but not completed. I
mean that it looks as the final checkout step did not apply (I guess it
is because the checkout form is sent to paypal and to our good old
checkout_step view, right ?) and as order.complete(request) was never
called.
As a result, cart is not empty, etc...
Does anybody had already been there ? Did I miss something ? Is there a
way to jump back in the checkout workflow, maybe with Paypal IPN ?

thanks for your ideas and many thanks to all who build this awesome
piece of code that mezzanine/cartridge is.

Luc

Luc Milland

unread,
Nov 14, 2013, 5:13:39 AM11/14/13
to mezzani...@googlegroups.com
Answering to myself : I will try to do something using the IPN part of
https://github.com/dcramer/django-paypal .
This signal stuff could be handy. I will let you know, if anyone
interested.

Luc

Iain Mac Donald

unread,
Nov 14, 2013, 7:52:05 AM11/14/13
to mezzani...@googlegroups.com
On Thu, 14 Nov 2013 11:13:39 +0100
Luc Milland <l...@hekenet.com> wrote:

> if anyone interested.

Yes, me. A Mezzanine newbie.

I am probably a few days behind you as I have yet to get the Paypal Dev
account and upload Cartridge to a public server.

I did notice a user reported a working Paypal configuration in June:
http://grokbase.com/t/gg/mezzanine-users/136kx0t0zf/payment-processing-in-cartridge

Is it possible that your cart isn't clearing because the correct
information is not coming back from Paypal?

Based on my previous experience with Django/Paypal solutions you need
to have the correct information entered on the Paypal pages for IPN
and PDT. IPN - Instant Payment Notification, is used by Paypal to
inform whether the payment is successful. It should result in "Paid".
PDT - Payment Data Transfer, gives information to the sellers website
about the sale. The Paypal required procedure is to display a "Thank
you for your order" type page with confirmation of the order details.

More info at:
https://cms.paypal.com/uk/cgi-bin/?cmd=_render-content&content_ID=developer/howto_html_wp_standard_overview

Regards,
Iain.

Luc Milland

unread,
Nov 14, 2013, 10:41:26 AM11/14/13
to mezzani...@googlegroups.com
Hello Iain,

> I did notice a user reported a working Paypal configuration in June:
> http://grokbase.com/t/gg/mezzanine-users/136kx0t0zf/payment-processing-in-cartridge
As I understood, the current Paypal implementation in Cartridge works
well provided all payment informations are collected on the cartridge
site and sent to Paypal. This is perfectly integrated in the Cartridge
checkout workflow and SHOP_HANDLER_PAYMENT can be used.
But as I don't want to deal with the payment informations (credit card
numbers, etc..), I choose to use Paypal Express Checkout with
cartridge-payments. In this scenario, I understand that as the customer
is redirected to the Paypal page, the checkout form can not be validated
at the payment step.

> Is it possible that your cart isn't clearing because the correct
> information is not coming back from Paypal?
I think so, and more : except if I missed something, there is nothing to
handle it that comes with Cartridge or cartridge-payment.
My idea is currently to use Paypal IPN and write something to trigger
the final checkout step (complete or cancel order) depending on what IPN
object says.
I guess that almost all pieces of the code are here in Cartridge,
cartridge-payement and django-paypal but I still have to make them work
together for this particular use case.

regards,
Luc

Luc Milland

unread,
Nov 18, 2013, 2:26:52 AM11/18/13
to mezzani...@googlegroups.com
Just replying to myself again.
I don't find any easy and clean solution to my problem. I just saw a
closed issue on the cartridge-payments github which talk about the same
thing : https://github.com/explodes/cartridge-payments/issues/3 .
I don't like the idea of monkey patching the order.complete method, but
if it is the best solution I will do.
I was thinking at something else, but it is kind of hacky (and I don't
know if it will work). The idea is to use the callback_uuid in the
Paypal IPN handler. In the Paypal order form provided by
cartridge-payments it is used in the 'invoice field', but we could pass
it to the 'custom' field to.
With this uuid in the Paypal notification object, we can :
- retrieve the good order object,
- retrieve the session key attached to it (order.key),
- retrieve the good session object,
- retrieve cart.id from from session object.
Here comes the ugly :
- build a fake request object with the session we retrieved and add the
request.cart parameter we need
- call order.complete(fake_request) to complete the order as intended.

How does it sound to you ?

regards,
Luc

Jeremy Epstein

unread,
Jan 26, 2014, 10:04:53 PM1/26/14
to mezzani...@googlegroups.com
Hi,

I've just finished building a site powered by Mezzanine / Cartridge, and I've managed to integrate it with PayPal WPS (i.e. after completing checkout, user is redirected to PayPal for payment, no SSL cert needed on the site).

I'm going to do a more detailed write-up when I get the chance - but for now, here's roughly what I did to cover the all-import "final missing step" that's being discussed in this thread:

1. Install cartridge-payments and django-paypal, make sure they're enabled:

INSTALLED_APPS = [
    # ...
    "payments.multipayments",
    "paypal.standard.ipn",
    # ...
]

(follow other steps in the install instructions for these apps, I'm not going to go through it all here).

2. Make sure you set PAYPAL_IPN_URL as follows:

PAYPAL_IPN_URL = lambda cart, uuid, order_form: ('paypal.standard.ipn.views.ipn', None, {})

3. Place the following code somewhere in your codebase (per the django-paypal docs, I placed it in the models.py file for one of my apps):

# ...

from importlib import import_module

from mezzanine.conf import settings

from cartridge.shop.models import Cart, Order, ProductVariation, DiscountCode
from paypal.standard.ipn.signals import payment_was_successful

# ...


def payment_complete(sender, **kwargs):
    """Performs the same logic as the code in cartridge.shop.models.Order.complete(), but fetches the session, order, and cart objects from storage, rather than relying on the request object being passed in (which it isn't, since this is triggered on PayPal IPN callback)."""

    ipn_obj = sender

    if ipn_obj.custom and ipn_obj.invoice:
        s_key, cart_pk = ipn_obj.custom.split(',')
        SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
        session = SessionStore(s_key)

        try:
            cart = Cart.objects.get(id=cart_pk)
            try:
                order = Order.objects.get(transaction_id=ipn_obj.invoice)
                for field in order.session_fields:
                    if field in session:
                        del session[field]
                try:
                    del session["order"]
                except KeyError:
                    pass

                # Since we're manually changing session data outside of
                # a normal request, need to force the session object to
                # save after modifying its data.
                session.save()

                for item in cart:
                    try:
                        variation = ProductVariation.objects.get(sku=item.sku)
                    except ProductVariation.DoesNotExist:
                        pass
                    else:
                        variation.update_stock(item.quantity * -1)
                        variation.product.actions.purchased()

                code = session.get('discount_code')
                if code:
                    DiscountCode.objects.active().filter(code=code).update(
                        uses_remaining=F('uses_remaining') - 1)
                cart.delete()
            except Order.DoesNotExist:
                pass
        except Cart.DoesNotExist:
            pass

payment_was_successful.connect(payment_complete)

4. Apply some hacks to cartridge-payments and django-paypal:

a) lib/python2.7/site-packages/payments/multipayments/forms/paypal.py modify per diff at:


b) src/django-paypal/paypal/standard/forms.py modify per:


I think that's about it - will test more thoroughly and will re-create when I do a better write-up. But basically, with this, the PayPal IPN callback does everything that cartridge.shop.models.Order.complete() would usually do, if it got called (which it doesn't if you're using PayPal WPS, and I believe also PayPal EC).

Hope that helps you guys for now.

Jeremy.

Luc Milland

unread,
Jan 27, 2014, 2:49:32 AM1/27/14
to mezzani...@googlegroups.com
Hello,
awesome, I eventually ended with something *very* similar.
I was planning to expose my solution after the shop I am working on is
finished (I am very late on this project).
I made a lot of other changes and will have some suggestion to improve
Cartridge.

Regarding, the external payment stuff, we basically made the *same
thing* : using signal in my_app/models.py, retrieve order through ipn
stuff (I used order_uuid alone since I don't really now if it's a good
idea to let session keys travel through requests) and close the order.
to close the order, I did not mimic order.complete() code, but I call
the original code through a fake request. It's something like :

fake_request = HttpRequest()
fake_request.session = session
fake_request.cart = cart
order.complete(fake_request)
session.save()

I tried to implement some kind of IPN order status handling to complete
order only when status == "Completed" and not "Pending", and to trap
error to (for now, any other IPN status than completed or pending leads
to cancel the order).

E-mails are sent on order complete or cancel.

Besides, I use signals on order status transitions to have mail sent
automatically.

Finally, I wrote a little app to handle shipping services but I will
expose this later, since it's a whole subject and would require some
more code to be really usefull (basically, I would need another checkout
step to handle shipping service choice).
This is called ponyexpress and it allows to define shipping services
through admin. Each shipping service is associated to destination zones
(countries, for now) which are bound to price ranges depending on
weight.
Adding a weight field to ProductVariation then allow to calculate
shipping cost automatically for the chosen service, depending on
customer country.
While no order is made, the customer country is guessed from IP so an
estimated shipping cost can be provided in the shopping cart.
I find this pretty cool :)

I will publish this code and discuss it as soon as I have finish the
shop I am working on.

hope that will help too !

Luc

Jeremy Epstein

unread,
Apr 15, 2014, 8:27:57 PM4/15/14
to mezzani...@googlegroups.com
I've published a more detailed write-up of the process I went through,
you can find it here:

http://greenash.net.au/thoughts/2014/03/using-paypal-wps-with-cartridge-mezzanine-django/

Luc, looking forward to reading more details about how you went about
it too. And I'm sure other devs would also appreciate it - then
they'll have several options to choose from, for implementing PayPal
WPS with Cartridge.

I think that both approaches have their disadvantages. With mine,
there's code repetition - I copied the code from Cartridge's order
complete method and modified it. With yours, Luc, there's a fake
request object being used.

Ideally, the architecture of Cartridge's order completion system will
be modified for the better, such that (a) it can be called easily from
an external code snippet like a PayPal IPN callback, and (b) it's
de-coupled from the request, and there's no need to use a fake request
object. That way, both of our hacks / solutions will become
unnecessary.

Cheers,
Jeremy.
> --
> You received this message because you are subscribed to a topic in the Google Groups "Mezzanine Users" group.
> To unsubscribe from this topic, visit https://groups.google.com/d/topic/mezzanine-users/n2d6SODIF1o/unsubscribe.
> To unsubscribe from this group and all its topics, send an email to mezzanine-use...@googlegroups.com.
> For more options, visit https://groups.google.com/groups/opt_out.

Stephen McDonald

unread,
Apr 15, 2014, 8:39:45 PM4/15/14
to mezzani...@googlegroups.com
Great write-up Jeremy, thanks a lot for sharing that.

The art rebellion site looks great too!


You received this message because you are subscribed to the Google Groups "Mezzanine Users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to mezzanine-use...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.



--
Stephen McDonald
http://jupo.org

Luc Milland

unread,
Apr 16, 2014, 3:47:03 AM4/16/14
to mezzani...@googlegroups.com

> Luc, looking forward to reading more details about how you went about
> it too. And I'm sure other devs would also appreciate it - then
> they'll have several options to choose from, for implementing PayPal
> WPS with Cartridge.
You are right, I have to do this. Right now I *need* to build my own
website since I feel naked as a siteless developper :) But After that,
yes, I will do.

> Ideally, the architecture of Cartridge's order completion system will
> be modified for the better, such that (a) it can be called easily from
> an external code snippet like a PayPal IPN callback, and (b) it's
> de-coupled from the request, and there's no need to use a fake request
> object. That way, both of our hacks / solutions will become
> unnecessary.
really true. As I recall, it could be as simple as making the request
argument optional for the order.complete method.

Another caveat by using paypal standard is that when user is redirected
to order_complete url, we do not know if it has been completed or not
(what if payment was refused ?). There is some race condition with the
ipn request too.
My solution for this is to sort the situation out with a custom
"complete_checkout" view and test if order already completed or not and
return a "payment pending" template if not.
This breaks the google analytics stuff (but there must be some way to
fix it with Universal Analytics and the hability to send statitistics
through a POST request).

By the way, here is the shop for which I had to use Papyal WPS (all in
french, sorry) : https://www.ischnura.fr

cheers,
Luc



Guillaume Pellerin

unread,
Oct 14, 2015, 4:07:54 AM10/14/15
to Mezzanine Users
Thank you so much Jeremy for this tutorial.

I've been using you method to implement a record shop and everything works well!

The only special thing not explained in your tyto but I've had to do - even it is well specified in the mezzanine's urls.py comments - is to add the paypal-ipn url *before* all mezzanine ones.

Cheers,
Guillaume

ping yang

unread,
Jan 16, 2018, 6:34:02 PM1/16/18
to Mezzanine Users
Hi Guillaume,
I really interested in how to make the paypal payment working from this post.
Will you able to throw me ax example project source code for me to have a look(with the paypal payment implemented)?
Looking forward to hearing from  you.
Regards,
Ping Yang (Peter)
Reply all
Reply to author
Forward
0 new messages