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 }}