Skip to content
This repository has been archived by the owner on Aug 1, 2023. It is now read-only.

Commit

Permalink
Coupon support
Browse files Browse the repository at this point in the history
  • Loading branch information
yorinasub17 committed Aug 21, 2014
1 parent 7161b61 commit 8885c1b
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 3 deletions.
10 changes: 10 additions & 0 deletions mocurly/backend.py
Expand Up @@ -40,6 +40,12 @@ class BillingInfoBackend(BaseBackend):
class InvoiceBackend(BaseBackend):
pass

class CouponBackend(BaseBackend):
pass

class CouponRedemptionBackend(BaseBackend):
pass

class PlanBackend(BaseBackend):
pass

Expand All @@ -58,6 +64,8 @@ class AdjustmentBackend(BaseBackend):
accounts_backend = AccountBackend()
billing_info_backend = BillingInfoBackend()
invoices_backend = InvoiceBackend()
coupons_backend = CouponBackend()
coupon_redemptions_backend = CouponRedemptionBackend()
plans_backend = PlanBackend()
plan_add_ons_backend = PlanAddOnBackend()
subscriptions_backend = SubscriptionBackend()
Expand All @@ -68,6 +76,8 @@ def clear_backends():
accounts_backend.clear_all()
billing_info_backend.clear_all()
invoices_backend.clear_all()
coupons_backend.clear_all()
coupon_redemptions_backend.clear_all()
plans_backend.clear_all()
plan_add_ons_backend.clear_all()
subscriptions_backend.clear_all()
Expand Down
4 changes: 4 additions & 0 deletions mocurly/core.py
Expand Up @@ -96,13 +96,17 @@ def stop(self):

def _register(self):
from .endpoints import AccountsEndpoint
from .endpoints import AdjustmentsEndpoint
from .endpoints import TransactionsEndpoint
from .endpoints import CouponsEndpoint
from .endpoints import InvoicesEndpoint
from .endpoints import PlansEndpoint
from .endpoints import SubscriptionsEndpoint

endpoints = [AccountsEndpoint(),
AdjustmentsEndpoint(),
TransactionsEndpoint(),
CouponsEndpoint(),
InvoicesEndpoint(),
PlansEndpoint(),
SubscriptionsEndpoint()]
Expand Down
71 changes: 68 additions & 3 deletions mocurly/endpoints.py
Expand Up @@ -7,7 +7,7 @@
import dateutil.parser

from .core import details_route, serialize, serialize_list, deserialize
from .backend import accounts_backend, billing_info_backend, transactions_backend, invoices_backend, subscriptions_backend, plans_backend, plan_add_ons_backend, adjustments_backend
from .backend import accounts_backend, billing_info_backend, transactions_backend, invoices_backend, subscriptions_backend, plans_backend, plan_add_ons_backend, adjustments_backend, coupons_backend, coupon_redemptions_backend

class BaseRecurlyEndpoint(object):
pk_attr = 'uuid'
Expand Down Expand Up @@ -310,6 +310,72 @@ def generate_invoice_number():
return '1000'
return str(max(int(invoice['invoice_number']) for invoice in InvoicesEndpoint.backend.list_objects()) + 1)

class CouponsEndpoint(BaseRecurlyEndpoint):
base_uri = 'coupons'
backend = coupons_backend
object_type = 'coupon'
object_type_plural = 'coupons'
pk_attr = 'coupon_code'
template = 'coupon.xml'
defaults = {
'state': 'redeemable',
'applies_to_all_plans': True,
'single_use': False
}

def uris(self, obj):
uri_out = super(CouponsEndpoint, self).uris(obj)
uri_out['redemptions_uri'] = uri_out['object_uri'] + '/redemptions'
uri_out['redeem_uri'] = uri_out['object_uri'] + '/redeem'
return uri_out

def create(self, create_info, format=BaseRecurlyEndpoint.XML):
defaults = CouponsEndpoint.defaults.copy()
defaults.update(create_info)
return super(CouponsEndpoint, self).create(defaults, format)

def generate_coupon_redemption_uuid(self, coupon_code, account_code):
return '__'.join([coupon_code, account_code])

def hydrate_coupon_redemption_foreign_keys(self, obj):
if isinstance(obj['coupon'], six.string_types):
obj['coupon'] = CouponsEndpoint.backend.get_object(obj['coupon'])
return obj

def coupon_redemption_uris(self, obj):
uri_out = {}
uri_out['coupon_uri'] = CouponsEndpoint().get_object_uri(obj['coupon'])
pseudo_account_object = {}
pseudo_account_object[AccountsEndpoint.pk_attr] = obj['account_code']
uri_out['account_uri'] = AccountsEndpoint().get_object_uri(pseudo_account_object)
uri_out['object_uri'] = uri_out['account_uri'] + '/redemption'
return uri_out

def serialize_coupon_redemption(self, obj, format=BaseRecurlyEndpoint.XML):
obj = self.hydrate_coupon_redemption_foreign_keys(obj)
if format == BaseRecurlyEndpoint.RAW:
return obj

if type(obj) == list:
for o in obj:
o['uris'] = self.coupon_redemption_uris(o)
return serialize_list('redemption.xml', 'redemptions', 'redemption', obj)
else:
obj['uris'] = self.coupon_redemption_uris(obj)
return serialize('redemption.xml', 'redemption', obj)

@details_route('GET', 'redemptions', is_list=True)
def get_coupon_redemptions(self, pk, format=BaseRecurlyEndpoint.XML):
obj_list = coupon_redemptions_backend.list_objects(lambda redemption: redemption['coupon'] == pk)
return self.serialize_coupon_redemption(obj_list, format=format)

@details_route('POST', 'redeem')
def redeem_coupon(self, pk, redeem_info, format=BaseRecurlyEndpoint.XML):
assert CouponsEndpoint.backend.has_object(pk)
redeem_info['coupon'] = pk
redeem_info['created_at'] = datetime.datetime.now().isoformat()
return self.serialize_coupon_redemption(coupon_redemptions_backend.add_object(self.generate_coupon_redemption_uuid(pk, redeem_info['account_code']), redeem_info), format=format)

class PlansEndpoint(BaseRecurlyEndpoint):
base_uri = 'plans'
backend = plans_backend
Expand Down Expand Up @@ -343,15 +409,14 @@ def plan_add_on_uris(self, obj):
uri_out = {}
pseudo_plan_object = {}
pseudo_plan_object[PlansEndpoint.pk_attr] = obj['plan']
uri_out['plan_uri'] = PlansEndpoint().create_object_uri(pseudo_plan_object)
uri_out['plan_uri'] = PlansEndpoint().get_object_uri(pseudo_plan_object)
uri_out['object_uri'] = uri_out['plan_uri'] + '/add_ons/' + obj['uuid']
return uri_out

def serialize_plan_add_on(self, obj, format=BaseRecurlyEndpoint.XML):
if format == BaseRecurlyEndpoint.RAW:
return obj

obj['uris'] = self.plan_add_on_uris(obj)
if type(obj) == list:
for o in obj:
o['uris'] = self.plan_add_on_uris(o)
Expand Down
42 changes: 42 additions & 0 deletions mocurly/templates/coupon.xml
@@ -0,0 +1,42 @@
<coupon href="{{ coupon.uris.object_uri }}">
<redemptions href="{{ coupon.uris.redemptions_uri }}"/>
<coupon_code>{{ coupon.coupon_code }}</coupon_code>
<name>{{ coupon.name }}</name>
<state>{{ coupon.state }}</state>
<discount_type>{{ coupon.discount_type }}</discount_type>
{% if coupon.discount_type == 'percent' %}
<discount_percent type="integer">{{ coupon.discount_percent }}</discount_percent>
{% else %}
<discount_in_cents>
{% for currency, value in plan.unit_amount_in_cents.items() %}
<{{ currency }} type="integer">{{ value }}</{{ currency }}>
{% endfor %}
</discount_in_cents>
{% endif %}
{% if coupon.redeem_by_date %}
<redeem_by_date type="datetime">{{ coupon.redeem_by_date }}</redeem_by_date>
{% else %}
<redeem_by_date nil="nil"></redeem_by_date>
{% endif %}
<single_use type="boolean">{% if coupon.single_use %}true{% else %}false{% endif %}</single_use>
{% if coupon.applies_for_months %}
<applies_for_months>{{ coupon.applies_for_months }}</applies_for_months>
{% else %}
<applies_for_months nil="nil"></applies_for_months>
{% endif %}
{% if coupon.max_redemptions %}
<max_redemptions type="integer">{{ coupon.max_redemptions }}</max_redemptions>
{% else %}
<max_redemptions nil="nil"></max_redemptions>
{% endif %}
<applies_to_all_plans type="boolean">{% if coupon.applies_to_all_plans %}true{% else %}false{% endif %}</applies_to_all_plans>
<created_at type="datetime">{{ coupon.created_at }}</created_at>
{% if not coupon.applies_to_all_plans %}
<plan_codes type="array">
{% for plan_code in coupon.plan_codes %}
<plan_code>{{ plan_code }}</plan_code>
{% endfor %}
</plan_codes>
{% endif %}
<a name="redeem" href="{{ coupon.uris.redeem_uri }}" method="post"/>
</coupon>
7 changes: 7 additions & 0 deletions mocurly/templates/redemption.xml
@@ -0,0 +1,7 @@
<redemption href="{{ redemption.uris.object_uri }}">
<coupon href="{{ redemption.uris.coupon_uri }}"/>
<account href="{{ redemption.uris.account_uri }}"/>
<single_use type="boolean">{{ redemption.coupon.single_use }}</single_use>
<currency>{{ redemption.currency }}</currency>
<created_at type="datetime">{{ redemption.created_at }}</created_at>
</redemption>
65 changes: 65 additions & 0 deletions tests/test_coupons.py
@@ -0,0 +1,65 @@
import unittest
import datetime
import iso8601
import recurly
recurly.API_KEY = 'blah'

import mocurly.core
import mocurly.backend

class TestCoupons(unittest.TestCase):
def setUp(self):
self.mocurly_ = mocurly.core.mocurly()
self.mocurly_.start()

self.base_address_data = {
'address1': '123 Jackson St.',
'address2': 'Data City',
'state': 'CA',
'zip': '94105'
}
self.base_billing_info_data = {
'uuid': 'blah',
'first_name': 'Foo',
'last_name': 'Bar'
}
self.base_account_data = {
'uuid': 'blah',
'account_code': 'blah',
'email': 'foo@bar.com',
'first_name': 'Foo',
'last_name': 'Bar',
'address': self.base_address_data,
'hosted_login_token': 'abcd1234',
'created_at': '2014-08-11'
}
mocurly.backend.accounts_backend.add_object(self.base_account_data['uuid'], self.base_account_data)
mocurly.backend.billing_info_backend.add_object(self.base_billing_info_data['uuid'], self.base_billing_info_data)

self.base_coupon_data = {
'coupon_code': 'special',
'name': 'Special 10% off',
'discount_type': 'percent',
'discount_percent': 10
}

def tearDown(self):
self.mocurly_.stop()

def test_simple_coupon_creation(self):
self.assertEqual(len(mocurly.backend.coupons_backend.datastore), 0)

coupon = recurly.Coupon(**self.base_coupon_data)
coupon.save()

self.assertEqual(len(mocurly.backend.coupons_backend.datastore), 1)

def test_coupon_redemption(self):
self.assertEqual(len(mocurly.backend.coupon_redemptions_backend.datastore), 0)
mocurly.backend.coupons_backend.add_object(self.base_coupon_data['coupon_code'], self.base_coupon_data)

coupon = recurly.Coupon.get(self.base_coupon_data['coupon_code'])
redemption = recurly.Redemption(account_code=self.base_account_data['account_code'], currency='USD')
redemption = coupon.redeem(redemption)

self.assertEqual(len(mocurly.backend.coupon_redemptions_backend.datastore), 1)

0 comments on commit 8885c1b

Please sign in to comment.