Skip to content

Commit

Permalink
Handle missing SubscriptionItem.quantity on metered plans
Browse files Browse the repository at this point in the history
  • Loading branch information
therefromhere committed Mar 17, 2019
1 parent aec23be commit 032fe36
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 4 deletions.
1 change: 1 addition & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ This is a bugfix-only version:
At some point 2018 Stripe silently changed the ID used for test events and
``evt_00000000000000`` is not used anymore.
- Fixed OperationalError seen in migration 0003 on postgres (#850).
- Fixup missing ``SubscriptionItem.quantity`` on Plans with ``usage_type="metered"`` (#865).

2.0.0 (2019-03-01)
------------------
Expand Down
9 changes: 9 additions & 0 deletions djstripe/models/billing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1199,6 +1199,7 @@ class SubscriptionItem(StripeModel):
related_name="subscription_items",
help_text="The plan the customer is subscribed to.",
)
# TODO - quantity should be nullable, since it's not set for metered plans
quantity = models.PositiveIntegerField(
help_text=("The quantity of the plan to which the customer should be subscribed.")
)
Expand All @@ -1209,6 +1210,14 @@ class SubscriptionItem(StripeModel):
help_text="The subscription this subscription item belongs to.",
)

@classmethod
def _manipulate_stripe_object_hook(cls, data):
# for metered plans quantity isn't set, so should be nullable
# arguably this should default to 1 instead of 0 (to match subscription)
data["quantity"] = data.get("quantity") or 0

return data


class UsageRecord(StripeModel):
"""
Expand Down
60 changes: 59 additions & 1 deletion tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,27 @@ def capture(self):
],
}

FAKE_PLAN_METERED = {
"id": "plan_fakemetered",
"object": "plan",
"active": True,
"aggregate_usage": "sum",
"amount": 200,
"billing_scheme": "per_unit",
"created": 1552632817,
"currency": "usd",
"interval": "month",
"interval_count": 1,
"livemode": False,
"metadata": {},
"nickname": "Sum Metered Plan",
"tiers": None,
"tiers_mode": None,
"transform_usage": None,
"trial_period_days": None,
"usage_type": "metered",
}


class SubscriptionDict(dict):
def __setattr__(self, name, value):
Expand All @@ -778,7 +799,7 @@ def __setattr__(self, name, value):

# Special case for plan
if name == "plan":
for plan in [FAKE_PLAN, FAKE_PLAN_II]:
for plan in [FAKE_PLAN, FAKE_PLAN_II, FAKE_TIER_PLAN, FAKE_PLAN_METERED]:
if value == plan["id"]:
value = plan

Expand Down Expand Up @@ -925,6 +946,43 @@ def save(self, idempotency_key=None):
)


FAKE_SUBSCRIPTION_METERED = SubscriptionDict(
{
"id": "sub_1rn1dp7WgjMtx9",
"object": "subscription",
"application_fee_percent": None,
"billing": "charge_automatically",
"cancel_at_period_end": False,
"canceled_at": None,
"current_period_end": 1441907581,
"current_period_start": 1439229181,
"customer": "cus_6lsBvm5rJ0zyHc",
"discount": None,
"ended_at": None,
"metadata": {},
"items": {
"data": [
{
"created": 1441907581,
"id": "si_UXYmKmJp6aWTw6",
"metadata": {},
"object": "subscription_item",
"plan": deepcopy(FAKE_PLAN_METERED),
"subscription": "sub_1rn1dp7WgjMtx9",
}
]
},
"plan": deepcopy(FAKE_PLAN_METERED),
"quantity": 1,
"start": 1439229181,
"status": "active",
"tax_percent": None,
"trial_end": None,
"trial_start": None,
}
)


class Sources(object):
def __init__(self, card_fakes):
self.card_fakes = card_fakes
Expand Down
17 changes: 16 additions & 1 deletion tests/test_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
from django.test import TestCase

from djstripe.admin import PlanAdmin
from djstripe.enums import PlanUsageType
from djstripe.models import Plan
from djstripe.settings import STRIPE_SECRET_KEY

from . import FAKE_PLAN, FAKE_PLAN_II, FAKE_TIER_PLAN, AssertStripeFksMixin
from . import (
FAKE_PLAN, FAKE_PLAN_II, FAKE_PLAN_METERED, FAKE_TIER_PLAN, AssertStripeFksMixin
)


class TestPlanAdmin(TestCase):
Expand Down Expand Up @@ -116,6 +119,18 @@ def test_stripe_tier_plan(self, plan_retrieve_mock):
plan, expected_blank_fks={"djstripe.Customer.coupon", "djstripe.Plan.product"}
)

@patch("stripe.Plan.retrieve")
def test_stripe_metered_plan(self, plan_retrieve_mock):
plan_data = deepcopy(FAKE_PLAN_METERED)
plan = Plan.sync_from_stripe_data(plan_data)
self.assertEqual(plan.id, plan_data["id"])
self.assertEqual(plan.usage_type, PlanUsageType.metered)
self.assertIsNotNone(plan.amount)

self.assert_fks(
plan, expected_blank_fks={"djstripe.Customer.coupon", "djstripe.Plan.product"}
)


class HumanReadablePlanTest(TestCase):
def test_human_readable_free_usd_daily(self):
Expand Down
47 changes: 45 additions & 2 deletions tests/test_subscription.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
from djstripe.models import Plan, Subscription

from . import (
FAKE_CUSTOMER, FAKE_CUSTOMER_II, FAKE_PLAN, FAKE_PLAN_II,
FAKE_SUBSCRIPTION, FAKE_SUBSCRIPTION_CANCELED,
FAKE_CUSTOMER, FAKE_CUSTOMER_II, FAKE_PLAN, FAKE_PLAN_II, FAKE_PLAN_METERED,
FAKE_SUBSCRIPTION, FAKE_SUBSCRIPTION_CANCELED, FAKE_SUBSCRIPTION_METERED,
FAKE_SUBSCRIPTION_MULTI_PLAN, AssertStripeFksMixin, datetime_to_unix
)

Expand Down Expand Up @@ -464,3 +464,46 @@ def test_sync_multi_plan(

items = subscription.items.all()
self.assertEqual(2, len(items))

self.assert_fks(
subscription,
expected_blank_fks={
"djstripe.Customer.coupon",
"djstripe.Customer.subscriber",
"djstripe.Plan.product",
"djstripe.Subscription.plan",
},
)

@patch("stripe.Plan.retrieve")
@patch("stripe.Customer.retrieve", return_value=deepcopy(FAKE_CUSTOMER_II))
@patch(
"stripe.Subscription.retrieve", return_value=deepcopy(FAKE_SUBSCRIPTION_METERED)
)
def test_sync_metered_plan(
self, subscription_retrieve_mock, customer_retrieve_mock, plan_retrieve_mock
):
subscription_fake = deepcopy(FAKE_SUBSCRIPTION_METERED)
self.assertNotIn(
"quantity",
subscription_fake["items"]["data"],
"Expect Metered plan SubscriptionItem to have no quantity",
)

subscription = Subscription.sync_from_stripe_data(subscription_fake)

items = subscription.items.all()
self.assertEqual(1, len(items))

item = items[0]

self.assertEqual(subscription.quantity, 1)
# Note that subscription.quantity is 1, but item.quantity isn't set on metered plans,
# it probably should be nullable
self.assertEqual(item.quantity, 0)
self.assertEqual(item.plan.id, FAKE_PLAN_METERED["id"])

self.assert_fks(
subscription,
expected_blank_fks={"djstripe.Customer.coupon", "djstripe.Plan.product"},
)

0 comments on commit 032fe36

Please sign in to comment.