From 8885c1b683f12cca94511ae8aae538436feda6db Mon Sep 17 00:00:00 2001 From: Yoriyasu Yano Date: Thu, 21 Aug 2014 13:25:17 -0700 Subject: [PATCH] Coupon support --- mocurly/backend.py | 10 +++++ mocurly/core.py | 4 ++ mocurly/endpoints.py | 71 ++++++++++++++++++++++++++++++-- mocurly/templates/coupon.xml | 42 +++++++++++++++++++ mocurly/templates/redemption.xml | 7 ++++ tests/test_coupons.py | 65 +++++++++++++++++++++++++++++ 6 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 mocurly/templates/coupon.xml create mode 100644 mocurly/templates/redemption.xml create mode 100644 tests/test_coupons.py diff --git a/mocurly/backend.py b/mocurly/backend.py index 56c9650..aa1afb4 100644 --- a/mocurly/backend.py +++ b/mocurly/backend.py @@ -40,6 +40,12 @@ class BillingInfoBackend(BaseBackend): class InvoiceBackend(BaseBackend): pass +class CouponBackend(BaseBackend): + pass + +class CouponRedemptionBackend(BaseBackend): + pass + class PlanBackend(BaseBackend): pass @@ -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() @@ -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() diff --git a/mocurly/core.py b/mocurly/core.py index 000a0f8..3afc153 100644 --- a/mocurly/core.py +++ b/mocurly/core.py @@ -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()] diff --git a/mocurly/endpoints.py b/mocurly/endpoints.py index a9a4777..bf39ed3 100644 --- a/mocurly/endpoints.py +++ b/mocurly/endpoints.py @@ -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' @@ -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 @@ -343,7 +409,7 @@ 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 @@ -351,7 +417,6 @@ 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) diff --git a/mocurly/templates/coupon.xml b/mocurly/templates/coupon.xml new file mode 100644 index 0000000..552b372 --- /dev/null +++ b/mocurly/templates/coupon.xml @@ -0,0 +1,42 @@ + + + {{ coupon.coupon_code }} + {{ coupon.name }} + {{ coupon.state }} + {{ coupon.discount_type }} + {% if coupon.discount_type == 'percent' %} + {{ coupon.discount_percent }} + {% else %} + + {% for currency, value in plan.unit_amount_in_cents.items() %} + <{{ currency }} type="integer">{{ value }} + {% endfor %} + + {% endif %} + {% if coupon.redeem_by_date %} + {{ coupon.redeem_by_date }} + {% else %} + + {% endif %} + {% if coupon.single_use %}true{% else %}false{% endif %} + {% if coupon.applies_for_months %} + {{ coupon.applies_for_months }} + {% else %} + + {% endif %} + {% if coupon.max_redemptions %} + {{ coupon.max_redemptions }} + {% else %} + + {% endif %} + {% if coupon.applies_to_all_plans %}true{% else %}false{% endif %} + {{ coupon.created_at }} + {% if not coupon.applies_to_all_plans %} + + {% for plan_code in coupon.plan_codes %} + {{ plan_code }} + {% endfor %} + + {% endif %} + + diff --git a/mocurly/templates/redemption.xml b/mocurly/templates/redemption.xml new file mode 100644 index 0000000..e9f162a --- /dev/null +++ b/mocurly/templates/redemption.xml @@ -0,0 +1,7 @@ + + + + {{ redemption.coupon.single_use }} + {{ redemption.currency }} + {{ redemption.created_at }} + diff --git a/tests/test_coupons.py b/tests/test_coupons.py new file mode 100644 index 0000000..1fad181 --- /dev/null +++ b/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)