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/migrations/0003_auto_20181117_2328.py b/djstripe/migrations/0003_auto_20181117_2328.py
--- a/djstripe/migrations/0003_auto_20181117_2328.py
+++ b/djstripe/migrations/0003_auto_20181117_2328.py
@@ -1263,4 +1263,15 @@ class Migration(migrations.Migration):
 				null=True,
 			),
 		),
+		migrations.AlterField(
+			model_name="plan",
+			name="amount",
+			field=djstripe.fields.StripeDecimalCurrencyAmountField(
+				blank=True,
+				decimal_places=2,
+				help_text="Amount to be charged on the interval specified.",
+				max_digits=8,
+				null=True,
+			),
+		),
 	]
diff --git a/djstripe/models/billing.py b/djstripe/models/billing.py
--- a/djstripe/models/billing.py
+++ b/djstripe/models/billing.py
@@ -655,7 +655,7 @@ class Plan(StripeModel):
 		),
 	)
 	amount = StripeDecimalCurrencyAmountField(
-		help_text="Amount to be charged on the interval specified."
+		null=True, blank=True, help_text="Amount to be charged on the interval specified."
 	)
 	billing_scheme = StripeEnumField(
 		enum=enums.PlanBillingScheme,


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -704,6 +704,28 @@ def capture(self):
 	"usage_type": "licensed",
 }
 
+FAKE_TIER_PLAN = {
+	"id": "tier21323",
+	"object": "plan",
+	"active": True,
+	"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": None, "unit_amount": 900, "up_to": None},
+	],
+}
+
 
 class SubscriptionDict(dict):
 	def __setattr__(self, name, value):
diff --git a/tests/test_plan.py b/tests/test_plan.py
--- a/tests/test_plan.py
+++ b/tests/test_plan.py
@@ -12,7 +12,7 @@
 from djstripe.models import Plan
 from djstripe.settings import STRIPE_SECRET_KEY
 
-from . import FAKE_PLAN, FAKE_PLAN_II, AssertStripeFksMixin
+from . import FAKE_PLAN, FAKE_PLAN_II, FAKE_TIER_PLAN, AssertStripeFksMixin
 
 
 class TestPlanAdmin(TestCase):
@@ -104,6 +104,18 @@ def test_stripe_plan(self, plan_retrieve_mock):
 			plan, expected_blank_fks={"djstripe.Customer.coupon", "djstripe.Plan.product"}
 		)
 
+	@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.id, tier_plan_data["id"])
+		self.assertIsNone(plan.amount)
+		self.assertIsNotNone(plan.tiers)
+
+		self.assert_fks(
+			plan, expected_blank_fks={"djstripe.Customer.coupon", "djstripe.Plan.product"}
+		)
+
 
 class HumanReadablePlanTest(TestCase):
 	def test_human_readable_free_usd_daily(self):