stripe: Fix free trial pay by invoice customer billed twice.

This was a result of us moving `billing_cycle_anchor` ahead in time
of the `LicenseLedger` entry the customer paid for. Thus, this
confused our logic in thinking that customer hasn't paid for
the current billing cycle.
This commit is contained in:
Aman Agrawal
2025-07-10 19:34:04 +05:30
committed by Tim Abbott
parent ac0cc78327
commit c105bcc322
9 changed files with 82 additions and 37 deletions

View File

@@ -2301,6 +2301,24 @@ class BillingSession(ABC):
# This will create invoice for any additional licenses that user has at the time of # This will create invoice for any additional licenses that user has at the time of
# switching from free trial to paid plan since they already paid for the plan's this billing cycle. # switching from free trial to paid plan since they already paid for the plan's this billing cycle.
is_renewal = False is_renewal = False
# Since we need to move the `billing_cycle_anchor` forward below, we also
# need to update the `event_time` of the last renewal ledger entry to avoid
# our logic from thinking that licenses for the current billing cycle hasn't
# been paid for.
last_renewal_ledger_entry = (
LicenseLedger.objects.filter(
plan=plan,
is_renewal=True,
)
.order_by("-id")
.first()
)
assert last_renewal_ledger_entry is not None
last_renewal_ledger_entry.event_time = next_billing_cycle.replace(
microsecond=0
)
last_renewal_ledger_entry.save(update_fields=["event_time"])
else: else:
# We end the free trial since customer hasn't paid. # We end the free trial since customer hasn't paid.
plan.status = CustomerPlan.DOWNGRADE_AT_END_OF_FREE_TRIAL plan.status = CustomerPlan.DOWNGRADE_AT_END_OF_FREE_TRIAL

View File

@@ -8,15 +8,15 @@
"account_country": "US", "account_country": "US",
"account_name": "NORMALIZED", "account_name": "NORMALIZED",
"account_tax_ids": null, "account_tax_ids": null,
"amount_due": 152000, "amount_due": 984000,
"amount_overpaid": 0, "amount_overpaid": 0,
"amount_paid": 0, "amount_paid": 984000,
"amount_remaining": 152000, "amount_remaining": 0,
"amount_shipping": 0, "amount_shipping": 0,
"application": null, "application": null,
"attempt_count": 0, "attempt_count": 0,
"attempted": false, "attempted": false,
"auto_advance": true, "auto_advance": false,
"automatic_tax": { "automatic_tax": {
"disabled_reason": null, "disabled_reason": null,
"enabled": false, "enabled": false,
@@ -26,7 +26,7 @@
}, },
"automatically_finalizes_at": null, "automatically_finalizes_at": null,
"billing_reason": "manual", "billing_reason": "manual",
"collection_method": "charge_automatically", "collection_method": "send_invoice",
"created": 1000000000, "created": 1000000000,
"currency": "usd", "currency": "usd",
"custom_fields": null, "custom_fields": null,
@@ -43,7 +43,7 @@
"default_tax_rates": [], "default_tax_rates": [],
"description": null, "description": null,
"discounts": [], "discounts": [],
"due_date": null, "due_date": 1000000000,
"effective_at": 1000000000, "effective_at": 1000000000,
"ending_balance": 0, "ending_balance": 0,
"footer": null, "footer": null,
@@ -59,9 +59,9 @@
"lines": { "lines": {
"data": [ "data": [
{ {
"amount": 152000, "amount": 984000,
"currency": "usd", "currency": "usd",
"description": "Zulip Cloud Standard - renewal", "description": "Zulip Cloud Standard",
"discount_amounts": [], "discount_amounts": [],
"discountable": false, "discountable": false,
"discounts": [], "discounts": [],
@@ -83,8 +83,8 @@
"type": "invoice_item_details" "type": "invoice_item_details"
}, },
"period": { "period": {
"end": 1393729445, "end": 1362193445,
"start": 1362193445 "start": 1330657445
}, },
"pretax_credit_amounts": [], "pretax_credit_amounts": [],
"pricing": { "pricing": {
@@ -95,7 +95,7 @@
"type": "price_details", "type": "price_details",
"unit_amount_decimal": "8000" "unit_amount_decimal": "8000"
}, },
"quantity": 19, "quantity": 123,
"taxes": [] "taxes": []
} }
], ],
@@ -105,8 +105,16 @@
"url": "/v1/invoices/free_trial_upgrade_by_invoice--Event.list.1.json/lines" "url": "/v1/invoices/free_trial_upgrade_by_invoice--Event.list.1.json/lines"
}, },
"livemode": false, "livemode": false,
"metadata": {}, "metadata": {
"next_payment_attempt": 1000000000, "billing_schedule": "1",
"current_plan_id": "1",
"license_management": "manual",
"licenses": "123",
"on_free_trial": "True",
"plan_tier": "1",
"user_id": "10"
},
"next_payment_attempt": null,
"number": "NORMALIZED", "number": "NORMALIZED",
"object": "invoice", "object": "invoice",
"on_behalf_of": null, "on_behalf_of": null,
@@ -133,19 +141,19 @@
"shipping_details": null, "shipping_details": null,
"starting_balance": 0, "starting_balance": 0,
"statement_descriptor": "Zulip Cloud Standard", "statement_descriptor": "Zulip Cloud Standard",
"status": "open", "status": "paid",
"status_transitions": { "status_transitions": {
"finalized_at": 1000000000, "finalized_at": 1000000000,
"marked_uncollectible_at": null, "marked_uncollectible_at": null,
"paid_at": null, "paid_at": 1000000000,
"voided_at": null "voided_at": null
}, },
"subtotal": 152000, "subtotal": 984000,
"subtotal_excluding_tax": 152000, "subtotal_excluding_tax": 984000,
"test_clock": null, "test_clock": null,
"total": 152000, "total": 984000,
"total_discount_amounts": [], "total_discount_amounts": [],
"total_excluding_tax": 152000, "total_excluding_tax": 984000,
"total_pretax_credit_amounts": [], "total_pretax_credit_amounts": [],
"total_taxes": [], "total_taxes": [],
"webhooks_delivered_at": 1000000000 "webhooks_delivered_at": 1000000000
@@ -159,7 +167,7 @@
"id": "free_trial_upgrade_by_invoice--Event.list.1.json", "id": "free_trial_upgrade_by_invoice--Event.list.1.json",
"idempotency_key": "00000000-0000-0000-0000-000000000000" "idempotency_key": "00000000-0000-0000-0000-000000000000"
}, },
"type": "invoice.finalized" "type": "invoice.paid"
} }
], ],
"has_more": true, "has_more": true,

View File

@@ -107,7 +107,7 @@
"livemode": false, "livemode": false,
"metadata": { "metadata": {
"billing_schedule": "1", "billing_schedule": "1",
"current_plan_id": "39", "current_plan_id": "1",
"license_management": "manual", "license_management": "manual",
"licenses": "123", "licenses": "123",
"on_free_trial": "True", "on_free_trial": "True",
@@ -156,7 +156,7 @@
"total_excluding_tax": 984000, "total_excluding_tax": 984000,
"total_pretax_credit_amounts": [], "total_pretax_credit_amounts": [],
"total_taxes": [], "total_taxes": [],
"webhooks_delivered_at": 1000000000 "webhooks_delivered_at": null
} }
}, },
"id": "free_trial_upgrade_by_invoice--Event.list.2.json", "id": "free_trial_upgrade_by_invoice--Event.list.2.json",

View File

@@ -107,7 +107,7 @@
"livemode": false, "livemode": false,
"metadata": { "metadata": {
"billing_schedule": "1", "billing_schedule": "1",
"current_plan_id": "39", "current_plan_id": "1",
"license_management": "manual", "license_management": "manual",
"licenses": "123", "licenses": "123",
"on_free_trial": "True", "on_free_trial": "True",
@@ -156,13 +156,13 @@
"total_excluding_tax": 984000, "total_excluding_tax": 984000,
"total_pretax_credit_amounts": [], "total_pretax_credit_amounts": [],
"total_taxes": [], "total_taxes": [],
"webhooks_delivered_at": 1000000000 "webhooks_delivered_at": null
} }
}, },
"id": "free_trial_upgrade_by_invoice--Event.list.3.json", "id": "free_trial_upgrade_by_invoice--Event.list.3.json",
"livemode": false, "livemode": false,
"object": "event", "object": "event",
"pending_webhooks": 0, "pending_webhooks": 2,
"request": { "request": {
"id": "free_trial_upgrade_by_invoice--Event.list.3.json", "id": "free_trial_upgrade_by_invoice--Event.list.3.json",
"idempotency_key": "00000000-0000-0000-0000-000000000000" "idempotency_key": "00000000-0000-0000-0000-000000000000"
@@ -276,7 +276,7 @@
"livemode": false, "livemode": false,
"metadata": { "metadata": {
"billing_schedule": "1", "billing_schedule": "1",
"current_plan_id": "39", "current_plan_id": "1",
"license_management": "manual", "license_management": "manual",
"licenses": "123", "licenses": "123",
"on_free_trial": "True", "on_free_trial": "True",
@@ -325,7 +325,7 @@
"total_excluding_tax": 984000, "total_excluding_tax": 984000,
"total_pretax_credit_amounts": [], "total_pretax_credit_amounts": [],
"total_taxes": [], "total_taxes": [],
"webhooks_delivered_at": 1000000000 "webhooks_delivered_at": null
}, },
"previous_attributes": { "previous_attributes": {
"amount_paid": 0, "amount_paid": 0,
@@ -340,7 +340,7 @@
"id": "free_trial_upgrade_by_invoice--Event.list.3.json", "id": "free_trial_upgrade_by_invoice--Event.list.3.json",
"livemode": false, "livemode": false,
"object": "event", "object": "event",
"pending_webhooks": 0, "pending_webhooks": 2,
"request": { "request": {
"id": "free_trial_upgrade_by_invoice--Event.list.3.json", "id": "free_trial_upgrade_by_invoice--Event.list.3.json",
"idempotency_key": "00000000-0000-0000-0000-000000000000" "idempotency_key": "00000000-0000-0000-0000-000000000000"
@@ -411,7 +411,7 @@
"id": "free_trial_upgrade_by_invoice--Event.list.3.json", "id": "free_trial_upgrade_by_invoice--Event.list.3.json",
"livemode": false, "livemode": false,
"object": "event", "object": "event",
"pending_webhooks": 0, "pending_webhooks": 2,
"request": { "request": {
"id": "free_trial_upgrade_by_invoice--Event.list.3.json", "id": "free_trial_upgrade_by_invoice--Event.list.3.json",
"idempotency_key": "00000000-0000-0000-0000-000000000000" "idempotency_key": "00000000-0000-0000-0000-000000000000"

View File

@@ -60,7 +60,7 @@
"livemode": false, "livemode": false,
"metadata": { "metadata": {
"billing_schedule": "1", "billing_schedule": "1",
"current_plan_id": "39", "current_plan_id": "1",
"license_management": "manual", "license_management": "manual",
"licenses": "123", "licenses": "123",
"on_free_trial": "True", "on_free_trial": "True",
@@ -109,5 +109,5 @@
"total_excluding_tax": 0, "total_excluding_tax": 0,
"total_pretax_credit_amounts": [], "total_pretax_credit_amounts": [],
"total_taxes": [], "total_taxes": [],
"webhooks_delivered_at": 1000000000 "webhooks_delivered_at": null
} }

View File

@@ -101,7 +101,7 @@
"livemode": false, "livemode": false,
"metadata": { "metadata": {
"billing_schedule": "1", "billing_schedule": "1",
"current_plan_id": "39", "current_plan_id": "1",
"license_management": "manual", "license_management": "manual",
"licenses": "123", "licenses": "123",
"on_free_trial": "True", "on_free_trial": "True",
@@ -150,5 +150,5 @@
"total_excluding_tax": 984000, "total_excluding_tax": 984000,
"total_pretax_credit_amounts": [], "total_pretax_credit_amounts": [],
"total_taxes": [], "total_taxes": [],
"webhooks_delivered_at": 1000000000 "webhooks_delivered_at": null
} }

View File

@@ -103,7 +103,7 @@
"livemode": false, "livemode": false,
"metadata": { "metadata": {
"billing_schedule": "1", "billing_schedule": "1",
"current_plan_id": "39", "current_plan_id": "1",
"license_management": "manual", "license_management": "manual",
"licenses": "123", "licenses": "123",
"on_free_trial": "True", "on_free_trial": "True",
@@ -152,7 +152,7 @@
"total_excluding_tax": 984000, "total_excluding_tax": 984000,
"total_pretax_credit_amounts": [], "total_pretax_credit_amounts": [],
"total_taxes": [], "total_taxes": [],
"webhooks_delivered_at": 1000000000 "webhooks_delivered_at": null
} }
], ],
"has_more": false, "has_more": false,

View File

@@ -101,7 +101,7 @@
"livemode": false, "livemode": false,
"metadata": { "metadata": {
"billing_schedule": "1", "billing_schedule": "1",
"current_plan_id": "39", "current_plan_id": "1",
"license_management": "manual", "license_management": "manual",
"licenses": "123", "licenses": "123",
"on_free_trial": "True", "on_free_trial": "True",
@@ -150,5 +150,5 @@
"total_excluding_tax": 984000, "total_excluding_tax": 984000,
"total_pretax_credit_amounts": [], "total_pretax_credit_amounts": [],
"total_taxes": [], "total_taxes": [],
"webhooks_delivered_at": 1000000000 "webhooks_delivered_at": null
} }

View File

@@ -1795,6 +1795,15 @@ class StripeTest(StripeTestCase):
self.assertEqual(customer_plan.next_invoice_date, free_trial_end_date) self.assertEqual(customer_plan.next_invoice_date, free_trial_end_date)
[last_event] = iter(stripe.Event.list(limit=1)) [last_event] = iter(stripe.Event.list(limit=1))
last_renewal_ledger = (
LicenseLedger.objects.filter(plan=plan, is_renewal=True).order_by("-id").first()
)
assert last_renewal_ledger is not None
self.assertEqual(
last_renewal_ledger.event_time,
self.now,
)
# Customer pays the invoice # Customer pays the invoice
assert invoice.id is not None assert invoice.id is not None
stripe.Invoice.pay(invoice.id, paid_out_of_band=True) stripe.Invoice.pay(invoice.id, paid_out_of_band=True)
@@ -1806,11 +1815,21 @@ class StripeTest(StripeTestCase):
self.assert_in_success_response(["You have no outstanding invoices."], response) self.assert_in_success_response(["You have no outstanding invoices."], response)
invoice_plans_as_needed(free_trial_end_date) invoice_plans_as_needed(free_trial_end_date)
last_renewal_ledger.refresh_from_db()
customer_plan.refresh_from_db() customer_plan.refresh_from_db()
realm.refresh_from_db() realm.refresh_from_db()
plan.refresh_from_db()
self.assertEqual(customer_plan.status, CustomerPlan.ACTIVE) self.assertEqual(customer_plan.status, CustomerPlan.ACTIVE)
self.assertEqual(customer_plan.next_invoice_date, add_months(free_trial_end_date, 1)) self.assertEqual(customer_plan.next_invoice_date, add_months(free_trial_end_date, 1))
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_STANDARD) self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_STANDARD)
self.assertEqual(last_renewal_ledger.event_time, free_trial_end_date)
self.assertEqual(customer_plan.billing_cycle_anchor, free_trial_end_date)
before_ledger_count = LicenseLedger.objects.filter(plan=plan).count()
self.billing_session.make_end_of_cycle_updates_if_needed(plan, free_trial_end_date)
after_ledger_count = LicenseLedger.objects.filter(plan=plan).count()
# No additional ledger entries are created.
self.assertEqual(before_ledger_count, after_ledger_count)
@mock_stripe() @mock_stripe()
def test_free_trial_upgrade_by_invoice_customer_fails_to_pay(self, *mocks: Mock) -> None: def test_free_trial_upgrade_by_invoice_customer_fails_to_pay(self, *mocks: Mock) -> None: