Skip to content

Commit 80de0f8

Browse files
Merge pull request #574 from recurly/ramp-plans-support
Added support for ramp-pricing plans
2 parents 78146c3 + dcf4b55 commit 80de0f8

File tree

6 files changed

+288
-3
lines changed

6 files changed

+288
-3
lines changed

recurly/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1649,6 +1649,20 @@ def refund(self, **kwargs):
16491649
Transaction._classes_for_nodename['transaction'] = Transaction
16501650

16511651

1652+
class PlanRampInterval(Resource):
1653+
"""A plan ramp
1654+
representing a price point and the billing_cycle to begin that price point
1655+
"""
1656+
1657+
nodename = 'ramp_interval'
1658+
collection_path = 'ramp_intervals'
1659+
1660+
attributes = {
1661+
'starting_billing_cycle',
1662+
'unit_amount_in_cents'
1663+
}
1664+
1665+
16521666
class Plan(Resource):
16531667

16541668
"""A service level for your service to which a customer account
@@ -1690,8 +1704,12 @@ class Plan(Resource):
16901704
'auto_renew',
16911705
'allow_any_item_on_subscriptions',
16921706
'dunning_campaign_id',
1707+
'pricing_model',
1708+
'ramp_intervals',
16931709
)
16941710

1711+
_classes_for_nodename = {'ramp_interval': PlanRampInterval }
1712+
#
16951713
def get_add_on(self, add_on_code):
16961714
"""Return the `AddOn` for this plan with the given add-on code."""
16971715
url = urljoin(self._url, '/add_ons/%s' % (add_on_code,))

recurly/resource.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -445,11 +445,34 @@ def value_for_element(cls, elem):
445445
return value_class.from_element(elem)
446446

447447
# Untyped complex elements should still be resource instances. Guess from the nodename.
448-
if len(elem): # has children
448+
if len(elem) == 1:
449449
value_class = cls._subclass_for_nodename(elem.tag)
450450
log.debug("Converting %r tag into a %s", elem.tag, value_class.__name__)
451451
return value_class.from_element(elem)
452452

453+
# Tries to deserialize arrays of elements without type 'array' definition or resource
454+
if len(elem) > 1:
455+
first_tag = elem[0].tag
456+
last_tag = elem[-1].tag
457+
# Check if the element have and array of items
458+
# <items>
459+
# <item>...</item>
460+
# <item>...</item>
461+
# <item>...</item>
462+
# </items>
463+
if(first_tag == last_tag):
464+
return [cls._subclass_for_nodename(sub_elem.tag).from_element(sub_elem) for sub_elem in elem]
465+
# De-serialize one resource
466+
# <resource>
467+
# <name>name</name>
468+
# <description>description</name>
469+
# <other_attribute>other text</other_description>
470+
# </resource>
471+
else:
472+
value_class = cls._subclass_for_nodename(elem.tag)
473+
log.debug("Converting %r tag into a %s", elem.tag, value_class.__name__)
474+
return value_class.from_element(elem)
475+
453476
value = elem.text or ''
454477
return value.strip()
455478

@@ -606,7 +629,7 @@ def relatitator(**kwargs):
606629
url = elem.attrib['href']
607630

608631
# has no url or has children
609-
if url is '' or len(elem) > 0:
632+
if url == '' or len(elem) > 0:
610633
return self.value_for_element(elem)
611634
else:
612635
return make_relatitator(url)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
POST https://api.recurly.com/v2/plans HTTP/1.1
2+
X-Api-Version: {api-version}
3+
Accept: application/xml
4+
Authorization: Basic YXBpa2V5Og==
5+
User-Agent: {user-agent}
6+
Content-Type: application/xml; charset=utf-8
7+
8+
<?xml version="1.0" encoding="UTF-8"?>
9+
<plan>
10+
<plan_code>planmock</plan_code>
11+
<name>Mock Plan</name>
12+
<setup_fee_in_cents>
13+
<USD type="integer">200</USD>
14+
</setup_fee_in_cents>
15+
<total_billing_cycles type="integer">10</total_billing_cycles>
16+
<pricing_model>ramp</pricing_model>
17+
<ramp_intervals>
18+
<ramp_interval>
19+
<unit_amount_in_cents>
20+
<USD type="integer">2000</USD>
21+
</unit_amount_in_cents>
22+
<starting_billing_cycle type="integer">1</starting_billing_cycle>
23+
</ramp_interval>
24+
<ramp_interval>
25+
<unit_amount_in_cents>
26+
<USD type="integer">3000</USD>
27+
</unit_amount_in_cents>
28+
<starting_billing_cycle type="integer">2</starting_billing_cycle>
29+
</ramp_interval>
30+
</ramp_intervals>
31+
</plan>
32+

33+
HTTP/1.1 201 Created
34+
Content-Type: application/xml; charset=utf-8
35+
Location: https://api.recurly.com/v2/plans/planmock
36+
37+
<?xml version="1.0" encoding="UTF-8"?>
38+
<plan href="https://api.recurly.com/v2/plans/planmock">
39+
<add_ons href="https://api.recurly.com/v2/plans/planmock/add_ons"/>
40+
<plan_code>planmock</plan_code>
41+
<name>Mock Plan</name>
42+
<description nil="nil"></description>
43+
<success_url nil="nil"></success_url>
44+
<cancel_url nil="nil"></cancel_url>
45+
<display_donation_amounts type="boolean">false</display_donation_amounts>
46+
<display_quantity type="boolean">false</display_quantity>
47+
<display_phone_number type="boolean">false</display_phone_number>
48+
<bypass_hosted_confirmation type="boolean">false</bypass_hosted_confirmation>
49+
<unit_name>unit</unit_name>
50+
<payment_page_tos_link nil="nil"></payment_page_tos_link>
51+
<plan_interval_length type="integer">1</plan_interval_length>
52+
<plan_interval_unit>months</plan_interval_unit>
53+
<trial_interval_length type="integer">0</trial_interval_length>
54+
<trial_interval_unit>days</trial_interval_unit>
55+
<total_billing_cycles type="integer">10</total_billing_cycles>
56+
<created_at type="datetime">2011-10-03T22:23:12Z</created_at>
57+
<pricing_model>ramp</pricing_model>
58+
<setup_fee_in_cents>
59+
<USD type="integer">200</USD>
60+
</setup_fee_in_cents>
61+
<ramp_intervals>
62+
<ramp_interval>
63+
<starting_billing_cycle type="integer">1</starting_billing_cycle>
64+
<unit_amount_in_cents>
65+
<USD type="integer">2000</USD>
66+
</unit_amount_in_cents>
67+
</ramp_interval>
68+
<ramp_interval>
69+
<starting_billing_cycle type="integer">2</starting_billing_cycle>
70+
<unit_amount_in_cents>
71+
<USD type="integer">3000</USD>
72+
</unit_amount_in_cents>
73+
</ramp_interval>
74+
</ramp_intervals>
75+
</plan>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
GET https://api.recurly.com/v2/plans/planmock HTTP/1.1
2+
X-Api-Version: {api-version}
3+
Accept: application/xml
4+
Authorization: Basic YXBpa2V5Og==
5+
User-Agent: {user-agent}
6+
7+

8+
HTTP/1.1 200 OK
9+
Content-Type: application/xml; charset=utf-8
10+
11+
<?xml version="1.0" encoding="UTF-8"?>
12+
<plan href="https://api.recurly.com/v2/plans/planmock">
13+
<add_ons href="https://api.recurly.com/v2/plans/planmock/add_ons"/>
14+
<plan_code>planmock</plan_code>
15+
<name>Mock Plan</name>
16+
<description nil="nil"></description>
17+
<success_url nil="nil"></success_url>
18+
<cancel_url nil="nil"></cancel_url>
19+
<display_donation_amounts type="boolean">false</display_donation_amounts>
20+
<display_quantity type="boolean">false</display_quantity>
21+
<display_phone_number type="boolean">false</display_phone_number>
22+
<bypass_hosted_confirmation type="boolean">false</bypass_hosted_confirmation>
23+
<unit_name>unit</unit_name>
24+
<payment_page_tos_link nil="nil"></payment_page_tos_link>
25+
<plan_interval_length type="integer">1</plan_interval_length>
26+
<plan_interval_unit>months</plan_interval_unit>
27+
<trial_interval_length type="integer">0</trial_interval_length>
28+
<trial_interval_unit>days</trial_interval_unit>
29+
<total_billing_cycles type="integer">10</total_billing_cycles>
30+
<created_at type="datetime">2011-10-03T22:23:12Z</created_at>
31+
<trial_requires_billing_info type="boolean">false</trial_requires_billing_info>
32+
<unit_amount_in_cents>
33+
</unit_amount_in_cents>
34+
<pricing_model>ramp</pricing_model>
35+
<setup_fee_in_cents>
36+
<USD type="integer">200</USD>
37+
</setup_fee_in_cents>
38+
<ramp_intervals>
39+
<ramp_interval>
40+
<starting_billing_cycle type="integer">1</starting_billing_cycle>
41+
<unit_amount_in_cents>
42+
<USD type="integer">2000</USD>
43+
</unit_amount_in_cents>
44+
</ramp_interval>
45+
<ramp_interval>
46+
<starting_billing_cycle type="integer">2</starting_billing_cycle>
47+
<unit_amount_in_cents>
48+
<USD type="integer">3000</USD>
49+
</unit_amount_in_cents>
50+
</ramp_interval>
51+
</ramp_intervals>
52+
</plan>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
PUT https://api.recurly.com/v2/plans/planmock HTTP/1.1
2+
X-Api-Version: {api-version}
3+
Accept: application/xml
4+
Authorization: Basic YXBpa2V5Og==
5+
User-Agent: {user-agent}
6+
Content-Type: application/xml; charset=utf-8
7+
8+
<?xml version="1.0" encoding="UTF-8"?>
9+
<plan>
10+
<ramp_intervals>
11+
<ramp_interval>
12+
<unit_amount_in_cents>
13+
<USD type="integer">3000</USD>
14+
</unit_amount_in_cents>
15+
<starting_billing_cycle type="integer">1</starting_billing_cycle>
16+
</ramp_interval>
17+
<ramp_interval>
18+
<unit_amount_in_cents>
19+
<USD type="integer">4000</USD>
20+
</unit_amount_in_cents>
21+
<starting_billing_cycle type="integer">2</starting_billing_cycle>
22+
</ramp_interval>
23+
</ramp_intervals>
24+
</plan>
25+

26+
HTTP/1.1 200 OK
27+
Content-Type: application/xml; charset=utf-8
28+
29+
<?xml version="1.0" encoding="UTF-8"?>
30+
<plan href="https://api.recurly.com/v2/plans/planmock">
31+
<add_ons href="https://api.recurly.com/v2/plans/planmock/add_ons"/>
32+
<plan_code>planmock</plan_code>
33+
<name>Mock Plan</name>
34+
<description nil="nil"></description>
35+
<success_url nil="nil"></success_url>
36+
<cancel_url nil="nil"></cancel_url>
37+
<display_donation_amounts type="boolean">false</display_donation_amounts>
38+
<display_quantity type="boolean">false</display_quantity>
39+
<display_phone_number type="boolean">false</display_phone_number>
40+
<bypass_hosted_confirmation type="boolean">false</bypass_hosted_confirmation>
41+
<unit_name>unit</unit_name>
42+
<payment_page_tos_link nil="nil"></payment_page_tos_link>
43+
<plan_interval_length type="integer">2</plan_interval_length>
44+
<plan_interval_unit>months</plan_interval_unit>
45+
<trial_interval_length type="integer">0</trial_interval_length>
46+
<trial_interval_unit>days</trial_interval_unit>
47+
<setup_fee_accounting_code>Setup Fee AC</setup_fee_accounting_code>
48+
<created_at type="datetime">2011-10-03T22:23:12Z</created_at>
49+
<setup_fee_in_cents>
50+
<USD type="integer">200</USD>
51+
</setup_fee_in_cents>
52+
<ramp_intervals>
53+
<ramp_interval>
54+
<starting_billing_cycle type="integer">1</starting_billing_cycle>
55+
<unit_amount_in_cents>
56+
<USD type="integer">3000</USD>
57+
</unit_amount_in_cents>
58+
</ramp_interval>
59+
<ramp_interval>
60+
<starting_billing_cycle type="integer">2</starting_billing_cycle>
61+
<unit_amount_in_cents>
62+
<USD type="integer">4000</USD>
63+
</unit_amount_in_cents>
64+
</ramp_interval>
65+
</ramp_intervals>
66+
</plan>

tests/test_resources.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from recurly import Account, AddOn, Address, Adjustment, BillingInfo, Coupon, Item, Plan, Redemption, Subscription, \
1010
SubscriptionAddOn, Transaction, MeasuredUnit, Usage, GiftCard, Delivery, ShippingAddress, AccountAcquisition, \
1111
Purchase, Invoice, InvoiceCollection, CreditPayment, CustomField, ExportDate, ExportDateFile, DunningCampaign, \
12-
DunningCycle, InvoiceTemplate
12+
DunningCycle, InvoiceTemplate, PlanRampInterval
1313
from recurly import Money, NotFoundError, ValidationError, BadRequestError, PageError
1414
from recurly import recurly_logging as logging
1515
from recurlytests import RecurlyTest
@@ -1471,6 +1471,57 @@ def test_plan(self):
14711471
plan = Plan.get(plan_code)
14721472
self.assertTrue(plan.tax_exempt)
14731473

1474+
def test_plan_with_ramps(self):
1475+
plan_code = 'plan%s' % self.test_id
1476+
with self.mock_request('plan/does-not-exist.xml'):
1477+
self.assertRaises(NotFoundError, Plan.get, plan_code)
1478+
1479+
ramp_interval_1 = PlanRampInterval(
1480+
unit_amount_in_cents=Money(USD=2000),
1481+
starting_billing_cycle=1,
1482+
)
1483+
ramp_interval_2 = PlanRampInterval(
1484+
unit_amount_in_cents=Money(USD=3000),
1485+
starting_billing_cycle=2,
1486+
)
1487+
ramp_intervals = [ramp_interval_1, ramp_interval_2]
1488+
1489+
plan = Plan(
1490+
plan_code=plan_code,
1491+
name='Mock Plan',
1492+
setup_fee_in_cents=Money(200),
1493+
pricing_model='ramp',
1494+
ramp_intervals=ramp_intervals,
1495+
total_billing_cycles=10
1496+
)
1497+
with self.mock_request('plan/created_with_ramps.xml'):
1498+
plan.save()
1499+
1500+
self.assertEqual(plan.plan_code, plan_code)
1501+
self.assertEqual(len(plan.ramp_intervals), len(ramp_intervals))
1502+
self.assertEqual(plan.pricing_model, 'ramp')
1503+
1504+
self.assertEqual(plan.plan_code, plan_code)
1505+
1506+
with self.mock_request('plan/exists_with_ramps.xml'):
1507+
same_plan = Plan.get(plan_code)
1508+
self.assertEqual(same_plan.plan_code, plan_code)
1509+
self.assertEqual(len(plan.ramp_intervals), len(ramp_intervals))
1510+
self.assertEqual(plan.pricing_model, 'ramp')
1511+
1512+
plan.ramp_intervals = [
1513+
PlanRampInterval(
1514+
starting_billing_cycle=1,
1515+
unit_amount_in_cents=Money(USD=3000)
1516+
),
1517+
PlanRampInterval(
1518+
starting_billing_cycle=2,
1519+
unit_amount_in_cents=Money(USD=4000)
1520+
),
1521+
]
1522+
with self.mock_request('plan/updated_with_ramps.xml'):
1523+
plan.save()
1524+
14741525
def test_preview_subscription_change(self):
14751526
with self.mock_request('subscription/show.xml'):
14761527
sub = Subscription.get('123456789012345678901234567890ab')

0 commit comments

Comments
 (0)