diff --git a/.travis.yml b/.travis.yml
index c330cf4..a568c4c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,7 +2,7 @@ language: python
python:
- "2.7"
install:
- - pip install -r requirements.txt --use-mirrors
+ - pip install -r requirements.txt
- pip install python-coveralls coverage
script:
- coverage run --source mocurly setup.py test
diff --git a/mocurly/core.py b/mocurly/core.py
index 3780aa0..12e6445 100644
--- a/mocurly/core.py
+++ b/mocurly/core.py
@@ -152,16 +152,25 @@ def _register(self):
for endpoint in endpoints:
# register list views
list_uri = recurly.base_uri() + endpoint.base_uri
+ list_uri_re = re.compile(list_uri + r'$')
def list_callback(request, uri, headers, endpoint=endpoint):
xml, item_count = endpoint.list()
headers['X-Records'] = item_count
return 200, headers, xml
- HTTPretty.register_uri(HTTPretty.GET, list_uri, body=_callback(self)(list_callback), content_type="application/xml")
+ HTTPretty.register_uri(
+ HTTPretty.GET,
+ list_uri_re,
+ body=_callback(self)(list_callback),
+ content_type="application/xml")
def create_callback(request, uri, headers, endpoint=endpoint):
return 200, headers, endpoint.create(deserialize(request.body)[1])
- HTTPretty.register_uri(HTTPretty.POST, list_uri, body=_callback(self)(create_callback), content_type="application/xml")
+ HTTPretty.register_uri(
+ HTTPretty.POST,
+ list_uri_re,
+ body=_callback(self)(create_callback),
+ content_type="application/xml")
# register details views
detail_uri = recurly.base_uri() + endpoint.base_uri + r'/([^/ ]+)'
@@ -170,27 +179,48 @@ def create_callback(request, uri, headers, endpoint=endpoint):
def retrieve_callback(request, uri, headers, endpoint=endpoint, detail_uri_re=detail_uri_re):
pk = detail_uri_re.match(uri).group(1)
return 200, headers, endpoint.retrieve(pk)
- HTTPretty.register_uri(HTTPretty.GET, detail_uri_re, body=_callback(self)(retrieve_callback), content_type="application/xml")
+ HTTPretty.register_uri(
+ HTTPretty.GET,
+ detail_uri_re,
+ body=_callback(self)(retrieve_callback),
+ content_type="application/xml")
def update_callback(request, uri, headers, endpoint=endpoint, detail_uri_re=detail_uri_re):
pk = detail_uri_re.match(uri).group(1)
return 200, headers, endpoint.update(pk, deserialize(request.body)[1])
- HTTPretty.register_uri(HTTPretty.PUT, detail_uri_re, body=_callback(self)(update_callback), content_type="application/xml")
+ HTTPretty.register_uri(
+ HTTPretty.PUT,
+ detail_uri_re,
+ body=_callback(self)(update_callback),
+ content_type="application/xml")
def delete_callback(request, uri, headers, endpoint=endpoint, detail_uri_re=detail_uri_re):
parsed_url = urlparse(uri)
- pk = detail_uri_re.match('{0}://{1}{2}'.format(parsed_url.scheme, parsed_url.netloc, parsed_url.path)).group(1)
+ url_domain_part = '{0}://{1}{2}'.format(parsed_url.scheme, parsed_url.netloc, parsed_url.path)
+ pk = detail_uri_re.match(url_domain_part).group(1)
endpoint.delete(pk, **parse_qs(parsed_url.query))
return 204, headers, ''
- HTTPretty.register_uri(HTTPretty.DELETE, detail_uri_re, body=_callback(self)(delete_callback))
+ HTTPretty.register_uri(
+ HTTPretty.DELETE,
+ detail_uri_re,
+ body=_callback(self)(delete_callback))
# register extra views
- for method in filter(lambda method: callable(method) and getattr(method, 'is_route', False), (getattr(endpoint, m) for m in dir(endpoint))):
+ extra_views = filter(
+ lambda method: callable(method) and getattr(method, 'is_route', False),
+ (getattr(endpoint, m)
+ for m in dir(endpoint)))
+ for method in extra_views:
uri = detail_uri + '/' + method.uri
uri_re = re.compile(uri)
- def extra_route_callback(request, uri, headers, method=method, uri_re=uri_re):
- pk = uri_re.match(uri).group(1)
+ def extra_route_callback(
+ request,
+ uri,
+ headers,
+ method=method,
+ uri_re=uri_re):
+ uri_args = uri_re.match(uri).groups()
if method.method == 'DELETE':
status = 204
else:
@@ -199,13 +229,14 @@ def extra_route_callback(request, uri, headers, method=method, uri_re=uri_re):
post_data = request.querystring.copy()
if request.body:
post_data.update(deserialize(request.body)[1])
- result = method(pk, post_data)
+ method_args = uri_args + (post_data,)
+ result = method(*method_args)
elif method.is_list:
- result = method(pk, filters=request.querystring)
+ result = method(*uri_args, filters=request.querystring)
headers['X-Records'] = result[1]
result = result[0]
else:
- result = method(pk)
+ result = method(*uri_args)
return status, headers, result
if method.method == 'DELETE':
HTTPretty.register_uri(HTTPretty.DELETE, uri_re, body=_callback(self)(extra_route_callback))
diff --git a/mocurly/endpoints.py b/mocurly/endpoints.py
index cf21570..275016d 100644
--- a/mocurly/endpoints.py
+++ b/mocurly/endpoints.py
@@ -138,7 +138,7 @@ def uris(self, obj):
if billing_info_backend.has_object(obj[AccountsEndpoint.pk_attr]):
uri_out['billing_info_uri'] = uri_out['object_uri'] + '/billing_info'
uri_out['invoices_uri'] = uri_out['object_uri'] + '/invoices'
- uri_out['redemption_uri'] = uri_out['object_uri'] + '/redemption'
+ uri_out['redemption_uri'] = uri_out['object_uri'] + '/redemptions'
uri_out['subscriptions_uri'] = uri_out['object_uri'] + '/subscriptions'
uri_out['transactions_uri'] = uri_out['object_uri'] + '/transactions'
return uri_out
@@ -228,30 +228,21 @@ def filter_subscriptions(subscription):
out = SubscriptionsEndpoint.backend.list_objects(filter_subscriptions)
return subscriptions_endpoint.serialize(out, format=format)
- @details_route('GET', 'redemption')
- def get_coupon_redemption_view(self, account_code, format=BaseRecurlyEndpoint.XML):
- coupon_redemption = self.get_coupon_redemption(account_code)
- if coupon_redemption is None:
- raise ResponseError(404, '')
- return coupons_endpoint.serialize_coupon_redemption(coupon_redemption, format=format)
-
- def get_coupon_redemption(self, account_code):
+ @details_route('GET', 'redemptions$', is_list=True)
+ def get_coupon_redemptions(self, account_code, filters=None, format=BaseRecurlyEndpoint.XML):
account_coupon_redemptions = coupon_redemptions_backend.list_objects(lambda redemption: redemption['account_code'] == account_code)
- if len(account_coupon_redemptions) == 0:
- return None
-
- assert len(account_coupon_redemptions) == 1
- coupon_redemption = account_coupon_redemptions[0]
- return coupons_endpoint.hydrate_coupon_redemption_foreign_keys(coupon_redemption)
-
- @details_route('DELETE', 'redemption')
- def delete_coupon_redemption(self, account_code, format=BaseRecurlyEndpoint.XML):
- coupon_redemption = self.get_coupon_redemption(account_code)
- if coupon_redemption is None:
+ return coupons_endpoint.serialize_coupon_redemption(account_coupon_redemptions, format=format)
+
+ @details_route('DELETE', 'redemptions/([^/ ]+)')
+ def delete_coupon_redemption(self, account_code, redemption_uuid, format=BaseRecurlyEndpoint.XML):
+ account_coupon_redemptions = coupon_redemptions_backend.list_objects(
+ lambda redemption: \
+ (redemption['account_code'] == account_code and
+ coupons_endpoint.generate_coupon_redemption_uuid(redemption['coupon'], redemption['account_code']) == redemption_uuid))
+ if not account_coupon_redemptions:
raise ResponseError(404, '')
- coupon_redemption_uuid = coupons_endpoint.generate_coupon_redemption_uuid(coupon_redemption['coupon']['coupon_code'], account_code)
- coupon_redemptions_backend.delete_object(coupon_redemption_uuid)
+ coupon_redemptions_backend.delete_object(redemption_uuid)
return ''
@@ -648,27 +639,29 @@ def hydrate_coupon_redemption_foreign_keys(self, obj):
return obj
def coupon_redemption_uris(self, obj):
+ uuid = self.generate_coupon_redemption_uuid(obj['coupon']['coupon_code'], obj['account_code'])
uri_out = {}
uri_out['coupon_uri'] = coupons_endpoint.get_object_uri(obj['coupon'])
pseudo_account_object = {}
pseudo_account_object[AccountsEndpoint.pk_attr] = obj['account_code']
uri_out['account_uri'] = accounts_endpoint.get_object_uri(pseudo_account_object)
- uri_out['object_uri'] = uri_out['account_uri'] + '/redemption'
+ uri_out['object_uri'] = uri_out['account_uri'] + '/redemptions/' + uuid
return uri_out
def serialize_coupon_redemption(self, obj, format=BaseRecurlyEndpoint.XML):
- if format == BaseRecurlyEndpoint.RAW:
- obj = self.hydrate_coupon_redemption_foreign_keys(obj)
- return obj
-
- if type(obj) == list:
+ if isinstance(obj, list):
obj = [self.hydrate_coupon_redemption_foreign_keys(o) for o in obj]
for o in obj:
o['uris'] = self.coupon_redemption_uris(o)
- return serialize_list('redemption.xml', 'redemptions', 'redemption', obj)
else:
obj = self.hydrate_coupon_redemption_foreign_keys(obj)
obj['uris'] = self.coupon_redemption_uris(obj)
+
+ if format == BaseRecurlyEndpoint.RAW:
+ return obj
+ elif isinstance(obj, list):
+ return serialize_list('redemption.xml', 'redemptions', 'redemption', obj)
+ else:
return serialize('redemption.xml', 'redemption', obj)
@details_route('GET', 'redemptions', is_list=True)
@@ -681,7 +674,9 @@ def redeem_coupon(self, pk, redeem_info, format=BaseRecurlyEndpoint.XML):
assert CouponsEndpoint.backend.has_object(pk)
redeem_info['coupon'] = pk
redeem_info['created_at'] = current_time().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)
+ redemption_uuid = self.generate_coupon_redemption_uuid(pk, redeem_info['account_code'])
+ new_redemption = coupon_redemptions_backend.add_object(redemption_uuid, redeem_info)
+ return self.serialize_coupon_redemption(new_redemption, format=format)
def determine_coupon_discount(self, coupon, charge):
type = coupon['discount_type']
@@ -858,10 +853,13 @@ def create(self, create_info, format=BaseRecurlyEndpoint.XML):
# If there are addons, make sure they exist in the system
if 'subscription_add_ons' in create_info:
- for add_on in create_info['subscription_add_ons']:
+ add_ons = create_info['subscription_add_ons']
+ if isinstance(add_ons, dict):
+ add_ons = add_ons.values()
+ for add_on in add_ons:
add_on_uuid = plans_endpoint.generate_plan_add_on_uuid(create_info['plan_code'], add_on['add_on_code'])
assert plan_add_ons_backend.has_object(add_on_uuid)
- create_info['subscription_add_ons'] = [add_on['add_on_code'] for add_on in create_info['subscription_add_ons']]
+ create_info['subscription_add_ons'] = [add_on['add_on_code'] for add_on in add_ons]
defaults = SubscriptionsEndpoint.defaults.copy()
defaults['unit_amount_in_cents'] = plan['unit_amount_in_cents'][create_info['currency']]
@@ -917,12 +915,10 @@ def create(self, create_info, format=BaseRecurlyEndpoint.XML):
adjustment_infos.append(plan_charge_line_item)
# now calculate discounts
- coupon_redemption = accounts_endpoint.get_coupon_redemption(new_sub['account'])
- if coupon_redemption:
- for plan_charge_line_item in adjustment_infos:
- discount = coupons_endpoint.determine_coupon_discount(coupon_redemption['coupon'], plan_charge_line_item['unit_amount_in_cents'])
- plan_charge_line_item['discount_in_cents'] = discount
- total -= plan_charge_line_item['discount_in_cents']
+ coupon_redemptions = accounts_endpoint.get_coupon_redemptions(
+ new_sub['account'], format=BaseRecurlyEndpoint.RAW)
+ if coupon_redemptions:
+ total -= self._apply_coupons(coupon_redemptions, adjustment_infos)
# create a transaction if the subscription is started
new_transaction = {}
@@ -999,6 +995,20 @@ def reactivate_subscription(self, pk, reactivate_info, format=format):
'canceled_at': None
}), format=format)
+ def _apply_coupons(self, coupon_redemptions, adjustment_infos):
+ total_discounts = 0
+ for redemption in coupon_redemptions:
+ for plan_charge_line_item in adjustment_infos:
+ discount = coupons_endpoint.determine_coupon_discount(
+ redemption['coupon'],
+ plan_charge_line_item['unit_amount_in_cents'])
+ if 'discount_in_cents' in plan_charge_line_item:
+ plan_charge_line_item['discount_in_cents'] += discount
+ else:
+ plan_charge_line_item['discount_in_cents'] = discount
+ total_discounts += discount
+ return total_discounts
+
accounts_endpoint = AccountsEndpoint()
adjustments_endpoint = AdjustmentsEndpoint()
transactions_endpoint = TransactionsEndpoint()
diff --git a/mocurly/templates/account.xml b/mocurly/templates/account.xml
index 37bf53b..cc2f592 100644
--- a/mocurly/templates/account.xml
+++ b/mocurly/templates/account.xml
@@ -4,7 +4,7 @@
{% endif %}
-
+
{{ account.account_code }}