diff --git a/recurly/__init__.py b/recurly/__init__.py
index 80811564..9c1e4325 100644
--- a/recurly/__init__.py
+++ b/recurly/__init__.py
@@ -1649,6 +1649,20 @@ def refund(self, **kwargs):
Transaction._classes_for_nodename['transaction'] = Transaction
+class PlanRampInterval(Resource):
+ """A plan ramp
+ representing a price point and the billing_cycle to begin that price point
+ """
+
+ nodename = 'ramp_interval'
+ collection_path = 'ramp_intervals'
+
+ attributes = {
+ 'starting_billing_cycle',
+ 'unit_amount_in_cents'
+ }
+
+
class Plan(Resource):
"""A service level for your service to which a customer account
@@ -1690,8 +1704,12 @@ class Plan(Resource):
'auto_renew',
'allow_any_item_on_subscriptions',
'dunning_campaign_id',
+ 'pricing_model',
+ 'ramp_intervals',
)
+ _classes_for_nodename = {'ramp_interval': PlanRampInterval }
+#
def get_add_on(self, add_on_code):
"""Return the `AddOn` for this plan with the given add-on code."""
url = urljoin(self._url, '/add_ons/%s' % (add_on_code,))
diff --git a/recurly/resource.py b/recurly/resource.py
index 295b1e2c..8ce4fad5 100644
--- a/recurly/resource.py
+++ b/recurly/resource.py
@@ -445,11 +445,34 @@ def value_for_element(cls, elem):
return value_class.from_element(elem)
# Untyped complex elements should still be resource instances. Guess from the nodename.
- if len(elem): # has children
+ if len(elem) == 1:
value_class = cls._subclass_for_nodename(elem.tag)
log.debug("Converting %r tag into a %s", elem.tag, value_class.__name__)
return value_class.from_element(elem)
+ # Tries to deserialize arrays of elements without type 'array' definition or resource
+ if len(elem) > 1:
+ first_tag = elem[0].tag
+ last_tag = elem[-1].tag
+ # Check if the element have and array of items
+ #
+ # - ...
+ # - ...
+ # - ...
+ #
+ if(first_tag == last_tag):
+ return [cls._subclass_for_nodename(sub_elem.tag).from_element(sub_elem) for sub_elem in elem]
+ # De-serialize one resource
+ #
+ # name
+ # description
+ # other text
+ #
+ else:
+ value_class = cls._subclass_for_nodename(elem.tag)
+ log.debug("Converting %r tag into a %s", elem.tag, value_class.__name__)
+ return value_class.from_element(elem)
+
value = elem.text or ''
return value.strip()
@@ -606,7 +629,7 @@ def relatitator(**kwargs):
url = elem.attrib['href']
# has no url or has children
- if url is '' or len(elem) > 0:
+ if url == '' or len(elem) > 0:
return self.value_for_element(elem)
else:
return make_relatitator(url)
diff --git a/tests/fixtures/plan/created_with_ramps.xml b/tests/fixtures/plan/created_with_ramps.xml
new file mode 100644
index 00000000..4109da4a
--- /dev/null
+++ b/tests/fixtures/plan/created_with_ramps.xml
@@ -0,0 +1,75 @@
+POST https://api.recurly.com/v2/plans HTTP/1.1
+X-Api-Version: {api-version}
+Accept: application/xml
+Authorization: Basic YXBpa2V5Og==
+User-Agent: {user-agent}
+Content-Type: application/xml; charset=utf-8
+
+
+
+ planmock
+ Mock Plan
+
+ 200
+
+ 10
+ ramp
+
+
+
+ 2000
+
+ 1
+
+
+
+ 3000
+
+ 2
+
+
+
+
+HTTP/1.1 201 Created
+Content-Type: application/xml; charset=utf-8
+Location: https://api.recurly.com/v2/plans/planmock
+
+
+
+
+ planmock
+ Mock Plan
+
+
+
+ false
+ false
+ false
+ false
+ unit
+
+ 1
+ months
+ 0
+ days
+ 10
+ 2011-10-03T22:23:12Z
+ ramp
+
+ 200
+
+
+
+ 1
+
+ 2000
+
+
+
+ 2
+
+ 3000
+
+
+
+
diff --git a/tests/fixtures/plan/exists_with_ramps.xml b/tests/fixtures/plan/exists_with_ramps.xml
new file mode 100644
index 00000000..11ff4aee
--- /dev/null
+++ b/tests/fixtures/plan/exists_with_ramps.xml
@@ -0,0 +1,52 @@
+GET https://api.recurly.com/v2/plans/planmock HTTP/1.1
+X-Api-Version: {api-version}
+Accept: application/xml
+Authorization: Basic YXBpa2V5Og==
+User-Agent: {user-agent}
+
+
+HTTP/1.1 200 OK
+Content-Type: application/xml; charset=utf-8
+
+
+
+
+ planmock
+ Mock Plan
+
+
+
+ false
+ false
+ false
+ false
+ unit
+
+ 1
+ months
+ 0
+ days
+ 10
+ 2011-10-03T22:23:12Z
+ false
+
+
+ ramp
+
+ 200
+
+
+
+ 1
+
+ 2000
+
+
+
+ 2
+
+ 3000
+
+
+
+
diff --git a/tests/fixtures/plan/updated_with_ramps.xml b/tests/fixtures/plan/updated_with_ramps.xml
new file mode 100644
index 00000000..6754ca67
--- /dev/null
+++ b/tests/fixtures/plan/updated_with_ramps.xml
@@ -0,0 +1,66 @@
+PUT https://api.recurly.com/v2/plans/planmock HTTP/1.1
+X-Api-Version: {api-version}
+Accept: application/xml
+Authorization: Basic YXBpa2V5Og==
+User-Agent: {user-agent}
+Content-Type: application/xml; charset=utf-8
+
+
+
+
+
+
+ 3000
+
+ 1
+
+
+
+ 4000
+
+ 2
+
+
+
+
+HTTP/1.1 200 OK
+Content-Type: application/xml; charset=utf-8
+
+
+
+
+ planmock
+ Mock Plan
+
+
+
+ false
+ false
+ false
+ false
+ unit
+
+ 2
+ months
+ 0
+ days
+ Setup Fee AC
+ 2011-10-03T22:23:12Z
+
+ 200
+
+
+
+ 1
+
+ 3000
+
+
+
+ 2
+
+ 4000
+
+
+
+
diff --git a/tests/test_resources.py b/tests/test_resources.py
index d6033550..fd625ec4 100644
--- a/tests/test_resources.py
+++ b/tests/test_resources.py
@@ -9,7 +9,7 @@
from recurly import Account, AddOn, Address, Adjustment, BillingInfo, Coupon, Item, Plan, Redemption, Subscription, \
SubscriptionAddOn, Transaction, MeasuredUnit, Usage, GiftCard, Delivery, ShippingAddress, AccountAcquisition, \
Purchase, Invoice, InvoiceCollection, CreditPayment, CustomField, ExportDate, ExportDateFile, DunningCampaign, \
- DunningCycle, InvoiceTemplate
+ DunningCycle, InvoiceTemplate, PlanRampInterval
from recurly import Money, NotFoundError, ValidationError, BadRequestError, PageError
from recurly import recurly_logging as logging
from recurlytests import RecurlyTest
@@ -1471,6 +1471,57 @@ def test_plan(self):
plan = Plan.get(plan_code)
self.assertTrue(plan.tax_exempt)
+ def test_plan_with_ramps(self):
+ plan_code = 'plan%s' % self.test_id
+ with self.mock_request('plan/does-not-exist.xml'):
+ self.assertRaises(NotFoundError, Plan.get, plan_code)
+
+ ramp_interval_1 = PlanRampInterval(
+ unit_amount_in_cents=Money(USD=2000),
+ starting_billing_cycle=1,
+ )
+ ramp_interval_2 = PlanRampInterval(
+ unit_amount_in_cents=Money(USD=3000),
+ starting_billing_cycle=2,
+ )
+ ramp_intervals = [ramp_interval_1, ramp_interval_2]
+
+ plan = Plan(
+ plan_code=plan_code,
+ name='Mock Plan',
+ setup_fee_in_cents=Money(200),
+ pricing_model='ramp',
+ ramp_intervals=ramp_intervals,
+ total_billing_cycles=10
+ )
+ with self.mock_request('plan/created_with_ramps.xml'):
+ plan.save()
+
+ self.assertEqual(plan.plan_code, plan_code)
+ self.assertEqual(len(plan.ramp_intervals), len(ramp_intervals))
+ self.assertEqual(plan.pricing_model, 'ramp')
+
+ self.assertEqual(plan.plan_code, plan_code)
+
+ with self.mock_request('plan/exists_with_ramps.xml'):
+ same_plan = Plan.get(plan_code)
+ self.assertEqual(same_plan.plan_code, plan_code)
+ self.assertEqual(len(plan.ramp_intervals), len(ramp_intervals))
+ self.assertEqual(plan.pricing_model, 'ramp')
+
+ plan.ramp_intervals = [
+ PlanRampInterval(
+ starting_billing_cycle=1,
+ unit_amount_in_cents=Money(USD=3000)
+ ),
+ PlanRampInterval(
+ starting_billing_cycle=2,
+ unit_amount_in_cents=Money(USD=4000)
+ ),
+ ]
+ with self.mock_request('plan/updated_with_ramps.xml'):
+ plan.save()
+
def test_preview_subscription_change(self):
with self.mock_request('subscription/show.xml'):
sub = Subscription.get('123456789012345678901234567890ab')