From 84a31ca800d292541576802b34e0e8bc45b2fc9d Mon Sep 17 00:00:00 2001 From: Rishi Gupta Date: Tue, 11 Dec 2018 21:48:43 -0800 Subject: [PATCH] billing: Remove BillingProcessor. Leaving the model in place, so that we can do the database migration by hand. --- corporate/lib/stripe.py | 104 +--------- .../commands/process_billing_updates.py | 47 ----- corporate/models.py | 1 + ..._changes_end_to_end:Customer.create.1.json | Bin 1583 -> 0 bytes ...hanges_end_to_end:Customer.retrieve.1.json | Bin 4882 -> 0 bytes ...hanges_end_to_end:Customer.retrieve.2.json | Bin 4882 -> 0 bytes ...hanges_end_to_end:Customer.retrieve.3.json | Bin 4882 -> 0 bytes ...hanges_end_to_end:Customer.retrieve.4.json | Bin 4882 -> 0 bytes ...hanges_end_to_end:Customer.retrieve.5.json | Bin 4882 -> 0 bytes ...nges_end_to_end:Subscription.create.1.json | Bin 2160 -> 0 bytes ...ity_changes_end_to_end:Token.create.1.json | Bin 826 -> 0 bytes corporate/tests/test_stripe.py | 195 +----------------- zproject/settings.py | 4 - 13 files changed, 5 insertions(+), 346 deletions(-) delete mode 100644 corporate/management/commands/process_billing_updates.py delete mode 100644 corporate/tests/stripe_fixtures/billing_quantity_changes_end_to_end:Customer.create.1.json delete mode 100644 corporate/tests/stripe_fixtures/billing_quantity_changes_end_to_end:Customer.retrieve.1.json delete mode 100644 corporate/tests/stripe_fixtures/billing_quantity_changes_end_to_end:Customer.retrieve.2.json delete mode 100644 corporate/tests/stripe_fixtures/billing_quantity_changes_end_to_end:Customer.retrieve.3.json delete mode 100644 corporate/tests/stripe_fixtures/billing_quantity_changes_end_to_end:Customer.retrieve.4.json delete mode 100644 corporate/tests/stripe_fixtures/billing_quantity_changes_end_to_end:Customer.retrieve.5.json delete mode 100644 corporate/tests/stripe_fixtures/billing_quantity_changes_end_to_end:Subscription.create.1.json delete mode 100644 corporate/tests/stripe_fixtures/billing_quantity_changes_end_to_end:Token.create.1.json diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index b774749965..87c7928ceb 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -18,7 +18,7 @@ from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime from zerver.lib.utils import generate_random_token from zerver.lib.actions import do_change_plan_type from zerver.models import Realm, UserProfile, RealmAuditLog -from corporate.models import Customer, Plan, Coupon, BillingProcessor +from corporate.models import Customer, Plan, Coupon from zproject.settings import get_secret STRIPE_PUBLISHABLE_KEY = get_secret('stripe_publishable_key') @@ -320,105 +320,3 @@ def process_downgrade(user: UserProfile) -> None: # Keeping it out of the transaction.atomic block because it will # eventually have a lot of stuff going on. do_change_plan_type(user.realm, Realm.LIMITED) - -## Process RealmAuditLog - -def do_set_subscription_quantity( - customer: Customer, timestamp: int, idempotency_key: str, quantity: int) -> None: - stripe_customer = stripe_get_customer(customer.stripe_customer_id) - stripe_subscription = extract_current_subscription(stripe_customer) - stripe_subscription.quantity = quantity - stripe_subscription.proration_date = timestamp - stripe.Subscription.save(stripe_subscription, idempotency_key=idempotency_key) - -def do_adjust_subscription_quantity( - customer: Customer, timestamp: int, idempotency_key: str, delta: int) -> None: - stripe_customer = stripe_get_customer(customer.stripe_customer_id) - stripe_subscription = extract_current_subscription(stripe_customer) - stripe_subscription.quantity = stripe_subscription.quantity + delta - stripe_subscription.proration_date = timestamp - stripe.Subscription.save(stripe_subscription, idempotency_key=idempotency_key) - -def increment_subscription_quantity( - customer: Customer, timestamp: int, idempotency_key: str) -> None: - return do_adjust_subscription_quantity(customer, timestamp, idempotency_key, 1) - -def decrement_subscription_quantity( - customer: Customer, timestamp: int, idempotency_key: str) -> None: - return do_adjust_subscription_quantity(customer, timestamp, idempotency_key, -1) - -@catch_stripe_errors -def process_billing_log_entry(processor: BillingProcessor, log_row: RealmAuditLog) -> None: - processor.state = BillingProcessor.STARTED - processor.log_row = log_row - processor.save() - - customer = Customer.objects.get(realm=log_row.realm) - timestamp = datetime_to_timestamp(log_row.event_time) - idempotency_key = 'process_billing_log_entry:%s' % (log_row.id,) - if settings.TEST_SUITE: - idempotency_key += '+' + generate_random_token(10) - extra_args = {} # type: Dict[str, Any] - if log_row.extra_data is not None: - extra_args = ujson.loads(log_row.extra_data) - processing_functions = { - RealmAuditLog.STRIPE_PLAN_QUANTITY_RESET: do_set_subscription_quantity, - RealmAuditLog.USER_CREATED: increment_subscription_quantity, - RealmAuditLog.USER_ACTIVATED: increment_subscription_quantity, - RealmAuditLog.USER_DEACTIVATED: decrement_subscription_quantity, - RealmAuditLog.USER_REACTIVATED: increment_subscription_quantity, - } # type: Dict[str, Callable[..., None]] - processing_functions[log_row.event_type](customer, timestamp, idempotency_key, **extra_args) - - processor.state = BillingProcessor.DONE - processor.save() - -def get_next_billing_log_entry(processor: BillingProcessor) -> Optional[RealmAuditLog]: - if processor.state == BillingProcessor.STARTED: - return processor.log_row - assert processor.state != BillingProcessor.STALLED - if processor.state not in [BillingProcessor.DONE, BillingProcessor.SKIPPED]: - raise BillingError( - 'unknown processor state', - "Check for typos, since this value is sometimes set by hand: %s" % (processor.state,)) - - if processor.realm is None: - realms_with_processors = BillingProcessor.objects.exclude( - realm=None).values_list('realm', flat=True) - query = RealmAuditLog.objects.exclude(realm__in=realms_with_processors) - else: - global_processor = BillingProcessor.objects.get(realm=None) - query = RealmAuditLog.objects.filter( - realm=processor.realm, id__lt=global_processor.log_row.id) - return query.filter(id__gt=processor.log_row.id, - requires_billing_update=True).order_by('id').first() - -def run_billing_processor_one_step(processor: BillingProcessor) -> bool: - # Returns True if a row was processed, or if processing was attempted - log_row = get_next_billing_log_entry(processor) - if log_row is None: - if processor.realm is not None: - processor.delete() - return False - try: - process_billing_log_entry(processor, log_row) - return True - except Exception as e: - # Possible errors include processing subscription quantity entries - # after downgrade, since the downgrade code doesn't check that - # billing processor is up to date - billing_logger.error("Error on log_row.realm=%s, event_type=%s, log_row.id=%s, " - "processor.id=%s, processor.realm=%s" % ( - processor.log_row.realm.string_id, processor.log_row.event_type, - processor.log_row.id, processor.id, processor.realm)) - if isinstance(e, StripeCardError): - if processor.realm is None: - BillingProcessor.objects.create(log_row=processor.log_row, - realm=processor.log_row.realm, - state=BillingProcessor.STALLED) - processor.state = BillingProcessor.SKIPPED - else: - processor.state = BillingProcessor.STALLED - processor.save() - return True - raise diff --git a/corporate/management/commands/process_billing_updates.py b/corporate/management/commands/process_billing_updates.py deleted file mode 100644 index 6aae5b5137..0000000000 --- a/corporate/management/commands/process_billing_updates.py +++ /dev/null @@ -1,47 +0,0 @@ -"""\ -Run BillingProcessors. - -This management command is run via supervisor. Do not run on multiple -machines, as the code has not been made robust to race conditions from doing -so. (Alternatively, you can set `BILLING_PROCESSOR_ENABLED=False` on all but -one machine to make the command have no effect.) -""" - -import time -from typing import Any - -from django.conf import settings -from django.core.management.base import BaseCommand - -from zerver.lib.context_managers import lockfile -from zerver.lib.management import sleep_forever -from corporate.lib.stripe import StripeConnectionError, \ - run_billing_processor_one_step -from corporate.models import BillingProcessor - -class Command(BaseCommand): - help = """Run BillingProcessors, to sync billing-relevant updates into Stripe. - -Run this command under supervisor. - -Usage: ./manage.py process_billing_updates -""" - - 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) diff --git a/corporate/models.py b/corporate/models.py index 6b1c40a10f..22aee8a6c8 100644 --- a/corporate/models.py +++ b/corporate/models.py @@ -29,6 +29,7 @@ class Coupon(models.Model): def __str__(self) -> str: return '' % (self.percent_off, self.stripe_coupon_id, self.id) +# legacy class BillingProcessor(models.Model): log_row = models.ForeignKey(RealmAuditLog, on_delete=models.CASCADE) # RealmAuditLog # Exactly one processor, the global processor, has realm=None. diff --git a/corporate/tests/stripe_fixtures/billing_quantity_changes_end_to_end:Customer.create.1.json b/corporate/tests/stripe_fixtures/billing_quantity_changes_end_to_end:Customer.create.1.json deleted file mode 100644 index 43c157cda1b5de0d97fe218aac0c66ae1b59abfd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1583 zcmb_cOK%e~5We?Utb78gBF_^iDwP8gFHx045waZbEE(#x7uy>(sq){M@!H9zG=c+r zaWao@zIi@gRFzO#yWU2*Qbt(~;&nB>WV!aBBGmMMHZ9+v=)DK4x1_PXF-cZKqk0o% zaJ^3$LMva(kDtDLxO)HY?(KC!NbgK!CxiA^4>l51qfCG-4%%ZE(OHssLQjaQm;YZvqPuCS;RRRlq8>!3ryv_dWRi@FZg?o1nJLH zSb1gBNPNYh#@=bLaz$=V6s1KJ^UNG&{(7U&2d=kPwWy_0Auj(+4jww$IvdxdTJVr$ zOqOp!@f6Lci?ODmK>^=+Osg~u?s4tP95Re(dRq@;#T#B@9Osue|McW^! ze*R=tDf1(~!BEl`(M4rsIVH0U(ff2qCY#x$tYzR|PcFx#Y$U}W);aX6!}WS*QU3$Q lbe$=kf2MfC=s+~82Z^?ExsaTnnPJyBg>K+QTbrB~$BnSwKOG&Kv6_P7hHvHc^GbC5s z6?d(k>O-t>KF$p1@y*E3XNv_BQZ`-f_)3(bmWutbxOju-(kS6n0smiK#2+u{j8V1R zgM@XqV02ihwdhL6ZPOW2@(emqdr=r=EtlHSg};Qqae$h zTp3Yg9NFL6ig=5H9$tLkA9mI?l`;sHo#lVtefZ=3@4tQi^%gR)>`^&@OtX z>8VnfV(5OXp+raYlU-k4U&i~Da-tAUfad3y;3+%{6T;pX0t>^K2yRyYC`o>z!FcML z2UY7Q;dE1T(6?>SC(JY?nsl;4K`DLf6g_e+N~_Qm99wC0ix<(}Q&;MC@!e87fLF7eJ*RHbr1)77`Ksr;1?m3&T_u6K==inm6s^&@Jbu!oehQtwpN z6c~bot7)8k{X)Z*75A3ysPXgZaj`;6mu^kfSeT zdl60teU$I{A!NSq2iW=EKZa&|cbcT53e78J@Jkva|_-RO|%k$Wg zeLLH3)w9=WjoP-QmYycp3g}0{>?THxZI5pP_gA`v?(<~76=tKj=)f_!DMeZC1Mj9# zF88toSS_~=QqORQAgxfwxNwwQIJB&rf-9PnJ;x1TssJEp8o4&Lro_9ntc(ogof#sb z`3;(zz@9LtDIA5^TMoO8E_u-nmkKW*?@goUWkyYS8N_NLB)*fN8b%e9rU`be>QEy= z<%rV#si%AY8ULMJ3=a?h1t0XZ3m(oU+BHZIeUlb9NAHu@1aJ!rY(Jj{-U#Vngo4bO zjs|@y1#iV>W7Gz=KHgceIZjiAGg0Bd0!25l&iH^o2$oPcB`kmn|F8i!%>tlH?>n{Cx~T;(=!i=m@zx0cC&j) z3IJQP=>>VM2z8d?0{jz zMfVwrfy!y?n!>PYD%ZlzLgUuhY zZ=C>^p)XlCI))6JJ|3C)5&ok{fPXNrl+WG)_z3Jf(C@^nk+C8o5e2cT$O$EQhbVxU`P!HU| zoOn!DzKgfXKnu?MeQd~nK@_c`FmT8Jw+Rmx-+mv)_vw1wB-y1m;~C$<#tDCjqUARL I&R)*`1sbLP761SM diff --git a/corporate/tests/stripe_fixtures/billing_quantity_changes_end_to_end:Customer.retrieve.2.json b/corporate/tests/stripe_fixtures/billing_quantity_changes_end_to_end:Customer.retrieve.2.json deleted file mode 100644 index 2234b2f40d8860a30bbca1d4b2e9f3cfd7504424..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4882 zcmc&&OK%%D5Wf3Y2t2hw5L>nnPJyBg>K+QTbrB~$BnSwKOG&Kv6_P7hHvHc^GbC5s z6?d(k>O-t>KF$p1@y*E3XNv_BQZ`-f_)3(bmWutbxOju-(kS6n0smiK#2+u{j8V1R zgM@XqV02ihwdhL6ZPOW2@(emqdr=r=EtlHSg};Qqae$h zTp3Yg9NFL6ig=5H9$tLkA9mI?l`;sHo#lVtefZ=3@4tQi^%gR)>`^&@OtX z>8VnfV(5OXp+raYlU-k4U&i~Da-tAUfad3y;3+%{6T;pX0t>^K2yRyYC`o>z!FcML z2UY7Q;dE1T(6?>SC(JY?nsl;4K`DLf6g_e+N~_Qm99wC0ix<(}Q&;MC@!e87fLF7eJ*RHbr1)77`Ksr;1?m3&T_u6K==inm6s^&@Jbu!oehQtwpN z6c~bot7)8k{X)Z*75A3ysPXgZaj`;6mu^kfSeT zdl60teU$I{A!NSq2iW=EKZa&|cbcT53e78J@Jkva|_-RO|%k$Wg zeLLH3)w9=WjoP-QmYycp3g}0{>?THxZI5pP_gA`v?(<~76=tKj=)f_!DMeZC1Mj9# zF88toSS_~=QqORQAgxfwxNwwQIJB&rf-9PnJ;x1TssJEp8o4&Lro_9ntc(ogof#sb z`3;(zz@9LtDIA5^TMoO8E_u-nmkKW*?@goUWkyYS8N_NLB)*fN8b%e9rU`be>QEy= z<%rV#si%AY8ULMJ3=a?h1t0XZ3m(oU+BHZIeUlb9NAHu@1aJ!rY(Jj{-U#Vngo4bO zjs|@y1#iV>W7Gz=KHgceIZjiAGg0Bd0!25l&iH^o2$oPcB`kmn|F8i!%>tlH?>n{Cx~T;(=!i=m@zx0cC&j) z3IJQP=>>VM2z8d?0{jz zMfVwrfy!y?n!>PYD%ZlzLgUuhY zZ=C>^p)XlCI))6JJ|3C)5&ok{fPXNrl+WG)_z3Jf(C@^nk+C8o5e2cT$O$EQhbVxU`P!HU| zoOn!DzKgfXKnu?MeQd~nK@_c`FmT8Jw+Rmx-+mv)_vw1wB-y1m;~C$<#tDCjqUARL I&R)*`1sbLP761SM diff --git a/corporate/tests/stripe_fixtures/billing_quantity_changes_end_to_end:Customer.retrieve.3.json b/corporate/tests/stripe_fixtures/billing_quantity_changes_end_to_end:Customer.retrieve.3.json deleted file mode 100644 index 2234b2f40d8860a30bbca1d4b2e9f3cfd7504424..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4882 zcmc&&OK%%D5Wf3Y2t2hw5L>nnPJyBg>K+QTbrB~$BnSwKOG&Kv6_P7hHvHc^GbC5s z6?d(k>O-t>KF$p1@y*E3XNv_BQZ`-f_)3(bmWutbxOju-(kS6n0smiK#2+u{j8V1R zgM@XqV02ihwdhL6ZPOW2@(emqdr=r=EtlHSg};Qqae$h zTp3Yg9NFL6ig=5H9$tLkA9mI?l`;sHo#lVtefZ=3@4tQi^%gR)>`^&@OtX z>8VnfV(5OXp+raYlU-k4U&i~Da-tAUfad3y;3+%{6T;pX0t>^K2yRyYC`o>z!FcML z2UY7Q;dE1T(6?>SC(JY?nsl;4K`DLf6g_e+N~_Qm99wC0ix<(}Q&;MC@!e87fLF7eJ*RHbr1)77`Ksr;1?m3&T_u6K==inm6s^&@Jbu!oehQtwpN z6c~bot7)8k{X)Z*75A3ysPXgZaj`;6mu^kfSeT zdl60teU$I{A!NSq2iW=EKZa&|cbcT53e78J@Jkva|_-RO|%k$Wg zeLLH3)w9=WjoP-QmYycp3g}0{>?THxZI5pP_gA`v?(<~76=tKj=)f_!DMeZC1Mj9# zF88toSS_~=QqORQAgxfwxNwwQIJB&rf-9PnJ;x1TssJEp8o4&Lro_9ntc(ogof#sb z`3;(zz@9LtDIA5^TMoO8E_u-nmkKW*?@goUWkyYS8N_NLB)*fN8b%e9rU`be>QEy= z<%rV#si%AY8ULMJ3=a?h1t0XZ3m(oU+BHZIeUlb9NAHu@1aJ!rY(Jj{-U#Vngo4bO zjs|@y1#iV>W7Gz=KHgceIZjiAGg0Bd0!25l&iH^o2$oPcB`kmn|F8i!%>tlH?>n{Cx~T;(=!i=m@zx0cC&j) z3IJQP=>>VM2z8d?0{jz zMfVwrfy!y?n!>PYD%ZlzLgUuhY zZ=C>^p)XlCI))6JJ|3C)5&ok{fPXNrl+WG)_z3Jf(C@^nk+C8o5e2cT$O$EQhbVxU`P!HU| zoOn!DzKgfXKnu?MeQd~nK@_c`FmT8Jw+Rmx-+mv)_vw1wB-y1m;~C$<#tDCjqUARL I&R)*`1sbLP761SM diff --git a/corporate/tests/stripe_fixtures/billing_quantity_changes_end_to_end:Customer.retrieve.4.json b/corporate/tests/stripe_fixtures/billing_quantity_changes_end_to_end:Customer.retrieve.4.json deleted file mode 100644 index 2234b2f40d8860a30bbca1d4b2e9f3cfd7504424..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4882 zcmc&&OK%%D5Wf3Y2t2hw5L>nnPJyBg>K+QTbrB~$BnSwKOG&Kv6_P7hHvHc^GbC5s z6?d(k>O-t>KF$p1@y*E3XNv_BQZ`-f_)3(bmWutbxOju-(kS6n0smiK#2+u{j8V1R zgM@XqV02ihwdhL6ZPOW2@(emqdr=r=EtlHSg};Qqae$h zTp3Yg9NFL6ig=5H9$tLkA9mI?l`;sHo#lVtefZ=3@4tQi^%gR)>`^&@OtX z>8VnfV(5OXp+raYlU-k4U&i~Da-tAUfad3y;3+%{6T;pX0t>^K2yRyYC`o>z!FcML z2UY7Q;dE1T(6?>SC(JY?nsl;4K`DLf6g_e+N~_Qm99wC0ix<(}Q&;MC@!e87fLF7eJ*RHbr1)77`Ksr;1?m3&T_u6K==inm6s^&@Jbu!oehQtwpN z6c~bot7)8k{X)Z*75A3ysPXgZaj`;6mu^kfSeT zdl60teU$I{A!NSq2iW=EKZa&|cbcT53e78J@Jkva|_-RO|%k$Wg zeLLH3)w9=WjoP-QmYycp3g}0{>?THxZI5pP_gA`v?(<~76=tKj=)f_!DMeZC1Mj9# zF88toSS_~=QqORQAgxfwxNwwQIJB&rf-9PnJ;x1TssJEp8o4&Lro_9ntc(ogof#sb z`3;(zz@9LtDIA5^TMoO8E_u-nmkKW*?@goUWkyYS8N_NLB)*fN8b%e9rU`be>QEy= z<%rV#si%AY8ULMJ3=a?h1t0XZ3m(oU+BHZIeUlb9NAHu@1aJ!rY(Jj{-U#Vngo4bO zjs|@y1#iV>W7Gz=KHgceIZjiAGg0Bd0!25l&iH^o2$oPcB`kmn|F8i!%>tlH?>n{Cx~T;(=!i=m@zx0cC&j) z3IJQP=>>VM2z8d?0{jz zMfVwrfy!y?n!>PYD%ZlzLgUuhY zZ=C>^p)XlCI))6JJ|3C)5&ok{fPXNrl+WG)_z3Jf(C@^nk+C8o5e2cT$O$EQhbVxU`P!HU| zoOn!DzKgfXKnu?MeQd~nK@_c`FmT8Jw+Rmx-+mv)_vw1wB-y1m;~C$<#tDCjqUARL I&R)*`1sbLP761SM diff --git a/corporate/tests/stripe_fixtures/billing_quantity_changes_end_to_end:Customer.retrieve.5.json b/corporate/tests/stripe_fixtures/billing_quantity_changes_end_to_end:Customer.retrieve.5.json deleted file mode 100644 index 2234b2f40d8860a30bbca1d4b2e9f3cfd7504424..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4882 zcmc&&OK%%D5Wf3Y2t2hw5L>nnPJyBg>K+QTbrB~$BnSwKOG&Kv6_P7hHvHc^GbC5s z6?d(k>O-t>KF$p1@y*E3XNv_BQZ`-f_)3(bmWutbxOju-(kS6n0smiK#2+u{j8V1R zgM@XqV02ihwdhL6ZPOW2@(emqdr=r=EtlHSg};Qqae$h zTp3Yg9NFL6ig=5H9$tLkA9mI?l`;sHo#lVtefZ=3@4tQi^%gR)>`^&@OtX z>8VnfV(5OXp+raYlU-k4U&i~Da-tAUfad3y;3+%{6T;pX0t>^K2yRyYC`o>z!FcML z2UY7Q;dE1T(6?>SC(JY?nsl;4K`DLf6g_e+N~_Qm99wC0ix<(}Q&;MC@!e87fLF7eJ*RHbr1)77`Ksr;1?m3&T_u6K==inm6s^&@Jbu!oehQtwpN z6c~bot7)8k{X)Z*75A3ysPXgZaj`;6mu^kfSeT zdl60teU$I{A!NSq2iW=EKZa&|cbcT53e78J@Jkva|_-RO|%k$Wg zeLLH3)w9=WjoP-QmYycp3g}0{>?THxZI5pP_gA`v?(<~76=tKj=)f_!DMeZC1Mj9# zF88toSS_~=QqORQAgxfwxNwwQIJB&rf-9PnJ;x1TssJEp8o4&Lro_9ntc(ogof#sb z`3;(zz@9LtDIA5^TMoO8E_u-nmkKW*?@goUWkyYS8N_NLB)*fN8b%e9rU`be>QEy= z<%rV#si%AY8ULMJ3=a?h1t0XZ3m(oU+BHZIeUlb9NAHu@1aJ!rY(Jj{-U#Vngo4bO zjs|@y1#iV>W7Gz=KHgceIZjiAGg0Bd0!25l&iH^o2$oPcB`kmn|F8i!%>tlH?>n{Cx~T;(=!i=m@zx0cC&j) z3IJQP=>>VM2z8d?0{jz zMfVwrfy!y?n!>PYD%ZlzLgUuhY zZ=C>^p)XlCI))6JJ|3C)5&ok{fPXNrl+WG)_z3Jf(C@^nk+C8o5e2cT$O$EQhbVxU`P!HU| zoOn!DzKgfXKnu?MeQd~nK@_c`FmT8Jw+Rmx-+mv)_vw1wB-y1m;~C$<#tDCjqUARL I&R)*`1sbLP761SM diff --git a/corporate/tests/stripe_fixtures/billing_quantity_changes_end_to_end:Subscription.create.1.json b/corporate/tests/stripe_fixtures/billing_quantity_changes_end_to_end:Subscription.create.1.json deleted file mode 100644 index 705c1038cfe8a01b69b5784458d9803877b01d71..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2160 zcma)8O^*{X488BKsB%W^R_vjRgb<<}kbp`HClHF2Had8xy0*u&ImQKtc+E0uU?1aA^vP+JJU43y=kc6zPOjbtFxwEKVnd)f&vD&}0LFY9FS8)<>LEPNLs-Yvq=( z<=(+IAKW#hOygS+5Z8LH#NkG;OzTP-N6X+s&77rwJdi#yK)JZ~+o3GEhIE$Fo#Xe<<}BNR zqxS3#Fzh;5tz64Ch80pXHwZItBPZ_n^ZTA~+}qsrWB=1QH1rVkyVywj&QP$ zG}(YCC*{bXFU0oUFfPFf@r}qX?Wl3VF*Pi;i@gWI(CkP)z3EL!Dw}L3CiY)4VtoNM zroMC-PKbt_Q*xo*MLTp*C=G{h#lc42g=9t_q}Y-qsDU9IvcCrpFXJ*!B{lEyb^|11 Ln?0EC&UgO+|5aE> diff --git a/corporate/tests/stripe_fixtures/billing_quantity_changes_end_to_end:Token.create.1.json b/corporate/tests/stripe_fixtures/billing_quantity_changes_end_to_end:Token.create.1.json deleted file mode 100644 index 60d47536e7c17c03b5152a712f27efc6362cb64e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 826 zcmaJe?5488L!s+`eO3a8zWw!@@-X^0(msZ3&4k2b9m7p)-v`{F>^Rw2?$YCk`} z7kfWS5~aZzb(`$NJ^X<&4!zGcaTlWM6*R3$tMMRRx0YQ*EG;pbWWm7bhq#g~PK0M^ z4Z4I2OgzH)A}rSh>WzR~8?R_C;j)0RZCMT!dFg*SwWCH@r*ejjA$O(k%7K-~QSaoT zFV%m%FV4ZOzQSWV3l-_S1kbboBJsP)tJ-oA!qbx{iFF7rgtBznN7rOO(KU`Ni=^k5 zx2O5z!^izyDreFwS+~{%q(HPoL)<4QI+1bbgW5>&m!sFIX4x$3>nmh1zz~@Chm+D^ zOCDX#4af#K*MsZT7wV`p?$-&<^#(2NfT^~5;45m;B-0G*6*-kI2`%Txk)=2Bi=^Tv zj93aLz2H=M)zMhszV1tvw8g46B6tm@$5{L;G>SX6;;xCd&e~P$W9L{mI*fh*S9axJ diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index 5b87fd8575..cf834d9143 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -26,11 +26,10 @@ from zerver.models import Realm, UserProfile, get_realm, RealmAuditLog from corporate.lib.stripe import catch_stripe_errors, \ do_subscribe_customer_to_plan, attach_discount_to_realm, \ get_seat_count, extract_current_subscription, sign_string, unsign_string, \ - get_next_billing_log_entry, run_billing_processor_one_step, \ BillingError, StripeCardError, StripeConnectionError, stripe_get_customer, \ DEFAULT_INVOICE_DAYS_UNTIL_DUE, MIN_INVOICED_SEAT_COUNT, do_create_customer, \ process_downgrade -from corporate.models import Customer, Plan, Coupon, BillingProcessor +from corporate.models import Customer, Plan, Coupon from corporate.views import payment_method_string import corporate.urls @@ -210,13 +209,6 @@ class Kandra(object): def __eq__(self, other: Any) -> bool: return True -def process_all_billing_log_entries() -> None: - assert not RealmAuditLog.objects.get(pk=1).requires_billing_update - processor = BillingProcessor.objects.create( - log_row=RealmAuditLog.objects.get(pk=1), realm=None, state=BillingProcessor.DONE) - while run_billing_processor_one_step(processor): - pass - class StripeTest(ZulipTestCase): @mock_stripe(generate=False) def setUp(self, *mocks: Mock) -> None: @@ -517,7 +509,6 @@ class StripeTest(ZulipTestCase): user = self.example_user("hamlet") self.login(user.email) self.upgrade(invoice=True) - process_all_billing_log_entries() # Check that we correctly created a Customer in Stripe stripe_customer = stripe_get_customer(Customer.objects.get(realm=user.realm).stripe_customer_id) @@ -536,7 +527,8 @@ class StripeTest(ZulipTestCase): self.assertEqual(stripe_subscription.days_until_due, DEFAULT_INVOICE_DAYS_UNTIL_DUE) self.assertEqual(stripe_subscription.plan.id, Plan.objects.get(nickname=Plan.CLOUD_ANNUAL).stripe_plan_id) - self.assertEqual(stripe_subscription.quantity, get_seat_count(user.realm)) + # In the middle of migrating off of this billing algorithm + # self.assertEqual(stripe_subscription.quantity, get_seat_count(user.realm)) self.assertEqual(stripe_subscription.status, 'active') # Check that we correctly created an initial Invoice in Stripe for stripe_invoice in stripe.Invoice.list(customer=stripe_customer.id, limit=1): @@ -796,48 +788,6 @@ class StripeTest(ZulipTestCase): self.assertEqual(number_of_sources, 1) self.assertFalse(RealmAuditLog.objects.filter(event_type=RealmAuditLog.STRIPE_CARD_CHANGED).exists()) - @mock_stripe() - def test_billing_quantity_changes_end_to_end(self, *mocks: Mock) -> None: - # A full end to end check would check the InvoiceItems, but this test is partway there - self.login(self.example_email("hamlet")) - processor = BillingProcessor.objects.create( - log_row=RealmAuditLog.objects.order_by('id').first(), state=BillingProcessor.DONE) - - 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.split('+')[0], - 'process_billing_log_entry:%s' % (log_row.id,)) - self.assertEqual(subscription.proration_date, datetime_to_timestamp(log_row.event_time)) - with patch('stripe.Subscription.save', side_effect=check_subscription_save): - run_billing_processor_one_step(processor) - - # Test STRIPE_PLAN_QUANTITY_RESET - new_seat_count = 123 - # change the seat count while the user is going through the upgrade flow - with patch('corporate.lib.stripe.get_seat_count', return_value=new_seat_count): - self.upgrade() - check_billing_processor_update(RealmAuditLog.STRIPE_PLAN_QUANTITY_RESET, new_seat_count) - - # Test USER_CREATED - user = do_create_user('newuser@zulip.com', 'password', get_realm('zulip'), 'full name', 'short name') - check_billing_processor_update(RealmAuditLog.USER_CREATED, self.seat_count + 1) - - # Test USER_DEACTIVATED - do_deactivate_user(user) - check_billing_processor_update(RealmAuditLog.USER_DEACTIVATED, self.seat_count - 1) - - # Test USER_REACTIVATED - do_reactivate_user(user) - check_billing_processor_update(RealmAuditLog.USER_REACTIVATED, self.seat_count + 1) - - # Test USER_ACTIVATED - # Not a proper use of do_activate_user, but it's fine to call it like this for this test - do_activate_user(user) - check_billing_processor_update(RealmAuditLog.USER_ACTIVATED, self.seat_count + 1) - class RequiresBillingUpdateTest(ZulipTestCase): def test_activity_change_requires_seat_update(self) -> None: # Realm doesn't have a seat based plan @@ -921,142 +871,3 @@ class RequiresBillingAccessTest(ZulipTestCase): json_endpoints.remove("json/billing/upgrade") self.assertEqual(len(json_endpoints), len(params)) - -class BillingProcessorTest(ZulipTestCase): - def add_log_entry(self, realm: Realm=get_realm('zulip'), - event_type: str=RealmAuditLog.USER_CREATED, - requires_billing_update: bool=True) -> RealmAuditLog: - return RealmAuditLog.objects.create( - realm=realm, event_time=datetime.datetime(2001, 2, 3, 4, 5, 6).replace(tzinfo=timezone_utc), - event_type=event_type, requires_billing_update=requires_billing_update) - - def test_get_next_billing_log_entry(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.STARTED) - - # Test STARTED, STALLED, and typo'ed state entry - self.assertEqual(entry2, get_next_billing_log_entry(processor)) - processor.state = BillingProcessor.STALLED - processor.save() - with self.assertRaises(AssertionError): - get_next_billing_log_entry(processor) - processor.state = 'typo' - processor.save() - with self.assertRaisesRegex(BillingError, 'unknown processor state'): - get_next_billing_log_entry(processor) - - # Test global processor is handled correctly - processor.state = BillingProcessor.DONE - processor.save() - # test it ignores entries with requires_billing_update=False - entry3 = self.add_log_entry(requires_billing_update=False) - # test it ignores entries with realm processors - entry4 = self.add_log_entry(realm=second_realm) - self.assertIsNone(get_next_billing_log_entry(processor)) - # test it does catch entries it should - entry5 = self.add_log_entry() - self.assertEqual(entry5, get_next_billing_log_entry(processor)) - - # Test realm processor is handled correctly - # test it gets the entry with its realm, and ignores the entry with - # requires_billing_update=False, when global processor is up ahead - processor.log_row = entry5 - processor.save() - self.assertEqual(entry4, get_next_billing_log_entry(realm_processor)) - - # test it doesn't run past the global processor - processor.log_row = entry3 - processor.save() - self.assertIsNone(get_next_billing_log_entry(realm_processor)) - - 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()) - - @patch("corporate.lib.stripe.billing_logger.error") - 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() - - @patch("corporate.lib.stripe.billing_logger.error") - 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()) diff --git a/zproject/settings.py b/zproject/settings.py index 099384431e..0c29fec6b6 100644 --- a/zproject/settings.py +++ b/zproject/settings.py @@ -454,10 +454,6 @@ DEFAULT_SETTINGS.update({ # Enables billing pages and plan-based feature gates. If False, all features # are available to all realms. 'BILLING_ENABLED': False, - - # Controls whether we run the worker that syncs billing-related updates - # into Stripe. Should be True on at most one machine. - 'BILLING_PROCESSOR_ENABLED': False, })