diff --git a/README.rst b/README.rst index 630b8bb..c5dcb99 100644 --- a/README.rst +++ b/README.rst @@ -30,6 +30,8 @@ Getting Started To get started with Sharpy, simply install it like you would any other python package +.. code:: + pip install sharpy Optionally, you can also install `lxml `_ on your @@ -50,8 +52,19 @@ Code You can checkout and download Sharpy's latest code at `Github `_. +Installing elementtree for Development and Unit Testing +======================================================= +When trying to install elementtree, pip may report that there is no such package. If this happens to you, you can work around by downloading and installing it manually. + +.. code:: + + wget http://effbot.org/media/downloads/elementtree-1.2.6-20050316.zip + unzip elementtree-1.2.6-20050316.zip + cd elementtree-1.2.6-20050316/ + pip install . + TODOs ===== * Flesh out the documentation to cover the full API. -* Add support for the various filtering options in the `get_customers` call. \ No newline at end of file +* Add support for the various filtering options in the `get_customers` call. diff --git a/setup.py b/setup.py index fd44c3d..ce2c96b 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ #!/usr/bin/env python try: - from setuptools import setup + from setuptools import setup except ImportError, err: from distutils.core import setup diff --git a/sharpy/__init__.py b/sharpy/__init__.py index 1bc90e9..c07c373 100644 --- a/sharpy/__init__.py +++ b/sharpy/__init__.py @@ -1 +1 @@ -VERSION = (0, 8) +VERSION = (0, 9) diff --git a/sharpy/client.py b/sharpy/client.py index 88f940f..980d204 100644 --- a/sharpy/client.py +++ b/sharpy/client.py @@ -1,17 +1,26 @@ import base64 import logging from urllib import urlencode -from decimal import getcontext from dateutil.tz import tzutc import httplib2 -from sharpy.exceptions import CheddarError, AccessDenied, BadRequest, NotFound, PreconditionFailed, CheddarFailure, NaughtyGateway, UnprocessableEntity +from sharpy.exceptions import AccessDenied +from sharpy.exceptions import BadRequest +from sharpy.exceptions import CheddarError +from sharpy.exceptions import CheddarFailure +from sharpy.exceptions import NaughtyGateway +from sharpy.exceptions import NotFound +from sharpy.exceptions import PreconditionFailed +from sharpy.exceptions import UnprocessableEntity client_log = logging.getLogger('SharpyClient') + class Client(object): default_endpoint = 'https://cheddargetter.com/xml' - def __init__(self, username, password, product_code, cache=None, timeout=None, endpoint=None): + + def __init__(self, username, password, product_code, cache=None, + timeout=None, endpoint=None): ''' username - Your cheddargetter username (probably an email address) password - Your cheddargetter password @@ -83,7 +92,8 @@ def make_request(self, path, params=None, data=None, method=None): method = 'POST' body = urlencode(data) headers = { - 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', + 'content-type': + 'application/x-www-form-urlencoded; charset=UTF-8', } client_log.debug('Request Method: %s' % method) @@ -92,10 +102,10 @@ def make_request(self, path, params=None, data=None, method=None): # Setup http client h = httplib2.Http(cache=self.cache, timeout=self.timeout) - #h.add_credentials(self.username, self.password) - # Skip the normal http client behavior and send auth headers immediately - # to save an http request. - headers['Authorization'] = "Basic %s" % base64.standard_b64encode(self.username + ':' + self.password).strip() + # Skip the normal http client behavior and send auth headers + # immediately to save an http request. + headers['Authorization'] = "Basic %s" % base64.standard_b64encode( + self.username + ':' + self.password).strip() # Make request response, content = h.request(url, method, body=body, headers=headers) @@ -123,5 +133,3 @@ def make_request(self, path, params=None, data=None, method=None): response.content = content return response - - diff --git a/sharpy/exceptions.py b/sharpy/exceptions.py index 4841dae..ab2cf17 100644 --- a/sharpy/exceptions.py +++ b/sharpy/exceptions.py @@ -2,16 +2,16 @@ class CheddarError(Exception): "Base class for exceptions returned by cheddar" - + def __init__(self, response, content, *args, **kwargs): # Importing in method to break circular dependecy from sharpy.parsers import parse_error - + super(CheddarError, self).__init__(*args, **kwargs) error_info = parse_error(content) self.response = response self.error_info = error_info - + def __str__(self): return '%s (%s) %s - %s' % ( self.response.status, @@ -20,42 +20,50 @@ def __str__(self): self.error_info['message'], ) + class AccessDenied(CheddarError): "A request to cheddar returned a status code of 401" pass - + + class BadRequest(CheddarError): "A request to cheddar was invalid in some way" pass + class NotFound(CheddarError): "A request to chedder was made for a resource which doesn't exist" pass - + + class CheddarFailure(CheddarError): "A request to cheddar encountered an unexpected error on the cheddar side" pass - + + class PreconditionFailed(CheddarError): "A request to cheddar was made but failed CG's validation in some way." pass - + + class NaughtyGateway(CheddarError): """ Cheddar either couldn't contact the gateway or the gateway did something very unexpected. """ pass - + + class UnprocessableEntity(CheddarError): """ An error occurred during processing. Please fix the error and try again. """ pass - + + class ParseError(Exception): """ Sharpy recieved unknown output from cheddar and doesn't know what to do with it. """ - pass \ No newline at end of file + pass diff --git a/sharpy/parsers.py b/sharpy/parsers.py index 6d4b417..846fcd7 100644 --- a/sharpy/parsers.py +++ b/sharpy/parsers.py @@ -1,4 +1,3 @@ -from datetime import datetime, date from decimal import Decimal, InvalidOperation import logging @@ -8,11 +7,12 @@ from lxml.etree import XML except ImportError: from elementtree.ElementTree import XML - + from sharpy.exceptions import ParseError client_log = logging.getLogger('SharpyClient') + def parse_error(xml_str): error = {} doc = XML(xml_str) @@ -27,9 +27,9 @@ def parse_error(xml_str): error['code'] = elem.attrib['code'] error['aux_code'] = elem.attrib['auxCode'] error['message'] = elem.text - + return error - + class CheddarOutputParser(object): ''' @@ -45,9 +45,9 @@ def parse_bool(self, content): value = False else: raise ParseError("Can't parse '%s' as a bool." % content) - + return value - + def parse_int(self, content): value = None if content != '' and content is not None: @@ -55,9 +55,9 @@ def parse_int(self, content): value = int(content) except ValueError: raise ParseError("Can't parse '%s' as an int." % content) - + return value - + def parse_decimal(self, content): value = None if content != '' and content is not None: @@ -65,9 +65,9 @@ def parse_decimal(self, content): value = Decimal(content) except InvalidOperation: raise ParseError("Can't parse '%s' as a decimal." % content) - + return value - + def parse_datetime(self, content): value = None if content: @@ -75,10 +75,10 @@ def parse_datetime(self, content): value = date_parser.parse(content) except ValueError: raise ParseError("Can't parse '%s' as a datetime." % content) - - + return value - + + class PlansParser(CheddarOutputParser): ''' A utility class for parsing cheddar's xml output for pricing plans. @@ -89,9 +89,9 @@ def parse_xml(self, xml_str): for plan_xml in plans_xml: plan = self.parse_plan(plan_xml) plans.append(plan) - + return plans - + def parse_plan(self, plan_element): plan = {} plan['id'] = plan_element.attrib['id'] @@ -101,45 +101,58 @@ def parse_plan(self, plan_element): plan['is_active'] = self.parse_bool(plan_element.findtext('isActive')) plan['is_free'] = self.parse_bool(plan_element.findtext('isFree')) plan['trial_days'] = self.parse_int(plan_element.findtext('trialDays')) - plan['initial_bill_count'] = self.parse_int(plan_element.findtext('initialBillCount')) - plan['initial_bill_count_unit'] = plan_element.findtext('initialBillCountUnit') + plan['initial_bill_count'] = self.parse_int(plan_element.findtext( + 'initialBillCount')) + plan['initial_bill_count_unit'] = plan_element.findtext( + 'initialBillCountUnit') plan['billing_frequency'] = plan_element.findtext('billingFrequency') - plan['billing_frequency_per'] = plan_element.findtext('billingFrequencyPer') - plan['billing_frequency_unit'] = plan_element.findtext('billingFrequencyUnit') - plan['billing_frequency_quantity'] = self.parse_int(plan_element.findtext('billingFrequencyQuantity')) + plan['billing_frequency_per'] = plan_element.findtext( + 'billingFrequencyPer') + plan['billing_frequency_unit'] = plan_element.findtext( + 'billingFrequencyUnit') + plan['billing_frequency_quantity'] = self.parse_int( + plan_element.findtext('billingFrequencyQuantity')) plan['setup_charge_code'] = plan_element.findtext('setupChargeCode') - plan['setup_charge_amount'] = self.parse_decimal(plan_element.findtext('setupChargeAmount')) - plan['recurring_charge_code'] = plan_element.findtext('recurringChargeCode') - plan['recurring_charge_amount'] = self.parse_decimal(plan_element.findtext('recurringChargeAmount')) - plan['created_datetime'] = self.parse_datetime(plan_element.findtext('createdDatetime')) - + plan['setup_charge_amount'] = self.parse_decimal( + plan_element.findtext('setupChargeAmount')) + plan['recurring_charge_code'] = plan_element.findtext( + 'recurringChargeCode') + plan['recurring_charge_amount'] = self.parse_decimal( + plan_element.findtext('recurringChargeAmount')) + plan['created_datetime'] = self.parse_datetime( + plan_element.findtext('createdDatetime')) + plan['items'] = self.parse_plan_items(plan_element.find('items')) - + return plan - + def parse_plan_items(self, items_element): items = [] - + if items_element is not None: for item_element in items_element: items.append(self.parse_plan_item(item_element)) - + return items - + def parse_plan_item(self, item_element): item = {} - + item['id'] = item_element.attrib['id'] item['code'] = item_element.attrib['code'] item['name'] = item_element.findtext('name') - item['quantity_included'] = self.parse_decimal(item_element.findtext('quantityIncluded')) - item['is_periodic'] = self.parse_bool(item_element.findtext('isPeriodic')) - item['overage_amount'] = self.parse_decimal(item_element.findtext('overageAmount')) - item['created_datetime'] = self.parse_datetime(item_element.findtext('createdDatetime')) - + item['quantity_included'] = self.parse_decimal( + item_element.findtext('quantityIncluded')) + item['is_periodic'] = self.parse_bool( + item_element.findtext('isPeriodic')) + item['overage_amount'] = self.parse_decimal( + item_element.findtext('overageAmount')) + item['created_datetime'] = self.parse_datetime( + item_element.findtext('createdDatetime')) + return item - - + + class CustomersParser(CheddarOutputParser): ''' Utility class for parsing cheddar's xml output for customers. @@ -150,12 +163,12 @@ def parse_xml(self, xml_str): for customer_xml in customers_xml: customer = self.parse_customer(customer_xml) customers.append(customer) - + return customers - + def parse_customer(self, customer_element): customer = {} - + # Basic info customer['id'] = customer_element.attrib['id'] customer['code'] = customer_element.attrib['code'] @@ -167,60 +180,72 @@ def parse_customer(self, customer_element): customer['gateway_token'] = customer_element.findtext('gateway_token') customer['is_vat_exempt'] = customer_element.findtext('isVatExempt') customer['vat_number'] = customer_element.findtext('vatNumber') - customer['first_contact_datetime'] = self.parse_datetime(customer_element.findtext('firstContactDatetime')) + customer['first_contact_datetime'] = self.parse_datetime( + customer_element.findtext('firstContactDatetime')) customer['referer'] = customer_element.findtext('referer') customer['referer_host'] = customer_element.findtext('refererHost') - customer['campaign_source'] = customer_element.findtext('campaignSource') - customer['campaign_medium'] = customer_element.findtext('campaignMedium') + customer['campaign_source'] = customer_element.findtext( + 'campaignSource') + customer['campaign_medium'] = customer_element.findtext( + 'campaignMedium') customer['campaign_term'] = customer_element.findtext('campaignTerm') - customer['campaign_content'] = customer_element.findtext('campaignContent') + customer['campaign_content'] = customer_element.findtext( + 'campaignContent') customer['campaign_name'] = customer_element.findtext('campaignName') - customer['created_datetime'] = self.parse_datetime(customer_element.findtext('createdDatetime')) - customer['modified_datetime'] = self.parse_datetime(customer_element.findtext('modifiedDatetime')) - + customer['created_datetime'] = self.parse_datetime( + customer_element.findtext('createdDatetime')) + customer['modified_datetime'] = self.parse_datetime( + customer_element.findtext('modifiedDatetime')) + # Metadata - customer['meta_data'] = self.parse_meta_data(customer_element.find('metaData')) - + customer['meta_data'] = self.parse_meta_data( + customer_element.find('metaData')) + # Subscriptions - customer['subscriptions'] = self.parse_subscriptions(customer_element.find('subscriptions')) - + customer['subscriptions'] = self.parse_subscriptions( + customer_element.find('subscriptions')) + return customer - + def parse_meta_data(self, meta_data_element): meta_data = [] for meta_datum_element in meta_data_element: meta_data.append(self.parse_meta_datum(meta_datum_element)) - + return meta_data - + def parse_meta_datum(self, meta_datum_element): meta_datum = {} - + meta_datum['id'] = meta_datum_element.attrib['id'] meta_datum['name'] = meta_datum_element.findtext('name') meta_datum['value'] = meta_datum_element.findtext('value') - meta_datum['created_datetime'] = self.parse_datetime(meta_datum_element.findtext('createdDatetime')) - meta_datum['modified_datetime'] = self.parse_datetime(meta_datum_element.findtext('modifiedDatetime')) - + meta_datum['created_datetime'] = self.parse_datetime( + meta_datum_element.findtext('createdDatetime')) + meta_datum['modified_datetime'] = self.parse_datetime( + meta_datum_element.findtext('modifiedDatetime')) + return meta_datum - - + def parse_subscriptions(self, subscriptions_element): subscriptions = [] for subscription_element in subscriptions_element: subscription = self.parse_subscription(subscription_element) subscriptions.append(subscription) - + return subscriptions - + def parse_subscription(self, subscription_element): subscription = {} - + # Basic info subscription['id'] = subscription_element.attrib['id'] - subscription['gateway_token'] = subscription_element.findtext('gatewayToken') - subscription['cc_first_name'] = subscription_element.findtext('ccFirstName') - subscription['cc_last_name'] = subscription_element.findtext('ccLastName') + subscription['gateway_token'] = subscription_element.findtext( + 'gatewayToken') + subscription['cc_first_name'] = subscription_element.findtext( + 'ccFirstName') + subscription['cc_last_name'] = subscription_element.findtext( + 'ccLastName') subscription['cc_company'] = subscription_element.findtext('ccCompany') subscription['cc_country'] = subscription_element.findtext('ccCountry') subscription['cc_address'] = subscription_element.findtext('ccAddress') @@ -229,12 +254,20 @@ def parse_subscription(self, subscription_element): subscription['cc_zip'] = subscription_element.findtext('ccZip') subscription['cc_type'] = subscription_element.findtext('ccType') subscription['cc_email'] = subscription_element.findtext('ccEmail') - subscription['cc_last_four'] = subscription_element.findtext('ccLastFour') - subscription['cc_expiration_date'] = subscription_element.findtext('ccExpirationDate') - subscription['cancel_type'] = subscription_element.findtext('cancelType') - subscription['cancel_reason'] = subscription_element.findtext('cancelReason') - subscription['canceled_datetime'] = self.parse_datetime(subscription_element.findtext('canceledDatetime')) - subscription['created_datetime'] = self.parse_datetime(subscription_element.findtext('createdDatetime')) + subscription['cc_last_four'] = subscription_element.findtext( + 'ccLastFour') + subscription['cc_expiration_date'] = subscription_element.findtext( + 'ccExpirationDate') + subscription['cancel_type'] = subscription_element.findtext( + 'cancelType') + subscription['cancel_reason'] = subscription_element.findtext( + 'cancelReason') + subscription['canceled_datetime'] = self.parse_datetime( + subscription_element.findtext('canceledDatetime')) + subscription['created_datetime'] = self.parse_datetime( + subscription_element.findtext('createdDatetime')) + subscription['coupon_code'] = subscription_element.findtext( + 'couponCode') gateway_account_element = subscription_element.find('gatewayAccount') if gateway_account_element is not None: subscription['gateway_account'] = { @@ -242,90 +275,175 @@ def parse_subscription(self, subscription_element): 'gateway': gateway_account_element.findtext('gateway'), 'type': gateway_account_element.findtext('type') } - subscription['redirect_url'] = subscription_element.findtext('redirectUrl') - + subscription['redirect_url'] = subscription_element.findtext( + 'redirectUrl') + # Plans - subscription['plans'] = self.parse_plans(subscription_element.find('plans')) - + subscription['plans'] = self.parse_plans( + subscription_element.find('plans')) + # Invoices - subscription['invoices'] = self.parse_invoices(subscription_element.find('invoices')) - - subscription['items'] = self.parse_subscription_items(subscription_element.find('items')) - + subscription['invoices'] = self.parse_invoices( + subscription_element.find('invoices')) + + subscription['items'] = self.parse_subscription_items( + subscription_element.find('items')) + return subscription - + def parse_plans(self, plans_element): plans_parser = PlansParser() plans = [] - + if plans_element is not None: for plan_element in plans_element: plans.append(plans_parser.parse_plan(plan_element)) - + return plans - + def parse_invoices(self, invoices_element): invoices = [] if invoices_element is not None: for invoice_element in invoices_element: invoices.append(self.parse_invoice(invoice_element)) - + return invoices - + def parse_invoice(self, invoice_element): invoice = {} - + invoice['id'] = invoice_element.attrib['id'] invoice['number'] = invoice_element.findtext('number') invoice['type'] = invoice_element.findtext('type') invoice['vat_rate'] = invoice_element.findtext('vatRate') - invoice['billing_datetime'] = self.parse_datetime(invoice_element.findtext('billingDatetime')) - invoice['paid_transaction_id'] = invoice_element.findtext('paidTransactionId') - invoice['created_datetime'] = self.parse_datetime(invoice_element.findtext('createdDatetime')) - - invoice['charges'] = self.parse_charges(invoice_element.find('charges')) - + invoice['billing_datetime'] = self.parse_datetime( + invoice_element.findtext('billingDatetime')) + invoice['paid_transaction_id'] = invoice_element.findtext( + 'paidTransactionId') + invoice['created_datetime'] = self.parse_datetime( + invoice_element.findtext('createdDatetime')) + + invoice['charges'] = self.parse_charges( + invoice_element.find('charges')) + return invoice - + def parse_charges(self, charges_element): charges = [] - + for charge_element in charges_element: charges.append(self.parse_charge(charge_element)) - + return charges - + def parse_charge(self, charge_element): charge = {} - + charge['id'] = charge_element.attrib['id'] charge['code'] = charge_element.attrib['code'] charge['type'] = charge_element.findtext('type') - charge['quantity'] = self.parse_decimal(charge_element.findtext('quantity')) - charge['each_amount'] = self.parse_decimal(charge_element.findtext('eachAmount')) + charge['quantity'] = self.parse_decimal( + charge_element.findtext('quantity')) + charge['each_amount'] = self.parse_decimal( + charge_element.findtext('eachAmount')) charge['description'] = charge_element.findtext('description') - charge['created_datetime'] = self.parse_datetime(charge_element.findtext('createdDatetime')) - + charge['created_datetime'] = self.parse_datetime( + charge_element.findtext('createdDatetime')) + return charge - + def parse_subscription_items(self, items_element): items = [] - + if items_element is not None: for item_element in items_element: items.append(self.parse_subscription_item(item_element)) - + return items - + def parse_subscription_item(self, item_element): item = {} item['id'] = item_element.attrib['id'] item['code'] = item_element.attrib['code'] item['name'] = item_element.findtext('name') - item['quantity'] = self.parse_decimal(item_element.findtext('quantity')) - item['created_datetime'] = self.parse_datetime(item_element.findtext('createdDatetime')) - item['modified_datetime'] = self.parse_datetime(item_element.findtext('modifiedDatetime')) - + item['quantity'] = self.parse_decimal( + item_element.findtext('quantity')) + item['created_datetime'] = self.parse_datetime( + item_element.findtext('createdDatetime')) + item['modified_datetime'] = self.parse_datetime( + item_element.findtext('modifiedDatetime')) + return item - + + +class PromotionsParser(CheddarOutputParser): + ''' + A utility class for parsing cheddar's xml output for promotions. + ''' + def parse_xml(self, xml_str): + promotions = [] + promotions_xml = XML(xml_str) + for promotion_xml in promotions_xml: + promotion = self.parse_promotion(promotion_xml) + promotions.append(promotion) + + return promotions + + def parse_promotion(self, promotion_element): + promotion = {} + promotion['id'] = promotion_element.attrib['id'] + promotion['name'] = promotion_element.findtext('name') + promotion['description'] = promotion_element.findtext('description') + promotion['created_datetime'] = self.parse_datetime( + promotion_element.findtext('createdDatetime')) + + promotion['incentives'] = self.parse_incentives( + promotion_element.find('incentives')) + promotion['coupons'] = self.parse_coupons( + promotion_element.find('coupons')) + + return promotion + + def parse_incentives(self, incentives_element): + incentives = [] + + if incentives_element is not None: + for incentive_element in incentives_element: + incentives.append(self.parse_incentive(incentive_element)) + + return incentives + + def parse_incentive(self, incentive_element): + incentive = {} + + incentive['id'] = incentive_element.attrib['id'] + incentive['type'] = incentive_element.findtext('type') + incentive['percentage'] = incentive_element.findtext('percentage') + incentive['months'] = incentive_element.findtext('months') + incentive['created_datetime'] = self.parse_datetime( + incentive_element.findtext('createdDatetime')) + + return incentive + + def parse_coupons(self, coupons_element): + coupons = [] + + if coupons_element is not None: + for coupon_element in coupons_element: + coupons.append(self.parse_coupon(coupon_element)) + + return coupons + + def parse_coupon(self, coupon_element): + coupon = {} + + coupon['id'] = coupon_element.attrib['id'] + coupon['code'] = coupon_element.attrib['code'] + coupon['max_redemptions'] = coupon_element.findtext('maxRedemptions') + coupon['expiration_datetime'] = self.parse_datetime( + coupon_element.findtext('expirationDatetime')) + coupon['created_datetime'] = self.parse_datetime( + coupon_element.findtext('createdDatetime')) + + return coupon diff --git a/sharpy/product.py b/sharpy/product.py index cd6c0bc..188b80a 100644 --- a/sharpy/product.py +++ b/sharpy/product.py @@ -1,17 +1,19 @@ from copy import copy -from datetime import date, datetime -from decimal import Decimal, getcontext as get_decimal_context +from datetime import datetime +from decimal import Decimal from time import time from dateutil.relativedelta import relativedelta from sharpy.client import Client from sharpy.exceptions import NotFound -from sharpy.parsers import PlansParser, CustomersParser +from sharpy.parsers import PlansParser, CustomersParser, PromotionsParser + class CheddarProduct(object): - - def __init__(self, username, password, product_code, cache=None, timeout=None, endpoint=None): + + def __init__(self, username, password, product_code, cache=None, + timeout=None, endpoint=None): self.product_code = product_code self.client = Client( username, @@ -21,20 +23,20 @@ def __init__(self, username, password, product_code, cache=None, timeout=None, e timeout, endpoint, ) - + super(CheddarProduct, self).__init__() - + def __repr__(self): return u'CheddarProduct: %s' % self.product_code - + def get_all_plans(self): response = self.client.make_request(path='plans/get') plans_parser = PlansParser() plans_data = plans_parser.parse_xml(response.content) plans = [PricingPlan(**plan_data) for plan_data in plans_data] - + return plans - + def get_plan(self, code): response = self.client.make_request( path='plans/get', @@ -43,83 +45,96 @@ def get_plan(self, code): plans_parser = PlansParser() plans_data = plans_parser.parse_xml(response.content) plans = [PricingPlan(**plan_data) for plan_data in plans_data] - + return plans[0] - - def create_customer(self, code, first_name, last_name, email, plan_code, \ - company=None, is_vat_exempt=None, vat_number=None, \ - notes=None, first_contact_datetime=None, \ - referer=None, campaign_term=None, \ - campaign_name=None, campaign_source=None, \ - campaign_medium=None, campaign_content=None, \ - meta_data=None, initial_bill_date=None, method=None,\ - cc_number=None, cc_expiration=None, \ - cc_card_code=None, cc_first_name=None, \ - cc_last_name=None, cc_email=None, cc_company=None, \ - cc_country=None, cc_address=None, cc_city=None, \ - cc_state=None, cc_zip=None, return_url=None, \ - cancel_url=None, charges=None, items=None): - - data = self.build_customer_post_data(code, first_name, last_name, \ - email, plan_code, company, is_vat_exempt, vat_number, \ - notes, first_contact_datetime, referer, campaign_term, \ - campaign_name, campaign_source, campaign_medium, \ - campaign_content, meta_data, initial_bill_date, method, \ - cc_number, cc_expiration, cc_card_code, cc_first_name, \ - cc_last_name, cc_email, cc_company, cc_country, cc_address, \ - cc_city, cc_state, cc_zip, return_url, cancel_url) - + + def create_customer(self, code, first_name, last_name, email, plan_code, + company=None, is_vat_exempt=None, vat_number=None, + notes=None, first_contact_datetime=None, + referer=None, campaign_term=None, + campaign_name=None, campaign_source=None, + campaign_medium=None, campaign_content=None, + meta_data=None, initial_bill_date=None, method=None, + cc_number=None, cc_expiration=None, + cc_card_code=None, cc_first_name=None, + cc_last_name=None, cc_email=None, cc_company=None, + cc_country=None, cc_address=None, cc_city=None, + cc_state=None, cc_zip=None, coupon_code=None, + return_url=None, cancel_url=None, charges=None, + items=None): + + data = self.build_customer_post_data(code, first_name, last_name, + email, plan_code, company, + is_vat_exempt, vat_number, + notes, first_contact_datetime, + referer, campaign_term, + campaign_name, campaign_source, + campaign_medium, campaign_content, + meta_data, initial_bill_date, + method, cc_number, cc_expiration, + cc_card_code, cc_first_name, + cc_last_name, cc_email, + cc_company, cc_country, + cc_address, cc_city, cc_state, + cc_zip, coupon_code, return_url, + cancel_url) + if charges: for i, charge in enumerate(charges): data['charges[%d][chargeCode]' % i] = charge['code'] data['charges[%d][quantity]' % i] = charge.get('quantity', 1) - data['charges[%d][eachAmount]' % i] = '%.2f' % charge['each_amount'] - data['charges[%d][description]' % i] = charge.get('description', '') + data['charges[%d][eachAmount]' % i] = '%.2f' % ( + charge['each_amount']) + data['charges[%d][description]' % i] = charge.get( + 'description', '') if items: for i, item in enumerate(items): data['items[%d][itemCode]' % i] = item['code'] data['items[%d][quantity]' % i] = item.get('quantity', 1) - + response = self.client.make_request(path='customers/new', data=data) customer_parser = CustomersParser() customers_data = customer_parser.parse_xml(response.content) customer = Customer(product=self, **customers_data[0]) - + return customer - - def build_customer_post_data(self, code=None, first_name=None,\ - last_name=None, email=None, plan_code=None, \ - company=None, is_vat_exempt=None, vat_number=None, \ - notes=None, first_contact_datetime=None, \ - referer=None, campaign_term=None, \ - campaign_name=None, campaign_source=None, \ - campaign_medium=None, campaign_content=None, \ - meta_data=None, initial_bill_date=None, method=None,\ - cc_number=None, cc_expiration=None, \ - cc_card_code=None, cc_first_name=None, \ - cc_last_name=None, cc_email=None, cc_company=None, \ - cc_country=None, cc_address=None, cc_city=None, \ - cc_state=None, cc_zip=None, return_url=None, cancel_url=None, \ - bill_date=None): + + def build_customer_post_data(self, code=None, first_name=None, + last_name=None, email=None, plan_code=None, + company=None, is_vat_exempt=None, + vat_number=None, notes=None, + first_contact_datetime=None, referer=None, + campaign_term=None, campaign_name=None, + campaign_source=None, campaign_medium=None, + campaign_content=None, meta_data=None, + initial_bill_date=None, method=None, + cc_number=None, cc_expiration=None, + cc_card_code=None, cc_first_name=None, + cc_last_name=None, cc_email=None, + cc_company=None, cc_country=None, + cc_address=None, cc_city=None, + cc_state=None, cc_zip=None, coupon_code=None, + return_url=None, cancel_url=None, + bill_date=None): data = {} - + if code: data['code'] = code - + if first_name: data['firstName'] = first_name - + if last_name: data['lastName'] = last_name - + if email: data['email'] = email - + if plan_code: data['subscription[planCode]'] = plan_code - + if company: data['company'] = company @@ -136,7 +151,8 @@ def build_customer_post_data(self, code=None, first_name=None,\ data['notes'] = notes if first_contact_datetime: - data['firstContactDatetime'] = self.client.format_datetime(first_contact_datetime) + data['firstContactDatetime'] = self.client.format_datetime( + first_contact_datetime) if referer: data['referer'] = referer @@ -159,8 +175,9 @@ def build_customer_post_data(self, code=None, first_name=None,\ data[full_key] = value if initial_bill_date: - data['subscription[initialBillDate]'] = self.client.format_date(initial_bill_date) - + data['subscription[initialBillDate]'] = self.client.format_date( + initial_bill_date) + if method: data['subscription[method]'] = method @@ -200,21 +217,25 @@ def build_customer_post_data(self, code=None, first_name=None,\ if cc_zip: data['subscription[ccZip]'] = cc_zip + if coupon_code: + data['subscription[couponCode]'] = coupon_code + if return_url: data['subscription[returnUrl]'] = return_url - + if cancel_url: data['subscription[cancelUrl]'] = cancel_url - + if bill_date: - data['subscription[changeBillDate]'] = self.client.format_datetime(bill_date) + data['subscription[changeBillDate]'] = self.client.format_datetime( + bill_date) return data - + def get_customers(self, filter_data=None): ''' - Returns all customers. Sometimes they are too much and cause internal - server errors on CG. API call permits post parameters for filtering + Returns all customers. Sometimes they are too much and cause internal + server errors on CG. API call permits post parameters for filtering which tends to fix this https://cheddargetter.com/developers#all-customers @@ -226,81 +247,116 @@ def get_customers(self, filter_data=None): ] ''' customers = [] - + try: - response = self.client.make_request(path='customers/get', data=filter_data) + response = self.client.make_request(path='customers/get', + data=filter_data) except NotFound: response = None - + if response: customer_parser = CustomersParser() customers_data = customer_parser.parse_xml(response.content) for customer_data in customers_data: customers.append(Customer(product=self, **customer_data)) - + return customers - + def get_customer(self, code): - + response = self.client.make_request( path='customers/get', params={'code': code}, ) customer_parser = CustomersParser() customers_data = customer_parser.parse_xml(response.content) - + return Customer(product=self, **customers_data[0]) - + def delete_all_customers(self): ''' This method does exactly what you think it does. Calling this method deletes all customer data in your cheddar product and the configured gateway. This action cannot be undone. - + DO NOT RUN THIS UNLESS YOU REALLY, REALLY, REALLY MEAN TO! ''' - response = self.client.make_request( + self.client.make_request( path='customers/delete-all/confirm/%d' % int(time()), method='POST' ) - - + + def get_all_promotions(self): + ''' + Returns all promotions. + https://cheddargetter.com/developers#promotions + ''' + promotions = [] + + try: + response = self.client.make_request(path='promotions/get') + except NotFound: + response = None + + if response: + promotions_parser = PromotionsParser() + promotions_data = promotions_parser.parse_xml(response.content) + promotions = [Promotion(**promotion_data) for promotion_data in promotions_data] + + return promotions + + def get_promotion(self, code): + ''' + Get the promotion with the specified coupon code. + https://cheddargetter.com/developers#single-promotion + ''' + + response = self.client.make_request( + path='promotions/get', + params={'code': code}, + ) + promotion_parser = PromotionsParser() + promotion_data = promotion_parser.parse_xml(response.content) + + return Promotion(**promotion_data[0]) + + class PricingPlan(object): - + def __init__(self, name, code, id, description, is_active, is_free, - trial_days, initial_bill_count, initial_bill_count_unit, + trial_days, initial_bill_count, initial_bill_count_unit, billing_frequency, billing_frequency_per, billing_frequency_quantity, billing_frequency_unit, setup_charge_code, setup_charge_amount, recurring_charge_code, recurring_charge_amount, created_datetime, items, subscription=None): - + self.load_data(name=name, code=code, id=id, description=description, - is_active=is_active, is_free=is_free, - trial_days=trial_days, - initial_bill_count=initial_bill_count, - initial_bill_count_unit=initial_bill_count_unit, - billing_frequency=billing_frequency, - billing_frequency_per=billing_frequency_per, - billing_frequency_quantity=billing_frequency_quantity, - billing_frequency_unit=billing_frequency_unit, - setup_charge_code=setup_charge_code, - setup_charge_amount=setup_charge_amount, - recurring_charge_code=recurring_charge_code, - recurring_charge_amount=recurring_charge_amount, - created_datetime=created_datetime, items=items, - subscription=subscription) - + is_active=is_active, is_free=is_free, + trial_days=trial_days, + initial_bill_count=initial_bill_count, + initial_bill_count_unit=initial_bill_count_unit, + billing_frequency=billing_frequency, + billing_frequency_per=billing_frequency_per, + billing_frequency_quantity=billing_frequency_quantity, + billing_frequency_unit=billing_frequency_unit, + setup_charge_code=setup_charge_code, + setup_charge_amount=setup_charge_amount, + recurring_charge_code=recurring_charge_code, + recurring_charge_amount=recurring_charge_amount, + created_datetime=created_datetime, items=items, + subscription=subscription) + super(PricingPlan, self).__init__() - + def load_data(self, name, code, id, description, is_active, is_free, - trial_days, initial_bill_count, initial_bill_count_unit, - billing_frequency, billing_frequency_per, - billing_frequency_quantity, billing_frequency_unit, - setup_charge_code, setup_charge_amount, - recurring_charge_code, recurring_charge_amount, - created_datetime, items, subscription=None): - + trial_days, initial_bill_count, initial_bill_count_unit, + billing_frequency, billing_frequency_per, + billing_frequency_quantity, billing_frequency_unit, + setup_charge_code, setup_charge_amount, + recurring_charge_code, recurring_charge_amount, + created_datetime, items, subscription=None): + self.name = name self.code = code self.id = id @@ -323,10 +379,10 @@ def load_data(self, name, code, id, description, is_active, is_free, if subscription: self.subscription = subscription - + def __repr__(self): return u'PricingPlan: %s (%s)' % (self.name, self.code) - + @property def initial_bill_date(self): ''' @@ -334,30 +390,29 @@ def initial_bill_date(self): based on available plan info. ''' time_to_start = None - + if self.initial_bill_count_unit == 'months': time_to_start = relativedelta(months=self.initial_bill_count) else: time_to_start = relativedelta(days=self.initial_bill_count) - + initial_bill_date = datetime.utcnow().date() + time_to_start - + return initial_bill_date - - + class Customer(object): - - def __init__(self, code, first_name, last_name, email, product, id=None, \ - company=None, notes=None, gateway_token=None, \ - is_vat_exempt=None, vat_number=None, \ - first_contact_datetime=None, referer=None, \ - referer_host=None, campaign_source=None, \ - campaign_medium=None, campaign_term=None, \ - campaign_content=None, campaign_name=None, \ - created_datetime=None, modified_datetime=None, \ - meta_data=None, subscriptions=None): - + + def __init__(self, code, first_name, last_name, email, product, id=None, + company=None, notes=None, gateway_token=None, + is_vat_exempt=None, vat_number=None, + first_contact_datetime=None, referer=None, + referer_host=None, campaign_source=None, + campaign_medium=None, campaign_term=None, + campaign_content=None, campaign_name=None, + created_datetime=None, modified_datetime=None, + coupon_code=None, meta_data=None, subscriptions=None): + self.load_data(code=code, first_name=first_name, last_name=last_name, email=email, product=product, id=id, @@ -374,21 +429,20 @@ def __init__(self, code, first_name, last_name, email, product, id=None, \ campaign_name=campaign_name, created_datetime=created_datetime, modified_datetime=modified_datetime, - meta_data=meta_data, - subscriptions=subscriptions - ) - + coupon_code=coupon_code, meta_data=meta_data, + subscriptions=subscriptions) + super(Customer, self).__init__() - - def load_data(self, code, first_name, last_name, email, product, id=None,\ - company=None, notes=None, gateway_token=None, \ - is_vat_exempt=None, vat_number=None, \ - first_contact_datetime=None, referer=None, \ - referer_host=None, campaign_source=None, \ - campaign_medium=None, campaign_term=None, \ - campaign_content=None, campaign_name=None, \ - created_datetime=None, modified_datetime=None, \ - meta_data=None, subscriptions=None): + + def load_data(self, code, first_name, last_name, email, product, id=None, + company=None, notes=None, gateway_token=None, + is_vat_exempt=None, vat_number=None, + first_contact_datetime=None, referer=None, + referer_host=None, campaign_source=None, + campaign_medium=None, campaign_term=None, + campaign_content=None, campaign_name=None, + created_datetime=None, modified_datetime=None, + coupon_code=None, meta_data=None, subscriptions=None): self.code = code self.id = id self.first_name = first_name @@ -409,81 +463,76 @@ def load_data(self, code, first_name, last_name, email, product, id=None,\ self.campaign_name = campaign_name self.created = created_datetime self.modified = modified_datetime + self.coupon_code = coupon_code self.meta_data = {} if meta_data: - for datum in meta_data: - self.meta_data[datum['name']] = datum['value'] + for datum in meta_data: + self.meta_data[datum['name']] = datum['value'] subscription_data = subscriptions[0] subscription_data['customer'] = self if hasattr(self, 'subscription'): self.subscription.load_data(**subscription_data) else: self.subscription = Subscription(**subscription_data) - + def load_data_from_xml(self, xml): customer_parser = CustomersParser() customers_data = customer_parser.parse_xml(xml) customer_data = customers_data[0] self.load_data(product=self.product, **customer_data) - - def update(self, first_name=None, last_name=None, email=None, \ - company=None, is_vat_exempt=None, vat_number=None, \ - notes=None, first_contact_datetime=None, \ - referer=None, campaign_term=None, \ - campaign_name=None, campaign_source=None, \ - campaign_medium=None, campaign_content=None, \ - meta_data=None, method=None, \ - cc_number=None, cc_expiration=None, \ - cc_card_code=None, cc_first_name=None, \ - cc_last_name=None, cc_company=None, cc_email=None,\ - cc_country=None, cc_address=None, cc_city=None, \ - cc_state=None, cc_zip=None, plan_code=None, bill_date=None, - return_url=None, cancel_url=None,): - - data = self.product.build_customer_post_data( first_name=first_name, - last_name=last_name, email=email, plan_code=plan_code, - company=company, is_vat_exempt=is_vat_exempt, - vat_number=vat_number, notes=notes, referer=referer, - campaign_term=campaign_term, - campaign_name=campaign_name, - campaign_source=campaign_source, - campaign_medium=campaign_medium, - campaign_content=campaign_content, - meta_data=meta_data, method=method, - cc_number=cc_number, cc_expiration=cc_expiration, - cc_card_code=cc_card_code, - cc_first_name=cc_first_name, - cc_last_name=cc_last_name, cc_company=cc_company, - cc_email=cc_email, cc_country=cc_country, - cc_address=cc_address, cc_city=cc_city, - cc_state=cc_state, cc_zip=cc_zip, bill_date=bill_date, - return_url=return_url, cancel_url=cancel_url,) - + + def update(self, first_name=None, last_name=None, email=None, + company=None, is_vat_exempt=None, vat_number=None, + notes=None, first_contact_datetime=None, + referer=None, campaign_term=None, campaign_name=None, + campaign_source=None, campaign_medium=None, + campaign_content=None, meta_data=None, method=None, + cc_number=None, cc_expiration=None, + cc_card_code=None, cc_first_name=None, + cc_last_name=None, cc_company=None, cc_email=None, + cc_country=None, cc_address=None, cc_city=None, + cc_state=None, cc_zip=None, plan_code=None, bill_date=None, + coupon_code=None, return_url=None, cancel_url=None,): + + data = self.product.build_customer_post_data( + first_name=first_name, last_name=last_name, email=email, + plan_code=plan_code, company=company, is_vat_exempt=is_vat_exempt, + vat_number=vat_number, notes=notes, referer=referer, + campaign_term=campaign_term, campaign_name=campaign_name, + campaign_source=campaign_source, campaign_medium=campaign_medium, + campaign_content=campaign_content, meta_data=meta_data, + method=method, cc_number=cc_number, cc_expiration=cc_expiration, + cc_card_code=cc_card_code, + cc_first_name=cc_first_name, cc_last_name=cc_last_name, + cc_company=cc_company, cc_email=cc_email, cc_country=cc_country, + cc_address=cc_address, cc_city=cc_city, cc_state=cc_state, + cc_zip=cc_zip, bill_date=bill_date, coupon_code=coupon_code, + return_url=return_url, cancel_url=cancel_url,) + path = 'customers/edit' params = {'code': self.code} - + response = self.product.client.make_request( - path = path, - params = params, - data = data, + path=path, + params=params, + data=data, ) return self.load_data_from_xml(response.content) - - + def delete(self): path = 'customers/delete' params = {'code': self.code} - response = self.product.client.make_request( - path = path, - params = params, + self.product.client.make_request( + path=path, + params=params, ) - + def charge(self, code, each_amount, quantity=1, description=None): ''' Add an arbitrary charge or credit to a customer's account. A positive number will create a charge. A negative number will create a credit. - + each_amount is normalized to a Decimal with a precision of 2 as that is the level of precision which the cheddar API supports. ''' @@ -496,7 +545,7 @@ def charge(self, code, each_amount, quantity=1, description=None): } if description: data['description'] = description - + response = self.product.client.make_request( path='customers/add-charge', params={'code': self.code}, @@ -507,14 +556,14 @@ def charge(self, code, each_amount, quantity=1, description=None): def create_one_time_invoice(self, charges): ''' Charges should be a list of charges to execute immediately. Each - value in the charges diectionary should be a dictionary with the + value in the charges diectionary should be a dictionary with the following keys: code - Your code for this charge. This code will be displayed in the + Your code for this charge. This code will be displayed in the user's invoice and is limited to 36 characters. quantity - A positive integer quantity. If not provided this value will + A positive integer quantity. If not provided this value will default to 1. each_amount Positive or negative integer or decimal with two digit precision. @@ -528,7 +577,7 @@ def create_one_time_invoice(self, charges): for n, charge in enumerate(charges): each_amount = Decimal(charge['each_amount']) each_amount = each_amount.quantize(Decimal('.01')) - data['charges[%d][chargeCode]' % n ] = charge['code'] + data['charges[%d][chargeCode]' % n] = charge['code'] data['charges[%d][quantity]' % n] = charge.get('quantity', 1) data['charges[%d][eachAmount]' % n] = '%.2f' % each_amount if 'description' in charge.keys(): @@ -540,49 +589,51 @@ def create_one_time_invoice(self, charges): data=data, ) return self.load_data_from_xml(response.content) - + def __repr__(self): return u'Customer: %s %s (%s)' % ( self.first_name, self.last_name, self.code ) - + + class Subscription(object): - + def __init__(self, id, gateway_token, cc_first_name, cc_last_name, cc_company, cc_country, cc_address, cc_city, cc_state, cc_zip, cc_type, cc_last_four, cc_expiration_date, customer, - canceled_datetime=None ,created_datetime=None, + canceled_datetime=None, created_datetime=None, plans=None, invoices=None, items=None, gateway_account=None, cancel_reason=None, cancel_type=None, cc_email=None, - redirect_url=None): - - self.load_data(id=id, gateway_token=gateway_token, - cc_first_name=cc_first_name, - cc_last_name=cc_last_name, - cc_company=cc_company, cc_country=cc_country, - cc_address=cc_address, cc_city=cc_city, - cc_state=cc_state, cc_zip=cc_zip, cc_type=cc_type, - cc_last_four=cc_last_four, - cc_expiration_date=cc_expiration_date, cc_email=cc_email, - customer=customer, - canceled_datetime=canceled_datetime, - created_datetime=created_datetime, plans=plans, - invoices=invoices, items=items, - gateway_account=gateway_account, - cancel_reason=cancel_reason, cancel_type=cancel_type, - redirect_url=redirect_url) - + coupon_code=None, redirect_url=None): + + self.load_data(id=id, gateway_token=gateway_token, + cc_first_name=cc_first_name, + cc_last_name=cc_last_name, + cc_company=cc_company, cc_country=cc_country, + cc_address=cc_address, cc_city=cc_city, + cc_state=cc_state, cc_zip=cc_zip, cc_type=cc_type, + cc_last_four=cc_last_four, + cc_expiration_date=cc_expiration_date, + cc_email=cc_email, customer=customer, + canceled_datetime=canceled_datetime, + created_datetime=created_datetime, plans=plans, + invoices=invoices, items=items, + gateway_account=gateway_account, + cancel_reason=cancel_reason, cancel_type=cancel_type, + coupon_code=coupon_code, redirect_url=redirect_url) + super(Subscription, self).__init__() - - def load_data(self, id, gateway_token, cc_first_name, cc_last_name, \ - cc_company, cc_country, cc_address, cc_city, cc_state, \ - cc_zip, cc_type, cc_last_four, cc_expiration_date, customer,\ - cc_email=None, canceled_datetime=None ,created_datetime=None, \ - plans=None, invoices=None, items=None, gateway_account=None, \ - cancel_reason=None, cancel_type=None, redirect_url=None): - + + def load_data(self, id, gateway_token, cc_first_name, cc_last_name, + cc_company, cc_country, cc_address, cc_city, cc_state, + cc_zip, cc_type, cc_last_four, cc_expiration_date, customer, + cc_email=None, canceled_datetime=None, created_datetime=None, + plans=None, invoices=None, items=None, gateway_account=None, + cancel_reason=None, cancel_type=None, coupon_code=None, + redirect_url=None): + self.id = id self.gateway_token = gateway_token self.cc_first_name = cc_first_name @@ -604,6 +655,7 @@ def load_data(self, id, gateway_token, cc_first_name, cc_last_name, \ self.gateway_account = gateway_account self.cancel_type = cancel_type self.cancel_reason = cancel_reason + self.coupon_code = coupon_code self.redirect_url = redirect_url # Organize item data into something more useful @@ -613,7 +665,7 @@ def load_data(self, id, gateway_token, cc_first_name, cc_last_name, \ plan_data = plans[0] for item in plan_data['items']: items_map[item['code']]['plan_data'] = item - + if not hasattr(self, 'items'): self.items = {} for code, item_map in items_map.iteritems(): @@ -622,7 +674,7 @@ def load_data(self, id, gateway_token, cc_first_name, cc_last_name, \ item_data = copy(plan_item_data) item_data.update(subscription_item_data) item_data['subscription'] = self - + if code in self.items.keys(): item = self.items[code] item.load_data(**item_data) @@ -634,17 +686,17 @@ def load_data(self, id, gateway_token, cc_first_name, cc_last_name, \ self.plan.load_data(**plan_data) else: self.plan = PricingPlan(**plan_data) - + def __repr__(self): return u'Subscription: %s' % self.id - + def cancel(self): client = self.customer.product.client response = client.make_request( path='customers/cancel', params={'code': self.customer.code}, ) - + customer_parser = CustomersParser() customers_data = customer_parser.parse_xml(response.content) customer_data = customers_data[0] @@ -652,27 +704,28 @@ def cancel(self): product=self.customer.product, **customer_data ) - + + class Item(object): - + def __init__(self, code, subscription, id=None, name=None, quantity_included=None, is_periodic=None, overage_amount=None, created_datetime=None, modified_datetime=None, quantity=None): - + self.load_data(code=code, subscription=subscription, id=id, name=name, - quantity_included=quantity_included, - is_periodic=is_periodic, overage_amount=overage_amount, - created_datetime=created_datetime, - modified_datetime=modified_datetime, quantity=quantity) - + quantity_included=quantity_included, + is_periodic=is_periodic, overage_amount=overage_amount, + created_datetime=created_datetime, + modified_datetime=modified_datetime, quantity=quantity) + super(Item, self).__init__() - + def load_data(self, code, subscription, id=None, name=None, - quantity_included=None, is_periodic=None, - overage_amount=None, created_datetime=None, - modified_datetime=None, quantity=None): - + quantity_included=None, is_periodic=None, + overage_amount=None, created_datetime=None, + modified_datetime=None, quantity=None): + self.code = code self.subscription = subscription self.id = id @@ -683,86 +736,117 @@ def load_data(self, code, subscription, id=None, name=None, self.overage_amount = overage_amount self.created = created_datetime self.modified = modified_datetime - + def __repr__(self): return u'Item: %s for %s' % ( self.code, self.subscription.customer.code, ) - + def _normalize_quantity(self, quantity=None): if quantity is not None: quantity = Decimal(quantity) quantity = quantity.quantize(Decimal('.0001')) - + return quantity - + def increment(self, quantity=None): ''' Increment the item's quantity by the passed in amount. If nothing is passed in, a quantity of 1 is assumed. If a decimal value is passsed - in, it is rounded to the 4th decimal place as that is the level of + in, it is rounded to the 4th decimal place as that is the level of precision which the Cheddar API accepts. ''' data = {} if quantity: data['quantity'] = self._normalize_quantity(quantity) - + response = self.subscription.customer.product.client.make_request( - path = 'customers/add-item-quantity', - params = { + path='customers/add-item-quantity', + params={ 'code': self.subscription.customer.code, 'itemCode': self.code, }, - data = data, - method = 'POST', + data=data, + method='POST', ) - + return self.subscription.customer.load_data_from_xml(response.content) - + def decrement(self, quantity=None): ''' Decrement the item's quantity by the passed in amount. If nothing is passed in, a quantity of 1 is assumed. If a decimal value is passsed - in, it is rounded to the 4th decimal place as that is the level of + in, it is rounded to the 4th decimal place as that is the level of precision which the Cheddar API accepts. ''' data = {} if quantity: data['quantity'] = self._normalize_quantity(quantity) - + response = self.subscription.customer.product.client.make_request( - path = 'customers/remove-item-quantity', - params = { + path='customers/remove-item-quantity', + params={ 'code': self.subscription.customer.code, 'itemCode': self.code, }, - data = data, - method = 'POST', + data=data, + method='POST', ) - + return self.subscription.customer.load_data_from_xml(response.content) - + def set(self, quantity): ''' Set the item's quantity to the passed in amount. If nothing is passed in, a quantity of 1 is assumed. If a decimal value is passsed - in, it is rounded to the 4th decimal place as that is the level of + in, it is rounded to the 4th decimal place as that is the level of precision which the Cheddar API accepts. ''' data = {} data['quantity'] = self._normalize_quantity(quantity) - + response = self.subscription.customer.product.client.make_request( - path = 'customers/set-item-quantity', - params = { + path='customers/set-item-quantity', + params={ 'code': self.subscription.customer.code, 'itemCode': self.code, }, - data = data, - method = 'POST', + data=data, + method='POST', ) - + return self.subscription.customer.load_data_from_xml(response.content) - - + + +class Promotion(object): + def __init__(self, id=None, code=None, name=None, description=None, + created_datetime=None, incentives=None, coupons=None): + + self.load_data(code=code, id=id, name=name, description=description, + created_datetime=created_datetime, + incentives=incentives, coupons=coupons) + + super(Promotion, self).__init__() + + def __repr__(self): + return u'Promotion: %s (%s)' % (self.name, self.code,) + + def __unicode__(self): + return u'{0} ({1})'.format(self.name, self.code) + + def load_data(self, id=None, code=None, name=None, description=None, + created_datetime=None, incentives=None, coupons=None): + + self.code = code + self.id = id + self.name = name + self.description = description + self.created = created_datetime + + self.incentives = incentives + self.coupons = coupons + + # Bring coupon code up to parent promotion + if self.code is None and self.coupons and len(self.coupons) > 0: + self.code = self.coupons[0].get('code') diff --git a/tests/client_tests.py b/tests/client_tests.py index 571cc64..370da62 100644 --- a/tests/client_tests.py +++ b/tests/client_tests.py @@ -1,6 +1,5 @@ from copy import copy from datetime import date, timedelta, datetime -from decimal import Decimal import unittest from dateutil.tz import tzoffset @@ -9,47 +8,59 @@ from testconfig import config from sharpy.client import Client -from sharpy.exceptions import AccessDenied, BadRequest, NotFound, CheddarFailure, NaughtyGateway, UnprocessableEntity, PreconditionFailed +from sharpy.exceptions import AccessDenied +from sharpy.exceptions import BadRequest +from sharpy.exceptions import CheddarFailure +from sharpy.exceptions import NaughtyGateway +from sharpy.exceptions import NotFound +from sharpy.exceptions import PreconditionFailed +from sharpy.exceptions import UnprocessableEntity from testing_tools.decorators import clear_users + class ClientTests(unittest.TestCase): - client_defaults = { + client_defaults = { 'username': config['cheddar']['username'], 'password': config['cheddar']['password'], 'product_code': config['cheddar']['product_code'], 'endpoint': config['cheddar']['endpoint'], } - + def get_client(self, **kwargs): + ''' Helper mthod for instantiating client. ''' client_kwargs = copy(self.client_defaults) client_kwargs.update(kwargs) - + c = Client(**client_kwargs) - + return c - + def try_client(self, **kwargs): + ''' Helper method for getting client.''' args = copy(self.client_defaults) args.update(kwargs) client = self.get_client(**kwargs) - + self.assertEquals(args['username'], client.username) - self.assertEquals(self.client_defaults['password'] ,client.password) - self.assertEquals(self.client_defaults['product_code'], client.product_code) + self.assertEquals(self.client_defaults['password'], client.password) + self.assertEquals(self.client_defaults['product_code'], + client.product_code) if 'endpoint' in args.keys(): self.assertEquals(args['endpoint'], client.endpoint) else: self.assertEquals(client.default_endpoint, client.endpoint) - + def test_basic_init(self): + ''' Test basic initialization. ''' self.try_client() - + def test_custom_endpoint_init(self): - self.try_client(endpoint = 'http://cheddar-test.saaspire.com') - - + ''' Test initialization with custom endpoint. ''' + self.try_client(endpoint='http://cheddar-test.saaspire.com') + def try_url_build(self, path, params=None): + ''' Helper method for client build_url method. ''' c = self.get_client() expected = u'%s/%s/productCode/%s' % ( c.endpoint, @@ -63,50 +74,57 @@ def try_url_build(self, path, params=None): result = c.build_url(path=path, params=params) self.assertEquals(expected, result) - + def test_basic_build_url(self): + ''' Test basic client build_url. ''' path = 'users' self.try_url_build(path) - - + def test_single_param_build_url(self): + ''' Test client build_url with single parameter. ''' path = 'users' params = {'key': 'value'} self.try_url_build(path, params) - + def test_multi_param_build_url(self): + ''' Test client build_url with multiple parameters. ''' path = 'users' params = {'key1': 'value1', 'key2': 'value2'} self.try_url_build(path, params) - + def test_make_request(self): + ''' Test client make_request method. ''' path = 'plans/get' client = self.get_client() response = client.make_request(path) - + self.assertEquals(response.status, 200) - + @raises(AccessDenied) def test_make_request_access_denied(self): + ''' Test client make_request method with bad username. ''' path = 'plans/get' bad_username = self.client_defaults['username'] + '_bad' client = self.get_client(username=bad_username) client.make_request(path) - - @raises(BadRequest) + + @raises(NotFound) def test_make_request_bad_request(self): + ''' Attempt to grab the plans without adding /get to the url. ''' path = 'plans' client = self.get_client() client.make_request(path) - + @raises(NotFound) def test_make_request_not_found(self): + ''' Test client make_request method with bad path. ''' path = 'things-which-dont-exist' client = self.get_client() client.make_request(path) - + @clear_users def test_post_request(self): + ''' Test client make_request method as HTTP POST. ''' path = 'customers/new' data = { 'code': 'post_test', @@ -117,9 +135,9 @@ def test_post_request(self): } client = self.get_client() client.make_request(path, data=data) - - def generate_error_response(self, auxcode=None, path=None, params=None, **overrides): + def generate_error_response(self, auxcode=None, path=None, params=None, + **overrides): ''' Creates a request to cheddar which should return an error with the provided aux code. See the urls below for details @@ -135,7 +153,7 @@ def generate_error_response(self, auxcode=None, path=None, params=None, **overri zip_code = '0%d' % auxcode else: zip_code = '12345' - + data = { 'code': 'post_test', 'firstName': 'post', @@ -150,11 +168,13 @@ def generate_error_response(self, auxcode=None, path=None, params=None, **overri 'subscription[ccZip]': zip_code, } data.update(overrides) - + client = self.get_client() client.make_request(path, params=params, data=data) - - def assertCheddarError(self, auxcode, expected_exception, path=None, params=None): + + def assertCheddarError(self, auxcode, expected_exception, path=None, + params=None): + ''' Helper method for verifing a Cheddar Error is raised. ''' assert_raises( expected_exception, self.generate_error_response, @@ -162,73 +182,86 @@ def assertCheddarError(self, auxcode, expected_exception, path=None, params=None path=path, params=params, ) - + def assertCheddarErrorForAuxCodes(self, auxcodes, expected_exception): + ''' Helper method for verifing a Cheddar Aux Code Error is raised. ''' for auxcode in auxcodes: self.assertCheddarError(auxcode, expected_exception) - + @clear_users def test_cheddar_500s(self): + ''' Test a 500 HTTP status code on CheddarGetter. ''' auxcodes = (1000, 1002, 1003) expected_exception = CheddarFailure self.assertCheddarErrorForAuxCodes(auxcodes, expected_exception) - + @clear_users def test_cheddar_400(self): ''' The cheddar docs at http://support.cheddargetter.com/kb/api-8/error-handling - say that this aux code should return a 502 but in practice + say that this aux code should return a 502 but in practice the API returns a 400. Not sure if this is a bug or typo in the docs but for now we're assuming the API is correct. ''' self.assertCheddarError(auxcode=1001, expected_exception=BadRequest) - + @clear_users def test_cheddar_401s(self): + ''' Test a 401 HTTP status code on CheddarGetter. ''' auxcodes = (2000, 2001, 2002, 2003) expected_exception = AccessDenied self.assertCheddarErrorForAuxCodes(auxcodes, expected_exception) - + @clear_users def test_cheddar_502s(self): + ''' Test a 502 HTTP status code on CheddarGetter. ''' auxcodes = (3000, 4000) expected_exception = NaughtyGateway self.assertCheddarErrorForAuxCodes(auxcodes, expected_exception) - + @clear_users def test_cheddar_422s(self): + ''' Test a 422 HTTP status code on CheddarGetter. ''' auxcodes = (5000, 5001, 5002, 5003, 6000, 6001, 6002, 7000) expected_exception = UnprocessableEntity self.assertCheddarErrorForAuxCodes(auxcodes, expected_exception) - + @clear_users @raises(PreconditionFailed) def test_cheddar_412s(self): + ''' Test a 412 HTTP status code on CheddarGetter. ''' self.generate_error_response(auxcode=2345, firstName='') - + def test_format_datetime_with_datetime(self): + ''' Test client format_datetime method. ''' client = self.get_client() - result = client.format_datetime(datetime(year=2010,month=9,day=19,hour=20,minute=10,second=39)) + result = client.format_datetime(datetime( + year=2010, month=9, day=19, hour=20, minute=10, second=39)) expected = '2010-09-19T20:10:39+00:00' self.assertEquals(expected, result) def test_format_datetime_with_datetime_with_tz(self): + ''' Test client format_datetime method with timezone info. ''' client = self.get_client() - result = client.format_datetime(datetime(year=2010,month=9,day=19,hour=20,minute=10,second=39, tzinfo=tzoffset("BRST", -10800))) + result = client.format_datetime(datetime( + year=2010, month=9, day=19, hour=20, minute=10, second=39, + tzinfo=tzoffset("BRST", -10800))) expected = '2010-09-19T23:10:39+00:00' self.assertEquals(expected, result) def test_format_datetime_with_date(self): + ''' Test client format_datetime method with date. ''' client = self.get_client() - result = client.format_datetime(date(year=2010,month=9,day=19)) + result = client.format_datetime(date(year=2010, month=9, day=19)) expected = '2010-09-19T00:00:00+00:00' self.assertEquals(expected, result) - + def test_format_date_with_now(self): + ''' Test client format_date method with now. ''' client = self.get_client() result = client.format_date('now') expected = 'now' @@ -236,6 +269,7 @@ def test_format_date_with_now(self): self.assertEquals(expected, result) def test_format_datetime_with_now(self): + ''' Test client format_datetime method with now. ''' client = self.get_client() result = client.format_datetime('now') expected = 'now' @@ -244,7 +278,9 @@ def test_format_datetime_with_now(self): @clear_users def test_chedder_update_customer_error(self): - # Overriding the zipcode so a customer actually gets created + ''' + Test overriding the zipcode so a customer actually gets updated. + ''' overrides = { 'subscription[ccZip]': 12345 } @@ -255,4 +291,4 @@ def test_chedder_update_customer_error(self): expected_exception=UnprocessableEntity, path='customers/edit', params={'code': 'post_test'} - ) \ No newline at end of file + ) diff --git a/tests/parser_tests.py b/tests/parser_tests.py index 70835f7..ae00e68 100644 --- a/tests/parser_tests.py +++ b/tests/parser_tests.py @@ -7,95 +7,110 @@ from nose.tools import raises from sharpy.exceptions import ParseError -from sharpy.parsers import CheddarOutputParser, parse_error, PlansParser, CustomersParser +from sharpy.parsers import CheddarOutputParser +from sharpy.parsers import parse_error +from sharpy.parsers import PlansParser +from sharpy.parsers import CustomersParser + class ParserTests(unittest.TestCase): - + def load_file(self, filename): + ''' Helper method to load an xml file from the files directory. ''' path = os.path.join(os.path.dirname(__file__), 'files', filename) f = open(path) content = f.read() f.close() return content - - + def test_bool_parsing_true(self): + ''' Test boolean parsing evaluates to true. ''' parser = CheddarOutputParser() - + expected = True result = parser.parse_bool('1') - + self.assertEquals(expected, result) - + def test_bool_parsing_false(self): + ''' Test boolean parsing evaluates to false. ''' parser = CheddarOutputParser() - + expected = False result = parser.parse_bool('0') - + self.assertEquals(expected, result) - + @raises(ParseError) def test_bool_parsing_error(self): + ''' Test boolean parsing with non-boolean string. ''' parser = CheddarOutputParser() - + parser.parse_bool('test') - + def test_bool_parsing_empty(self): + ''' Test boolean parsing with empty string. ''' parser = CheddarOutputParser() expected = None result = parser.parse_bool('') self.assertEquals(expected, result) - + def test_int_parsing(self): + ''' Test integer parsing with integer as string. ''' parser = CheddarOutputParser() - + expected = 234 result = parser.parse_int('234') - + self.assertEquals(expected, result) - + @raises(ParseError) def test_int_parsing_error(self): + ''' Test integer parsing with non-integer string. ''' parser = CheddarOutputParser() - + parser.parse_int('test') - + def test_int_parsing_empty(self): + ''' Test integer parsing with empty string. ''' parser = CheddarOutputParser() expected = None result = parser.parse_int('') self.assertEquals(expected, result) - + def test_decimal_parsing(self): + ''' Test decimal parsing with decimal string. ''' parser = CheddarOutputParser() - + expected = Decimal('2.345') result = parser.parse_decimal('2.345') - + self.assertEquals(expected, result) - + @raises(ParseError) def test_decimal_parsing_error(self): + ''' Test decimal parsing with non-decimal string. ''' parser = CheddarOutputParser() - + parser.parse_decimal('test') - + def test_decimal_parsing_empty(self): + ''' Test decimal parsing with empty string. ''' parser = CheddarOutputParser() expected = None result = parser.parse_decimal('') self.assertEquals(expected, result) - + def test_datetime_parsing(self): + ''' Test datetime parsing. ''' parser = CheddarOutputParser() - + expected = datetime( year=2011, month=1, @@ -106,27 +121,29 @@ def test_datetime_parsing(self): tzinfo=tzutc(), ) result = parser.parse_datetime('2011-01-07T20:46:43+00:00') - + self.assertEquals(expected, result) - + @raises(ParseError) def test_datetime_parsing_error(self): + ''' Test datetime parsing with non-date string. ''' parser = CheddarOutputParser() - + parser.parse_datetime('test') - + def test_datetime_parsing_empty(self): + ''' Test datetime parsing with empty string. ''' parser = CheddarOutputParser() - + expected = None result = parser.parse_datetime('') - + self.assertEquals(expected, result) - - + def test_error_parser(self): + ''' Test error parser. ''' error_xml = self.load_file('error.xml') - + expected = { 'aux_code': '', 'code': '400', @@ -134,19 +151,23 @@ def test_error_parser(self): 'message': 'No product selected. Need a productId or productCode.', } result = parse_error(error_xml) - + self.assertEquals(expected, result) - + def test_plans_parser(self): + ''' Test plans parser. ''' plans_xml = self.load_file('plans.xml') parser = PlansParser() - - expected = [ { 'billing_frequency': 'monthly', + + expected = [ + { + 'billing_frequency': 'monthly', 'billing_frequency_per': 'month', 'billing_frequency_quantity': 1, 'billing_frequency_unit': 'months', 'code': 'FREE_MONTHLY', - 'created_datetime': datetime(2011, 1, 7, 20, 46, 43, tzinfo=tzutc()), + 'created_datetime': datetime(2011, 1, 7, 20, 46, 43, + tzinfo=tzutc()), 'description': 'A free monthly plan', 'id': '6b0d13f4-6bef-102e-b098-40402145ee8b', 'initial_bill_count': 1, @@ -160,12 +181,14 @@ def test_plans_parser(self): 'setup_charge_amount': Decimal('0.00'), 'setup_charge_code': '', 'trial_days': 0}, - { 'billing_frequency': 'monthly', + { + 'billing_frequency': 'monthly', 'billing_frequency_per': 'month', 'billing_frequency_quantity': 1, 'billing_frequency_unit': 'months', 'code': 'PAID_MONTHLY', - 'created_datetime': datetime(2011, 1, 7, 21, 5, 42, tzinfo=tzutc()), + 'created_datetime': datetime(2011, 1, 7, 21, 5, 42, + tzinfo=tzutc()), 'description': '', 'id': '11af9cfc-6bf2-102e-b098-40402145ee8b', 'initial_bill_count': 1, @@ -184,445 +207,565 @@ def test_plans_parser(self): pp = pprint.PrettyPrinter(indent=4) pp.pprint(result) self.assertEquals(expected, result) - + def test_plans_parser_with_items(self): + ''' Test plans parser with items. ''' plans_xml = self.load_file('plans_with_items.xml') parser = PlansParser() - expected = [ { 'billing_frequency': 'monthly', - 'billing_frequency_per': 'month', - 'billing_frequency_quantity': 1, - 'billing_frequency_unit': 'months', - 'code': 'FREE_MONTHLY', - 'created_datetime': datetime(2011, 1, 7, 20, 46, 43, tzinfo=tzutc()), - 'description': 'A free monthly plan', - 'id': '6b0d13f4-6bef-102e-b098-40402145ee8b', - 'initial_bill_count': 1, - 'initial_bill_count_unit': 'months', - 'is_active': True, - 'is_free': True, - 'items': [ { 'code': 'MONTHLY_ITEM', - 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, tzinfo=tzutc()), - 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', - 'is_periodic': False, - 'name': 'Monthly Item', - 'overage_amount': Decimal('0.00'), - 'quantity_included': Decimal('0')}, - { 'code': 'ONCE_ITEM', - 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, tzinfo=tzutc()), - 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', - 'is_periodic': False, - 'name': 'Once Item', - 'overage_amount': Decimal('0.00'), - 'quantity_included': Decimal('0')}], - 'name': 'Free Monthly', - 'recurring_charge_amount': Decimal('0.00'), - 'recurring_charge_code': 'FREE_MONTHLY_RECURRING', - 'setup_charge_amount': Decimal('0.00'), - 'setup_charge_code': '', - 'trial_days': 0}, - { 'billing_frequency': 'monthly', - 'billing_frequency_per': 'month', - 'billing_frequency_quantity': 1, - 'billing_frequency_unit': 'months', - 'code': 'TRACKED_MONTHLY', - 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, tzinfo=tzutc()), - 'description': '', - 'id': 'd19974a6-6e5a-102e-b098-40402145ee8b', - 'initial_bill_count': 1, - 'initial_bill_count_unit': 'months', - 'is_active': True, - 'is_free': False, - 'items': [ { 'code': 'MONTHLY_ITEM', - 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, tzinfo=tzutc()), - 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', - 'is_periodic': True, - 'name': 'Monthly Item', - 'overage_amount': Decimal('10.00'), - 'quantity_included': Decimal('2')}, - { 'code': 'ONCE_ITEM', - 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, tzinfo=tzutc()), - 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', - 'is_periodic': False, - 'name': 'Once Item', - 'overage_amount': Decimal('10.00'), - 'quantity_included': Decimal('0')}], - 'name': 'Tracked Monthly', - 'recurring_charge_amount': Decimal('10.00'), - 'recurring_charge_code': 'TRACKED_MONTHLY_RECURRING', - 'setup_charge_amount': Decimal('0.00'), - 'setup_charge_code': '', - 'trial_days': 0}, - { 'billing_frequency': 'monthly', - 'billing_frequency_per': 'month', - 'billing_frequency_quantity': 1, - 'billing_frequency_unit': 'months', - 'code': 'PAID_MONTHLY', - 'created_datetime': datetime(2011, 1, 7, 21, 5, 42, tzinfo=tzutc()), - 'description': '', - 'id': '11af9cfc-6bf2-102e-b098-40402145ee8b', - 'initial_bill_count': 1, - 'initial_bill_count_unit': 'months', - 'is_active': True, - 'is_free': False, - 'items': [ { 'code': 'MONTHLY_ITEM', - 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, tzinfo=tzutc()), - 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', - 'is_periodic': False, - 'name': 'Monthly Item', - 'overage_amount': Decimal('0.00'), - 'quantity_included': Decimal('0')}, - { 'code': 'ONCE_ITEM', - 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, tzinfo=tzutc()), - 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', - 'is_periodic': False, - 'name': 'Once Item', - 'overage_amount': Decimal('0.00'), - 'quantity_included': Decimal('0')}], - 'name': 'Paid Monthly', - 'recurring_charge_amount': Decimal('20.00'), - 'recurring_charge_code': 'PAID_MONTHLY_RECURRING', - 'setup_charge_amount': Decimal('0.00'), - 'setup_charge_code': '', - 'trial_days': 0}] + expected = [ + { + 'billing_frequency': 'monthly', + 'billing_frequency_per': 'month', + 'billing_frequency_quantity': 1, + 'billing_frequency_unit': 'months', + 'code': 'FREE_MONTHLY', + 'created_datetime': datetime(2011, 1, 7, 20, 46, 43, + tzinfo=tzutc()), + 'description': 'A free monthly plan', + 'id': '6b0d13f4-6bef-102e-b098-40402145ee8b', + 'initial_bill_count': 1, + 'initial_bill_count_unit': 'months', + 'is_active': True, + 'is_free': True, + 'items': [ + { + 'code': 'MONTHLY_ITEM', + 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, + tzinfo=tzutc()), + 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', + 'is_periodic': False, + 'name': 'Monthly Item', + 'overage_amount': Decimal('0.00'), + 'quantity_included': Decimal('0')}, + { + 'code': 'ONCE_ITEM', + 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, + tzinfo=tzutc()), + 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', + 'is_periodic': False, + 'name': 'Once Item', + 'overage_amount': Decimal('0.00'), + 'quantity_included': Decimal('0')}], + 'name': 'Free Monthly', + 'recurring_charge_amount': Decimal('0.00'), + 'recurring_charge_code': 'FREE_MONTHLY_RECURRING', + 'setup_charge_amount': Decimal('0.00'), + 'setup_charge_code': '', + 'trial_days': 0}, + { + 'billing_frequency': 'monthly', + 'billing_frequency_per': 'month', + 'billing_frequency_quantity': 1, + 'billing_frequency_unit': 'months', + 'code': 'TRACKED_MONTHLY', + 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, + tzinfo=tzutc()), + 'description': '', + 'id': 'd19974a6-6e5a-102e-b098-40402145ee8b', + 'initial_bill_count': 1, + 'initial_bill_count_unit': 'months', + 'is_active': True, + 'is_free': False, + 'items': [ + { + 'code': 'MONTHLY_ITEM', + 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, + tzinfo=tzutc()), + 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', + 'is_periodic': True, + 'name': 'Monthly Item', + 'overage_amount': Decimal('10.00'), + 'quantity_included': Decimal('2')}, + { + 'code': 'ONCE_ITEM', + 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, + tzinfo=tzutc()), + 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', + 'is_periodic': False, + 'name': 'Once Item', + 'overage_amount': Decimal('10.00'), + 'quantity_included': Decimal('0')}], + 'name': 'Tracked Monthly', + 'recurring_charge_amount': Decimal('10.00'), + 'recurring_charge_code': 'TRACKED_MONTHLY_RECURRING', + 'setup_charge_amount': Decimal('0.00'), + 'setup_charge_code': '', + 'trial_days': 0}, + { + 'billing_frequency': 'monthly', + 'billing_frequency_per': 'month', + 'billing_frequency_quantity': 1, + 'billing_frequency_unit': 'months', + 'code': 'PAID_MONTHLY', + 'created_datetime': datetime(2011, 1, 7, 21, 5, 42, + tzinfo=tzutc()), + 'description': '', + 'id': '11af9cfc-6bf2-102e-b098-40402145ee8b', + 'initial_bill_count': 1, + 'initial_bill_count_unit': 'months', + 'is_active': True, + 'is_free': False, + 'items': [ + { + 'code': 'MONTHLY_ITEM', + 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, + tzinfo=tzutc()), + 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', + 'is_periodic': False, + 'name': 'Monthly Item', + 'overage_amount': Decimal('0.00'), + 'quantity_included': Decimal('0')}, + { + 'code': 'ONCE_ITEM', + 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, + tzinfo=tzutc()), + 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', + 'is_periodic': False, + 'name': 'Once Item', + 'overage_amount': Decimal('0.00'), + 'quantity_included': Decimal('0')}], + 'name': 'Paid Monthly', + 'recurring_charge_amount': Decimal('20.00'), + 'recurring_charge_code': 'PAID_MONTHLY_RECURRING', + 'setup_charge_amount': Decimal('0.00'), + 'setup_charge_code': '', + 'trial_days': 0}] result = parser.parse_xml(plans_xml) import pprint pp = pprint.PrettyPrinter(indent=4) pp.pprint(result) self.assertEquals(expected, result) - + def test_customers_parser_with_no_items(self): + ''' Test customers parser with no items. ''' customers_xml = self.load_file('customers-without-items.xml') parser = CustomersParser() - - expected = [ { 'campaign_content': '', - 'campaign_medium': '', - 'campaign_name': '', - 'campaign_source': '', - 'campaign_term': '', - 'code': 'test', - 'company': '', - 'created_datetime': datetime(2011, 1, 10, 5, 45, 51, tzinfo=tzutc()), - 'email': 'garbage@saaspire.com', - 'first_contact_datetime': None, - 'first_name': 'Test', - 'gateway_token': None, - 'id': '10681b62-6dcd-102e-b098-40402145ee8b', - 'is_vat_exempt': '0', - 'last_name': 'User', - 'meta_data': [ { 'created_datetime': datetime(2011, 1, 10, 5, 45, 51, tzinfo=tzutc()), - 'id': '106953e2-6dcd-102e-b098-40402145ee8b', - 'modified_datetime': datetime(2011, 1, 10, 5, 45, 51, tzinfo=tzutc()), - 'name': 'key2', - 'value': 'value_2'}, - { 'created_datetime': datetime(2011, 1, 10, 5, 45, 51, tzinfo=tzutc()), - 'id': '1068b7a2-6dcd-102e-b098-40402145ee8b', - 'modified_datetime': datetime(2011, 1, 10, 5, 45, 51, tzinfo=tzutc()), - 'name': 'key_1', - 'value': 'value_1'}], - 'modified_datetime': datetime(2011, 1, 10, 5, 45, 51, tzinfo=tzutc()), - 'notes': '', - 'referer': '', - 'referer_host': '', - 'subscriptions': [ { 'cancel_reason': None, - 'cancel_type': None, - 'canceled_datetime': None, - 'cc_address': '', - 'cc_city': '', - 'cc_company': '', - 'cc_country': '', - 'cc_email': None, - 'cc_expiration_date': '', - 'cc_first_name': '', - 'cc_last_four': '', - 'cc_last_name': '', - 'cc_state': '', - 'cc_type': '', - 'cc_zip': '', - 'created_datetime': datetime(2011, 1, 10, 5, 45, 51, tzinfo=tzutc()), - 'gateway_token': '', - 'id': '106953e3-6dcd-102e-b098-40402145ee8b', - 'invoices': [ { 'billing_datetime': datetime(2011, 2, 10, 5, 45, 51, tzinfo=tzutc()), - 'charges': [ { 'code': 'FREE_MONTHLY_RECURRING', - 'created_datetime': datetime(2011, 2, 10, 5, 45, 51, tzinfo=tzutc()), - 'description': '', - 'each_amount': Decimal('0.00'), - 'id': '', - 'quantity': Decimal('1'), - 'type': 'recurring'}], - 'created_datetime': datetime(2011, 1, 10, 5, 45, 51, tzinfo=tzutc()), - 'id': '106ed222-6dcd-102e-b098-40402145ee8b', - 'number': '1', - 'paid_transaction_id': '', - 'type': 'subscription', - 'vat_rate': ''}], - 'items': [], - 'plans': [ { 'billing_frequency': 'monthly', - 'billing_frequency_per': 'month', - 'billing_frequency_quantity': 1, - 'billing_frequency_unit': 'months', - 'code': 'FREE_MONTHLY', - 'created_datetime': datetime(2011, 1, 7, 20, 46, 43, tzinfo=tzutc()), - 'description': 'A free monthly plan', - 'id': '6b0d13f4-6bef-102e-b098-40402145ee8b', - 'initial_bill_count': 1, - 'initial_bill_count_unit': 'months', - 'is_active': True, - 'is_free': True, - 'items': [], - 'name': 'Free Monthly', - 'recurring_charge_amount': Decimal('0.00'), - 'recurring_charge_code': 'FREE_MONTHLY_RECURRING', - 'setup_charge_amount': Decimal('0.00'), - 'setup_charge_code': '', - 'trial_days': 0}], - 'redirect_url': None}], - 'vat_number': ''}] + + expected = [ + { + 'campaign_content': '', + 'campaign_medium': '', + 'campaign_name': '', + 'campaign_source': '', + 'campaign_term': '', + 'code': 'test', + 'company': '', + 'created_datetime': datetime(2011, 1, 10, 5, 45, 51, + tzinfo=tzutc()), + 'email': 'garbage@saaspire.com', + 'first_contact_datetime': None, + 'first_name': 'Test', + 'gateway_token': None, + 'id': '10681b62-6dcd-102e-b098-40402145ee8b', + 'is_vat_exempt': '0', + 'last_name': 'User', + 'meta_data': [ + { + 'created_datetime': datetime(2011, 1, 10, 5, 45, 51, + tzinfo=tzutc()), + 'id': '106953e2-6dcd-102e-b098-40402145ee8b', + 'modified_datetime': datetime(2011, 1, 10, 5, 45, 51, + tzinfo=tzutc()), + 'name': 'key2', + 'value': 'value_2'}, + { + 'created_datetime': datetime(2011, 1, 10, 5, 45, 51, + tzinfo=tzutc()), + 'id': '1068b7a2-6dcd-102e-b098-40402145ee8b', + 'modified_datetime': datetime(2011, 1, 10, 5, 45, 51, + tzinfo=tzutc()), + 'name': 'key_1', + 'value': 'value_1'}], + 'modified_datetime': datetime(2011, 1, 10, 5, 45, 51, + tzinfo=tzutc()), + 'notes': '', + 'referer': '', + 'referer_host': '', + 'subscriptions': [ + { + 'cancel_reason': None, + 'cancel_type': None, + 'canceled_datetime': None, + 'cc_address': '', + 'coupon_code': None, + 'cc_city': '', + 'cc_company': '', + 'cc_country': '', + 'cc_email': None, + 'cc_expiration_date': '', + 'cc_first_name': '', + 'cc_last_four': '', + 'cc_last_name': '', + 'cc_state': '', + 'cc_type': '', + 'cc_zip': '', + 'created_datetime': datetime(2011, 1, 10, 5, 45, 51, + tzinfo=tzutc()), + 'gateway_token': '', + 'id': '106953e3-6dcd-102e-b098-40402145ee8b', + 'invoices': [ + { + 'billing_datetime': datetime( + 2011, 2, 10, 5, 45, 51, tzinfo=tzutc()), + 'charges': [ + { + 'code': 'FREE_MONTHLY_RECURRING', + 'created_datetime': datetime( + 2011, 2, 10, 5, 45, 51, + tzinfo=tzutc()), + 'description': '', + 'each_amount': Decimal('0.00'), + 'id': '', + 'quantity': Decimal('1'), + 'type': 'recurring'}], + 'created_datetime': datetime( + 2011, 1, 10, 5, 45, 51, tzinfo=tzutc()), + 'id': '106ed222-6dcd-102e-b098-40402145ee8b', + 'number': '1', + 'paid_transaction_id': '', + 'type': 'subscription', + 'vat_rate': ''}], + 'items': [], + 'plans': [ + { + 'billing_frequency': 'monthly', + 'billing_frequency_per': 'month', + 'billing_frequency_quantity': 1, + 'billing_frequency_unit': 'months', + 'code': 'FREE_MONTHLY', + 'created_datetime': datetime( + 2011, 1, 7, 20, 46, 43, + tzinfo=tzutc()), + 'description': 'A free monthly plan', + 'id': '6b0d13f4-6bef-102e-b098-40402145ee8b', + 'initial_bill_count': 1, + 'initial_bill_count_unit': 'months', + 'is_active': True, + 'is_free': True, + 'items': [], + 'name': 'Free Monthly', + 'recurring_charge_amount': Decimal('0.00'), + 'recurring_charge_code': 'FREE_MONTHLY_RECURRING', + 'setup_charge_amount': Decimal('0.00'), + 'setup_charge_code': '', + 'trial_days': 0}], + 'redirect_url': None}], + 'vat_number': ''}] result = parser.parse_xml(customers_xml) - + import pprint pp = pprint.PrettyPrinter(indent=4) pp.pprint(result) - + self.assertEquals(expected, result) - + def test_customers_parser_with_items(self): + ''' Test customers parser with items. ''' customers_xml = self.load_file('customers-with-items.xml') parser = CustomersParser() - - expected = [ { 'campaign_content': '', - 'campaign_medium': '', - 'campaign_name': '', - 'campaign_source': '', - 'campaign_term': '', - 'code': 'test', - 'company': '', - 'created_datetime': datetime(2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), - 'email': 'garbage@saaspire.com', - 'first_contact_datetime': None, - 'first_name': 'Test', - 'gateway_token': None, - 'id': 'a1f143e0-6e65-102e-b098-40402145ee8b', - 'is_vat_exempt': '0', - 'last_name': 'User', - 'meta_data': [], - 'modified_datetime': datetime(2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), - 'notes': '', - 'referer': '', - 'referer_host': '', - 'subscriptions': [ { 'cancel_reason': None, - 'cancel_type': None, - 'canceled_datetime': None, - 'cc_address': '123 Something St', - 'cc_city': 'Someplace', - 'cc_company': 'Some Co LLC', - 'cc_country': 'United States', - 'cc_email': None, - 'cc_expiration_date': '2011-07-31T00:00:00+00:00', - 'cc_first_name': 'Test', - 'cc_last_four': '1111', - 'cc_last_name': 'User', - 'cc_state': 'NY', - 'cc_type': 'visa', - 'cc_zip': '12345', - 'created_datetime': datetime(2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), - 'gateway_token': 'SIMULATED', - 'id': 'a1f27c60-6e65-102e-b098-40402145ee8b', - 'invoices': [ { 'billing_datetime': datetime(2011, 2, 10, 23, 57, 58, tzinfo=tzutc()), - 'charges': [ { 'code': 'TRACKED_MONTHLY_RECURRING', - 'created_datetime': datetime(2011, 2, 10, 23, 57, 58, tzinfo=tzutc()), - 'description': '', - 'each_amount': Decimal('10.00'), - 'id': '', - 'quantity': Decimal('1'), - 'type': 'recurring'}, - { 'code': 'MONTHLY_ITEM', - 'created_datetime': datetime(2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), - 'description': '', - 'each_amount': Decimal('10.00'), - 'id': '', - 'quantity': Decimal('1'), - 'type': 'item'}, - { 'code': 'ONCE_ITEM', - 'created_datetime': datetime(2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), - 'description': '', - 'each_amount': Decimal('10.00'), - 'id': '', - 'quantity': Decimal('1'), - 'type': 'item'}], - 'created_datetime': datetime(2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), - 'id': 'a1f7faaa-6e65-102e-b098-40402145ee8b', - 'number': '1', - 'paid_transaction_id': '', - 'type': 'subscription', - 'vat_rate': ''}], - 'items': [ { 'code': 'MONTHLY_ITEM', - 'created_datetime': datetime(2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), - 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', - 'modified_datetime': datetime(2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), - 'name': 'Monthly Item', - 'quantity': Decimal('3')}, - { 'code': 'ONCE_ITEM', - 'created_datetime': datetime(2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), - 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', - 'modified_datetime': datetime(2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), - 'name': 'Once Item', - 'quantity': Decimal('1')}], - 'plans': [ { 'billing_frequency': 'monthly', - 'billing_frequency_per': 'month', - 'billing_frequency_quantity': 1, - 'billing_frequency_unit': 'months', - 'code': 'TRACKED_MONTHLY', - 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, tzinfo=tzutc()), - 'description': '', - 'id': 'd19974a6-6e5a-102e-b098-40402145ee8b', - 'initial_bill_count': 1, - 'initial_bill_count_unit': 'months', - 'is_active': True, - 'is_free': False, - 'items': [ { 'code': 'MONTHLY_ITEM', - 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, tzinfo=tzutc()), - 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', - 'is_periodic': True, - 'name': 'Monthly Item', - 'overage_amount': Decimal('10.00'), - 'quantity_included': Decimal('2')}, - { 'code': 'ONCE_ITEM', - 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, tzinfo=tzutc()), - 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', - 'is_periodic': False, - 'name': 'Once Item', - 'overage_amount': Decimal('10.00'), - 'quantity_included': Decimal('0')}], - 'name': 'Tracked Monthly', - 'recurring_charge_amount': Decimal('10.00'), - 'recurring_charge_code': 'TRACKED_MONTHLY_RECURRING', - 'setup_charge_amount': Decimal('0.00'), - 'setup_charge_code': '', - 'trial_days': 0}], - 'redirect_url': None}], - 'vat_number': ''}] + + expected = [ + { + 'campaign_content': '', + 'campaign_medium': '', + 'campaign_name': '', + 'campaign_source': '', + 'campaign_term': '', + 'code': 'test', + 'company': '', + 'created_datetime': datetime(2011, 1, 10, 23, 57, 58, + tzinfo=tzutc()), + 'email': 'garbage@saaspire.com', + 'first_contact_datetime': None, + 'first_name': 'Test', + 'gateway_token': None, + 'id': 'a1f143e0-6e65-102e-b098-40402145ee8b', + 'is_vat_exempt': '0', + 'last_name': 'User', + 'meta_data': [], + 'modified_datetime': datetime(2011, 1, 10, 23, 57, 58, + tzinfo=tzutc()), + 'notes': '', + 'referer': '', + 'referer_host': '', + 'subscriptions': [ + { + 'cancel_reason': None, + 'cancel_type': None, + 'canceled_datetime': None, + 'cc_address': '123 Something St', + 'coupon_code': None, + 'cc_city': 'Someplace', + 'cc_company': 'Some Co LLC', + 'cc_country': 'United States', + 'cc_email': None, + 'cc_expiration_date': '2011-07-31T00:00:00+00:00', + 'cc_first_name': 'Test', + 'cc_last_four': '1111', + 'cc_last_name': 'User', + 'cc_state': 'NY', + 'cc_type': 'visa', + 'cc_zip': '12345', + 'created_datetime': datetime(2011, 1, 10, 23, 57, 58, + tzinfo=tzutc()), + 'gateway_token': 'SIMULATED', + 'id': 'a1f27c60-6e65-102e-b098-40402145ee8b', + 'invoices': [ + { + 'billing_datetime': datetime(2011, 2, 10, 23, + 57, 58, + tzinfo=tzutc()), + 'charges': [ + { + 'code': 'TRACKED_MONTHLY_RECURRING', + 'created_datetime': datetime( + 2011, 2, 10, 23, 57, 58, + tzinfo=tzutc()), + 'description': '', + 'each_amount': Decimal('10.00'), + 'id': '', + 'quantity': Decimal('1'), + 'type': 'recurring'}, + { + 'code': 'MONTHLY_ITEM', + 'created_datetime': datetime( + 2011, 1, 10, 23, 57, 58, + tzinfo=tzutc()), + 'description': '', + 'each_amount': Decimal('10.00'), + 'id': '', + 'quantity': Decimal('1'), + 'type': 'item'}, + { + 'code': 'ONCE_ITEM', + 'created_datetime': datetime( + 2011, 1, 10, 23, 57, 58, + tzinfo=tzutc()), + 'description': '', + 'each_amount': Decimal('10.00'), + 'id': '', + 'quantity': Decimal('1'), + 'type': 'item'}], + 'created_datetime': datetime( + 2011, 1, 10, 23, 57, 58, + tzinfo=tzutc()), + 'id': 'a1f7faaa-6e65-102e-b098-40402145ee8b', + 'number': '1', + 'paid_transaction_id': '', + 'type': 'subscription', + 'vat_rate': ''}], + 'items': [ + { + 'code': 'MONTHLY_ITEM', + 'created_datetime': datetime( + 2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), + 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', + 'modified_datetime': datetime( + 2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), + 'name': 'Monthly Item', + 'quantity': Decimal('3')}, + { + 'code': 'ONCE_ITEM', + 'created_datetime': datetime( + 2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), + 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', + 'modified_datetime': datetime( + 2011, 1, 10, 23, 57, 58, tzinfo=tzutc()), + 'name': 'Once Item', + 'quantity': Decimal('1')}], + 'plans': [ + { + 'billing_frequency': 'monthly', + 'billing_frequency_per': 'month', + 'billing_frequency_quantity': 1, + 'billing_frequency_unit': 'months', + 'code': 'TRACKED_MONTHLY', + 'created_datetime': datetime( + 2011, 1, 10, 22, 40, 34, tzinfo=tzutc()), + 'description': '', + 'id': 'd19974a6-6e5a-102e-b098-40402145ee8b', + 'initial_bill_count': 1, + 'initial_bill_count_unit': 'months', + 'is_active': True, + 'is_free': False, + 'items': [ + { + 'code': 'MONTHLY_ITEM', + 'created_datetime': datetime( + 2011, 1, 10, 22, 40, 34, + tzinfo=tzutc()), + 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', + 'is_periodic': True, + 'name': 'Monthly Item', + 'overage_amount': Decimal('10.00'), + 'quantity_included': Decimal('2')}, + { + 'code': 'ONCE_ITEM', + 'created_datetime': datetime( + 2011, 1, 10, 22, 40, 34, + tzinfo=tzutc()), + 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', + 'is_periodic': False, + 'name': 'Once Item', + 'overage_amount': Decimal('10.00'), + 'quantity_included': Decimal('0')}], + 'name': 'Tracked Monthly', + 'recurring_charge_amount': Decimal('10.00'), + 'recurring_charge_code': 'TRACKED_MONTHLY_RECURRING', + 'setup_charge_amount': Decimal('0.00'), + 'setup_charge_code': '', + 'trial_days': 0}], + 'redirect_url': None}], + 'vat_number': ''}] result = parser.parse_xml(customers_xml) - + import pprint pp = pprint.PrettyPrinter(indent=4) pp.pprint(result) self.assertEquals(expected, result) - #import pprint - #pp = pprint.PrettyPrinter(indent=4) - #pp.pprint(result) - #assert False def test_paypal_customer_parse(self): + ''' Test customer parser with paypal customer. ''' customers_xml = self.load_file('paypal_customer.xml') parser = CustomersParser() - - expected = [ { 'campaign_content': '', - 'campaign_medium': '', - 'campaign_name': '', - 'campaign_source': '', - 'campaign_term': '', - 'code': 'test', - 'company': '', - 'created_datetime': datetime(2011, 5, 16, 16, 36, 1, tzinfo=tzutc()), - 'email': 'garbage@saaspire.com', - 'first_contact_datetime': None, - 'first_name': 'Test', - 'gateway_token': None, - 'id': '95d7696a-7fda-11e0-a51b-40403c39f8d9', - 'is_vat_exempt': '0', - 'last_name': 'User', - 'meta_data': [], - 'modified_datetime': datetime(2011, 5, 16, 16, 36, 1, tzinfo=tzutc()), - 'notes': '', - 'referer': '', - 'referer_host': '', - 'subscriptions': [ { 'cancel_reason': 'PayPal preapproval is pending', - 'cancel_type': 'paypal-wait', - 'canceled_datetime': datetime(2011, 5, 16, 16, 36, 1, tzinfo=tzutc()), - 'cc_address': '', - 'cc_city': '', - 'cc_company': '', - 'cc_country': '', - 'cc_email': '', - 'cc_expiration_date': '2012-05-16T00:00:00+00:00', - 'cc_first_name': 'Test', - 'cc_last_four': '', - 'cc_last_name': 'User', - 'cc_state': '', - 'cc_type': '', - 'cc_zip': '', - 'created_datetime': datetime(2011, 5, 16, 16, 36, 1, tzinfo=tzutc()), - 'gateway_account': { 'gateway': 'PayPal_Simulator', - 'id': '303f9a50-7fda-11e0-a51b-40403c39f8d9', - 'type': 'paypal'}, - 'gateway_token': 'SIMULATED-4dd152718371a', - 'id': '95d804ba-7fda-11e0-a51b-40403c39f8d9', - 'invoices': [ { 'billing_datetime': datetime(2011, 6, 16, 16, 36, 1, tzinfo=tzutc()), - 'charges': [ { 'code': 'PAID_MONTHLY_RECURRING', - 'created_datetime': datetime(2011, 6, 16, 16, 36, 1, tzinfo=tzutc()), - 'description': '', - 'each_amount': Decimal('20.00'), - 'id': '', - 'quantity': Decimal('1'), - 'type': 'recurring'}], - 'created_datetime': datetime(2011, 5, 16, 16, 36, 1, tzinfo=tzutc()), - 'id': '95de499c-7fda-11e0-a51b-40403c39f8d9', - 'number': '1', - 'paid_transaction_id': '', - 'type': 'subscription', - 'vat_rate': ''}], - 'items': [ { 'code': 'MONTHLY_ITEM', - 'created_datetime': None, - 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', - 'modified_datetime': None, - 'name': 'Monthly Item', - 'quantity': Decimal('0')}, - { 'code': 'ONCE_ITEM', - 'created_datetime': None, - 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', - 'modified_datetime': None, - 'name': 'Once Item', - 'quantity': Decimal('0')}], - 'plans': [ { 'billing_frequency': 'monthly', - 'billing_frequency_per': 'month', - 'billing_frequency_quantity': 1, - 'billing_frequency_unit': 'months', - 'code': 'PAID_MONTHLY', - 'created_datetime': datetime(2011, 1, 7, 21, 5, 42, tzinfo=tzutc()), - 'description': '', - 'id': '11af9cfc-6bf2-102e-b098-40402145ee8b', - 'initial_bill_count': 1, - 'initial_bill_count_unit': 'months', - 'is_active': True, - 'is_free': False, - 'items': [ { 'code': 'MONTHLY_ITEM', - 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, tzinfo=tzutc()), - 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', - 'is_periodic': False, - 'name': 'Monthly Item', - 'overage_amount': Decimal('0.00'), - 'quantity_included': Decimal('0')}, - { 'code': 'ONCE_ITEM', - 'created_datetime': datetime(2011, 1, 10, 22, 40, 34, tzinfo=tzutc()), - 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', - 'is_periodic': False, - 'name': 'Once Item', - 'overage_amount': Decimal('0.00'), - 'quantity_included': Decimal('0')}], - 'name': 'Paid Monthly', - 'recurring_charge_amount': Decimal('20.00'), - 'recurring_charge_code': 'PAID_MONTHLY_RECURRING', - 'setup_charge_amount': Decimal('0.00'), - 'setup_charge_code': '', - 'trial_days': 0}], - 'redirect_url': 'https://cheddargetter.com/service/paypal/simulate/productId/2ccbecd6-6beb-102e-b098-40402145ee8b/id/95d7696a-7fda-11e0-a51b-40403c39f8d9?preapprovalkey=SIMULATED-4dd152718371a'}], - 'vat_number': ''}] + + expected = [ + { + 'campaign_content': '', + 'campaign_medium': '', + 'campaign_name': '', + 'campaign_source': '', + 'campaign_term': '', + 'code': 'test', + 'company': '', + 'created_datetime': datetime(2011, 5, 16, 16, 36, 1, + tzinfo=tzutc()), + 'email': 'garbage@saaspire.com', + 'first_contact_datetime': None, + 'first_name': 'Test', + 'gateway_token': None, + 'id': '95d7696a-7fda-11e0-a51b-40403c39f8d9', + 'is_vat_exempt': '0', + 'last_name': 'User', + 'meta_data': [], + 'modified_datetime': datetime(2011, 5, 16, 16, 36, 1, + tzinfo=tzutc()), + 'notes': '', + 'referer': '', + 'referer_host': '', + 'subscriptions': [ + { + 'cancel_reason': 'PayPal preapproval is pending', + 'cancel_type': 'paypal-wait', + 'canceled_datetime': datetime(2011, 5, 16, 16, 36, 1, + tzinfo=tzutc()), + 'cc_address': '', + 'coupon_code': None, + 'cc_city': '', + 'cc_company': '', + 'cc_country': '', + 'cc_email': '', + 'cc_expiration_date': '2012-05-16T00:00:00+00:00', + 'cc_first_name': 'Test', + 'cc_last_four': '', + 'cc_last_name': 'User', + 'cc_state': '', + 'cc_type': '', + 'cc_zip': '', + 'created_datetime': datetime(2011, 5, 16, 16, 36, 1, + tzinfo=tzutc()), + 'gateway_account': { + 'gateway': 'PayPal_Simulator', + 'id': '303f9a50-7fda-11e0-a51b-40403c39f8d9', + 'type': 'paypal'}, + 'gateway_token': 'SIMULATED-4dd152718371a', + 'id': '95d804ba-7fda-11e0-a51b-40403c39f8d9', + 'invoices': [ + { + 'billing_datetime': datetime( + 2011, 6, 16, 16, 36, 1, tzinfo=tzutc()), + 'charges': [ + { + 'code': 'PAID_MONTHLY_RECURRING', + 'created_datetime': datetime( + 2011, 6, 16, 16, 36, 1, + tzinfo=tzutc()), + 'description': '', + 'each_amount': Decimal('20.00'), + 'id': '', + 'quantity': Decimal('1'), + 'type': 'recurring'}], + 'created_datetime': datetime( + 2011, 5, 16, 16, 36, 1, tzinfo=tzutc()), + 'id': '95de499c-7fda-11e0-a51b-40403c39f8d9', + 'number': '1', + 'paid_transaction_id': '', + 'type': 'subscription', + 'vat_rate': ''}], + 'items': [ + { + 'code': 'MONTHLY_ITEM', + 'created_datetime': None, + 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', + 'modified_datetime': None, + 'name': 'Monthly Item', + 'quantity': Decimal('0')}, + { + 'code': 'ONCE_ITEM', + 'created_datetime': None, + 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', + 'modified_datetime': None, + 'name': 'Once Item', + 'quantity': Decimal('0')}], + 'plans': [ + { + 'billing_frequency': 'monthly', + 'billing_frequency_per': 'month', + 'billing_frequency_quantity': 1, + 'billing_frequency_unit': 'months', + 'code': 'PAID_MONTHLY', + 'created_datetime': datetime( + 2011, 1, 7, 21, 5, 42, tzinfo=tzutc()), + 'description': '', + 'id': '11af9cfc-6bf2-102e-b098-40402145ee8b', + 'initial_bill_count': 1, + 'initial_bill_count_unit': 'months', + 'is_active': True, + 'is_free': False, + 'items': [ + { + 'code': 'MONTHLY_ITEM', + 'created_datetime': datetime( + 2011, 1, 10, 22, 40, 34, + tzinfo=tzutc()), + 'id': 'd19b4970-6e5a-102e-b098-40402145ee8b', + 'is_periodic': False, + 'name': 'Monthly Item', + 'overage_amount': Decimal('0.00'), + 'quantity_included': Decimal('0')}, + { + 'code': 'ONCE_ITEM', + 'created_datetime': datetime( + 2011, 1, 10, 22, 40, 34, + tzinfo=tzutc()), + 'id': 'd19ef2f0-6e5a-102e-b098-40402145ee8b', + 'is_periodic': False, + 'name': 'Once Item', + 'overage_amount': Decimal('0.00'), + 'quantity_included': Decimal('0')}], + 'name': 'Paid Monthly', + 'recurring_charge_amount': Decimal('20.00'), + 'recurring_charge_code': 'PAID_MONTHLY_RECURRING', + 'setup_charge_amount': Decimal('0.00'), + 'setup_charge_code': '', + 'trial_days': 0}], + 'redirect_url': 'https://cheddargetter.com/service/paypal/simulate/productId/2ccbecd6-6beb-102e-b098-40402145ee8b/id/95d7696a-7fda-11e0-a51b-40403c39f8d9?preapprovalkey=SIMULATED-4dd152718371a'}], + 'vat_number': ''}] result = parser.parse_xml(customers_xml) - + import pprint pp = pprint.PrettyPrinter(indent=4) pp.pprint(result) self.assertEquals(expected, result) - diff --git a/tests/product_tests.py b/tests/product_tests.py index 16dcbc9..95d4f20 100644 --- a/tests/product_tests.py +++ b/tests/product_tests.py @@ -1,11 +1,13 @@ +import random +import string + from copy import copy -from datetime import date, datetime, timedelta +from datetime import datetime, timedelta from decimal import Decimal import unittest -from unittest.case import SkipTest from dateutil.relativedelta import relativedelta -from dateutil.tz import * +from dateutil.tz import tzutc from nose.tools import raises from testconfig import config @@ -14,26 +16,27 @@ from testing_tools.decorators import clear_users + class ProductTests(unittest.TestCase): - - client_defaults = { + + client_defaults = { 'username': config['cheddar']['username'], 'password': config['cheddar']['password'], 'product_code': config['cheddar']['product_code'], 'endpoint': config['cheddar']['endpoint'], } - + customer_defaults = { 'code': 'test', - 'email':'garbage@saaspire.com', + 'email': 'garbage@saaspire.com', 'first_name': 'Test', 'last_name': 'User', 'plan_code': 'FREE_MONTHLY', } - paypal_defaults = { + paypal_defaults = { 'code': 'test', - 'email':'garbage@saaspire.com', + 'email': 'garbage@saaspire.com', 'first_name': 'Test', 'last_name': 'User', 'plan_code': 'PAID_MONTHLY', @@ -44,9 +47,9 @@ class ProductTests(unittest.TestCase): 'return_url': 'http://example.com/return', 'cancel_url': 'http://example.com/cancel', } - + exipration = datetime.now() + timedelta(days=180) - + paid_defaults = { 'cc_number': '4111111111111111', 'cc_expiration': exipration.strftime('%m/%Y'), @@ -61,13 +64,15 @@ class ProductTests(unittest.TestCase): 'cc_zip': '12345', 'plan_code': 'PAID_MONTHLY', } - + def get_product(self): + ''' Helper method for getting product. ''' product = CheddarProduct(**self.client_defaults) - + return product def test_repr(self): + ''' Test Product __repr__ method. ''' product = self.get_product() expected = u'CheddarProduct: %s' % product.product_code result = product.__repr__() @@ -75,16 +80,18 @@ def test_repr(self): self.assertEquals(expected, result) def test_instantiate_product(self): + ''' Test product key. ''' product = self.get_product() - + for key, value in self.client_defaults.items(): self.assertEquals(value, getattr(product.client, key)) - + def test_get_all_plans(self): + ''' Test product get_all_plans method. ''' product = self.get_product() - + plans = product.get_all_plans() - + for plan in plans: if plan.code == 'FREE_MONTHLY': free_plan = plan @@ -92,38 +99,49 @@ def test_get_all_plans(self): paid_plan = plan elif plan.code == 'TRACKED_MONTHLY': tracked_plan = plan - + self.assertEquals('FREE_MONTHLY', free_plan.code) self.assertEquals('PAID_MONTHLY', paid_plan.code) self.assertEquals('TRACKED_MONTHLY', tracked_plan.code) - + def test_get_plan(self): + ''' Test product get_plan method with plan code. ''' product = self.get_product() code = 'PAID_MONTHLY' plan = product.get_plan(code) - + self.assertEquals(code, plan.code) - + def test_plan_initial_bill_date(self): + ''' Test plan with initial bill date. ''' product = self.get_product() code = 'PAID_MONTHLY' plan = product.get_plan(code) - + expected = datetime.utcnow().date() + relativedelta(months=1) result = plan.initial_bill_date - + self.assertEquals(expected, result) - + def get_customer(self, **kwargs): + ''' Test Product get_customer method with filtering. ''' customer_data = copy(self.customer_defaults) + # We need to make unique customers with the same data. + # Cheddar recomends we pass a garbage field. + # http://support.cheddargetter.com/discussions/problems/8342-duplicate-post + # http://support.cheddargetter.com/kb/api-8/error-handling#duplicate + random_string = ''.join(random.SystemRandom().choice( + string.ascii_uppercase + string.digits) for _ in range(5)) + customer_data.update({'notes': random_string}) customer_data.update(kwargs) product = self.get_product() - + customer = product.create_customer(**customer_data) - + return customer - + def get_customer_with_items(self, **kwargs): + ''' Test Product get_customer method with items. ''' data = copy(self.paid_defaults) if 'items' in kwargs.keys(): items = kwargs['items'] @@ -131,101 +149,129 @@ def get_customer_with_items(self, **kwargs): items = [] items.append({'code': 'MONTHLY_ITEM', 'quantity': 3}) items.append({'code': 'ONCE_ITEM'}) - + data['items'] = items data['plan_code'] = 'TRACKED_MONTHLY' customer = self.get_customer(**data) - + return customer - + @clear_users def test_simple_create_customer(self): + ''' Test Create Customer with only the get_customer helper. ''' self.get_customer() - + @clear_users def test_create_customer_with_company(self): + ''' Test Create Customer with company name. ''' self.get_customer(company='Test Co') - + @clear_users def test_create_customer_with_meta_data(self): - self.get_customer(meta_data = {'key_1': 'value_1', 'key2': 'value_2'}) - + ''' Test Create Customer with meta data. ''' + self.get_customer(meta_data={'key_1': 'value_1', 'key2': 'value_2'}) + @clear_users def test_create_customer_with_true_vat_exempt(self): + ''' Test Create Customer with vat exempt true. ''' self.get_customer(is_vat_exempt=True) - + @clear_users def test_create_customer_with_false_vat_exempt(self): + ''' Test Create Customer with vat exempt false. ''' self.get_customer(is_vat_exempt=False) - + @clear_users def test_create_customer_with_vat_number(self): + ''' Test Create Customer with vat number. ''' self.get_customer(vat_number=12345) - + @clear_users def test_create_customer_with_notes(self): + ''' Test Create Customer with notes. ''' self.get_customer(notes='This is a test note!') - + @clear_users def test_create_customer_with_first_contact_datetime(self): + ''' Test Create Customer with first contact datetime. ''' self.get_customer(first_contact_datetime=datetime.now()) - + @clear_users def test_create_customer_with_referer(self): + ''' Test Create Customer with referer. ''' self.get_customer(referer='http://saaspire.com/test.html') - + @clear_users def test_create_customer_with_campaign_term(self): + ''' Test Create Customer with campaign term. ''' self.get_customer(campaign_term='testing') - + @clear_users def test_create_customer_with_campaign_name(self): + ''' Test Create Customer with campaign name. ''' self.get_customer(campaign_name='testing') - + @clear_users def test_create_customer_with_campaign_source(self): + ''' Test Create Customer with campaign source. ''' self.get_customer(campaign_source='testing') - + @clear_users def test_create_customer_with_campaign_content(self): + ''' Test Create Customer with campaign content. ''' self.get_customer(campaign_content='testing') - + @clear_users def test_create_customer_with_initial_bill_date(self): + ''' Test Create Customer with initial bill date. ''' initial_bill_date = datetime.utcnow() + timedelta(days=60) customer = self.get_customer(initial_bill_date=initial_bill_date) invoice = customer.subscription.invoices[0] real_bill_date = invoice['billing_datetime'] - - # Sometimes cheddar getter will push the bill date to the next day + + # Sometimes cheddar getter will push the bill date to the next day # if the request is made around UTC midnight diff = initial_bill_date.date() - real_bill_date.date() self.assertLessEqual(diff.days, 1) - + @clear_users def test_create_paid_customer(self): + ''' Test Create Customer with payment. ''' self.get_customer(**self.paid_defaults) - + @clear_users def test_create_paid_customer_with_charges(self): + ''' Test Create Customer with payment and additional charges. ''' data = copy(self.paid_defaults) charges = [] - charges.append({'code': 'test_charge_1', 'each_amount':2}) + charges.append({'code': 'test_charge_1', 'each_amount': 2}) charges.append({'code': 'charge2', 'quantity': 3, 'each_amount': 4}) data['charges'] = charges self.get_customer(**data) - + + @clear_users + def test_create_paid_customer_with_coupon_code(self): + ''' Test Create Customer with payment and coupon codes. ''' + data = copy(self.paid_defaults) + data.update({'coupon_code': 'COUPON'}) + self.get_customer(**data) + @clear_users def test_create_paid_customer_with_decimal_charges(self): + ''' + Test Create Customer with payment and additional decimal charges. + ''' data = copy(self.paid_defaults) charges = [] - charges.append({'code': 'test_charge_1', 'each_amount': Decimal('2.30')}) + charges.append({'code': 'test_charge_1', + 'each_amount': Decimal('2.30')}) charges.append({'code': 'charge2', 'each_amount': Decimal('-4.5')}) data['charges'] = charges self.get_customer(**data) @clear_users def test_create_paid_customer_with_items(self): + ''' Test Create Customer with payment and additional items. ''' data = copy(self.paid_defaults) items = [] items.append({'code': 'MONTHLY_ITEM', 'quantity': 3}) @@ -234,9 +280,12 @@ def test_create_paid_customer_with_items(self): data['plan_code'] = 'TRACKED_MONTHLY' self.get_customer(**data) - @clear_users def test_create_paid_customer_with_decimal_quantity_items(self): + ''' + Test Create Customer with payment and additional decimal quantity + items. + ''' data = copy(self.paid_defaults) items = [] items.append({'code': 'MONTHLY_ITEM', 'quantity': Decimal('1.23456')}) @@ -247,11 +296,13 @@ def test_create_paid_customer_with_decimal_quantity_items(self): @clear_users def test_create_paypal_customer(self): + ''' Test Create Customer with paypal. ''' data = copy(self.paypal_defaults) self.get_customer(**data) @clear_users def test_update_paypal_customer(self): + ''' Test Update Customer with paypal. ''' data = copy(self.paypal_defaults) customer = self.get_customer(**data) customer.update( @@ -260,294 +311,337 @@ def test_update_paypal_customer(self): cancel_url='http://example.com/update-cancel/', ) - @clear_users def test_customer_repr(self): + ''' Test Customer __repr__ method. ''' customer = self.get_customer() expected = 'Customer: Test User (test)' result = repr(customer) self.assertEquals(expected, result) - + @clear_users def test_subscription_repr(self): + ''' Test Subscription __repr__ method. ''' customer = self.get_customer() subscription = customer.subscription - + expected = 'Subscription:' result = repr(subscription) - + self.assertIn(expected, result) - + @clear_users def test_pricing_plan_repr(self): + ''' Test PricingPlan __repr__ method. ''' customer = self.get_customer() subscription = customer.subscription plan = subscription.plan - + expected = 'PricingPlan: Free Monthly (FREE_MONTHLY)' result = repr(plan) - + self.assertEquals(expected, result) - - + @clear_users def test_item_repr(self): + ''' Test Item __repr__ method. ''' customer = self.get_customer_with_items() subscription = customer.subscription item = subscription.items['MONTHLY_ITEM'] - + expected = 'Item: MONTHLY_ITEM for test' result = repr(item) - + self.assertEquals(expected, result) - + @clear_users def test_get_customers(self): - customer1 = self.get_customer() + ''' Create two customers, verify 2 returned. ''' + self.get_customer() customer2_data = copy(self.paid_defaults) customer2_data.update({ 'code': 'test2', - 'email':'garbage+2@saaspire.com', + 'email': 'garbage+2@saaspire.com', 'first_name': 'Test2', 'last_name': 'User2', }) - customer2 = self.get_customer(**customer2_data) + self.get_customer(**customer2_data) product = self.get_product() - + + # This test fails intermitently. I'm assuming network race condition + # due to creating customers and fetching all customers so quickly. + import time + time.sleep(0.5) fetched_customers = product.get_customers() - self.assertEquals(2, len(fetched_customers)) - + @clear_users def test_get_customer(self): + ''' Test getting a customer by code.. ''' created_customer = self.get_customer() product = self.get_product() - + fetched_customer = product.get_customer(code=created_customer.code) - + self.assertEquals(created_customer.code, fetched_customer.code) - self.assertEquals(created_customer.first_name, fetched_customer.first_name) - self.assertEquals(created_customer.last_name, fetched_customer.last_name) + self.assertEquals(created_customer.first_name, + fetched_customer.first_name) + self.assertEquals(created_customer.last_name, + fetched_customer.last_name) self.assertEquals(created_customer.email, fetched_customer.email) - + @clear_users def test_simple_customer_update(self): + ''' Test Update Customer. ''' new_name = 'Different' customer = self.get_customer() product = self.get_product() - + customer.update(first_name=new_name) self.assertEquals(new_name, customer.first_name) - + fetched_customer = product.get_customer(code=customer.code) self.assertEquals(customer.first_name, fetched_customer.first_name) - + @clear_users @raises(NotFound) def test_delete_customer(self): + ''' Create a Customer and delete that customer. ''' customer = self.get_customer() product = self.get_product() - + fetched_customer = product.get_customer(code=customer.code) self.assertEquals(customer.first_name, fetched_customer.first_name) - + customer.delete() fetched_customer = product.get_customer(code=customer.code) - - + @clear_users def test_delete_all_customers(self): - customer_1 = self.get_customer() - customer_2 = self.get_customer(code='test2') + ''' + Create two customers, verify 2 returned, + delete and verify 0 customers. + ''' + self.get_customer() + self.get_customer(code='test2') product = self.get_product() - + + # This test fails intermitently. I'm assuming network race condition + # due to creating customers and fetching all customers so quickly. + import time + time.sleep(0.5) fetched_customers = product.get_customers() self.assertEquals(2, len(fetched_customers)) - + product.delete_all_customers() - + fetched_customers = product.get_customers() self.assertEquals(0, len(fetched_customers)) - + @clear_users def test_cancel_subscription(self): + ''' Test cancel subscription. ''' customer = self.get_customer() customer.subscription.cancel() - + now = datetime.now(tzutc()) canceled_on = customer.subscription.canceled diff = now - canceled_on limit = timedelta(seconds=10) self.assertLess(diff, limit) - + def assert_increment(self, quantity=None): + ''' Helper method for asserting increment in other tests. ''' customer = self.get_customer_with_items() product = self.get_product() item = customer.subscription.items['MONTHLY_ITEM'] - + old_quantity = item.quantity_used item.increment(quantity) new_quantity = item.quantity_used diff = new_quantity - old_quantity expected = Decimal(quantity or 1) self.assertAlmostEqual(expected, diff, places=2) - + fetched_customer = product.get_customer(code=customer.code) - fetched_item = customer.subscription.items[item.code] + fetched_item = fetched_customer.subscription.items[item.code] self.assertEquals(item.quantity_used, fetched_item.quantity_used) - + @clear_users def test_simple_increment(self): + ''' Test item increment. ''' self.assert_increment() - + @clear_users def test_int_increment(self): + ''' Test item increment with integer. ''' self.assert_increment(1) - + @clear_users def test_float_increment(self): + ''' Test item increment with float. ''' self.assert_increment(1.234) - + @clear_users def test_decimal_increment(self): + ''' Test item increment with decimal. ''' self.assert_increment(Decimal('1.234')) - + def assert_decrement(self, quantity=None): + ''' Helper method for asserting decrement in other tests. ''' customer = self.get_customer_with_items() product = self.get_product() item = customer.subscription.items['MONTHLY_ITEM'] - + old_quantity = item.quantity_used item.decrement(quantity) new_quantity = item.quantity_used diff = old_quantity - new_quantity expected = Decimal(quantity or 1) self.assertAlmostEqual(expected, diff, places=2) - + fetched_customer = product.get_customer(code=customer.code) - fetched_item = customer.subscription.items[item.code] + fetched_item = fetched_customer.subscription.items[item.code] self.assertEquals(item.quantity_used, fetched_item.quantity_used) - + @clear_users def test_simple_decrement(self): + ''' Test item decrement. ''' self.assert_decrement() - + @clear_users def test_int_decrement(self): + ''' Test item decrement with integer. ''' self.assert_decrement(1) - + @clear_users def test_float_decrement(self): + ''' Test item decrement with float. ''' self.assert_decrement(1.234) - + @clear_users def test_decimal_decrement(self): + ''' Test item decrement with decimal. ''' self.assert_decrement(Decimal('1.234')) - + def assert_set(self, quantity): + ''' + Helper method for asserting item quantity has been set as expected. + ''' customer = self.get_customer_with_items() product = self.get_product() item = customer.subscription.items['MONTHLY_ITEM'] - - old_quantity = item.quantity_used + item.set(quantity) new_quantity = item.quantity_used expected = Decimal(quantity) self.assertAlmostEqual(expected, new_quantity, places=2) - + fetched_customer = product.get_customer(code=customer.code) - fetched_item = customer.subscription.items[item.code] + fetched_item = fetched_customer.subscription.items[item.code] self.assertEquals(item.quantity_used, fetched_item.quantity_used) - + @clear_users def test_int_set(self): + ''' Test item set with integer. ''' self.assert_set(1) - + @clear_users def test_float_set(self): + ''' Test item set with float. ''' self.assert_set(1.234) - + @clear_users def test_decimal_set(self): + ''' Test item set with decimal. ''' self.assert_set(Decimal('1.234')) - + def assert_charged(self, code, each_amount, quantity=None, - description=None): + description=None): + ''' Helper method for asserting custom charges as expected. ''' customer = self.get_customer(**self.paid_defaults) product = self.get_product() - + customer.charge( code=code, each_amount=each_amount, quantity=quantity, description=description, ) - + if description is None: description = '' - + found_charge = None for invoice in customer.subscription.invoices: for charge in invoice['charges']: if charge['code'] == code: found_charge = charge - - self.assertAlmostEqual(Decimal(each_amount), found_charge['each_amount'], places=2) + + self.assertAlmostEqual(Decimal(each_amount), + found_charge['each_amount'], places=2) self.assertEqual(quantity, found_charge['quantity']) self.assertEqual(description, found_charge['description']) - + fetched_customer = product.get_customer(code=customer.code) fetched_charge = None for invoice in fetched_customer.subscription.invoices: for charge in invoice['charges']: if charge['code'] == code: fetched_charge = charge - - self.assertAlmostEqual(Decimal(each_amount), fetched_charge['each_amount'], places=2) + + self.assertAlmostEqual(Decimal(each_amount), + fetched_charge['each_amount'], places=2) self.assertEqual(quantity, fetched_charge['quantity']) self.assertEqual(description, fetched_charge['description']) - + @clear_users def test_add_charge(self): + ''' Test adding a custom charge to an invoice. ''' self.assert_charged(code='TEST-CHARGE', each_amount=1, quantity=1) - - @clear_users - def test_add_float_charge(self): - self.assert_charged(code='TEST-CHARGE', each_amount=2.3, quantity=2) - + @clear_users def test_add_float_charge(self): + ''' Test adding a custom charge to an invoice with float. ''' self.assert_charged(code='TEST-CHARGE', each_amount=2.3, quantity=2) - + @clear_users def test_add_decimal_charge(self): - self.assert_charged(code='TEST-CHARGE', each_amount=Decimal('2.3'), quantity=3) - + ''' Test adding a custom charge to an invoice with decimal. ''' + self.assert_charged(code='TEST-CHARGE', each_amount=Decimal('2.3'), + quantity=3) + @clear_users def test_add_charge_with_descriptions(self): - self.assert_charged(code='TEST-CHARGE', each_amount=1, quantity=1, description="A test charge") - + ''' Test adding a custom charge to an invoice with descriptions. ''' + self.assert_charged(code='TEST-CHARGE', each_amount=1, quantity=1, + description="A test charge") + @clear_users def test_add_credit(self): + ''' Test adding a custom charge to an invoice as credit. ''' self.assert_charged(code='TEST-CHARGE', each_amount=-1, quantity=1) - def assertCharge(self, customer, code, each_amount, quantity, description='', invoice_type=None): + def assertCharge(self, customer, code, each_amount, quantity, + description='', invoice_type=None): + ''' Helper method for asserting custom charge as expected. ''' found_charge = None for invoice in customer.subscription.invoices: if invoice_type is None or invoice['type'] == invoice_type: for charge in invoice['charges']: if charge['code'] == code: found_charge = charge - - self.assertAlmostEqual(Decimal(each_amount), found_charge['each_amount'], places=2) + + self.assertAlmostEqual(Decimal(each_amount), + found_charge['each_amount'], places=2) self.assertEqual(quantity, found_charge['quantity']) self.assertEqual(description, found_charge['description']) - def assertOneTimeInvoice(self, charges): + ''' Helper method for asserting one time invoice as expected. ''' customer = self.get_customer(**self.paid_defaults) product = self.get_product() @@ -556,59 +650,105 @@ def assertOneTimeInvoice(self, charges): for charge in charges: self.assertCharge( customer, - code = charge['code'], - quantity = charge['quantity'], - each_amount = charge['each_amount'], - description = charge.get('description', ''), - invoice_type = 'one-time', + code=charge['code'], + quantity=charge['quantity'], + each_amount=charge['each_amount'], + description=charge.get('description', ''), + invoice_type='one-time', ) fetched_customer = product.get_customer(code=customer.code) for charge in charges: self.assertCharge( fetched_customer, - code = charge['code'], - quantity = charge['quantity'], - each_amount = charge['each_amount'], - description = charge.get('description', ''), - invoice_type = 'one-time', + code=charge['code'], + quantity=charge['quantity'], + each_amount=charge['each_amount'], + description=charge.get('description', ''), + invoice_type='one-time', ) @clear_users def test_add_simple_one_time_invoice(self): - charges = [{ - 'code': 'immediate-test', - 'quantity': 1, - 'each_amount': Decimal(5.23) - },] + ''' Test adding a one time invoice. ''' + charges = [ + { + 'code': 'immediate-test', + 'quantity': 1, + 'each_amount': Decimal(5.23) + }] self.assertOneTimeInvoice(charges) @clear_users def test_add_one_time_invoice_with_description(self): - charges = [{ - 'code': 'immediate-test', - 'quantity': 1, - 'each_amount': Decimal(5.23), - 'description': 'This is a test charge' - },] + ''' Test adding a one time invoice with description. ''' + charges = [ + { + 'code': 'immediate-test', + 'quantity': 1, + 'each_amount': Decimal(5.23), + 'description': 'This is a test charge' + }] self.assertOneTimeInvoice(charges) - @clear_users def test_add_one_time_invoice_with_multiple_charges(self): - charges = [{ - 'code': 'immediate-test', - 'quantity': 1, - 'each_amount': Decimal(5.23), - 'description': 'This is a test charge' - }, - { - 'code': 'immediate-test-2', - 'quantity': 3, - 'each_amount': 15, - 'description': 'This is another test charge' - },] + ''' Test adding a one time invoice with multiple charges. ''' + charges = [ + { + 'code': 'immediate-test', + 'quantity': 1, + 'each_amount': Decimal(5.23), + 'description': 'This is a test charge' + }, + { + 'code': 'immediate-test-2', + 'quantity': 3, + 'each_amount': 15, + 'description': 'This is another test charge' + }] self.assertOneTimeInvoice(charges) + + def test_get_all_promotions(self): + ''' Test get all promotions. ''' + product = self.get_product() + promotions = product.get_all_promotions() + + self.assertEquals(2, len(promotions)) + for promotion in promotions: + assert promotion.coupons[0].get('code') in ('COUPON', 'COUPON2') + + def test_get_promotion(self): + ''' Test get a single promotion. ''' + product = self.get_product() + promotion = product.get_promotion('COUPON') + + self.assertEqual(unicode(promotion), 'Coupon (COUPON)') + self.assertEqual(promotion.name, 'Coupon') + self.assertEqual(promotion.coupons[0].get('code'), 'COUPON') + self.assertEqual(promotion.incentives[0].get('percentage'), '10') + self.assertEqual(promotion.incentives[0].get('expiration_datetime'), + None) + + def test_promotion_repr(self): + ''' Test the internal __repr___ method of Promotion. ''' + product = self.get_product() + promotion = product.get_promotion('COUPON') + + expected = 'Promotion: Coupon (COUPON)' + result = repr(promotion) + + self.assertEquals(expected, result) + + def test_promotion_unicode(self): + ''' Test the internal __unicode___ method of Promotion. ''' + product = self.get_product() + promotion = product.get_promotion('COUPON') + + expected = 'Coupon (COUPON)' + result = unicode(promotion) + + self.assertEquals(expected, result) diff --git a/tests/readme.rst b/tests/readme.rst new file mode 100644 index 0000000..325ac3d --- /dev/null +++ b/tests/readme.rst @@ -0,0 +1,74 @@ +Requirements +============ + +Inside a virtualenv, run: + +.. code:: + + pip install -r dev-requirements.txt + +Installing elementtree for Unit Testing +======================================================= +When trying to install elementtree, pip may report that there is no such package. If this happens to you, you can work around by downloading and installing it manually. + +.. code:: + + wget http://effbot.org/media/downloads/elementtree-1.2.6-20050316.zip + unzip elementtree-1.2.6-20050316.zip + cd elementtree-1.2.6-20050316/ + pip install . + +CheddarGetter Setup +============= +You will also need to setup the correct plans in cheddar. You may want to set up a product intended just for testing. + + + +The following tracked items are required for unit tests: + ++--------------+--------------+ +| Name | Code | ++==============+==============+ +| Once Item | ONCE_ITEM | ++--------------+--------------+ +| Monthly Item | MONTHLY_ITEM | ++--------------+--------------+ + +The following plan codes are required for unit tests: + ++-----------------+-----------------+---------+-----------+--------------+ +| Plan Name | Code | Price | ONCE_ITEM | MONTHLY_ITEM | ++=================+=================+=========+===========+==============+ +| Free Monthly | FREE_MONTHLY | $0.00 | 1 | 10 | ++-----------------+-----------------+---------+-----------+--------------+ +| Paid Monthly | PAID_MONTHLY | $10.00 | 1 | 10 | ++-----------------+-----------------+---------+-----------+--------------+ +| Tracked Monthly | TRACKED_MONTHLY | $10.00 | 1 | 10 | ++-----------------+-----------------+---------+-----------+--------------+ + + +The following promotions are required for unit tests: + ++----------------+---------------+--------+-----------+ +| Promotion Name | Coupon Code | % Off | Duration | ++================+===============+========+===========+ +| Coupon | COUPON | 10 | Forever | ++----------------+---------------+--------+-----------+ +| Coupon 2 | COUPON2 | 20 | Forever | ++----------------+---------------+--------+-----------+ + +Be sure to turn on the native gateway credit card option in Configuration > Product settings > Gateway Settings. +Be sure to turn on the paypal option in Configuration > Product settings > Gateway Settings or Quick Setup > Billing solution. I checked the "Use standard payments (PayPal account to PayPal account)" checkbox. + +Config +====== + +In the tests folder, copy the config.ini.template to config.ini. Fill in your email, password, and product code. + +Running Tests +============= +Run the test with nosetests. + +.. code:: + + nosetests diff --git a/tests/testing_tools/decorators.py b/tests/testing_tools/decorators.py index c5fe985..39dc774 100644 --- a/tests/testing_tools/decorators.py +++ b/tests/testing_tools/decorators.py @@ -2,20 +2,20 @@ from utils import clear_users as clear_users_func + def clear_users(func): ''' Calls cheddar's delete all users method no matter the test result ''' def new(*args, **kwargs): - raised_exception = None try: func(*args, **kwargs) - except Exception, e: + except Exception: clear_users_func() raise - + clear_users_func() - + new = make_decorator(func)(new) - - return new \ No newline at end of file + + return new diff --git a/tests/testing_tools/utils.py b/tests/testing_tools/utils.py index faa96bf..68cb0c8 100644 --- a/tests/testing_tools/utils.py +++ b/tests/testing_tools/utils.py @@ -3,6 +3,7 @@ from testconfig import config + def clear_users(): username = config['cheddar']['username'] password = config['cheddar']['password'] @@ -12,9 +13,13 @@ def clear_users(): h = httplib2.Http() h.add_credentials(username, password) - url = '%s/customers/delete-all/confirm/%d/productCode/%s' % (endpoint, int(time()), product) + url = '%s/customers/delete-all/confirm/%d/productCode/%s' % (endpoint, + int(time()), + product) response, content = h.request(url, 'POST') - if response.status != 200: - raise Exception('Could not clear users. Recieved a response of %s %s ' % (response.status, response.reason)) + if response.status != 200 or 'success' not in content: + raise Exception( + 'Could not clear users. Recieved a response of %s %s \n %s' % ( + response.status, response.reason, content))