def test_run_billing_processor_logic_when_no_errors(self) -> None: second_realm = Realm.objects.create(string_id='second', name='second') entry1 = self.add_log_entry(realm=second_realm) realm_processor = BillingProcessor.objects.create( realm=second_realm, log_row=entry1, state=BillingProcessor.DONE) entry2 = self.add_log_entry() # global processor processor = BillingProcessor.objects.create( log_row=entry2, state=BillingProcessor.DONE) # Test nothing to process # test nothing changes, for global processor self.assertFalse(run_billing_processor_one_step(processor)) self.assertEqual(2, BillingProcessor.objects.count()) # test realm processor gets deleted self.assertFalse(run_billing_processor_one_step(realm_processor)) self.assertEqual(1, BillingProcessor.objects.count()) self.assertEqual(1, BillingProcessor.objects.filter(realm=None).count()) # Test something to process processor.state = BillingProcessor.STARTED processor.save() realm_processor = BillingProcessor.objects.create( realm=second_realm, log_row=entry1, state=BillingProcessor.STARTED) Customer.objects.create(realm=get_realm('zulip'), stripe_customer_id='cust_1') Customer.objects.create(realm=second_realm, stripe_customer_id='cust_2') with patch('corporate.lib.stripe.do_adjust_subscription_quantity'): # test return values self.assertTrue(run_billing_processor_one_step(processor)) self.assertTrue(run_billing_processor_one_step(realm_processor)) # test no processors get added or deleted self.assertEqual(2, BillingProcessor.objects.count())
def test_run_billing_processor_with_card_error(self, mock_billing_logger_error: Mock) -> None: second_realm = Realm.objects.create(string_id='second', name='second') entry1 = self.add_log_entry(realm=second_realm) # global processor processor = BillingProcessor.objects.create( log_row=entry1, state=BillingProcessor.STARTED) Customer.objects.create(realm=second_realm, stripe_customer_id='cust_2') # card error on global processor should create a new realm processor with patch('corporate.lib.stripe.do_adjust_subscription_quantity', side_effect=stripe.error.CardError('message', 'param', 'code', json_body={})): self.assertTrue(run_billing_processor_one_step(processor)) self.assertEqual(2, BillingProcessor.objects.count()) self.assertTrue(BillingProcessor.objects.filter( realm=None, log_row=entry1, state=BillingProcessor.SKIPPED).exists()) self.assertTrue(BillingProcessor.objects.filter( realm=second_realm, log_row=entry1, state=BillingProcessor.STALLED).exists()) mock_billing_logger_error.assert_called() # card error on realm processor should change state to STALLED realm_processor = BillingProcessor.objects.filter(realm=second_realm).first() realm_processor.state = BillingProcessor.STARTED realm_processor.save() with patch('corporate.lib.stripe.do_adjust_subscription_quantity', side_effect=stripe.error.CardError('message', 'param', 'code', json_body={})): self.assertTrue(run_billing_processor_one_step(realm_processor)) self.assertEqual(2, BillingProcessor.objects.count()) self.assertTrue(BillingProcessor.objects.filter( realm=second_realm, log_row=entry1, state=BillingProcessor.STALLED).exists()) mock_billing_logger_error.assert_called()
def test_run_billing_processor_with_uncaught_error( self, mock_billing_logger_error: mock.Mock) -> None: # This tests three different things: # * That run_billing_processor_one_step passes through exceptions that # are not StripeCardError # * That process_billing_log_entry catches StripeErrors and re-raises them as BillingErrors # * That processor.state=STARTED for non-StripeCardError exceptions entry1 = self.add_log_entry() entry2 = self.add_log_entry() processor = BillingProcessor.objects.create( log_row=entry1, state=BillingProcessor.DONE) Customer.objects.create(realm=get_realm('zulip'), stripe_customer_id='cust_1') with mock.patch('corporate.lib.stripe.do_adjust_subscription_quantity', side_effect=stripe.error.StripeError('message', 'param', 'code', json_body={})): with self.assertRaises(BillingError): run_billing_processor_one_step(processor) mock_billing_logger_error.assert_called() # check processor.state is STARTED self.assertTrue( BillingProcessor.objects.filter( log_row=entry2, state=BillingProcessor.STARTED).exists())
def test_run_billing_processor_with_card_error(self, mock_billing_logger_error: Mock) -> None: second_realm = Realm.objects.create(string_id='second', name='second') entry1 = self.add_log_entry(realm=second_realm) # global processor processor = BillingProcessor.objects.create( log_row=entry1, state=BillingProcessor.STARTED) Customer.objects.create(realm=second_realm, stripe_customer_id='cust_2') # card error on global processor should create a new realm processor with patch('corporate.lib.stripe.do_adjust_subscription_quantity', side_effect=stripe.error.CardError('message', 'param', 'code', json_body={})): self.assertTrue(run_billing_processor_one_step(processor)) self.assertEqual(2, BillingProcessor.objects.count()) self.assertTrue(BillingProcessor.objects.filter( realm=None, log_row=entry1, state=BillingProcessor.SKIPPED).exists()) self.assertTrue(BillingProcessor.objects.filter( realm=second_realm, log_row=entry1, state=BillingProcessor.STALLED).exists()) mock_billing_logger_error.assert_called() # card error on realm processor should change state to STALLED realm_processor = BillingProcessor.objects.filter(realm=second_realm).first() realm_processor.state = BillingProcessor.STARTED realm_processor.save() with patch('corporate.lib.stripe.do_adjust_subscription_quantity', side_effect=stripe.error.CardError('message', 'param', 'code', json_body={})): self.assertTrue(run_billing_processor_one_step(realm_processor)) self.assertEqual(2, BillingProcessor.objects.count()) self.assertTrue(BillingProcessor.objects.filter( realm=second_realm, log_row=entry1, state=BillingProcessor.STALLED).exists()) mock_billing_logger_error.assert_called()
def test_run_billing_processor_logic_when_no_errors(self) -> None: second_realm = Realm.objects.create(string_id='second', name='second') entry1 = self.add_log_entry(realm=second_realm) realm_processor = BillingProcessor.objects.create( realm=second_realm, log_row=entry1, state=BillingProcessor.DONE) entry2 = self.add_log_entry() # global processor processor = BillingProcessor.objects.create( log_row=entry2, state=BillingProcessor.DONE) # Test nothing to process # test nothing changes, for global processor self.assertFalse(run_billing_processor_one_step(processor)) self.assertEqual(2, BillingProcessor.objects.count()) # test realm processor gets deleted self.assertFalse(run_billing_processor_one_step(realm_processor)) self.assertEqual(1, BillingProcessor.objects.count()) self.assertEqual(1, BillingProcessor.objects.filter(realm=None).count()) # Test something to process processor.state = BillingProcessor.STARTED processor.save() realm_processor = BillingProcessor.objects.create( realm=second_realm, log_row=entry1, state=BillingProcessor.STARTED) Customer.objects.create(realm=get_realm('zulip'), stripe_customer_id='cust_1') Customer.objects.create(realm=second_realm, stripe_customer_id='cust_2') with patch('corporate.lib.stripe.do_adjust_subscription_quantity'): # test return values self.assertTrue(run_billing_processor_one_step(processor)) self.assertTrue(run_billing_processor_one_step(realm_processor)) # test no processors get added or deleted self.assertEqual(2, BillingProcessor.objects.count())
def check_billing_processor_update(event_type: str, quantity: int) -> None: def check_subscription_save(subscription: stripe.Subscription, idempotency_key: str) -> None: self.assertEqual(subscription.quantity, quantity) log_row = RealmAuditLog.objects.filter( event_type=event_type, requires_billing_update=True).order_by('-id').first() self.assertEqual(idempotency_key, 'process_billing_log_entry:%s' % (log_row.id,)) self.assertEqual(subscription.proration_date, datetime_to_timestamp(log_row.event_time)) with patch.object(stripe.Subscription, 'save', autospec=True, side_effect=check_subscription_save): run_billing_processor_one_step(processor)
def check_billing_processor_update(event_type: str, quantity: int) -> None: def check_subscription_save(subscription: stripe.Subscription, idempotency_key: str) -> None: self.assertEqual(subscription.quantity, quantity) log_row = RealmAuditLog.objects.filter( event_type=event_type, requires_billing_update=True).order_by('-id').first() self.assertEqual(idempotency_key, 'process_billing_log_entry:%s' % (log_row.id,)) self.assertEqual(subscription.proration_date, datetime_to_timestamp(log_row.event_time)) with patch.object(stripe.Subscription, 'save', autospec=True, side_effect=check_subscription_save): run_billing_processor_one_step(processor)
def test_run_billing_processor_with_uncaught_error(self, mock_billing_logger_error: Mock) -> None: # This tests three different things: # * That run_billing_processor_one_step passes through exceptions that # are not StripeCardError # * That process_billing_log_entry catches StripeErrors and re-raises them as BillingErrors # * That processor.state=STARTED for non-StripeCardError exceptions entry1 = self.add_log_entry() entry2 = self.add_log_entry() processor = BillingProcessor.objects.create( log_row=entry1, state=BillingProcessor.DONE) Customer.objects.create(realm=get_realm('zulip'), stripe_customer_id='cust_1') with patch('corporate.lib.stripe.do_adjust_subscription_quantity', side_effect=stripe.error.StripeError('message', json_body={})): with self.assertRaises(BillingError): run_billing_processor_one_step(processor) mock_billing_logger_error.assert_called() # check processor.state is STARTED self.assertTrue(BillingProcessor.objects.filter( log_row=entry2, state=BillingProcessor.STARTED).exists())
def handle(self, *args: Any, **options: Any) -> None: if not settings.BILLING_PROCESSOR_ENABLED: sleep_forever() with lockfile("/tmp/zulip_billing_processor.lockfile"): while True: for processor in BillingProcessor.objects.exclude( state=BillingProcessor.STALLED): try: entry_processed = run_billing_processor_one_step(processor) except StripeConnectionError: time.sleep(5*60) # Less load on the db during times of activity # and more responsiveness when the load is low if entry_processed: time.sleep(10) else: time.sleep(2)