commit
3d5f8a55f4
|
@ -301,24 +301,29 @@ class Campaign(models.Model):
|
|||
self.save()
|
||||
action = CampaignAction(campaign=self, type='succeeded', comment = self.current_total)
|
||||
action.save()
|
||||
if send_notice:
|
||||
successful_campaign.send(sender=None,campaign=self)
|
||||
|
||||
if process_transactions:
|
||||
p = PaymentManager()
|
||||
results = p.execute_campaign(self)
|
||||
# should be more sophisticated in whether to return True -- look at all the transactions
|
||||
|
||||
if send_notice:
|
||||
successful_campaign.send(sender=None,campaign=self)
|
||||
|
||||
# should be more sophisticated in whether to return True -- look at all the transactions?
|
||||
return True
|
||||
elif self.deadline < now() and self.current_total < self.target:
|
||||
self.status = 'UNSUCCESSFUL'
|
||||
self.save()
|
||||
action = CampaignAction(campaign=self, type='failed', comment = self.current_total)
|
||||
action.save()
|
||||
if send_notice:
|
||||
regluit.core.signals.unsuccessful_campaign.send(sender=None,campaign=self)
|
||||
|
||||
if process_transactions:
|
||||
p = PaymentManager()
|
||||
results = p.cancel_campaign(self)
|
||||
# should be more sophisticated in whether to return True -- look at all the transactions
|
||||
results = p.cancel_campaign(self)
|
||||
|
||||
if send_notice:
|
||||
regluit.core.signals.unsuccessful_campaign.send(sender=None,campaign=self)
|
||||
# should be more sophisticated in whether to return True -- look at all the transactions?
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
|
|
@ -8,13 +8,16 @@ from django.contrib.sites.models import Site
|
|||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_noop as _
|
||||
|
||||
import datetime
|
||||
from regluit.utils.localdatetime import now
|
||||
|
||||
from notification import models as notification
|
||||
|
||||
from social_auth.signals import pre_update
|
||||
from social_auth.backends.facebook import FacebookBackend
|
||||
from tastypie.models import create_api_key
|
||||
|
||||
from regluit.payment.signals import transaction_charged, pledge_modified, pledge_created
|
||||
from regluit.payment.signals import transaction_charged, transaction_failed, pledge_modified, pledge_created
|
||||
|
||||
import registration.signals
|
||||
import django.dispatch
|
||||
|
@ -31,20 +34,21 @@ def facebook_extra_values(sender, user, response, details, **kwargs):
|
|||
|
||||
pre_update.connect(facebook_extra_values, sender=FacebookBackend)
|
||||
|
||||
|
||||
# create Wishlist and UserProfile to associate with User
|
||||
def create_user_objects(sender, created, instance, **kwargs):
|
||||
# use get_model to avoid circular import problem with models
|
||||
try:
|
||||
Wishlist = get_model('core', 'Wishlist')
|
||||
UserProfile = get_model('core', 'UserProfile')
|
||||
if created:
|
||||
Wishlist.objects.create(user=instance)
|
||||
UserProfile.objects.create(user=instance)
|
||||
except DatabaseError:
|
||||
# this can happen when creating superuser during syncdb since the
|
||||
# core_wishlist table doesn't exist yet
|
||||
return
|
||||
# don't create Wishlist or UserProfile if we are loading fixtures http://stackoverflow.com/a/3500009/7782
|
||||
if not kwargs.get('raw', False):
|
||||
try:
|
||||
Wishlist = get_model('core', 'Wishlist')
|
||||
UserProfile = get_model('core', 'UserProfile')
|
||||
if created:
|
||||
Wishlist.objects.create(user=instance)
|
||||
UserProfile.objects.create(user=instance)
|
||||
except DatabaseError:
|
||||
# this can happen when creating superuser during syncdb since the
|
||||
# core_wishlist table doesn't exist yet
|
||||
return
|
||||
|
||||
post_save.connect(create_user_objects, sender=User)
|
||||
|
||||
|
@ -93,6 +97,7 @@ def create_notice_types(app, created_models, verbosity, **kwargs):
|
|||
notification.create_notice_type("pledge_you_have_pledged", _("Thanks For Your Pledge!"), _("Your ungluing pledge has been entered."))
|
||||
notification.create_notice_type("pledge_status_change", _("Your Pledge Has Been Modified"), _("Your ungluing pledge has been modified."))
|
||||
notification.create_notice_type("pledge_charged", _("Your Pledge has been Executed"), _("You have contributed to a successful ungluing campaign."))
|
||||
notification.create_notice_type("pledge_failed", _("Unable to charge your credit card"), _("A charge to your credit card did not go through."))
|
||||
notification.create_notice_type("rights_holder_created", _("Agreement Accepted"), _("You have become a verified Unglue.it rights holder."))
|
||||
notification.create_notice_type("rights_holder_claim_approved", _("Claim Accepted"), _("A claim you've entered has been accepted."))
|
||||
notification.create_notice_type("wishlist_unsuccessful_amazon", _("Campaign shut down"), _("An ungluing campaign that you supported had to be shut down due to an Amazon Payments policy change."))
|
||||
|
@ -170,6 +175,26 @@ def handle_transaction_charged(sender,transaction=None, **kwargs):
|
|||
|
||||
transaction_charged.connect(handle_transaction_charged)
|
||||
|
||||
# dealing with failed transactions
|
||||
|
||||
def handle_transaction_failed(sender,transaction=None, **kwargs):
|
||||
if transaction is None:
|
||||
return
|
||||
|
||||
# window for recharging
|
||||
recharge_deadline = transaction.campaign.deadline + datetime.timedelta(settings.RECHARGE_WINDOW)
|
||||
|
||||
notification.queue([transaction.user], "pledge_failed", {
|
||||
'site':Site.objects.get_current(),
|
||||
'transaction':transaction,
|
||||
'recharge_deadline': recharge_deadline
|
||||
}, True)
|
||||
from regluit.core.tasks import emit_notifications
|
||||
emit_notifications.delay()
|
||||
|
||||
transaction_failed.connect(handle_transaction_failed)
|
||||
|
||||
|
||||
def handle_pledge_modified(sender, transaction=None, up_or_down=None, **kwargs):
|
||||
# we need to know if pledges were modified up or down because Amazon handles the
|
||||
# transactions in different ways, resulting in different user-visible behavior;
|
||||
|
@ -264,4 +289,4 @@ def handle_wishlist_near_deadline(campaign, **kwargs):
|
|||
from regluit.core.tasks import emit_notifications
|
||||
emit_notifications.delay()
|
||||
|
||||
deadline_impending.connect(handle_wishlist_near_deadline)
|
||||
deadline_impending.connect(handle_wishlist_near_deadline)
|
||||
|
|
|
@ -327,9 +327,6 @@ class BookLoaderTests(TestCase):
|
|||
ebook = bookloader.load_gutenberg_edition(title, gutenberg_etext_id, ol_work_id, seed_isbn, epub_url, format, license, lang, publication_date)
|
||||
self.assertEqual(ebook.url, epub_url)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class SearchTests(TestCase):
|
||||
|
||||
|
@ -381,7 +378,6 @@ class CampaignTests(TestCase):
|
|||
self.assertEqual(c.license_url, 'http://creativecommons.org/licenses/by-nc/3.0/')
|
||||
self.assertEqual(c.license_badge, 'https://i.creativecommons.org/l/by-nc/3.0/88x31.png')
|
||||
|
||||
|
||||
def test_campaign_status(self):
|
||||
|
||||
# need a user to associate with a transaction
|
||||
|
@ -696,4 +692,4 @@ class LocaldatetimeTest(TestCase):
|
|||
reload(localdatetime)
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -58,7 +58,8 @@ def grouper(iterable, page_size):
|
|||
if len(page) == page_size:
|
||||
yield page
|
||||
page= []
|
||||
yield page
|
||||
if len(page):
|
||||
yield page
|
||||
|
||||
class FreebaseException(Exception):
|
||||
pass
|
||||
|
|
|
@ -24,7 +24,6 @@ import operator
|
|||
import time
|
||||
|
||||
import re
|
||||
from itertools import islice, izip
|
||||
import logging
|
||||
import random
|
||||
import json
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{% load humanize %}Congratulations!
|
||||
|
||||
Thanks to you and other ungluers, {{ transaction.campaign.work.title }} will be released to the world in an unglued ebook edition. Your credit card will be charged ${{ transaction.amount|intcomma }}.
|
||||
Thanks to you and other ungluers, {{ transaction.campaign.work.title }} will be released to the world in an unglued ebook edition. Your credit card has been charged ${{ transaction.amount|intcomma }}.
|
||||
|
||||
Pledge summary
|
||||
{% include "notification/pledge_summary.txt" %}
|
||||
|
|
|
@ -6,13 +6,13 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block comments_graphical %}
|
||||
Hooray! The campaign for <a href="{% url work transaction.campaign.work.id %}">{{ transaction.campaign.work.title }}</a> has succeeded. Your credit card is being charged ${{ transaction.amount }}. Thank you again for your help.
|
||||
Hooray! The campaign for <a href="{% url work transaction.campaign.work.id %}">{{ transaction.campaign.work.title }}</a> has succeeded. Your credit card has been charged ${{ transaction.amount|intcomma }}. Thank you again for your help.
|
||||
{% endblock %}
|
||||
|
||||
{% block comments_textual %}
|
||||
<p>Congratulations!</p>
|
||||
|
||||
<p>Thanks to you and other ungluers, {{ transaction.campaign.work.title }} will be released to the world in an unglued ebook edition. {{ transaction.host|capfirst }} will now charge your credit card.</p>
|
||||
<p>Thanks to you and other ungluers, {{ transaction.campaign.work.title }} will be released to the world in an unglued ebook edition. {{ transaction.host|capfirst }} has been charged to your credit card.</p>
|
||||
|
||||
<p><b>Pledge Summary</b><br />
|
||||
Amount pledged: {{ transaction.amount|intcomma }}<br />
|
||||
|
|
|
@ -1 +1 @@
|
|||
Thanks to you, the campaign for {{transaction.campaign.work.title}} has succeeded!
|
||||
Your pledge to the campaign to unglue {{transaction.campaign.work.title}} has been charged.
|
|
@ -0,0 +1,15 @@
|
|||
{% load humanize %}Thanks to you and other ungluers, {{ transaction.campaign.work.title }} will be released to the world in an unglued ebook edition.
|
||||
|
||||
However, our attempt to charge your credit card in the amount of ${{ transaction.amount|intcomma }} failed ({{transaction.error}}). Don't worry -- typically this means the card on file for you is expired, and once you update your card information we'll be able to collect your pledge on behalf of {{ transaction.campaign.rightsholder }}. Please update your credit card information at
|
||||
https://{{ site.domain }}{% url manage_account %} by {{ recharge_deadline }} so that you can fulfill your pledge. Thank you!
|
||||
|
||||
Pledge summary
|
||||
{% include "notification/pledge_summary.txt" %}
|
||||
|
||||
If you'd like to visit the campaign page, click here:
|
||||
https://{{ site.domain }}{% url work transaction.campaign.work.id %}
|
||||
|
||||
Thank you again for your support.
|
||||
|
||||
{{ transaction.campaign.rightsholder }} and the Unglue.it team
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
{% extends "notification/notice_template.html" %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block comments_book %}
|
||||
<a href="{% url work transaction.campaign.work.id %}"><img src="{{ transaction.campaign.work.cover_image_small }}" alt="cover image for {{ transaction.campaign.work.title }}" /></a>
|
||||
{% endblock %}
|
||||
|
||||
{% block comments_graphical %}
|
||||
The campaign for <a href="{% url work transaction.campaign.work.id %}">{{ transaction.campaign.work.title }}</a> has succeeded. However, our attempt to charge your pledge for ${{ transaction.amount|intcomma }} to your credit card failed ({{transaction.error}}). Will you help us fix that?
|
||||
{% endblock %}
|
||||
|
||||
{% block comments_textual %}
|
||||
<p>Your attention needed!</p>
|
||||
|
||||
<p>Thanks to you and other ungluers, {{ transaction.campaign.work.title }} will be released to the world in an unglued ebook edition.
|
||||
However, our attempt to charge your credit card for ${{ transaction.amount|intcomma }} failed.</p>
|
||||
|
||||
<p>Don't worry - normally this just means the card we have on file for you is expired. Once you've updated your card information we'll be able to collect your pledge on behalf of {{ transaction.campaign.rightsholder }}. Please update your credit card information at
|
||||
https://{{ site.domain }}{% url manage_account %} by {{ recharge_deadline }} so that you can fulfill your pledge. Thank you!</p>
|
||||
|
||||
<p><b>Pledge Summary</b><br />
|
||||
Amount pledged: {{ transaction.amount|intcomma }}<br />
|
||||
Premium: {{ transaction.premium.description }} <br />
|
||||
</p>
|
||||
<p>We will notify you when the unglued ebook is available for you to read. If you've requested special premiums, the rights holder, {{ transaction.campaign.rightsholder }}, will be in touch with you via email to request any information needed to deliver your premium.
|
||||
</p>
|
||||
<p>For more information, visit the visit the <a href="{% url work transaction.campaign.work.id %}">campaign page</a>.
|
||||
|
||||
</p>
|
||||
<p>Thank you again for your support.
|
||||
</p>
|
||||
<p>{{ transaction.campaign.rightsholder }} and the Unglue.it team
|
||||
</p>
|
||||
{% endblock %}
|
|
@ -0,0 +1 @@
|
|||
Attention needed: your pledge to unglue {{transaction.campaign.work.title}}
|
|
@ -5,12 +5,20 @@ from django.test.client import Client
|
|||
from django.contrib.auth.models import User
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.conf import settings
|
||||
from django.core import mail
|
||||
|
||||
from regluit.core.models import Work, Campaign, RightsHolder, Claim
|
||||
from regluit.payment.models import Transaction
|
||||
from regluit.payment.manager import PaymentManager
|
||||
from regluit.payment.stripelib import StripeClient, TEST_CARDS, ERROR_TESTING, card
|
||||
|
||||
from notification.models import Notice
|
||||
|
||||
from decimal import Decimal as D
|
||||
from regluit.utils.localdatetime import now
|
||||
from datetime import timedelta
|
||||
import time
|
||||
import json
|
||||
|
||||
class WishlistTests(TestCase):
|
||||
|
||||
|
@ -173,4 +181,265 @@ class PledgingUiTests(TestCase):
|
|||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
|
||||
class UnifiedCampaignTests(TestCase):
|
||||
fixtures=['basic_campaign_test.json']
|
||||
def test_setup(self):
|
||||
# testing basics: are there 3 users?
|
||||
|
||||
self.assertEqual(User.objects.count(), 3)
|
||||
# make sure we know the passwords for the users
|
||||
#RaymondYee / raymond.yee@gmail.com / Test_Password_
|
||||
#hmelville / rdhyee@yahoo.com / gofish!
|
||||
#dataunbound / raymond.yee@dataunbound.com / numbers_unbound
|
||||
self.client = Client()
|
||||
self.assertTrue(self.client.login(username="RaymondYee", password="Test_Password_"))
|
||||
self.assertTrue(self.client.login(username="hmelville", password="gofish!"))
|
||||
self.assertTrue(self.client.login(username="dataunbound", password="numbers_unbound"))
|
||||
|
||||
# how many works and campaigns?
|
||||
self.assertEqual(Work.objects.count(), 3)
|
||||
self.assertEqual(Campaign.objects.count(), 2)
|
||||
|
||||
def test_junk_webhook(self):
|
||||
"""send in junk json and then an event that doesn't exist"""
|
||||
# non-json
|
||||
ipn_url = reverse("HandleIPN", args=('stripelib',))
|
||||
r = self.client.post(ipn_url, data="X", content_type="application/json; charset=utf-8")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
# junk event_id
|
||||
r = self.client.post(ipn_url, data='{"id": "evt_XXXXXXXXX"}', content_type="application/json; charset=utf-8")
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
def pledge_to_work_with_cc(self, username, password, work_id, card, preapproval_amount='10', premium_id='150'):
|
||||
|
||||
# how much of test.campaigntest.test_relaunch can be done here?
|
||||
self.assertTrue(self.client.login(username=username, password=password))
|
||||
|
||||
# Pro Web 2.0 Mashups
|
||||
self.work = Work.objects.get(id=work_id)
|
||||
r = self.client.get("/work/%s/" % (self.work.id))
|
||||
|
||||
r = self.client.get("/work/%s/" % self.work.id)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# go to pledge page
|
||||
r = self.client.get("/pledge/%s" % self.work.id, data={}, follow=True)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# submit to pledge page
|
||||
r = self.client.post("/pledge/%s/" % self.work.id, data={'preapproval_amount': preapproval_amount,
|
||||
'premium_id':premium_id}, follow=True)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# Should now be on the fund page
|
||||
pledge_fund_path = r.request.get('PATH_INFO')
|
||||
self.assertTrue(pledge_fund_path.startswith('/pledge/fund'))
|
||||
# pull out the transaction info
|
||||
t_id = int(pledge_fund_path.replace('/pledge/fund/',''))
|
||||
|
||||
# r.content holds the page content
|
||||
# create a stripe token to submit to form
|
||||
|
||||
# track start time and end time of these stipe interactions so that we can limit the window of Events to look for
|
||||
time0 = time.time()
|
||||
|
||||
sc = StripeClient()
|
||||
stripe_token = sc.create_token(card=card)
|
||||
r = self.client.post(pledge_fund_path, data={'stripe_token':stripe_token.id}, follow=True)
|
||||
|
||||
# where are we now?
|
||||
self.assertEqual(r.request.get('PATH_INFO'), '/pledge/complete/')
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# dig up the transaction and charge it
|
||||
pm = PaymentManager()
|
||||
transaction = Transaction.objects.get(id=t_id)
|
||||
|
||||
# catch any exception and pass it along
|
||||
try:
|
||||
self.assertTrue(pm.execute_transaction(transaction, ()))
|
||||
except Exception, charge_exception:
|
||||
pass
|
||||
else:
|
||||
charge_exception = None
|
||||
|
||||
time1 = time.time()
|
||||
|
||||
# retrieve events from this period -- need to pass in ints for event creation times
|
||||
events = list(sc._all_objs('Event', created={'gte':int(time0-1.0), 'lte':int(time1+1.0)}))
|
||||
|
||||
return (events, charge_exception)
|
||||
|
||||
def good_cc_scenario(self):
|
||||
# how much of test.campaigntest.test_relaunch can be done here?
|
||||
|
||||
card1 = card(number=TEST_CARDS[0][0], exp_month=1, exp_year='2020', cvc='123', name='Raymond Yee',
|
||||
address_line1="100 Jackson St.", address_line2="", address_zip="94706", address_state="CA", address_country=None) # good card
|
||||
|
||||
(events, charge_exception) = self.pledge_to_work_with_cc(username="RaymondYee", password="Test_Password_", work_id=1, card=card1,
|
||||
preapproval_amount='10', premium_id='150')
|
||||
|
||||
self.assertEqual(charge_exception, None)
|
||||
|
||||
# expect to have 2 events (there is a possibility that someone else could be running tests on this stripe account at the same time)
|
||||
# events returned sorted in reverse chronological order.
|
||||
|
||||
self.assertEqual(len(events), 2)
|
||||
self.assertEqual(events[0].type, 'charge.succeeded')
|
||||
self.assertEqual(events[1].type, 'customer.created')
|
||||
|
||||
# now feed each of the events to the IPN processor.
|
||||
ipn_url = reverse("HandleIPN", args=('stripelib',))
|
||||
|
||||
for (i, event) in enumerate(events):
|
||||
r = self.client.post(ipn_url, data=json.dumps({"id": event.id}), content_type="application/json; charset=utf-8")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# expected notices
|
||||
|
||||
self.assertEqual(len(Notice.objects.filter(notice_type__label='pledge_you_have_pledged', recipient__username='RaymondYee')), 1)
|
||||
self.assertEqual(len(Notice.objects.filter(notice_type__label='pledge_charged', recipient__username='RaymondYee')), 1)
|
||||
|
||||
|
||||
def bad_cc_scenario(self):
|
||||
"""Goal of this scenario: enter a CC that will cause a charge.failed event, have user repledge succesfully"""
|
||||
|
||||
card1 = card(number=ERROR_TESTING['BAD_ATTACHED_CARD'][0])
|
||||
|
||||
(events, charge_exception) = self.pledge_to_work_with_cc(username="dataunbound", password="numbers_unbound", work_id=2, card=card1,
|
||||
preapproval_amount='10', premium_id='150')
|
||||
|
||||
# we should have an exception when the charge was attempted
|
||||
self.assertTrue(charge_exception is not None)
|
||||
|
||||
# expect to have 2 events (there is a possibility that someone else could be running tests on this stripe account at the same time)
|
||||
# events returned sorted in reverse chronological order.
|
||||
|
||||
self.assertEqual(len(events), 2)
|
||||
self.assertEqual(events[0].type, 'charge.failed')
|
||||
self.assertEqual(events[1].type, 'customer.created')
|
||||
|
||||
# now feed each of the events to the IPN processor.
|
||||
ipn_url = reverse("HandleIPN", args=('stripelib',))
|
||||
|
||||
for (i, event) in enumerate(events):
|
||||
r = self.client.post(ipn_url, data=json.dumps({"id": event.id}), content_type="application/json; charset=utf-8")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
self.assertEqual(len(Notice.objects.filter(notice_type__label='pledge_you_have_pledged', recipient__username='dataunbound')), 1)
|
||||
self.assertEqual(len(Notice.objects.filter(notice_type__label='pledge_failed', recipient__username='dataunbound')), 1)
|
||||
|
||||
def recharge_with_new_card(self):
|
||||
|
||||
# mark campaign as SUCCESSFUL -- campaign for work 2
|
||||
c = Work.objects.get(id=2).last_campaign()
|
||||
c.status = 'SUCCESSFUL'
|
||||
c.save()
|
||||
|
||||
# set up a good card
|
||||
card1 = card(number=TEST_CARDS[0][0], exp_month=1, exp_year='2020', cvc='123', name='dataunbound',
|
||||
address_line1="100 Jackson St.", address_line2="", address_zip="94706", address_state="CA", address_country=None) # good card
|
||||
|
||||
# track start time and end time of these stipe interactions so that we can limit the window of Events to look for
|
||||
time0 = time.time()
|
||||
|
||||
sc = StripeClient()
|
||||
stripe_token = sc.create_token(card=card1)
|
||||
|
||||
r = self.client.post("/accounts/manage/", data={'stripe_token':stripe_token.id}, follow=True)
|
||||
|
||||
time1 = time.time()
|
||||
|
||||
# retrieve events from this period -- need to pass in ints for event creation times
|
||||
events = list(sc._all_objs('Event', created={'gte':int(time0-1.0), 'lte':int(time1+1.0)}))
|
||||
|
||||
# now feed each of the events to the IPN processor.
|
||||
ipn_url = reverse("HandleIPN", args=('stripelib',))
|
||||
|
||||
for (i, event) in enumerate(events):
|
||||
r = self.client.post(ipn_url, data=json.dumps({"id": event.id}), content_type="application/json; charset=utf-8")
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# a charge should now go through
|
||||
self.assertEqual(len(Notice.objects.filter(notice_type__label='pledge_charged', recipient__username='dataunbound')), 1)
|
||||
|
||||
|
||||
def test_good_bad_cc_scenarios(self):
|
||||
self.good_cc_scenario()
|
||||
self.bad_cc_scenario()
|
||||
self.recharge_with_new_card()
|
||||
self.stripe_token_none()
|
||||
self.confirm_num_mail()
|
||||
|
||||
def stripe_token_none(self):
|
||||
"""Test that if an empty stripe_token is submitted to pledge page, we catch that issue and present normal error page to user"""
|
||||
|
||||
username="hmelville"
|
||||
password="gofish!"
|
||||
work_id =1
|
||||
preapproval_amount='10'
|
||||
premium_id='150'
|
||||
|
||||
self.assertTrue(self.client.login(username=username, password=password))
|
||||
|
||||
# Pro Web 2.0 Mashups
|
||||
self.work = Work.objects.get(id=work_id)
|
||||
r = self.client.get("/work/%s/" % (self.work.id))
|
||||
|
||||
r = self.client.get("/work/%s/" % self.work.id)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# go to pledge page
|
||||
r = self.client.get("/pledge/%s" % self.work.id, data={}, follow=True)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# submit to pledge page
|
||||
r = self.client.post("/pledge/%s/" % self.work.id, data={'preapproval_amount': preapproval_amount,
|
||||
'premium_id':premium_id}, follow=True)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# Should now be on the fund page
|
||||
pledge_fund_path = r.request.get('PATH_INFO')
|
||||
self.assertTrue(pledge_fund_path.startswith('/pledge/fund'))
|
||||
# pull out the transaction info
|
||||
t_id = int(pledge_fund_path.replace('/pledge/fund/',''))
|
||||
|
||||
stripe_token = ''
|
||||
|
||||
r = self.client.post(pledge_fund_path, data={'stripe_token':stripe_token}, follow=True)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
|
||||
def confirm_num_mail(self):
|
||||
# look at emails generated through these scenarios
|
||||
#print len(mail.outbox)
|
||||
#for (i, m) in enumerate(mail.outbox):
|
||||
# print i, m.subject
|
||||
# if i in [5]:
|
||||
# print m.body
|
||||
|
||||
self.assertEqual(len(mail.outbox), 9)
|
||||
|
||||
# print out notices and eventually write tests here to check expected
|
||||
|
||||
#from notification.models import Notice
|
||||
#print [(n.id, n.notice_type.label, n.recipient, n.added) for n in Notice.objects.all()]
|
||||
|
||||
#[(6L, u'pledge_charged', <User: dataunbound>, datetime.datetime(2012, 11, 21, 18, 33, 15)),
|
||||
#(5L, u'pledge_failed', <User: dataunbound>, datetime.datetime(2012, 11, 21, 18, 33, 10)),
|
||||
#(4L, u'new_wisher', <User: hmelville>, datetime.datetime(2012, 11, 21, 18, 33, 8)),
|
||||
#(3L, u'pledge_you_have_pledged', <User: dataunbound>, datetime.datetime(2012, 11, 21, 18, 33, 7)),
|
||||
#(2L, u'pledge_charged', <User: RaymondYee>, datetime.datetime(2012, 11, 21, 18, 33, 3)),
|
||||
#(1L, u'pledge_you_have_pledged', <User: RaymondYee>, datetime.datetime(2012, 11, 21, 18, 32, 56))]
|
||||
|
||||
#0 [localhost:8000] Thank you for supporting Pro Web 2.0 Mashups at Unglue.it!
|
||||
#1 [localhost:8000] Thanks to you, the campaign for Pro Web 2.0 Mashups has succeeded!
|
||||
#2 Stripe Customer (id cus_0ji1hFS8xLluuZ; description: RaymondYee) created
|
||||
#3 [localhost:8000] Thank you for supporting Moby Dick at Unglue.it!
|
||||
#4 [localhost:8000] Someone new has wished for your work at Unglue.it
|
||||
#5 [localhost:8000] Thanks to you, the campaign for Moby Dick has succeeded! However, your credit card charge failed.
|
||||
#6 Stripe Customer (id cus_0ji2Cmu6sXKBCi; description: dataunbound) created
|
||||
#7 [localhost:8000] Thanks to you, the campaign for Moby Dick has succeeded!
|
||||
#8 Stripe Customer (id cus_0ji24dPDiFGWU2; description: dataunbound) created
|
||||
|
||||
|
|
|
@ -326,6 +326,8 @@ class PaymentManager( object ):
|
|||
# only allow active transactions to go through again, if there is an error, intervention is needed
|
||||
transactions = Transaction.objects.filter(campaign=campaign, status=TRANSACTION_STATUS_ACTIVE)
|
||||
|
||||
results = []
|
||||
|
||||
for t in transactions:
|
||||
#
|
||||
# Currently receivers are only used for paypal, so it is OK to leave the paypal info here
|
||||
|
@ -333,14 +335,14 @@ class PaymentManager( object ):
|
|||
receiver_list = [{'email':settings.PAYPAL_GLUEJAR_EMAIL, 'amount':t.amount},
|
||||
{'email':campaign.paypal_receiver, 'amount':D(t.amount) * (D('1.00') - D(str(settings.GLUEJAR_COMMISSION)))}]
|
||||
|
||||
self.execute_transaction(t, receiver_list)
|
||||
try:
|
||||
self.execute_transaction(t, receiver_list)
|
||||
except Exception as e:
|
||||
results.append((t, e))
|
||||
else:
|
||||
results.append((t, None))
|
||||
|
||||
# TO DO: update campaign status
|
||||
# Should this be done first before executing the transactions?
|
||||
# How does the success/failure of transactions affect states of campaigns
|
||||
|
||||
|
||||
return transactions
|
||||
return results
|
||||
|
||||
def finish_campaign(self, campaign):
|
||||
'''
|
||||
|
@ -482,8 +484,6 @@ class PaymentManager( object ):
|
|||
if p.success() and not p.error():
|
||||
transaction.pay_key = p.key()
|
||||
transaction.save()
|
||||
logger.info("execute_transaction Success")
|
||||
transaction_charged.send(sender=self, transaction=transaction)
|
||||
return True
|
||||
|
||||
else:
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
|
||||
from regluit.payment.parameters import *
|
||||
from regluit.payment.signals import credit_balance_added, pledge_created
|
||||
from regluit.utils.localdatetime import now
|
||||
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
|
||||
from decimal import Decimal
|
||||
from datetime import timedelta
|
||||
import datetime
|
||||
import uuid
|
||||
import urllib
|
||||
import logging
|
||||
|
@ -56,7 +60,7 @@ class Transaction(models.Model):
|
|||
# whether a Preapproval has been approved or not
|
||||
approved = models.NullBooleanField(null=True)
|
||||
|
||||
# error message from a PayPal transaction
|
||||
# error message from a transaction
|
||||
error = models.CharField(max_length=256, null=True)
|
||||
|
||||
# IPN.reason_code
|
||||
|
@ -136,7 +140,7 @@ class Transaction(models.Model):
|
|||
self.approved=True
|
||||
now_val = now()
|
||||
self.date_authorized = now_val
|
||||
self.date_expired = now_val + timedelta( days=settings.PREAPPROVAL_PERIOD )
|
||||
self.date_expired = now_val + datetime.timedelta( days=settings.PREAPPROVAL_PERIOD )
|
||||
self.save()
|
||||
pledge_created.send(sender=self, transaction=self)
|
||||
|
||||
|
@ -333,9 +337,6 @@ class Account(models.Model):
|
|||
self.date_deactivated = now()
|
||||
self.save()
|
||||
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
import regluit.payment.manager
|
||||
|
||||
# handle any save, updates to a payment.Transaction
|
||||
|
||||
def handle_transaction_change(sender, instance, created, **kwargs):
|
||||
|
@ -355,3 +356,30 @@ def handle_transaction_delete(sender, instance, **kwargs):
|
|||
post_save.connect(handle_transaction_change,sender=Transaction)
|
||||
post_delete.connect(handle_transaction_delete,sender=Transaction)
|
||||
|
||||
# handle recharging failed transactions
|
||||
|
||||
def recharge_failed_transactions(sender, created, instance, **kwargs):
|
||||
"""When a new Account is saved, check whether this is the new active account for a user. If so, recharge any
|
||||
outstanding failed transactions
|
||||
"""
|
||||
|
||||
# make sure the new account is active
|
||||
if instance.date_deactivated is not None:
|
||||
return False
|
||||
|
||||
transactions_to_recharge = instance.user.transaction_set.filter((Q(status=TRANSACTION_STATUS_FAILED) | Q(status=TRANSACTION_STATUS_ERROR)) & Q(campaign__status='SUCCESSFUL')).all()
|
||||
|
||||
if transactions_to_recharge:
|
||||
from regluit.payment.manager import PaymentManager
|
||||
pm = PaymentManager()
|
||||
for transaction in transactions_to_recharge:
|
||||
# check whether we are still within the window to recharge
|
||||
if (now() - transaction.campaign.deadline) < datetime.timedelta(settings.RECHARGE_WINDOW):
|
||||
logger.info("Recharging transaction {0} w/ status {1}".format(transaction.id, transaction.status))
|
||||
pm.execute_transaction(transaction, [])
|
||||
|
||||
post_save.connect(recharge_failed_transactions, sender=Account)
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -48,3 +48,5 @@ TRANSACTION_STATUS_REFUNDED = 'Refunded'
|
|||
# The transaction was refused/denied
|
||||
TRANSACTION_STATUS_FAILED = 'Failed'
|
||||
|
||||
# Transaction written off -- unable to successfully be charged after campaign succeeded
|
||||
TRANSACTION_STATUS_WRITTEN_OFF = 'Written-Off'
|
||||
|
|
|
@ -2,6 +2,10 @@ from notification import models as notification
|
|||
from django.dispatch import Signal
|
||||
|
||||
transaction_charged = Signal(providing_args=["transaction"])
|
||||
transaction_failed = Signal(providing_args=["transaction"])
|
||||
transaction_refunded = Signal(providing_args=["transaction"])
|
||||
transaction_disputed = Signal(providing_args=["transaction"])
|
||||
|
||||
pledge_created = Signal(providing_args=["transaction"])
|
||||
pledge_modified = Signal(providing_args=["transaction", "up_or_down"])
|
||||
credit_balance_added = Signal(providing_args=["amount"])
|
||||
|
@ -14,14 +18,16 @@ from django.contrib.auth.models import User
|
|||
# create Credit to associate with User
|
||||
def create_user_objects(sender, created, instance, **kwargs):
|
||||
# use get_model to avoid circular import problem with models
|
||||
try:
|
||||
Credit = get_model('payment', 'Credit')
|
||||
if created:
|
||||
Credit.objects.create(user=instance)
|
||||
except DatabaseError:
|
||||
# this can happen when creating superuser during syncdb since the
|
||||
# core_wishlist table doesn't exist yet
|
||||
return
|
||||
# don't create Credit if we are loading fixtures http://stackoverflow.com/a/3500009/7782
|
||||
if not kwargs.get('raw', False):
|
||||
try:
|
||||
Credit = get_model('payment', 'Credit')
|
||||
if created:
|
||||
Credit.objects.create(user=instance)
|
||||
except DatabaseError:
|
||||
# this can happen when creating superuser during syncdb since the
|
||||
# core_wishlist table doesn't exist yet
|
||||
return
|
||||
|
||||
post_save.connect(create_user_objects, sender=User)
|
||||
|
||||
|
|
|
@ -2,21 +2,53 @@
|
|||
# https://stripe.com/docs/api?lang=python#top
|
||||
|
||||
import logging
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from pytz import utc
|
||||
from itertools import islice
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
from django.core.mail import send_mail
|
||||
|
||||
from regluit.payment.models import Account
|
||||
from regluit.payment.models import Account, Transaction, PaymentResponse
|
||||
from regluit.payment.parameters import PAYMENT_HOST_STRIPE
|
||||
from regluit.payment.parameters import TRANSACTION_STATUS_ACTIVE, TRANSACTION_STATUS_COMPLETE, PAYMENT_TYPE_AUTHORIZATION, TRANSACTION_STATUS_CANCELED
|
||||
from regluit.payment.parameters import TRANSACTION_STATUS_ACTIVE, TRANSACTION_STATUS_COMPLETE, TRANSACTION_STATUS_ERROR, PAYMENT_TYPE_AUTHORIZATION, TRANSACTION_STATUS_CANCELED
|
||||
from regluit.payment import baseprocessor
|
||||
from regluit.payment.signals import transaction_charged, transaction_failed
|
||||
|
||||
from regluit.utils.localdatetime import now, zuluformat
|
||||
|
||||
import stripe
|
||||
|
||||
# as of 2012.11.05
|
||||
STRIPE_EVENT_TYPES = ['account.updated', 'account.application.deauthorized',
|
||||
'charge.succeeded', 'charge.failed', 'charge.refunded', 'charge.disputed',
|
||||
'customer.created', 'customer.updated', 'customer.deleted',
|
||||
'customer.subscription.created', 'customer.subscription.updated',
|
||||
'customer.subscription.deleted', 'customer.subscription.trial_will_end',
|
||||
'customer.discount.created', 'customer.discount.updated', 'customer.discount.deleted',
|
||||
'invoice.created', 'invoice.updated', 'invoice.payment_succeeded', 'invoice.payment_failed',
|
||||
'invoiceitem.created', 'invoiceitem.updated', 'invoiceitem.deleted',
|
||||
'plan.created', 'plan.updated', 'plan.deleted',
|
||||
'coupon.created', 'coupon.updated', 'coupon.deleted',
|
||||
'transfer.created', 'transfer.updated', 'transfer.failed',
|
||||
'ping']
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# http://stackoverflow.com/questions/2348317/how-to-write-a-pager-for-python-iterators/2350904#2350904
|
||||
def grouper(iterable, page_size):
|
||||
page= []
|
||||
for item in iterable:
|
||||
page.append( item )
|
||||
if len(page) == page_size:
|
||||
yield page
|
||||
page= []
|
||||
if len(page):
|
||||
yield page
|
||||
|
||||
class StripelibError(baseprocessor.ProcessorError):
|
||||
pass
|
||||
|
||||
|
@ -35,6 +67,9 @@ except:
|
|||
# moving towards not having the stripe api key for the non profit partner in the unglue.it code -- but in a logically
|
||||
# distinct application
|
||||
|
||||
TEST_STRIPE_PK = 'pk_0EajXPn195ZdF7Gt7pCxsqRhNN5BF'
|
||||
TEST_STRIPE_SK = 'sk_0EajIO4Dnh646KPIgLWGcO10f9qnH'
|
||||
|
||||
try:
|
||||
from regluit.core.models import Key
|
||||
STRIPE_PK = Key.objects.get(name="STRIPE_PK").value
|
||||
|
@ -43,8 +78,8 @@ try:
|
|||
except Exception, e:
|
||||
# currently test keys for Gluejar and for raymond.yee@gmail.com as standin for non-profit
|
||||
logger.info('Exception {0} Need to use TEST STRIPE_*_KEYs'.format(e))
|
||||
STRIPE_PK = 'pk_0EajXPn195ZdF7Gt7pCxsqRhNN5BF'
|
||||
STRIPE_SK = 'sk_0EajIO4Dnh646KPIgLWGcO10f9qnH'
|
||||
STRIPE_PK = TEST_STRIPE_PK
|
||||
STRIPE_SK = TEST_STRIPE_SK
|
||||
|
||||
# set default stripe api_key to that of unglue.it
|
||||
|
||||
|
@ -132,6 +167,12 @@ def card (number=TEST_CARDS[0][0], exp_month=1, exp_year=2020, cvc=None, name=No
|
|||
|
||||
return filter_none(card)
|
||||
|
||||
def _isListableAPIResource(x):
|
||||
"""test whether x is an instance of the stripe.ListableAPIResource class"""
|
||||
try:
|
||||
return issubclass(x, stripe.ListableAPIResource)
|
||||
except:
|
||||
return False
|
||||
|
||||
class StripeClient(object):
|
||||
def __init__(self, api_key=STRIPE_SK):
|
||||
|
@ -159,7 +200,6 @@ class StripeClient(object):
|
|||
def event(self):
|
||||
return stripe.Event(api_key=self.api_key)
|
||||
|
||||
|
||||
def create_token(self, card):
|
||||
return stripe.Token(api_key=self.api_key).create(card=card)
|
||||
|
||||
|
@ -200,13 +240,60 @@ class StripeClient(object):
|
|||
# https://stripe.com/docs/api?lang=python#refund_charge
|
||||
ch = stripe.Charge(api_key=self.api_key).retrieve(charge_id)
|
||||
ch.refund()
|
||||
return ch
|
||||
return ch
|
||||
|
||||
def _all_objs(self, class_type, **kwargs):
|
||||
"""a generic iterator for all classes of type stripe.ListableAPIResource"""
|
||||
# type=None, created=None, count=None, offset=0
|
||||
# obj_type: one of 'Charge','Coupon','Customer', 'Event','Invoice', 'InvoiceItem', 'Plan', 'Transfer'
|
||||
|
||||
def list_all_charges(self, count=None, offset=None, customer=None):
|
||||
# https://stripe.com/docs/api?lang=python#list_charges
|
||||
return stripe.Charge(api_key=self.api_key).all(count=count, offset=offset, customer=customer)
|
||||
|
||||
try:
|
||||
stripe_class = getattr(stripe, class_type)
|
||||
except:
|
||||
yield StopIteration
|
||||
else:
|
||||
if _isListableAPIResource(stripe_class):
|
||||
kwargs2 = kwargs.copy()
|
||||
kwargs2.setdefault('offset', 0)
|
||||
kwargs2.setdefault('count', 100)
|
||||
|
||||
more_items = True
|
||||
while more_items:
|
||||
|
||||
items = stripe_class(api_key=self.api_key).all(**kwargs2)['data']
|
||||
for item in items:
|
||||
yield item
|
||||
if len(items):
|
||||
kwargs2['offset'] += len(items)
|
||||
else:
|
||||
more_items = False
|
||||
else:
|
||||
yield StopIteration
|
||||
|
||||
def __getattribute__(self, name):
|
||||
""" handle list_* calls"""
|
||||
mapping = {'list_charges':"Charge",
|
||||
'list_coupons': "Coupon",
|
||||
'list_customers':"Customer",
|
||||
'list_events':"Event",
|
||||
'list_invoices':"Invoice",
|
||||
'list_invoiceitems':"InvoiceItem",
|
||||
'list_plans':"Plan",
|
||||
'list_transfers':"Transfer"
|
||||
}
|
||||
if name in mapping.keys():
|
||||
class_value = mapping[name]
|
||||
def list_events(**kwargs):
|
||||
for e in self._all_objs(class_value, **kwargs):
|
||||
yield e
|
||||
return list_events
|
||||
else:
|
||||
return object.__getattribute__(self, name)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# can't test Transfer in test mode: "There are no transfers in test mode."
|
||||
|
||||
#pledge scenario
|
||||
|
@ -254,6 +341,7 @@ class StripeClient(object):
|
|||
# * expired_card -- also not easily simulatable in test mode
|
||||
|
||||
|
||||
|
||||
class StripeErrorTest(TestCase):
|
||||
"""Make sure the exceptions returned by stripe act as expected"""
|
||||
|
||||
|
@ -305,7 +393,7 @@ class StripeErrorTest(TestCase):
|
|||
self.fail("Attempt to create customer did not throw expected exception.")
|
||||
except stripe.CardError as e:
|
||||
self.assertEqual(e.code, "card_declined")
|
||||
self.assertEqual(e.message, "Your card was declined.")
|
||||
self.assertEqual(e.message, "Your card was declined")
|
||||
|
||||
def test_charge_bad_cust(self):
|
||||
# expect the card to be declined -- and for us to get CardError
|
||||
|
@ -389,8 +477,7 @@ class StripeErrorTest(TestCase):
|
|||
except stripe.CardError as e:
|
||||
self.assertEqual(e.code, "missing")
|
||||
self.assertEqual(e.message, "Cannot charge a customer that has no active card")
|
||||
|
||||
|
||||
|
||||
|
||||
class PledgeScenarioTest(TestCase):
|
||||
@classmethod
|
||||
|
@ -475,9 +562,6 @@ class Processor(baseprocessor.Processor):
|
|||
user.profile.account.deactivate()
|
||||
account.save()
|
||||
return account
|
||||
|
||||
class Pay(StripePaymentRequest, baseprocessor.Processor.Pay):
|
||||
pass
|
||||
|
||||
class Preapproval(StripePaymentRequest, baseprocessor.Processor.Preapproval):
|
||||
|
||||
|
@ -535,40 +619,58 @@ class Processor(baseprocessor.Processor):
|
|||
class Execute(StripePaymentRequest):
|
||||
|
||||
'''
|
||||
The Execute function sends an existing token(generated via the URL from the pay operation), and collects
|
||||
the money.
|
||||
The Execute function attempts to charge the credit card of stripe Customer associated with user connected to transaction
|
||||
'''
|
||||
|
||||
def __init__(self, transaction=None):
|
||||
self.transaction = transaction
|
||||
|
||||
# execute transaction
|
||||
assert transaction.host == PAYMENT_HOST_STRIPE
|
||||
# make sure we are dealing with a stripe transaction
|
||||
if transaction.host <> PAYMENT_HOST_STRIPE:
|
||||
raise StripeLibError("transaction.host {0} is not the expected {1}".format(transaction.host, PAYMENT_HOST_STRIPE))
|
||||
|
||||
sc = StripeClient()
|
||||
|
||||
# look at transaction.preapproval_key
|
||||
# is it a customer or a token?
|
||||
# look first for transaction.user.profile.account.account_id
|
||||
try:
|
||||
customer_id = transaction.user.profile.account.account_id
|
||||
except:
|
||||
customer_id = transaction.preapproval_key
|
||||
|
||||
# BUGBUG: replace description with somethin more useful
|
||||
# TO DO: rewrapping StripeError to StripelibError -- but maybe we should be more specific
|
||||
if transaction.preapproval_key.startswith('cus_'):
|
||||
if customer_id is not None:
|
||||
try:
|
||||
charge = sc.create_charge(transaction.amount, customer=transaction.preapproval_key, description="${0} for test / retain cc".format(transaction.amount))
|
||||
except stripe.StripeError as e:
|
||||
raise StripelibError(e.message, e)
|
||||
elif transaction.preapproval_key.startswith('tok_'):
|
||||
try:
|
||||
charge = sc.create_charge(transaction.amount, card=transaction.preapproval_key, description="${0} for test / cc not retained".format(transaction.amount))
|
||||
# useful things to put in description: transaction.id, transaction.user.id, customer_id, transaction.amount
|
||||
charge = sc.create_charge(transaction.amount, customer=customer_id,
|
||||
description=json.dumps({"t.id":transaction.id,
|
||||
"email":transaction.user.email,
|
||||
"cus.id":customer_id,
|
||||
"tc.id": transaction.campaign.id,
|
||||
"amount": float(transaction.amount)}))
|
||||
except stripe.StripeError as e:
|
||||
# what to record in terms of errors? (error log?)
|
||||
# use PaymentResponse to store error
|
||||
|
||||
r = PaymentResponse.objects.create(api="stripelib.Execute", correlation_id=None,
|
||||
timestamp=now(), info=e.message,
|
||||
status=TRANSACTION_STATUS_ERROR, transaction=transaction)
|
||||
|
||||
transaction.status = TRANSACTION_STATUS_ERROR
|
||||
transaction.error = e.message
|
||||
transaction.save()
|
||||
|
||||
raise StripelibError(e.message, e)
|
||||
|
||||
transaction.status = TRANSACTION_STATUS_COMPLETE
|
||||
transaction.pay_key = charge.id
|
||||
transaction.date_payment = now()
|
||||
transaction.save()
|
||||
|
||||
self.charge = charge
|
||||
else:
|
||||
self.charge = charge
|
||||
|
||||
transaction.status = TRANSACTION_STATUS_COMPLETE
|
||||
transaction.pay_key = charge.id
|
||||
transaction.date_payment = now()
|
||||
transaction.save()
|
||||
else:
|
||||
# nothing to charge
|
||||
raise StripeLibError("No customer id available to charge for transaction {0}".format(transaction.id), None)
|
||||
|
||||
|
||||
def api(self):
|
||||
return "Base Pay"
|
||||
|
@ -602,8 +704,111 @@ class Processor(baseprocessor.Processor):
|
|||
# Set the other fields that are expected. We don't have values for these now, so just copy the transaction
|
||||
self.currency = transaction.currency
|
||||
self.amount = transaction.amount
|
||||
|
||||
def ProcessIPN(self, request):
|
||||
return HttpResponse("hello from Stripe IPN")
|
||||
# retrieve the request's body and parse it as JSON in, e.g. Django
|
||||
try:
|
||||
event_json = json.loads(request.body)
|
||||
except ValueError, e:
|
||||
# not able to parse request.body -- throw a "Bad Request" error
|
||||
logger.warning("Non-json being sent to Stripe IPN: {0}".format(e))
|
||||
return HttpResponse(status=400)
|
||||
else:
|
||||
# now parse out pieces of the webhook
|
||||
event_id = event_json.get("id")
|
||||
# use Stripe to ask for details -- ignore what we're sent for security
|
||||
|
||||
sc = StripeClient()
|
||||
try:
|
||||
event = sc.event.retrieve(event_id)
|
||||
except stripe.InvalidRequestError:
|
||||
logger.warning("Invalid Event ID: {0}".format(event_id))
|
||||
return HttpResponse(status=400)
|
||||
else:
|
||||
event_type = event.get("type")
|
||||
if event_type not in STRIPE_EVENT_TYPES:
|
||||
logger.warning("Unrecognized Stripe event type {0} for event {1}".format(event_type, event_id))
|
||||
# is this the right code to respond with?
|
||||
return HttpResponse(status=400)
|
||||
# https://stripe.com/docs/api?lang=python#event_types -- type to delegate things
|
||||
# parse out type as resource.action
|
||||
|
||||
try:
|
||||
(resource, action) = re.match("^(.+)\.([^\.]*)$", event_type).groups()
|
||||
except Exception, e:
|
||||
logger.warning("Parsing of event_type into resource, action failed: {0}".format(e))
|
||||
return HttpResponse(status=400)
|
||||
|
||||
try:
|
||||
ev_object = event.data.object
|
||||
except Exception, e:
|
||||
logger.warning("attempt to retrieve event object failed: {0}".format(e))
|
||||
return HttpResponse(status=400)
|
||||
|
||||
if event_type == 'account.updated':
|
||||
# should we alert ourselves?
|
||||
# how about account.application.deauthorized ?
|
||||
pass
|
||||
elif resource == 'charge':
|
||||
# we need to handle: succeeded, failed, refunded, disputed
|
||||
if action == 'succeeded':
|
||||
logger.info("charge.succeeded webhook for {0}".format(ev_object.get("id")))
|
||||
# try to parse description of object to pull related transaction if any
|
||||
# wrapping this in a try statement because it possible that we have a charge.succeeded outside context of unglue.it
|
||||
try:
|
||||
charge_meta = json.loads(ev_object["description"])
|
||||
transaction = Transaction.objects.get(id=charge_meta["t.id"])
|
||||
# now check that account associated with the transaction matches
|
||||
# ev.data.object.id, t.pay_key
|
||||
if ev_object.id == transaction.pay_key:
|
||||
logger.info("ev_object.id == transaction.pay_key: {0}".format(ev_object.id))
|
||||
else:
|
||||
logger.warning("ev_object.id {0} <> transaction.pay_key {1}".format(ev_object.id, transaction.pay_key))
|
||||
# now -- should fire off transaction_charged here -- if so we need to move it from ?
|
||||
transaction_charged.send(sender=self, transaction=transaction)
|
||||
except Exception, e:
|
||||
logger.warning(e)
|
||||
|
||||
elif action == 'failed':
|
||||
logger.info("charge.failed webhook for {0}".format(ev_object.get("id")))
|
||||
try:
|
||||
charge_meta = json.loads(ev_object["description"])
|
||||
transaction = Transaction.objects.get(id=charge_meta["t.id"])
|
||||
# now check that account associated with the transaction matches
|
||||
# ev.data.object.id, t.pay_key
|
||||
if ev_object.id == transaction.pay_key:
|
||||
logger.info("ev_object.id == transaction.pay_key: {0}".format(ev_object.id))
|
||||
else:
|
||||
logger.warning("ev_object.id {0} <> transaction.pay_key {1}".format(ev_object.id, transaction.pay_key))
|
||||
# now -- should fire off transaction_charged here -- if so we need to move it from ?
|
||||
transaction_failed.send(sender=self, transaction=transaction)
|
||||
except Exception, e:
|
||||
logger.warning(e)
|
||||
elif action == 'refunded':
|
||||
pass
|
||||
elif action == 'disputed':
|
||||
pass
|
||||
else:
|
||||
# unexpected
|
||||
pass
|
||||
elif resource == 'customer':
|
||||
if action == 'created':
|
||||
# test application: email Raymond
|
||||
# do we have a flag to indicate production vs non-production? -- or does it matter?
|
||||
# email RY whenever a new Customer created -- we probably want to replace this with some other
|
||||
# more useful long tem action.
|
||||
send_mail("Stripe Customer (id {0}; description: {1}) created".format(ev_object.get("id"), ev_object.get("description")),
|
||||
"Stripe Customer email: {0}".format(ev_object.get("email")),
|
||||
"notices@gluejar.com",
|
||||
["rdhyee@gluejar.com"])
|
||||
logger.info("email sent for customer.created for {0}".format(ev_object.get("id")))
|
||||
# handle updated, deleted
|
||||
else:
|
||||
pass
|
||||
else: # other events
|
||||
pass
|
||||
|
||||
return HttpResponse("event_id: {0} event_type: {1}".format(event_id, event_type))
|
||||
|
||||
|
||||
def suite():
|
||||
|
|
|
@ -323,8 +323,6 @@ class CreditTest(TestCase):
|
|||
self.assertEqual(self.user1.credit.balance, 0)
|
||||
self.assertEqual(self.user2.credit.balance, 50)
|
||||
|
||||
|
||||
|
||||
class TransactionTest(TestCase):
|
||||
def setUp(self):
|
||||
"""
|
||||
|
@ -387,7 +385,6 @@ class BasicGuiTest(TestCase):
|
|||
def tearDown(self):
|
||||
self.selenium.quit()
|
||||
|
||||
|
||||
class AccountTest(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
|
|
@ -301,14 +301,11 @@ def runTests(request):
|
|||
|
||||
@csrf_exempt
|
||||
def handleIPN(request, module):
|
||||
# Handler for paypal IPN notifications
|
||||
# Handler for IPN /webhook notifications
|
||||
|
||||
p = PaymentManager()
|
||||
p.processIPN(request, module)
|
||||
|
||||
logger.info(str(request.POST))
|
||||
return HttpResponse("ipn")
|
||||
|
||||
return p.processIPN(request, module)
|
||||
|
||||
def paymentcomplete(request):
|
||||
# pick up all get and post parameters and display
|
||||
|
|
|
@ -4,6 +4,8 @@ MySQL-python==1.2.3
|
|||
Pillow==1.7.7
|
||||
Pyzotero==0.9.51
|
||||
South==0.7.6
|
||||
WebOb==1.2.3
|
||||
WebTest==1.4.0
|
||||
amqplib==1.0.2
|
||||
anyjson==0.3.3
|
||||
billiard==2.7.3.12
|
||||
|
|
|
@ -226,6 +226,9 @@ GLUEJAR_COMMISSION = 0.06
|
|||
PREAPPROVAL_PERIOD = 365 # days to ask for in a preapproval
|
||||
PREAPPROVAL_PERIOD_AFTER_CAMPAIGN = 90 # if we ask for preapproval time after a campaign deadline
|
||||
|
||||
# How many days we will try to collect on failed transactions until they are written off
|
||||
RECHARGE_WINDOW = 7
|
||||
|
||||
GOODREADS_API_KEY = ""
|
||||
GOODREADS_API_SECRET = ""
|
||||
|
||||
|
|
|
@ -89,8 +89,12 @@ def instance_metrics(instance):
|
|||
|
||||
for metric in metrics:
|
||||
if 'InstanceId' in metric.dimensions:
|
||||
if instance.id in metric.dimensions['InstanceId']:
|
||||
my_metrics.append(metric)
|
||||
# instance.id not guaranteed to be there
|
||||
try:
|
||||
if instance.id in metric.dimensions['InstanceId']:
|
||||
my_metrics.append(metric)
|
||||
except:
|
||||
pass
|
||||
|
||||
return my_metrics
|
||||
|
||||
|
@ -346,4 +350,5 @@ if __name__ == '__main__':
|
|||
pprint (stats_for_instances(all_instances()))
|
||||
web1 = instance('web1')
|
||||
print instance_metrics(web1)
|
||||
print all_images()
|
||||
|
||||
|
|
|
@ -3,5 +3,5 @@
|
|||
django-admin.py syncdb --migrate <<'EOF'
|
||||
no
|
||||
EOF
|
||||
django-admin.py loaddata ../test/fixtures/basic_campaign_test.json
|
||||
django-admin.py loaddata ../core/fixtures/basic_campaign_test.json
|
||||
|
||||
|
|
Loading…
Reference in New Issue