Repository: dj-stripe/dj-stripe

Issue:
djstripe_sync_plans_from_stripe fails to sync tiered pricing plan
Django 2.0.1,
Python 3.6.5

When running `djstripe_sync_plans_from_stripe`, the script fails to sync one of my tiered pricing plans.

The error is: 

`django.db.utils.IntegrityError: null value in column "amount" violates not-null constraint`

The plan data being passed to `._get_or_create_from_stripe_object` is:

```
{
  "active": true,
  "aggregate_usage": null,
  "amount": null,
  "billing_scheme": "tiered",
  "created": 1543433190,
  "currency": "usd",
  "id": "plan_xxx",
  "interval": "year",
  "interval_count": 1,
  "livemode": false,
  "metadata": {},
  "nickname": "Yearly",
  "object": "plan",
  "product": "prod_xxx",
  "tiers": [
    {
      "amount": 89400,
      "flat_amount": null,
      "up_to": 1
    },
    {
      "amount": 80460,
      "flat_amount": null,
      "up_to": 3
    },
    {
      "amount": 71520,
      "flat_amount": null,
      "up_to": 19
    },
    {
      "amount": 53640,
      "flat_amount": null,
      "up_to": 29
    },
    {
      "amount": 44700,
      "flat_amount": null,
      "up_to": null
    }
  ],
  "tiers_mode": "graduated",
  "transform_usage": null,
  "trial_period_days": null,
  "usage_type": "licensed"
}
```

Does `djstripe` support tiered pricing plans?

Thank you!


Discussion:
Hi, it might not be relevant, but what version of djstripe are you using,
and what's your Stripe account's API version?  Thanks
I'm using djstripe 1.2.3 and Stripe API 2018-11-08 (however I think the library just uses 2018-05-21?)
Same issue. I'll look into it as able while I build out this app with djstripe. Offhand (ie: not thought out) I can see a couple solutions:
* Quick/dirty: allow `amount` to be `null`
* Multiple `Plan` objects for tiered plans, reading `amount` from the `tiers`
* `TieredPlan` as a new model... and handling it all
* `Plan.amount` as `HStoreField` or similar (postgresql only)?
I'm also having this issue, it's a blocker. Allowing `amount` to be null seems like a viable fix.

Running Python 3.7, Django 2.1.4, djstrip 1.2.3

Resolving patch:
diff --git a/djstripe/models.py b/djstripe/models.py
--- a/djstripe/models.py
+++ b/djstripe/models.py
@@ -2691,6 +2691,16 @@ def _attach_objects_hook(self, cls, data):
         if product:
             self.product = product
 
+    @classmethod
+    def _manipulate_stripe_object_hook(cls, data):
+        data = super(Plan, cls)._manipulate_stripe_object_hook(data)
+        # Amount can be null if tiered plan
+        # Set to 0 to maintain compatibility while avoiding a database
+        # migration on the stable branch
+        if not data['amount']:
+            data['amount'] = 0
+        return data
+
     @property
     def amount_in_cents(self):
         return int(self.amount * 100)


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -545,6 +545,35 @@ def capture(self):
     "usage_type": "licensed",
 }
 
+FAKE_TIER_PLAN = {
+    "id": "tier21323",
+    "object": "plan",
+    "amount": None,
+    "created": 1386247539,
+    "currency": "usd",
+    "interval": "month",
+    "interval_count": 1,
+    "livemode": False,
+    "metadata": {},
+    "name": "New plan name",
+    "statement_descriptor": None,
+    "trial_period_days": None,
+    "usage_type": "licensed",
+    "tiers_mode": "graduated",
+    "tiers": [
+        {
+            "flat_amount": 4900,
+            "unit_amount": 1000,
+            "up_to": 5
+        },
+        {
+            "flat_amount": "null",
+            "unit_amount": 900,
+            "up_to": None
+        },
+    ]
+}
+
 
 class SubscriptionDict(dict):
 
diff --git a/tests/test_plan.py b/tests/test_plan.py
--- a/tests/test_plan.py
+++ b/tests/test_plan.py
@@ -19,7 +19,7 @@
 from djstripe.admin import PlanAdmin
 from djstripe.models import Plan
 
-from . import FAKE_PLAN, FAKE_PLAN_II
+from . import FAKE_PLAN, FAKE_PLAN_II, FAKE_TIER_PLAN
 
 
 class TestPlanAdmin(TestCase):
@@ -100,6 +100,12 @@ def test_stripe_plan(self, plan_retrieve_mock):
         assert plan.amount_in_cents == plan.amount * 100
         assert isinstance(plan.amount_in_cents, int)
 
+    @patch("stripe.Plan.retrieve")
+    def test_stripe_tier_plan(self, plan_retrieve_mock):
+        tier_plan_data = deepcopy(FAKE_TIER_PLAN)
+        plan = Plan.sync_from_stripe_data(tier_plan_data)
+        self.assertEqual(plan.stripe_id, tier_plan_data['id'])
+
 
 class HumanReadablePlanTest(TestCase):
     def test_human_readable_free_usd_daily(self):