Skip to content

Commit

Permalink
billing: Fix the type annotation of Customer.stripe_customer_id.
Browse files Browse the repository at this point in the history
This also fixes a bug in void_all_open_invoices function. If a realm
with a local Customer object but without an associated stripe.Customer
is passed to void_all_open_invoices, then the function will end up
voiding the last 10 invoices created by billing system instead of voiding
no invoices at all. This is because stripe.Invoice.list(customer=None)
return last 10 invoices across all customers.

But this bug won't cauuse any issue in production since
void_all_open_invoices can be only invoked from /support page. And we
show the option to void invoices in support page only if the realm
has a paid plan. And it's not really possible for a realm to have
a paid plan without having an associated stripe_customer_id. Plus I
went through the void events in stripe stream since the PR to add
void invoices was merged and there does not seems to be any suspicious
events.
  • Loading branch information
hackerkid authored and timabbott committed Jul 2, 2021
1 parent 127d3de commit 1d579ec
Show file tree
Hide file tree
Showing 17 changed files with 933 additions and 41 deletions.
10 changes: 10 additions & 0 deletions corporate/lib/stripe.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ def do_replace_payment_source(
) -> stripe.Customer:
customer = get_customer_by_realm(user.realm)
assert customer is not None # for mypy
assert customer.stripe_customer_id is not None # for mypy

stripe_customer = stripe_get_customer(customer.stripe_customer_id)
stripe_customer.source = stripe_token
Expand Down Expand Up @@ -533,6 +534,8 @@ def process_initial_upgrade(
) -> None:
realm = user.realm
customer = update_or_create_stripe_customer(user, stripe_token=stripe_token)
assert customer.stripe_customer_id is not None # for mypy

charge_automatically = stripe_token is not None
free_trial = is_free_trial_offer_enabled()

Expand Down Expand Up @@ -710,6 +713,11 @@ def update_license_ledger_if_needed(realm: Realm, event_time: datetime) -> None:
def invoice_plan(plan: CustomerPlan, event_time: datetime) -> None:
if plan.invoicing_status == CustomerPlan.STARTED:
raise NotImplementedError("Plan with invoicing_status==STARTED needs manual resolution.")
if not plan.customer.stripe_customer_id:
raise BillingError(
f"Realm {plan.customer.realm.string_id} has a paid plan without a Stripe customer."
)

make_end_of_cycle_updates_if_needed(plan, event_time)

if plan.invoicing_status == CustomerPlan.INITIAL_INVOICE_TO_BE_SENT:
Expand Down Expand Up @@ -957,6 +965,8 @@ def void_all_open_invoices(realm: Realm) -> int:
customer = get_customer_by_realm(realm)
if customer is None:
return 0
if customer.stripe_customer_id is None:
return 0
invoices = stripe.Invoice.list(customer=customer.stripe_customer_id)
voided_invoices_count = 0
for invoice in invoices:
Expand Down
2 changes: 1 addition & 1 deletion corporate/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class Customer(models.Model):
"""

realm: Realm = models.OneToOneField(Realm, on_delete=CASCADE)
stripe_customer_id: str = models.CharField(max_length=255, null=True, unique=True)
stripe_customer_id: Optional[str] = models.CharField(max_length=255, null=True, unique=True)
sponsorship_pending: bool = models.BooleanField(default=False)
# A percentage, like 85.
default_discount: Optional[Decimal] = models.DecimalField(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"account_balance": 0,
"address": null,
"balance": 0,
"created": 1000000000,
"currency": null,
"default_source": null,
"delinquent": false,
"description": "lear (Lear & Co.)",
"discount": null,
"email": "[email protected]",
"id": "cus_NORMALIZED0002",
"invoice_prefix": "NORMA02",
"invoice_settings": {
"custom_fields": null,
"default_payment_method": null,
"footer": null
},
"livemode": false,
"metadata": {
"realm_id": "2",
"realm_str": "lear"
},
"name": null,
"next_invoice_sequence": 1,
"object": "customer",
"phone": null,
"preferred_locales": [],
"shipping": null,
"sources": {
"data": [],
"has_more": false,
"object": "list",
"total_count": 0,
"url": "/v1/customers/cus_NORMALIZED0002/sources"
},
"subscriptions": {
"data": [],
"has_more": false,
"object": "list",
"total_count": 0,
"url": "/v1/customers/cus_NORMALIZED0002/subscriptions"
},
"tax_exempt": "none",
"tax_ids": {
"data": [],
"has_more": false,
"object": "list",
"total_count": 0,
"url": "/v1/customers/cus_NORMALIZED0002/tax_ids"
},
"tax_info": null,
"tax_info_verification": null
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
{
"account_country": "US",
"account_name": null,
"account_name": "NORMALIZED-1",
"account_tax_ids": null,
"amount_due": 6400,
"amount_paid": 0,
"amount_remaining": 6400,
"application_fee": null,
"attempt_count": 0,
"attempted": false,
"auto_advance": true,
"automatic_tax": {
"enabled": false,
"status": null
},
"billing": "send_invoice",
"billing_reason": "manual",
"charge": null,
Expand Down Expand Up @@ -37,6 +42,7 @@
"hosted_invoice_url": null,
"id": "in_NORMALIZED00000000000001",
"invoice_pdf": null,
"last_finalization_error": null,
"lines": {
"data": [
{
Expand All @@ -61,13 +67,13 @@
"billing_scheme": "per_unit",
"created": 1000000000,
"currency": "usd",
"id": "price_1HFiX7D2X8vgpBNGZKqyye0n",
"id": "price_1J3ngLD2X8vgpBNGNhl10Ejf",
"livemode": false,
"lookup_key": null,
"metadata": {},
"nickname": null,
"object": "price",
"product": "prod_HpNH3ZK9Bpc89P",
"product": "prod_JhC49N48jYmZQ8",
"recurring": null,
"tiers_mode": null,
"transform_quantity": null,
Expand All @@ -81,7 +87,7 @@
"tax_amounts": [],
"tax_rates": [],
"type": "invoiceitem",
"unique_id": "il_1HFiX7D2X8vgpBNGYzYwN35m"
"unique_id": "il_1J3ngLD2X8vgpBNGyyqmhLYe"
}
],
"has_more": false,
Expand All @@ -94,8 +100,13 @@
"next_payment_attempt": null,
"number": "NORMALI-0001",
"object": "invoice",
"on_behalf_of": null,
"paid": false,
"payment_intent": null,
"payment_settings": {
"payment_method_options": null,
"payment_method_types": null
},
"period_end": 1000000000,
"period_start": 1000000000,
"post_payment_credit_notes_amount": 0,
Expand All @@ -118,5 +129,5 @@
"total_discount_amounts": [],
"total_tax_amounts": [],
"transfer_data": null,
"webhooks_delivered_at": 1000000000
"webhooks_delivered_at": null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
{
"account_country": "US",
"account_name": "NORMALIZED-1",
"account_tax_ids": null,
"amount_due": 6400,
"amount_paid": 0,
"amount_remaining": 6400,
"application_fee": null,
"attempt_count": 0,
"attempted": false,
"auto_advance": true,
"automatic_tax": {
"enabled": false,
"status": null
},
"billing": "send_invoice",
"billing_reason": "manual",
"charge": null,
"collection_method": "send_invoice",
"created": 1000000000,
"currency": "usd",
"custom_fields": null,
"customer": "cus_NORMALIZED0002",
"customer_address": null,
"customer_email": "[email protected]",
"customer_name": null,
"customer_phone": null,
"customer_shipping": null,
"customer_tax_exempt": "none",
"customer_tax_ids": [],
"date": 1000000000,
"default_payment_method": null,
"default_source": null,
"default_tax_rates": [],
"description": null,
"discount": null,
"discounts": [],
"due_date": 1000000000,
"ending_balance": null,
"finalized_at": null,
"footer": null,
"hosted_invoice_url": null,
"id": "in_NORMALIZED00000000000002",
"invoice_pdf": null,
"last_finalization_error": null,
"lines": {
"data": [
{
"amount": 6400,
"currency": "usd",
"description": "Zulip standard upgrade",
"discount_amounts": [],
"discountable": false,
"discounts": [],
"id": "ii_NORMALIZED00000000000002",
"invoice_item": "ii_NORMALIZED00000000000002",
"livemode": false,
"metadata": {},
"object": "line_item",
"period": {
"end": 1000000000,
"start": 1000000000
},
"plan": null,
"price": {
"active": false,
"billing_scheme": "per_unit",
"created": 1000000000,
"currency": "usd",
"id": "price_1J3ngOD2X8vgpBNG4pn2DcHN",
"livemode": false,
"lookup_key": null,
"metadata": {},
"nickname": null,
"object": "price",
"product": "prod_JhC4jKCcSa3kkS",
"recurring": null,
"tiers_mode": null,
"transform_quantity": null,
"type": "one_time",
"unit_amount": 800,
"unit_amount_decimal": "800"
},
"proration": false,
"quantity": 8,
"subscription": null,
"tax_amounts": [],
"tax_rates": [],
"type": "invoiceitem",
"unique_id": "il_1J3ngOD2X8vgpBNG5FHfGv4J"
}
],
"has_more": false,
"object": "list",
"total_count": 1,
"url": "/v1/invoices/in_NORMALIZED00000000000002/lines"
},
"livemode": false,
"metadata": {},
"next_payment_attempt": null,
"number": "NORMALI-0002",
"object": "invoice",
"on_behalf_of": null,
"paid": false,
"payment_intent": null,
"payment_settings": {
"payment_method_options": null,
"payment_method_types": null
},
"period_end": 1000000000,
"period_start": 1000000000,
"post_payment_credit_notes_amount": 0,
"pre_payment_credit_notes_amount": 0,
"receipt_number": null,
"starting_balance": 0,
"statement_descriptor": "Zulip Standard",
"status": "draft",
"status_transitions": {
"finalized_at": null,
"marked_uncollectible_at": null,
"paid_at": null,
"voided_at": null
},
"subscription": null,
"subtotal": 6400,
"tax": null,
"tax_percent": null,
"total": 6400,
"total_discount_amounts": [],
"total_tax_amounts": [],
"transfer_data": null,
"webhooks_delivered_at": null
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
{
"account_country": "US",
"account_name": null,
"account_name": "NORMALIZED-1",
"account_tax_ids": null,
"amount_due": 6400,
"amount_paid": 0,
"amount_remaining": 6400,
"application_fee": null,
"attempt_count": 0,
"attempted": false,
"auto_advance": true,
"automatic_tax": {
"enabled": false,
"status": null
},
"billing": "send_invoice",
"billing_reason": "manual",
"charge": null,
Expand All @@ -34,9 +39,10 @@
"ending_balance": 0,
"finalized_at": 1000000000,
"footer": null,
"hosted_invoice_url": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/invst_NORMALIZED0000000000000001CU7gS",
"hosted_invoice_url": "https://invoice.stripe.com/i/acct_NORMALIZED000001/invst_NORMALIZED0000000000000001avHhX",
"id": "in_NORMALIZED00000000000001",
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/invst_NORMALIZED0000000000000001CU7gS/pdf",
"invoice_pdf": "https://pay.stripe.com/invoice/acct_NORMALIZED000001/invst_NORMALIZED0000000000000001avHhX/pdf",
"last_finalization_error": null,
"lines": {
"data": [
{
Expand All @@ -61,13 +67,13 @@
"billing_scheme": "per_unit",
"created": 1000000000,
"currency": "usd",
"id": "price_1HFiX7D2X8vgpBNGZKqyye0n",
"id": "price_1J3ngLD2X8vgpBNGNhl10Ejf",
"livemode": false,
"lookup_key": null,
"metadata": {},
"nickname": null,
"object": "price",
"product": "prod_HpNH3ZK9Bpc89P",
"product": "prod_JhC49N48jYmZQ8",
"recurring": null,
"tiers_mode": null,
"transform_quantity": null,
Expand All @@ -81,7 +87,7 @@
"tax_amounts": [],
"tax_rates": [],
"type": "invoiceitem",
"unique_id": "il_1HFiX7D2X8vgpBNGYzYwN35m"
"unique_id": "il_1J3ngLD2X8vgpBNGyyqmhLYe"
}
],
"has_more": false,
Expand All @@ -94,8 +100,13 @@
"next_payment_attempt": null,
"number": "NORMALI-0001",
"object": "invoice",
"on_behalf_of": null,
"paid": false,
"payment_intent": "pi_1HFiX8D2X8vgpBNGGjAHPbBr",
"payment_intent": "pi_1J3ngND2X8vgpBNGOtTRK3Kn",
"payment_settings": {
"payment_method_options": null,
"payment_method_types": null
},
"period_end": 1000000000,
"period_start": 1000000000,
"post_payment_credit_notes_amount": 0,
Expand All @@ -118,5 +129,5 @@
"total_discount_amounts": [],
"total_tax_amounts": [],
"transfer_data": null,
"webhooks_delivered_at": 1000000000
"webhooks_delivered_at": null
}
Loading

0 comments on commit 1d579ec

Please sign in to comment.