Skip to content

Commit

Permalink
Merge pull request #574 from recurly/ramp-plans-support
Browse files Browse the repository at this point in the history
Added support for ramp-pricing plans
  • Loading branch information
Patrick-Duvall committed Aug 4, 2022
2 parents 78146c3 + dcf4b55 commit 80de0f8
Show file tree
Hide file tree
Showing 6 changed files with 288 additions and 3 deletions.
18 changes: 18 additions & 0 deletions recurly/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,))
Expand Down
27 changes: 25 additions & 2 deletions recurly/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# <items>
# <item>...</item>
# <item>...</item>
# <item>...</item>
# </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
# <resource>
# <name>name</name>
# <description>description</name>
# <other_attribute>other text</other_description>
# </resource>
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()

Expand Down Expand Up @@ -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)
Expand Down
75 changes: 75 additions & 0 deletions tests/fixtures/plan/created_with_ramps.xml
Original file line number Diff line number Diff line change
@@ -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

<?xml version="1.0" encoding="UTF-8"?>
<plan>
<plan_code>planmock</plan_code>
<name>Mock Plan</name>
<setup_fee_in_cents>
<USD type="integer">200</USD>
</setup_fee_in_cents>
<total_billing_cycles type="integer">10</total_billing_cycles>
<pricing_model>ramp</pricing_model>
<ramp_intervals>
<ramp_interval>
<unit_amount_in_cents>
<USD type="integer">2000</USD>
</unit_amount_in_cents>
<starting_billing_cycle type="integer">1</starting_billing_cycle>
</ramp_interval>
<ramp_interval>
<unit_amount_in_cents>
<USD type="integer">3000</USD>
</unit_amount_in_cents>
<starting_billing_cycle type="integer">2</starting_billing_cycle>
</ramp_interval>
</ramp_intervals>
</plan>

HTTP/1.1 201 Created
Content-Type: application/xml; charset=utf-8
Location: https://api.recurly.com/v2/plans/planmock

<?xml version="1.0" encoding="UTF-8"?>
<plan href="https://api.recurly.com/v2/plans/planmock">
<add_ons href="https://api.recurly.com/v2/plans/planmock/add_ons"/>
<plan_code>planmock</plan_code>
<name>Mock Plan</name>
<description nil="nil"></description>
<success_url nil="nil"></success_url>
<cancel_url nil="nil"></cancel_url>
<display_donation_amounts type="boolean">false</display_donation_amounts>
<display_quantity type="boolean">false</display_quantity>
<display_phone_number type="boolean">false</display_phone_number>
<bypass_hosted_confirmation type="boolean">false</bypass_hosted_confirmation>
<unit_name>unit</unit_name>
<payment_page_tos_link nil="nil"></payment_page_tos_link>
<plan_interval_length type="integer">1</plan_interval_length>
<plan_interval_unit>months</plan_interval_unit>
<trial_interval_length type="integer">0</trial_interval_length>
<trial_interval_unit>days</trial_interval_unit>
<total_billing_cycles type="integer">10</total_billing_cycles>
<created_at type="datetime">2011-10-03T22:23:12Z</created_at>
<pricing_model>ramp</pricing_model>
<setup_fee_in_cents>
<USD type="integer">200</USD>
</setup_fee_in_cents>
<ramp_intervals>
<ramp_interval>
<starting_billing_cycle type="integer">1</starting_billing_cycle>
<unit_amount_in_cents>
<USD type="integer">2000</USD>
</unit_amount_in_cents>
</ramp_interval>
<ramp_interval>
<starting_billing_cycle type="integer">2</starting_billing_cycle>
<unit_amount_in_cents>
<USD type="integer">3000</USD>
</unit_amount_in_cents>
</ramp_interval>
</ramp_intervals>
</plan>
52 changes: 52 additions & 0 deletions tests/fixtures/plan/exists_with_ramps.xml
Original file line number Diff line number Diff line change
@@ -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

<?xml version="1.0" encoding="UTF-8"?>
<plan href="https://api.recurly.com/v2/plans/planmock">
<add_ons href="https://api.recurly.com/v2/plans/planmock/add_ons"/>
<plan_code>planmock</plan_code>
<name>Mock Plan</name>
<description nil="nil"></description>
<success_url nil="nil"></success_url>
<cancel_url nil="nil"></cancel_url>
<display_donation_amounts type="boolean">false</display_donation_amounts>
<display_quantity type="boolean">false</display_quantity>
<display_phone_number type="boolean">false</display_phone_number>
<bypass_hosted_confirmation type="boolean">false</bypass_hosted_confirmation>
<unit_name>unit</unit_name>
<payment_page_tos_link nil="nil"></payment_page_tos_link>
<plan_interval_length type="integer">1</plan_interval_length>
<plan_interval_unit>months</plan_interval_unit>
<trial_interval_length type="integer">0</trial_interval_length>
<trial_interval_unit>days</trial_interval_unit>
<total_billing_cycles type="integer">10</total_billing_cycles>
<created_at type="datetime">2011-10-03T22:23:12Z</created_at>
<trial_requires_billing_info type="boolean">false</trial_requires_billing_info>
<unit_amount_in_cents>
</unit_amount_in_cents>
<pricing_model>ramp</pricing_model>
<setup_fee_in_cents>
<USD type="integer">200</USD>
</setup_fee_in_cents>
<ramp_intervals>
<ramp_interval>
<starting_billing_cycle type="integer">1</starting_billing_cycle>
<unit_amount_in_cents>
<USD type="integer">2000</USD>
</unit_amount_in_cents>
</ramp_interval>
<ramp_interval>
<starting_billing_cycle type="integer">2</starting_billing_cycle>
<unit_amount_in_cents>
<USD type="integer">3000</USD>
</unit_amount_in_cents>
</ramp_interval>
</ramp_intervals>
</plan>
66 changes: 66 additions & 0 deletions tests/fixtures/plan/updated_with_ramps.xml
Original file line number Diff line number Diff line change
@@ -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

<?xml version="1.0" encoding="UTF-8"?>
<plan>
<ramp_intervals>
<ramp_interval>
<unit_amount_in_cents>
<USD type="integer">3000</USD>
</unit_amount_in_cents>
<starting_billing_cycle type="integer">1</starting_billing_cycle>
</ramp_interval>
<ramp_interval>
<unit_amount_in_cents>
<USD type="integer">4000</USD>
</unit_amount_in_cents>
<starting_billing_cycle type="integer">2</starting_billing_cycle>
</ramp_interval>
</ramp_intervals>
</plan>

HTTP/1.1 200 OK
Content-Type: application/xml; charset=utf-8

<?xml version="1.0" encoding="UTF-8"?>
<plan href="https://api.recurly.com/v2/plans/planmock">
<add_ons href="https://api.recurly.com/v2/plans/planmock/add_ons"/>
<plan_code>planmock</plan_code>
<name>Mock Plan</name>
<description nil="nil"></description>
<success_url nil="nil"></success_url>
<cancel_url nil="nil"></cancel_url>
<display_donation_amounts type="boolean">false</display_donation_amounts>
<display_quantity type="boolean">false</display_quantity>
<display_phone_number type="boolean">false</display_phone_number>
<bypass_hosted_confirmation type="boolean">false</bypass_hosted_confirmation>
<unit_name>unit</unit_name>
<payment_page_tos_link nil="nil"></payment_page_tos_link>
<plan_interval_length type="integer">2</plan_interval_length>
<plan_interval_unit>months</plan_interval_unit>
<trial_interval_length type="integer">0</trial_interval_length>
<trial_interval_unit>days</trial_interval_unit>
<setup_fee_accounting_code>Setup Fee AC</setup_fee_accounting_code>
<created_at type="datetime">2011-10-03T22:23:12Z</created_at>
<setup_fee_in_cents>
<USD type="integer">200</USD>
</setup_fee_in_cents>
<ramp_intervals>
<ramp_interval>
<starting_billing_cycle type="integer">1</starting_billing_cycle>
<unit_amount_in_cents>
<USD type="integer">3000</USD>
</unit_amount_in_cents>
</ramp_interval>
<ramp_interval>
<starting_billing_cycle type="integer">2</starting_billing_cycle>
<unit_amount_in_cents>
<USD type="integer">4000</USD>
</unit_amount_in_cents>
</ramp_interval>
</ramp_intervals>
</plan>
53 changes: 52 additions & 1 deletion tests/test_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down

0 comments on commit 80de0f8

Please sign in to comment.