def test_upgrade_with_outdated_seat_count(self, mock_create_subscription: mock.Mock, mock_create_customer: mock.Mock) -> None: self.login(self.example_email("hamlet")) new_seat_count = 123 # Change the seat count while the user is going through the upgrade flow with mock.patch('zilencer.lib.stripe.get_seat_count', return_value=new_seat_count): self.client_post("/upgrade/", {'stripeToken': self.token, 'signed_seat_count': self.signed_seat_count, 'salt': self.salt, 'plan': Plan.CLOUD_ANNUAL}) # Check that the subscription call used the old quantity, not new_seat_count mock_create_subscription.assert_called_once_with( customer=self.stripe_customer_id, billing='charge_automatically', items=[{ 'plan': self.stripe_plan_id, 'quantity': self.quantity, }], prorate=True, tax_percent=0) # Check that we have the REALM_PLAN_QUANTITY_UPDATED entry, and that we # correctly handled the requires_billing_update field audit_log_entries = list(RealmAuditLog.objects.order_by('-id') .values_list('event_type', 'event_time', 'requires_billing_update')[:4])[::-1] self.assertEqual(audit_log_entries, [ (RealmAuditLog.REALM_STRIPE_INITIALIZED, timestamp_to_datetime(self.customer_created), False), (RealmAuditLog.REALM_CARD_ADDED, timestamp_to_datetime(self.customer_created), False), (RealmAuditLog.REALM_PLAN_STARTED, timestamp_to_datetime(self.subscription_created), False), (RealmAuditLog.REALM_PLAN_QUANTITY_UPDATED, timestamp_to_datetime(self.subscription_created), True), ]) self.assertEqual(ujson.loads(RealmAuditLog.objects.filter( event_type=RealmAuditLog.REALM_PLAN_QUANTITY_UPDATED).values_list('extra_data', flat=True).first()), {'quantity': new_seat_count})
def test_upgrade_with_outdated_seat_count( self, mock4: Mock, mock3: Mock, mock2: Mock, mock1: Mock) -> None: self.login(self.example_email("hamlet")) new_seat_count = 123 # Change the seat count while the user is going through the upgrade flow response = self.client_get("/upgrade/") with patch('corporate.lib.stripe.get_seat_count', return_value=new_seat_count): self.client_post("/upgrade/", { 'stripeToken': stripe_create_token().id, 'signed_seat_count': self.get_signed_seat_count_from_response(response), 'salt': self.get_salt_from_response(response), 'plan': Plan.CLOUD_ANNUAL}) # Check that the subscription call used the old quantity, not new_seat_count stripe_customer = stripe_get_customer( Customer.objects.get(realm=get_realm('zulip')).stripe_customer_id) stripe_subscription = extract_current_subscription(stripe_customer) self.assertEqual(stripe_subscription.quantity, self.quantity) # Check that we have the STRIPE_PLAN_QUANTITY_RESET entry, and that we # correctly handled the requires_billing_update field audit_log_entries = list(RealmAuditLog.objects.order_by('-id') .values_list('event_type', 'event_time', 'requires_billing_update')[:5])[::-1] self.assertEqual(audit_log_entries, [ (RealmAuditLog.STRIPE_CUSTOMER_CREATED, timestamp_to_datetime(stripe_customer.created), False), (RealmAuditLog.STRIPE_CARD_CHANGED, timestamp_to_datetime(stripe_customer.created), False), # TODO: Ideally this test would force stripe_customer.created != stripe_subscription.created (RealmAuditLog.STRIPE_PLAN_CHANGED, timestamp_to_datetime(stripe_subscription.created), False), (RealmAuditLog.STRIPE_PLAN_QUANTITY_RESET, timestamp_to_datetime(stripe_subscription.created), True), (RealmAuditLog.REALM_PLAN_TYPE_CHANGED, Kandra(), False), ]) self.assertEqual(ujson.loads(RealmAuditLog.objects.filter( event_type=RealmAuditLog.STRIPE_PLAN_QUANTITY_RESET).values_list('extra_data', flat=True).first()), {'quantity': new_seat_count})
def test_initial_upgrade(self, mock_create_subscription: mock.Mock, mock_create_customer: mock.Mock) -> None: user = self.example_user("hamlet") self.login(user.email) response = self.client_get("/upgrade/") self.assert_in_success_response(['We can also bill by invoice'], response) self.assertFalse(user.realm.has_seat_based_plan) self.assertNotEqual(user.realm.plan_type, Realm.PREMIUM) # Click "Make payment" in Stripe Checkout self.client_post("/upgrade/", { 'stripeToken': self.token, 'signed_seat_count': self.get_signed_seat_count_from_response(response), 'salt': self.get_salt_from_response(response), 'plan': Plan.CLOUD_ANNUAL}) # Check that we created a customer and subscription in stripe mock_create_customer.assert_called_once_with( description="zulip (Zulip Dev)", email=user.email, metadata={'realm_id': user.realm.id, 'realm_str': 'zulip'}, source=self.token, coupon=None) mock_create_subscription.assert_called_once_with( customer=self.stripe_customer_id, billing='charge_automatically', items=[{ 'plan': self.stripe_plan_id, 'quantity': self.quantity, }], prorate=True, tax_percent=0) # Check that we correctly populated Customer and RealmAuditLog in Zulip self.assertEqual(1, Customer.objects.filter(stripe_customer_id=self.stripe_customer_id, realm=user.realm).count()) audit_log_entries = list(RealmAuditLog.objects.filter(acting_user=user) .values_list('event_type', 'event_time').order_by('id')) self.assertEqual(audit_log_entries, [ (RealmAuditLog.STRIPE_CUSTOMER_CREATED, timestamp_to_datetime(self.customer_created)), (RealmAuditLog.STRIPE_CARD_ADDED, timestamp_to_datetime(self.customer_created)), (RealmAuditLog.STRIPE_PLAN_CHANGED, timestamp_to_datetime(self.subscription_created)), (RealmAuditLog.REALM_PLAN_TYPE_CHANGED, Kandra()), ]) # Check that we correctly updated Realm realm = get_realm("zulip") self.assertTrue(realm.has_seat_based_plan) self.assertEqual(realm.plan_type, Realm.PREMIUM) self.assertEqual(realm.max_invites, Realm.MAX_INVITES_PREMIUM) # Check that we can no longer access /upgrade response = self.client_get("/upgrade/") self.assertEqual(response.status_code, 302) self.assertEqual('/billing/', response.url)
def do_subscribe_customer_to_plan(user: UserProfile, stripe_customer: stripe.Customer, stripe_plan_id: str, seat_count: int, tax_percent: float) -> None: if extract_current_subscription(stripe_customer) is not None: # Most likely due to two people in the org going to the billing page, # and then both upgrading their plan. We don't send clients # real-time event updates for the billing pages, so this is more # likely than it would be in other parts of the app. billing_logger.error("Stripe customer %s trying to subscribe to %s, " "but has an active subscription" % (stripe_customer.id, stripe_plan_id)) raise BillingError('subscribing with existing subscription', BillingError.TRY_RELOADING) customer = Customer.objects.get(stripe_customer_id=stripe_customer.id) # Note that there is a race condition here, where if two users upgrade at exactly the # same time, they will have two subscriptions, and get charged twice. We could try to # reduce the chance of it with a well-designed idempotency_key, but it's not easy since # we also need to be careful not to block the customer from retrying if their # subscription attempt fails (e.g. due to insufficient funds). # Success here implies the stripe_customer was charged: https://stripe.com/docs/billing/lifecycle#active # Otherwise we should expect it to throw a stripe.error. stripe_subscription = stripe.Subscription.create( customer=stripe_customer.id, billing='charge_automatically', items=[{ 'plan': stripe_plan_id, 'quantity': seat_count, }], prorate=True, tax_percent=tax_percent) if PRINT_STRIPE_FIXTURE_DATA: print(''.join(['"create_subscription": ', str(stripe_subscription), ','])) # nocoverage with transaction.atomic(): customer.has_billing_relationship = True customer.save(update_fields=['has_billing_relationship']) customer.realm.has_seat_based_plan = True customer.realm.save(update_fields=['has_seat_based_plan']) RealmAuditLog.objects.create( realm=customer.realm, acting_user=user, event_type=RealmAuditLog.STRIPE_PLAN_CHANGED, event_time=timestamp_to_datetime(stripe_subscription.created), extra_data=ujson.dumps({'plan': stripe_plan_id, 'quantity': seat_count})) current_seat_count = get_seat_count(customer.realm) if seat_count != current_seat_count: RealmAuditLog.objects.create( realm=customer.realm, event_type=RealmAuditLog.STRIPE_PLAN_QUANTITY_RESET, event_time=timestamp_to_datetime(stripe_subscription.created), requires_billing_update=True, extra_data=ujson.dumps({'quantity': current_seat_count}))
def do_create_customer(user: UserProfile, stripe_token: Optional[str]=None, coupon: Optional[Coupon]=None) -> stripe.Customer: realm = user.realm stripe_coupon_id = None if coupon is not None: stripe_coupon_id = coupon.stripe_coupon_id # We could do a better job of handling race conditions here, but if two # people from a realm try to upgrade at exactly the same time, the main # bad thing that will happen is that we will create an extra stripe # customer that we can delete or ignore. stripe_customer = stripe.Customer.create( description="%s (%s)" % (realm.string_id, realm.name), email=user.email, metadata={'realm_id': realm.id, 'realm_str': realm.string_id}, source=stripe_token, coupon=stripe_coupon_id) if PRINT_STRIPE_FIXTURE_DATA: print(''.join(['"create_customer": ', str(stripe_customer), ','])) # nocoverage event_time = timestamp_to_datetime(stripe_customer.created) with transaction.atomic(): RealmAuditLog.objects.create( realm=user.realm, acting_user=user, event_type=RealmAuditLog.STRIPE_CUSTOMER_CREATED, event_time=event_time) if stripe_token is not None: RealmAuditLog.objects.create( realm=user.realm, acting_user=user, event_type=RealmAuditLog.STRIPE_CARD_CHANGED, event_time=event_time) Customer.objects.create(realm=realm, stripe_customer_id=stripe_customer.id) user.is_billing_admin = True user.save(update_fields=["is_billing_admin"]) return stripe_customer
def consume(self, event): logging.info("Received event: %s" % (event),) user_profile = get_user_profile_by_id(event["user_profile_id"]) client = get_client(event["client"]) log_time = timestamp_to_datetime(event["time"]) status = event["status"] do_update_user_presence(user_profile, client, log_time, status)
def consume(self, event: Mapping[str, Any]) -> None: logging.debug("Received presence event: %s" % (event),) user_profile = get_user_profile_by_id(event["user_profile_id"]) client = get_client(event["client"]) log_time = timestamp_to_datetime(event["time"]) status = event["status"] do_update_user_presence(user_profile, client, log_time, status)
def compute_stats(log_level): # type: (int) -> None logger = logging.getLogger() logger.setLevel(log_level) one_week_ago = timestamp_to_datetime(time.time()) - datetime.timedelta(weeks=1) mit_query = Message.objects.filter(sender__realm__string_id="mit", recipient__type=Recipient.STREAM, pub_date__gt=one_week_ago) for bot_sender_start in ["imap.", "rcmd.", "sys."]: mit_query = mit_query.exclude(sender__email__startswith=(bot_sender_start)) # Filtering for "/" covers tabbott/extra@ and all the daemon/foo bots. mit_query = mit_query.exclude(sender__email__contains=("/")) mit_query = mit_query.exclude(sender__email__contains=("aim.com")) mit_query = mit_query.exclude( sender__email__in=["*****@*****.**", "*****@*****.**", "*****@*****.**", "*****@*****.**", "*****@*****.**", "*****@*****.**", "*****@*****.**", "*****@*****.**", "www-data|[email protected]"]) user_counts = {} # type: Dict[str, Dict[str, int]] for m in mit_query.select_related("sending_client", "sender"): email = m.sender.email user_counts.setdefault(email, {}) user_counts[email].setdefault(m.sending_client.name, 0) user_counts[email][m.sending_client.name] += 1 total_counts = {} # type: Dict[str, int] total_user_counts = {} # type: Dict[str, int] for email, counts in user_counts.items(): total_user_counts.setdefault(email, 0) for client_name, count in counts.items(): total_counts.setdefault(client_name, 0) total_counts[client_name] += count total_user_counts[email] += count logging.debug("%40s | %10s | %s" % ("User", "Messages", "Percentage Zulip")) top_percents = {} # type: Dict[int, float] for size in [10, 25, 50, 100, 200, len(total_user_counts.keys())]: top_percents[size] = 0.0 for i, email in enumerate(sorted(total_user_counts.keys(), key=lambda x: -total_user_counts[x])): percent_zulip = round(100 - (user_counts[email].get("zephyr_mirror", 0)) * 100. / total_user_counts[email], 1) for size in top_percents.keys(): top_percents.setdefault(size, 0) if i < size: top_percents[size] += (percent_zulip * 1.0 / size) logging.debug("%40s | %10s | %s%%" % (email, total_user_counts[email], percent_zulip)) logging.info("") for size in sorted(top_percents.keys()): logging.info("Top %6s | %s%%" % (size, round(top_percents[size], 1))) grand_total = sum(total_counts.values()) print(grand_total) logging.info("%15s | %s" % ("Client", "Percentage")) for client in total_counts.keys(): logging.info("%15s | %s%%" % (client, round(100. * total_counts[client] / grand_total, 1)))
def receiver_is_idle(user_profile_id, realm_presences): # If a user has no message-receiving event queues, they've got no open zulip # session so we notify them all_client_descriptors = get_client_descriptors_for_user(user_profile_id) message_event_queues = [client for client in all_client_descriptors if client.accepts_messages()] off_zulip = len(message_event_queues) == 0 # It's possible a recipient is not in the realm of a sender. We don't have # presence information in this case (and it's hard to get without an additional # db query) so we simply don't try to guess if this cross-realm recipient # has been idle for too long if realm_presences is None or not user_profile_id in realm_presences: return off_zulip # We want to find the newest "active" presence entity and compare that to the # activity expiry threshold. user_presence = realm_presences[user_profile_id] latest_active_timestamp = None idle = False for client, status in user_presence.iteritems(): if (latest_active_timestamp is None or status['timestamp'] > latest_active_timestamp) and \ status['status'] == 'active': latest_active_timestamp = status['timestamp'] if latest_active_timestamp is None: idle = True else: active_datetime = timestamp_to_datetime(latest_active_timestamp) # 140 seconds is consistent with activity.js:OFFLINE_THRESHOLD_SECS idle = now() - active_datetime > datetime.timedelta(seconds=140) return off_zulip or idle
def do_create_stripe_customer(user: UserProfile, stripe_token: Optional[str]=None) -> Customer: realm = user.realm # We could do a better job of handling race conditions here, but if two # people from a realm try to upgrade at exactly the same time, the main # bad thing that will happen is that we will create an extra stripe # customer that we can delete or ignore. stripe_customer = stripe.Customer.create( description="%s (%s)" % (realm.string_id, realm.name), email=user.email, metadata={'realm_id': realm.id, 'realm_str': realm.string_id}, source=stripe_token) event_time = timestamp_to_datetime(stripe_customer.created) with transaction.atomic(): RealmAuditLog.objects.create( realm=user.realm, acting_user=user, event_type=RealmAuditLog.STRIPE_CUSTOMER_CREATED, event_time=event_time) if stripe_token is not None: RealmAuditLog.objects.create( realm=user.realm, acting_user=user, event_type=RealmAuditLog.STRIPE_CARD_CHANGED, event_time=event_time) customer, created = Customer.objects.update_or_create(realm=realm, defaults={ 'stripe_customer_id': stripe_customer.id}) user.is_billing_admin = True user.save(update_fields=["is_billing_admin"]) return customer
def consume(self, event): # type: (Mapping[str, Any]) -> None user_profile = get_user_profile_by_id(event["user_profile_id"]) client = get_client(event["client"]) log_time = timestamp_to_datetime(event["time"]) query = event["query"] do_update_user_activity(user_profile, client, query, log_time)
def process_downgrade(user: UserProfile) -> None: stripe_customer = stripe_get_customer( Customer.objects.filter(realm=user.realm).first().stripe_customer_id) subscription_balance = preview_invoice_total_for_downgrade(stripe_customer) # If subscription_balance > 0, they owe us money. This is likely due to # people they added in the last day, so we can just forgive it. # Stripe automatically forgives it when we delete the subscription, so nothing we need to do there. if subscription_balance < 0: stripe_customer.account_balance = stripe_customer.account_balance + subscription_balance stripe_subscription = extract_current_subscription(stripe_customer) # Wish these two could be transaction.atomic stripe_subscription = stripe_subscription.delete() stripe.Customer.save(stripe_customer) with transaction.atomic(): user.realm.has_seat_based_plan = False user.realm.save(update_fields=['has_seat_based_plan']) RealmAuditLog.objects.create( realm=user.realm, acting_user=user, event_type=RealmAuditLog.STRIPE_PLAN_CHANGED, event_time=timestamp_to_datetime(stripe_subscription.canceled_at), extra_data=ujson.dumps({'plan': None, 'quantity': stripe_subscription.quantity})) # Doing this last, since it results in user-visible confirmation (via # product changes) that the downgrade succeeded. # 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.LIMITED)
def billing_home(request: HttpRequest) -> HttpResponse: user = request.user customer = Customer.objects.filter(realm=user.realm).first() if customer is None: return HttpResponseRedirect(reverse('corporate.views.initial_upgrade')) if not customer.has_billing_relationship: return HttpResponseRedirect(reverse('corporate.views.initial_upgrade')) if not user.is_realm_admin and not user.is_billing_admin: context = {'admin_access': False} # type: Dict[str, Any] return render(request, 'corporate/billing.html', context=context) context = {'admin_access': True} stripe_customer = stripe_get_customer(customer.stripe_customer_id) subscription = extract_current_subscription(stripe_customer) prorated_charges = stripe_customer.account_balance if subscription: plan_name = PLAN_NAMES[Plan.objects.get(stripe_plan_id=subscription.plan.id).nickname] seat_count = subscription.quantity # Need user's timezone to do this properly renewal_date = '{dt:%B} {dt.day}, {dt.year}'.format( dt=timestamp_to_datetime(subscription.current_period_end)) upcoming_invoice = stripe_get_upcoming_invoice(customer.stripe_customer_id) renewal_amount = subscription.plan.amount * subscription.quantity prorated_charges += upcoming_invoice.total - renewal_amount # Can only get here by subscribing and then downgrading. We don't support downgrading # yet, but keeping this code here since we will soon. else: # nocoverage plan_name = "Zulip Free" seat_count = 0 renewal_date = '' renewal_amount = 0 prorated_credits = 0 if prorated_charges < 0: # nocoverage prorated_credits = -prorated_charges prorated_charges = 0 payment_method = None if stripe_customer.default_source is not None: payment_method = "Card ending in %(last4)s" % {'last4': stripe_customer.default_source.last4} context.update({ 'plan_name': plan_name, 'seat_count': seat_count, 'renewal_date': renewal_date, 'renewal_amount': '{:,.2f}'.format(renewal_amount / 100.), 'payment_method': payment_method, 'prorated_charges': '{:,.2f}'.format(prorated_charges / 100.), 'prorated_credits': '{:,.2f}'.format(prorated_credits / 100.), 'publishable_key': STRIPE_PUBLISHABLE_KEY, 'stripe_email': stripe_customer.email, }) return render(request, 'corporate/billing.html', context=context)
def check_apns_feedback(): feedback_connection = session.get_connection(settings.APNS_FEEDBACK, cert_file=settings.APNS_CERT_FILE) apns_client = APNs(feedback_connection, tail_timeout=20) for token, since in apns_client.feedback(): since_date = timestamp_to_datetime(since) logging.info("Found unavailable token %s, unavailable since %s" % (token, since_date)) PushDeviceToken.objects.filter(token=hex_to_b64(token), last_updates__lt=since_date, type=PushDeviceToken.APNS).delete() logging.info("Finished checking feedback for stale tokens")
def check_apns_feedback(): # type: () -> None feedback_connection = APNs(use_sandbox=settings.APNS_SANDBOX, cert_file=settings.APNS_CERT_FILE, key_file=settings.APNS_KEY_FILE) for token, since in feedback_connection.feedback_server.items(): since_date = timestamp_to_datetime(since) logging.info("Found unavailable token %s, unavailable since %s" % (token, since_date)) PushDeviceToken.objects.filter(token=hex_to_b64(token), last_updates__lt=since_date, type=PushDeviceToken.APNS).delete() logging.info("Finished checking feedback for stale tokens")
def test_datetime_and_timestamp_conversions(self) -> None: timestamp = 1483228800 for dt in [ parser.parse('2017-01-01 00:00:00.123 UTC'), parser.parse('2017-01-01 00:00:00.123').replace(tzinfo=timezone_utc), parser.parse('2017-01-01 00:00:00.123').replace(tzinfo=pytz.utc)]: self.assertEqual(timestamp_to_datetime(timestamp), dt-timedelta(microseconds=123000)) self.assertEqual(datetime_to_timestamp(dt), timestamp) for dt in [ parser.parse('2017-01-01 00:00:00.123+01:00'), parser.parse('2017-01-01 00:00:00.123')]: with self.assertRaises(TimezoneNotUTCException): datetime_to_timestamp(dt)
def billing_home(request: HttpRequest) -> HttpResponse: user = request.user customer = Customer.objects.filter(realm=user.realm).first() if customer is None: return HttpResponseRedirect(reverse('zilencer.views.initial_upgrade')) if not user.is_realm_admin and not user == customer.billing_user: context = {'admin_access': False} # type: Dict[str, Any] return render(request, 'zilencer/billing.html', context=context) context = {'admin_access': True} stripe_customer = get_stripe_customer(customer.stripe_customer_id) subscription = extract_current_subscription(stripe_customer) if subscription: plan_name = PLAN_NAMES[Plan.objects.get(stripe_plan_id=subscription.plan.id).nickname] seat_count = subscription.quantity # Need user's timezone to do this properly renewal_date = '{dt:%B} {dt.day}, {dt.year}'.format( dt=timestamp_to_datetime(subscription.current_period_end)) upcoming_invoice = get_upcoming_invoice(customer.stripe_customer_id) renewal_amount = subscription.plan.amount * subscription.quantity / 100. prorated_credits = 0 prorated_charges = upcoming_invoice.amount_due / 100. - renewal_amount if prorated_charges < 0: prorated_credits = -prorated_charges # nocoverage -- no way to get here yet prorated_charges = 0 # nocoverage else: # nocoverage -- no way to get here yet plan_name = "Zulip Free" seat_count = 0 renewal_date = '' renewal_amount = 0 prorated_credits = 0 prorated_charges = 0 payment_method = None if stripe_customer.default_source is not None: payment_method = "Card ending in %(last4)s" % {'last4': stripe_customer.default_source.last4} context.update({ 'plan_name': plan_name, 'seat_count': seat_count, 'renewal_date': renewal_date, 'renewal_amount': '{:,.2f}'.format(renewal_amount), 'payment_method': payment_method, 'prorated_charges': '{:,.2f}'.format(prorated_charges), 'prorated_credits': '{:,.2f}'.format(prorated_credits), }) return render(request, 'zilencer/billing.html', context=context)
def user_activity_intervals(): # type: () -> Tuple[mark_safe, Dict[str, float]] day_end = timestamp_to_datetime(time.time()) day_start = day_end - timedelta(hours=24) output = "Per-user online duration for the last 24 hours:\n" total_duration = timedelta(0) all_intervals = UserActivityInterval.objects.filter( end__gte=day_start, start__lte=day_end ).select_related( 'user_profile', 'user_profile__realm' ).only( 'start', 'end', 'user_profile__email', 'user_profile__realm__string_id' ).order_by( 'user_profile__realm__string_id', 'user_profile__email' ) by_string_id = lambda row: row.user_profile.realm.string_id by_email = lambda row: row.user_profile.email realm_minutes = {} for string_id, realm_intervals in itertools.groupby(all_intervals, by_string_id): realm_duration = timedelta(0) output += '<hr>%s\n' % (string_id,) for email, intervals in itertools.groupby(realm_intervals, by_email): duration = timedelta(0) for interval in intervals: start = max(day_start, interval.start) end = min(day_end, interval.end) duration += end - start total_duration += duration realm_duration += duration output += " %-*s%s\n" % (37, email, duration) realm_minutes[string_id] = realm_duration.total_seconds() / 60 output += "\nTotal Duration: %s\n" % (total_duration,) output += "\nTotal Duration in minutes: %s\n" % (total_duration.total_seconds() / 60.,) output += "Total Duration amortized to a month: %s" % (total_duration.total_seconds() * 30. / 60.,) content = mark_safe('<pre>' + output + '</pre>') return content, realm_minutes
def do_subscribe_customer_to_plan(stripe_customer: stripe.Customer, stripe_plan_id: str, seat_count: int, tax_percent: float) -> None: if extract_current_subscription(stripe_customer) is not None: billing_logger.error("Stripe customer %s trying to subscribe to %s, " "but has an active subscription" % (stripe_customer.id, stripe_plan_id)) # TODO: Change to an error sent to the frontend raise AssertionError("Customer already has an active subscription.") stripe_subscription = stripe.Subscription.create( customer=stripe_customer.id, billing='charge_automatically', items=[{ 'plan': stripe_plan_id, 'quantity': seat_count, }], prorate=True, tax_percent=tax_percent) if PRINT_STRIPE_FIXTURE_DATA: print(''.join(['"create_subscription": ', str(stripe_subscription), ','])) # nocoverage customer = Customer.objects.get(stripe_customer_id=stripe_customer.id) with transaction.atomic(): customer.realm.has_seat_based_plan = True customer.realm.save(update_fields=['has_seat_based_plan']) RealmAuditLog.objects.create( realm=customer.realm, acting_user=customer.billing_user, event_type=RealmAuditLog.REALM_PLAN_STARTED, event_time=timestamp_to_datetime(stripe_subscription.created), extra_data=ujson.dumps({'plan': stripe_plan_id, 'quantity': seat_count})) current_seat_count = get_seat_count(customer.realm) if seat_count != current_seat_count: RealmAuditLog.objects.create( realm=customer.realm, event_type=RealmAuditLog.REALM_PLAN_QUANTITY_UPDATED, event_time=timestamp_to_datetime(stripe_subscription.created), requires_billing_update=True, extra_data=ujson.dumps({'quantity': current_seat_count}))
def do_create_customer_with_payment_source(user: UserProfile, stripe_token: str) -> stripe.Customer: realm = user.realm stripe_customer = stripe.Customer.create( description="%s (%s)" % (realm.string_id, realm.name), metadata={'realm_id': realm.id, 'realm_str': realm.string_id}, source=stripe_token) if PRINT_STRIPE_FIXTURE_DATA: print(''.join(['"create_customer": ', str(stripe_customer), ','])) # nocoverage event_time = timestamp_to_datetime(stripe_customer.created) RealmAuditLog.objects.create( realm=user.realm, acting_user=user, event_type=RealmAuditLog.REALM_STRIPE_INITIALIZED, event_time=event_time) RealmAuditLog.objects.create( realm=user.realm, acting_user=user, event_type=RealmAuditLog.REALM_CARD_ADDED, event_time=event_time) Customer.objects.create( realm=realm, stripe_customer_id=stripe_customer.id, billing_user=user) return stripe_customer
def stringify(value: Any) -> str: if isinstance(value, int) and value > 1500000000 and value < 2000000000: return timestamp_to_datetime(value).strftime('%b %d, %Y, %H:%M:%S %Z') return str(value)
def test_upgrade_by_invoice(self, *mocks: Mock) -> None: user = self.example_user("hamlet") self.login(user.email) # Click "Make payment" in Stripe Checkout with patch('corporate.lib.stripe.timezone_now', return_value=self.now): self.upgrade(invoice=True) # Check that we correctly created a Customer in Stripe stripe_customer = stripe_get_customer(Customer.objects.get(realm=user.realm).stripe_customer_id) # It can take a second for Stripe to attach the source to the customer, and in # particular it may not be attached at the time stripe_get_customer is called above, # causing test flakes. # So commenting the next line out, but leaving it here so future readers know what # is supposed to happen here # self.assertEqual(stripe_customer.default_source.type, 'ach_credit_transfer') # Check Charges in Stripe self.assertFalse(stripe.Charge.list(customer=stripe_customer.id)) # Check Invoices in Stripe stripe_invoices = [invoice for invoice in stripe.Invoice.list(customer=stripe_customer.id)] self.assertEqual(len(stripe_invoices), 1) self.assertIsNotNone(stripe_invoices[0].due_date) self.assertIsNotNone(stripe_invoices[0].finalized_at) invoice_params = { 'amount_due': 8000 * 123, 'amount_paid': 0, 'attempt_count': 0, 'auto_advance': True, 'billing': 'send_invoice', 'statement_descriptor': 'Zulip Standard', 'status': 'open', 'total': 8000 * 123} for key, value in invoice_params.items(): self.assertEqual(stripe_invoices[0].get(key), value) # Check Line Items on Stripe Invoice stripe_line_items = [item for item in stripe_invoices[0].lines] self.assertEqual(len(stripe_line_items), 1) line_item_params = { 'amount': 8000 * 123, 'description': 'Zulip Standard', 'discountable': False, 'period': { 'end': datetime_to_timestamp(self.next_year), 'start': datetime_to_timestamp(self.now)}, 'plan': None, 'proration': False, 'quantity': 123} for key, value in line_item_params.items(): self.assertEqual(stripe_line_items[0].get(key), value) # Check that we correctly populated Customer and CustomerPlan in Zulip customer = Customer.objects.filter(stripe_customer_id=stripe_customer.id, realm=user.realm).first() self.assertTrue(CustomerPlan.objects.filter( customer=customer, licenses=123, automanage_licenses=False, charge_automatically=False, price_per_license=8000, fixed_price=None, discount=None, billing_cycle_anchor=self.now, billing_schedule=CustomerPlan.ANNUAL, billed_through=self.now, next_billing_date=self.next_year, tier=CustomerPlan.STANDARD, status=CustomerPlan.ACTIVE).exists()) # Check RealmAuditLog audit_log_entries = list(RealmAuditLog.objects.filter(acting_user=user) .values_list('event_type', 'event_time').order_by('id')) self.assertEqual(audit_log_entries, [ (RealmAuditLog.STRIPE_CUSTOMER_CREATED, timestamp_to_datetime(stripe_customer.created)), (RealmAuditLog.CUSTOMER_PLAN_CREATED, self.now), # TODO: Check for REALM_PLAN_TYPE_CHANGED # (RealmAuditLog.REALM_PLAN_TYPE_CHANGED, Kandra()), ]) self.assertEqual(ujson.loads(RealmAuditLog.objects.filter( event_type=RealmAuditLog.CUSTOMER_PLAN_CREATED).values_list( 'extra_data', flat=True).first())['licenses'], 123) # Check that we correctly updated Realm realm = get_realm("zulip") self.assertEqual(realm.plan_type, Realm.STANDARD) self.assertEqual(realm.max_invites, Realm.INVITES_STANDARD_REALM_DAILY_MAX) # Check that we can no longer access /upgrade response = self.client_get("/upgrade/") self.assertEqual(response.status_code, 302) self.assertEqual('/billing/', response.url) # Check /billing has the correct information response = self.client_get("/billing/") self.assert_not_in_success_response(['Pay annually', 'Update card'], response) for substring in [ 'Zulip Standard', str(123), 'Your plan will renew on', 'January 2, 2013', '$9,840.00', # 9840 = 80 * 123 'Billed by invoice']: self.assert_in_response(substring, response)
def test_upgrade_by_card(self, *mocks: Mock) -> None: user = self.example_user("hamlet") self.login(user.email) response = self.client_get("/upgrade/") self.assert_in_success_response(['Pay annually'], response) self.assertNotEqual(user.realm.plan_type, Realm.STANDARD) self.assertFalse(Customer.objects.filter(realm=user.realm).exists()) # Click "Make payment" in Stripe Checkout with patch('corporate.lib.stripe.timezone_now', return_value=self.now): self.upgrade() # Check that we correctly created a Customer object in Stripe stripe_customer = stripe_get_customer(Customer.objects.get(realm=user.realm).stripe_customer_id) self.assertEqual(stripe_customer.default_source.id[:5], 'card_') self.assertEqual(stripe_customer.description, "zulip (Zulip Dev)") self.assertEqual(stripe_customer.discount, None) self.assertEqual(stripe_customer.email, user.email) self.assertEqual(dict(stripe_customer.metadata), {'realm_id': str(user.realm.id), 'realm_str': 'zulip'}) # Check Charges in Stripe stripe_charges = [charge for charge in stripe.Charge.list(customer=stripe_customer.id)] self.assertEqual(len(stripe_charges), 1) self.assertEqual(stripe_charges[0].amount, 8000 * self.seat_count) # TODO: fix Decimal self.assertEqual(stripe_charges[0].description, "Upgrade to Zulip Standard, $80.0 x {}".format(self.seat_count)) self.assertEqual(stripe_charges[0].receipt_email, user.email) self.assertEqual(stripe_charges[0].statement_descriptor, "Zulip Standard") # Check Invoices in Stripe stripe_invoices = [invoice for invoice in stripe.Invoice.list(customer=stripe_customer.id)] self.assertEqual(len(stripe_invoices), 1) self.assertIsNotNone(stripe_invoices[0].finalized_at) invoice_params = { # auto_advance is False because the invoice has been paid 'amount_due': 0, 'amount_paid': 0, 'auto_advance': False, 'billing': 'charge_automatically', 'charge': None, 'status': 'paid', 'total': 0} for key, value in invoice_params.items(): self.assertEqual(stripe_invoices[0].get(key), value) # Check Line Items on Stripe Invoice stripe_line_items = [item for item in stripe_invoices[0].lines] self.assertEqual(len(stripe_line_items), 2) line_item_params = { 'amount': 8000 * self.seat_count, 'description': 'Zulip Standard', 'discountable': False, 'period': { 'end': datetime_to_timestamp(self.next_year), 'start': datetime_to_timestamp(self.now)}, # There's no unit_amount on Line Items, probably because it doesn't show up on the # user-facing invoice. We could pull the Invoice Item instead and test unit_amount there, # but testing the amount and quantity seems sufficient. 'plan': None, 'proration': False, 'quantity': self.seat_count} for key, value in line_item_params.items(): self.assertEqual(stripe_line_items[0].get(key), value) line_item_params = { 'amount': -8000 * self.seat_count, 'description': 'Payment (Card ending in 4242)', 'discountable': False, 'plan': None, 'proration': False, 'quantity': 1} for key, value in line_item_params.items(): self.assertEqual(stripe_line_items[1].get(key), value) # Check that we correctly populated Customer and CustomerPlan in Zulip customer = Customer.objects.filter(stripe_customer_id=stripe_customer.id, realm=user.realm).first() self.assertTrue(CustomerPlan.objects.filter( customer=customer, licenses=self.seat_count, automanage_licenses=True, price_per_license=8000, fixed_price=None, discount=None, billing_cycle_anchor=self.now, billing_schedule=CustomerPlan.ANNUAL, billed_through=self.now, next_billing_date=self.next_month, tier=CustomerPlan.STANDARD, status=CustomerPlan.ACTIVE).exists()) # Check RealmAuditLog audit_log_entries = list(RealmAuditLog.objects.filter(acting_user=user) .values_list('event_type', 'event_time').order_by('id')) self.assertEqual(audit_log_entries, [ (RealmAuditLog.STRIPE_CUSTOMER_CREATED, timestamp_to_datetime(stripe_customer.created)), (RealmAuditLog.STRIPE_CARD_CHANGED, timestamp_to_datetime(stripe_customer.created)), (RealmAuditLog.CUSTOMER_PLAN_CREATED, self.now), # TODO: Check for REALM_PLAN_TYPE_CHANGED # (RealmAuditLog.REALM_PLAN_TYPE_CHANGED, Kandra()), ]) self.assertEqual(ujson.loads(RealmAuditLog.objects.filter( event_type=RealmAuditLog.CUSTOMER_PLAN_CREATED).values_list( 'extra_data', flat=True).first())['licenses'], self.seat_count) # Check that we correctly updated Realm realm = get_realm("zulip") self.assertEqual(realm.plan_type, Realm.STANDARD) self.assertEqual(realm.max_invites, Realm.INVITES_STANDARD_REALM_DAILY_MAX) # Check that we can no longer access /upgrade response = self.client_get("/upgrade/") self.assertEqual(response.status_code, 302) self.assertEqual('/billing/', response.url) # Check /billing has the correct information response = self.client_get("/billing/") self.assert_not_in_success_response(['Pay annually'], response) for substring in [ 'Zulip Standard', str(self.seat_count), 'Your plan will renew on', 'January 2, 2013', '$%s.00' % (80 * self.seat_count,), 'Visa ending in 4242', 'Update card']: self.assert_in_response(substring, response)
def to_utc_datetime(timestamp: str) -> datetime.datetime: return timestamp_to_datetime(float(timestamp))
def consume(self, event: Mapping[str, Any]) -> None: user_profile = get_user_profile_by_id(event["user_profile_id"]) client = get_client(event["client"]) log_time = timestamp_to_datetime(event["time"]) query = event["query"] do_update_user_activity(user_profile, client, query, log_time)
def test_initial_upgrade(self, mock4: Mock, mock3: Mock, mock2: Mock, mock1: Mock) -> None: user = self.example_user("hamlet") self.login(user.email) response = self.client_get("/upgrade/") self.assert_in_success_response(['We can also bill by invoice'], response) self.assertFalse(user.realm.has_seat_based_plan) self.assertNotEqual(user.realm.plan_type, Realm.PREMIUM) self.assertFalse(Customer.objects.filter(realm=user.realm).exists()) # Click "Make payment" in Stripe Checkout self.client_post( "/upgrade/", { 'stripeToken': stripe_create_token().id, 'signed_seat_count': self.get_signed_seat_count_from_response(response), 'salt': self.get_salt_from_response(response), 'plan': Plan.CLOUD_ANNUAL }) # Check that we correctly created Customer and Subscription objects in Stripe stripe_customer = stripe_get_customer( Customer.objects.get(realm=user.realm).stripe_customer_id) self.assertEqual(stripe_customer.default_source.id[:5], 'card_') self.assertEqual(stripe_customer.description, "zulip (Zulip Dev)") self.assertEqual(stripe_customer.discount, None) self.assertEqual(stripe_customer.email, user.email) self.assertEqual(dict(stripe_customer.metadata), { 'realm_id': str(user.realm.id), 'realm_str': 'zulip' }) stripe_subscription = extract_current_subscription(stripe_customer) self.assertEqual(stripe_subscription.billing, 'charge_automatically') self.assertEqual(stripe_subscription.days_until_due, None) self.assertEqual( stripe_subscription.plan.id, Plan.objects.get(nickname=Plan.CLOUD_ANNUAL).stripe_plan_id) self.assertEqual(stripe_subscription.quantity, self.quantity) self.assertEqual(stripe_subscription.status, 'active') self.assertEqual(stripe_subscription.tax_percent, 0) # Check that we correctly populated Customer and RealmAuditLog in Zulip self.assertEqual( 1, Customer.objects.filter(stripe_customer_id=stripe_customer.id, realm=user.realm).count()) audit_log_entries = list( RealmAuditLog.objects.filter(acting_user=user).values_list( 'event_type', 'event_time').order_by('id')) self.assertEqual( audit_log_entries, [ (RealmAuditLog.STRIPE_CUSTOMER_CREATED, timestamp_to_datetime(stripe_customer.created)), (RealmAuditLog.STRIPE_CARD_CHANGED, timestamp_to_datetime(stripe_customer.created)), # TODO: Add a test where stripe_customer.created != stripe_subscription.created (RealmAuditLog.STRIPE_PLAN_CHANGED, timestamp_to_datetime(stripe_subscription.created)), (RealmAuditLog.REALM_PLAN_TYPE_CHANGED, Kandra()), ]) # Check that we correctly updated Realm realm = get_realm("zulip") self.assertTrue(realm.has_seat_based_plan) self.assertEqual(realm.plan_type, Realm.PREMIUM) self.assertEqual(realm.max_invites, Realm.MAX_INVITES_PREMIUM) # Check that we can no longer access /upgrade response = self.client_get("/upgrade/") self.assertEqual(response.status_code, 302) self.assertEqual('/billing/', response.url)
def do_subscribe_customer_to_plan(user: UserProfile, stripe_customer: stripe.Customer, stripe_plan_id: str, seat_count: int, tax_percent: float, charge_automatically: bool) -> None: if extract_current_subscription(stripe_customer) is not None: # nocoverage # Unlikely race condition from two people upgrading (clicking "Make payment") # at exactly the same time. Doesn't fully resolve the race condition, but having # a check here reduces the likelihood. billing_logger.error("Stripe customer %s trying to subscribe to %s, " "but has an active subscription" % (stripe_customer.id, stripe_plan_id)) raise BillingError('subscribing with existing subscription', BillingError.TRY_RELOADING) customer = Customer.objects.get(stripe_customer_id=stripe_customer.id) if charge_automatically: billing_method = 'charge_automatically' days_until_due = None else: billing_method = 'send_invoice' days_until_due = DEFAULT_INVOICE_DAYS_UNTIL_DUE # Note that there is a race condition here, where if two users upgrade at exactly the # same time, they will have two subscriptions, and get charged twice. We could try to # reduce the chance of it with a well-designed idempotency_key, but it's not easy since # we also need to be careful not to block the customer from retrying if their # subscription attempt fails (e.g. due to insufficient funds). # Success here implies the stripe_customer was charged: https://stripe.com/docs/billing/lifecycle#active # Otherwise we should expect it to throw a stripe.error. stripe_subscription = stripe.Subscription.create( customer=stripe_customer.id, billing=billing_method, days_until_due=days_until_due, items=[{ 'plan': stripe_plan_id, 'quantity': seat_count, }], prorate=True, tax_percent=tax_percent) with transaction.atomic(): customer.has_billing_relationship = True customer.save(update_fields=['has_billing_relationship']) customer.realm.has_seat_based_plan = True customer.realm.save(update_fields=['has_seat_based_plan']) RealmAuditLog.objects.create( realm=customer.realm, acting_user=user, event_type=RealmAuditLog.STRIPE_PLAN_CHANGED, event_time=timestamp_to_datetime(stripe_subscription.created), extra_data=ujson.dumps({ 'plan': stripe_plan_id, 'quantity': seat_count, 'billing_method': billing_method })) current_seat_count = get_seat_count(customer.realm) if seat_count != current_seat_count: RealmAuditLog.objects.create( realm=customer.realm, event_type=RealmAuditLog.STRIPE_PLAN_QUANTITY_RESET, event_time=timestamp_to_datetime(stripe_subscription.created), requires_billing_update=True, extra_data=ujson.dumps({'quantity': current_seat_count}))
def consume(self, event: Mapping[str, Any]) -> None: user_profile = get_user_profile_by_id(event["user_profile_id"]) log_time = timestamp_to_datetime(event["time"]) do_update_user_activity_interval(user_profile, log_time)
def compute_stats(log_level: int) -> None: logger = logging.getLogger() logger.setLevel(log_level) one_week_ago = timestamp_to_datetime( time.time()) - datetime.timedelta(weeks=1) mit_query = Message.objects.filter(sender__realm__string_id="zephyr", recipient__type=Recipient.STREAM, date_sent__gt=one_week_ago) for bot_sender_start in ["imap.", "rcmd.", "sys."]: mit_query = mit_query.exclude( sender__email__startswith=(bot_sender_start)) # Filtering for "/" covers tabbott/extra@ and all the daemon/foo bots. mit_query = mit_query.exclude(sender__email__contains=("/")) mit_query = mit_query.exclude(sender__email__contains=("aim.com")) mit_query = mit_query.exclude(sender__email__in=[ "*****@*****.**", "*****@*****.**", "*****@*****.**", "*****@*****.**", "*****@*****.**", "*****@*****.**", "*****@*****.**", "*****@*****.**", "www-data|[email protected]" ]) user_counts: Dict[str, Dict[str, int]] = {} for m in mit_query.select_related("sending_client", "sender"): email = m.sender.email user_counts.setdefault(email, {}) user_counts[email].setdefault(m.sending_client.name, 0) user_counts[email][m.sending_client.name] += 1 total_counts: Dict[str, int] = {} total_user_counts: Dict[str, int] = {} for email, counts in user_counts.items(): total_user_counts.setdefault(email, 0) for client_name, count in counts.items(): total_counts.setdefault(client_name, 0) total_counts[client_name] += count total_user_counts[email] += count logging.debug("%40s | %10s | %s", "User", "Messages", "Percentage Zulip") top_percents: Dict[int, float] = {} for size in [10, 25, 50, 100, 200, len(total_user_counts.keys())]: top_percents[size] = 0.0 for i, email in enumerate( sorted(total_user_counts.keys(), key=lambda x: -total_user_counts[x])): percent_zulip = round( 100 - (user_counts[email].get("zephyr_mirror", 0)) * 100. / total_user_counts[email], 1) for size in top_percents.keys(): top_percents.setdefault(size, 0) if i < size: top_percents[size] += (percent_zulip * 1.0 / size) logging.debug("%40s | %10s | %s%%", email, total_user_counts[email], percent_zulip) logging.info("") for size in sorted(top_percents.keys()): logging.info("Top %6s | %s%%", size, round(top_percents[size], 1)) grand_total = sum(total_counts.values()) print(grand_total) logging.info("%15s | %s", "Client", "Percentage") for client in total_counts.keys(): logging.info("%15s | %s%%", client, round(100. * total_counts[client] / grand_total, 1))
def test_initial_upgrade(self, mock5: Mock, mock4: Mock, mock3: Mock, mock2: Mock, mock1: Mock) -> None: user = self.example_user("hamlet") self.login(user.email) response = self.client_get("/upgrade/") self.assert_in_success_response(['We can also bill by invoice'], response) self.assertFalse(user.realm.has_seat_based_plan) self.assertNotEqual(user.realm.plan_type, Realm.STANDARD) self.assertFalse(Customer.objects.filter(realm=user.realm).exists()) # Click "Make payment" in Stripe Checkout self.client_post("/upgrade/", { 'stripeToken': stripe_create_token().id, 'signed_seat_count': self.get_signed_seat_count_from_response(response), 'salt': self.get_salt_from_response(response), 'plan': Plan.CLOUD_ANNUAL}) # Check that we correctly created Customer and Subscription objects in Stripe stripe_customer = stripe_get_customer(Customer.objects.get(realm=user.realm).stripe_customer_id) self.assertEqual(stripe_customer.default_source.id[:5], 'card_') self.assertEqual(stripe_customer.description, "zulip (Zulip Dev)") self.assertEqual(stripe_customer.discount, None) self.assertEqual(stripe_customer.email, user.email) self.assertEqual(dict(stripe_customer.metadata), {'realm_id': str(user.realm.id), 'realm_str': 'zulip'}) stripe_subscription = extract_current_subscription(stripe_customer) self.assertEqual(stripe_subscription.billing, 'charge_automatically') self.assertEqual(stripe_subscription.days_until_due, None) self.assertEqual(stripe_subscription.plan.id, Plan.objects.get(nickname=Plan.CLOUD_ANNUAL).stripe_plan_id) self.assertEqual(stripe_subscription.quantity, self.quantity) self.assertEqual(stripe_subscription.status, 'active') self.assertEqual(stripe_subscription.tax_percent, 0) # Check that we correctly populated Customer and RealmAuditLog in Zulip self.assertEqual(1, Customer.objects.filter(stripe_customer_id=stripe_customer.id, realm=user.realm).count()) audit_log_entries = list(RealmAuditLog.objects.filter(acting_user=user) .values_list('event_type', 'event_time').order_by('id')) self.assertEqual(audit_log_entries, [ (RealmAuditLog.STRIPE_CUSTOMER_CREATED, timestamp_to_datetime(stripe_customer.created)), (RealmAuditLog.STRIPE_CARD_CHANGED, timestamp_to_datetime(stripe_customer.created)), # TODO: Add a test where stripe_customer.created != stripe_subscription.created (RealmAuditLog.STRIPE_PLAN_CHANGED, timestamp_to_datetime(stripe_subscription.created)), (RealmAuditLog.REALM_PLAN_TYPE_CHANGED, Kandra()), ]) # Check that we correctly updated Realm realm = get_realm("zulip") self.assertTrue(realm.has_seat_based_plan) self.assertEqual(realm.plan_type, Realm.STANDARD) self.assertEqual(realm.max_invites, Realm.INVITES_STANDARD_REALM_DAILY_MAX) # Check that we can no longer access /upgrade response = self.client_get("/upgrade/") self.assertEqual(response.status_code, 302) self.assertEqual('/billing/', response.url) # Check /billing has the correct information response = self.client_get("/billing/") self.assert_not_in_success_response(['We can also bill by invoice'], response) for substring in ['Your plan will renew on', '$%s.00' % (80 * self.quantity,), 'Card ending in 4242']: self.assert_in_response(substring, response)
def consume(self, event): user_profile = get_user_profile_by_id(event["user_profile_id"]) log_time = timestamp_to_datetime(event["time"]) do_update_user_activity_interval(user_profile, log_time)
def test_initial_upgrade(self, mock_create_subscription: mock.Mock, mock_create_customer: mock.Mock) -> None: user = self.example_user("hamlet") self.login(user.email) response = self.client_get("/upgrade/") self.assert_in_success_response(['We can also bill by invoice'], response) self.assertFalse(user.realm.has_seat_based_plan) self.assertNotEqual(user.realm.plan_type, Realm.PREMIUM) # Click "Make payment" in Stripe Checkout self.client_post( "/upgrade/", { 'stripeToken': self.token, 'signed_seat_count': self.get_signed_seat_count_from_response(response), 'salt': self.get_salt_from_response(response), 'plan': Plan.CLOUD_ANNUAL }) # Check that we created a customer and subscription in stripe mock_create_customer.assert_called_once_with( description="zulip (Zulip Dev)", email=user.email, metadata={ 'realm_id': user.realm.id, 'realm_str': 'zulip' }, source=self.token, coupon=None) mock_create_subscription.assert_called_once_with( customer=self.stripe_customer_id, billing='charge_automatically', items=[{ 'plan': self.stripe_plan_id, 'quantity': self.quantity, }], prorate=True, tax_percent=0) # Check that we correctly populated Customer and RealmAuditLog in Zulip self.assertEqual( 1, Customer.objects.filter(realm=user.realm, stripe_customer_id=self.stripe_customer_id, billing_user=user).count()) audit_log_entries = list( RealmAuditLog.objects.filter(acting_user=user).values_list( 'event_type', 'event_time').order_by('id')) self.assertEqual(audit_log_entries, [ (RealmAuditLog.STRIPE_CUSTOMER_CREATED, timestamp_to_datetime(self.customer_created)), (RealmAuditLog.STRIPE_CARD_ADDED, timestamp_to_datetime(self.customer_created)), (RealmAuditLog.STRIPE_PLAN_CHANGED, timestamp_to_datetime(self.subscription_created)), (RealmAuditLog.REALM_PLAN_TYPE_CHANGED, Kandra()), ]) # Check that we correctly updated Realm realm = get_realm("zulip") self.assertTrue(realm.has_seat_based_plan) self.assertEqual(realm.plan_type, Realm.PREMIUM) # Check that we can no longer access /upgrade response = self.client_get("/upgrade/") self.assertEqual(response.status_code, 302) self.assertEqual('/billing/', response.url)
def to_utc_datetime(timestamp): # type: (Text) -> datetime.datetime return timestamp_to_datetime(float(timestamp))
def consume(self, event): # type: (Mapping[str, Any]) -> None user_profile = get_user_profile_by_id(event["user_profile_id"]) log_time = timestamp_to_datetime(event["time"]) do_update_user_activity_interval(user_profile, log_time)
def billing_home(request: HttpRequest) -> HttpResponse: user = request.user customer = Customer.objects.filter(realm=user.realm).first() if customer is None: return HttpResponseRedirect(reverse('corporate.views.initial_upgrade')) if not customer.has_billing_relationship: return HttpResponseRedirect(reverse('corporate.views.initial_upgrade')) if not user.is_realm_admin and not user.is_billing_admin: context = {'admin_access': False} # type: Dict[str, Any] return render(request, 'corporate/billing.html', context=context) context = {'admin_access': True} stripe_customer = stripe_get_customer(customer.stripe_customer_id) subscription = extract_current_subscription(stripe_customer) prorated_charges = stripe_customer.account_balance if subscription: plan_name = PLAN_NAMES[Plan.objects.get( stripe_plan_id=subscription.plan.id).nickname] seat_count = subscription.quantity # Need user's timezone to do this properly renewal_date = '{dt:%B} {dt.day}, {dt.year}'.format( dt=timestamp_to_datetime(subscription.current_period_end)) upcoming_invoice = stripe_get_upcoming_invoice( customer.stripe_customer_id) renewal_amount = subscription.plan.amount * subscription.quantity prorated_charges += upcoming_invoice.total - renewal_amount # Can only get here by subscribing and then downgrading. We don't support downgrading # yet, but keeping this code here since we will soon. else: # nocoverage plan_name = "Zulip Free" seat_count = 0 renewal_date = '' renewal_amount = 0 prorated_credits = 0 if prorated_charges < 0: # nocoverage prorated_credits = -prorated_charges prorated_charges = 0 payment_method = None if stripe_customer.default_source is not None: payment_method = "Card ending in %(last4)s" % { 'last4': stripe_customer.default_source.last4 } context.update({ 'plan_name': plan_name, 'seat_count': seat_count, 'renewal_date': renewal_date, 'renewal_amount': '{:,.2f}'.format(renewal_amount / 100.), 'payment_method': payment_method, 'prorated_charges': '{:,.2f}'.format(prorated_charges / 100.), 'prorated_credits': '{:,.2f}'.format(prorated_credits / 100.), 'publishable_key': STRIPE_PUBLISHABLE_KEY, 'stripe_email': stripe_customer.email, }) return render(request, 'corporate/billing.html', context=context)
def restore_saved_messages(): # type: () -> None old_messages = [] # type: List[Dict[str, Any]] duplicate_suppression_hash = {} # type: Dict[str, bool] stream_dict = {} # type: Dict[Tuple[text_type, text_type], Tuple[text_type, text_type]] user_set = set() # type: Set[Tuple[text_type, text_type, text_type, bool]] email_set = set([u.email for u in UserProfile.objects.all()]) # type: Set[text_type] realm_set = set() # type: Set[text_type] # Initial client_set is nonempty temporarily because we don't have # clients in logs at all right now -- later we can start with nothing. client_set = set(["populate_db", "website", "zephyr_mirror"]) huddle_user_set = set() # type: Set[Tuple[text_type, ...]] # First, determine all the objects our messages will need. print(datetime.datetime.now(), "Creating realms/streams/etc...") def process_line(line): # type: (str) -> None old_message_json = line.strip() # Due to populate_db's shakespeare mode, we have a lot of # duplicate messages in our log that only differ in their # logged ID numbers (same timestamp, content, etc.). With # sqlite, bulk creating those messages won't work properly: in # particular, the first 100 messages will actually only result # in 20 rows ending up in the target table, which screws up # the below accounting where for handling changing # subscriptions, we assume that the Nth row populate_db # created goes with the Nth non-subscription row of the input # So suppress the duplicates when using sqlite. if "sqlite" in settings.DATABASES["default"]["ENGINE"]: tmp_message = ujson.loads(old_message_json) tmp_message['id'] = '1' duplicate_suppression_key = ujson.dumps(tmp_message) if duplicate_suppression_key in duplicate_suppression_hash: return duplicate_suppression_hash[duplicate_suppression_key] = True old_message = ujson.loads(old_message_json) message_type = old_message["type"] # Lower case emails and domains; it will screw up # deduplication if we don't def fix_email(email): # type: (text_type) -> text_type return email.strip().lower() if message_type in ["stream", "huddle", "personal"]: old_message["sender_email"] = fix_email(old_message["sender_email"]) # Fix the length on too-long messages before we start processing them if len(old_message["content"]) > MAX_MESSAGE_LENGTH: old_message["content"] = "[ This message was deleted because it was too long ]" if message_type in ["subscription_added", "subscription_removed"]: old_message["domain"] = old_message["domain"].lower() old_message["user"] = fix_email(old_message["user"]) elif message_type == "subscription_property": old_message["user"] = fix_email(old_message["user"]) elif message_type == "user_email_changed": old_message["old_email"] = fix_email(old_message["old_email"]) old_message["new_email"] = fix_email(old_message["new_email"]) elif message_type.startswith("user_"): old_message["user"] = fix_email(old_message["user"]) elif message_type.startswith("enable_"): old_message["user"] = fix_email(old_message["user"]) if message_type == 'personal': old_message["recipient"][0]["email"] = fix_email(old_message["recipient"][0]["email"]) elif message_type == "huddle": for i in range(len(old_message["recipient"])): old_message["recipient"][i]["email"] = fix_email(old_message["recipient"][i]["email"]) old_messages.append(old_message) if message_type in ["subscription_added", "subscription_removed"]: stream_name = old_message["name"].strip() # type: text_type canon_stream_name = stream_name.lower() if canon_stream_name not in stream_dict: stream_dict[(old_message["domain"], canon_stream_name)] = \ (old_message["domain"], stream_name) elif message_type == "user_created": user_set.add((old_message["user"], old_message["full_name"], old_message["short_name"], False)) elif message_type == "realm_created": realm_set.add(old_message["domain"]) if message_type not in ["stream", "huddle", "personal"]: return sender_email = old_message["sender_email"] domain = split_email_to_domain(sender_email) realm_set.add(domain) if old_message["sender_email"] not in email_set: user_set.add((old_message["sender_email"], old_message["sender_full_name"], old_message["sender_short_name"], False)) if 'sending_client' in old_message: client_set.add(old_message['sending_client']) if message_type == 'stream': stream_name = old_message["recipient"].strip() canon_stream_name = stream_name.lower() if canon_stream_name not in stream_dict: stream_dict[(domain, canon_stream_name)] = (domain, stream_name) elif message_type == 'personal': u = old_message["recipient"][0] if u["email"] not in email_set: user_set.add((u["email"], u["full_name"], u["short_name"], False)) email_set.add(u["email"]) elif message_type == 'huddle': for u in old_message["recipient"]: user_set.add((u["email"], u["full_name"], u["short_name"], False)) if u["email"] not in email_set: user_set.add((u["email"], u["full_name"], u["short_name"], False)) email_set.add(u["email"]) huddle_user_set.add(tuple(sorted(set(u["email"] for u in old_message["recipient"])))) else: raise ValueError('Bad message type') event_glob = os.path.join(settings.EVENT_LOG_DIR, 'events.*') for filename in sorted(glob.glob(event_glob)): with open(filename, "r") as message_log: for line in message_log.readlines(): process_line(line) stream_recipients = {} # type: Dict[Tuple[int, text_type], Recipient] user_recipients = {} # type: Dict[text_type, Recipient] huddle_recipients = {} # type: Dict[text_type, Recipient] # Then, create the objects our messages need. print(datetime.datetime.now(), "Creating realms...") bulk_create_realms(realm_set) realms = {} # type: Dict[text_type, Realm] for realm in Realm.objects.all(): realms[realm.domain] = realm print(datetime.datetime.now(), "Creating clients...") bulk_create_clients(client_set) clients = {} # type: Dict[text_type, Client] for client in Client.objects.all(): clients[client.name] = client print(datetime.datetime.now(), "Creating streams...") bulk_create_streams(realms, list(stream_dict.values())) streams = {} # type: Dict[int, Stream] for stream in Stream.objects.all(): streams[stream.id] = stream for recipient in Recipient.objects.filter(type=Recipient.STREAM): stream_recipients[(streams[recipient.type_id].realm_id, streams[recipient.type_id].name.lower())] = recipient print(datetime.datetime.now(), "Creating users...") bulk_create_users(realms, user_set, tos_version=settings.TOS_VERSION) users = {} # type: Dict[text_type, UserProfile] users_by_id = {} # type: Dict[int, UserProfile] for user_profile in UserProfile.objects.select_related().all(): users[user_profile.email] = user_profile users_by_id[user_profile.id] = user_profile for recipient in Recipient.objects.filter(type=Recipient.PERSONAL): user_recipients[users_by_id[recipient.type_id].email] = recipient print(datetime.datetime.now(), "Creating huddles...") bulk_create_huddles(users, huddle_user_set) huddles_by_id = {} # type: Dict[int, Huddle] for huddle in Huddle.objects.all(): huddles_by_id[huddle.id] = huddle for recipient in Recipient.objects.filter(type=Recipient.HUDDLE): huddle_recipients[huddles_by_id[recipient.type_id].huddle_hash] = recipient # TODO: Add a special entry type in the log that is a subscription # change and import those as we go to make subscription changes # take effect! print(datetime.datetime.now(), "Importing subscriptions...") subscribers = {} # type: Dict[int, Set[int]] for s in Subscription.objects.select_related().all(): if s.active: subscribers.setdefault(s.recipient.id, set()).add(s.user_profile.id) # Then create all the messages, without talking to the DB! print(datetime.datetime.now(), "Importing messages, part 1...") first_message_id = None if Message.objects.exists(): first_message_id = Message.objects.all().order_by("-id")[0].id + 1 messages_to_create = [] # type: List[Message] for idx, old_message in enumerate(old_messages): message_type = old_message["type"] if message_type not in ["stream", "huddle", "personal"]: continue message = Message() sender_email = old_message["sender_email"] domain = split_email_to_domain(sender_email) realm = realms[domain] message.sender = users[sender_email] type_hash = {"stream": Recipient.STREAM, "huddle": Recipient.HUDDLE, "personal": Recipient.PERSONAL} if 'sending_client' in old_message: message.sending_client = clients[old_message['sending_client']] elif sender_email in ["*****@*****.**", "*****@*****.**", "*****@*****.**", "*****@*****.**", "*****@*****.**"]: message.sending_client = clients['populate_db'] elif realm.domain == "zulip.com": message.sending_client = clients["website"] elif realm.domain == "mit.edu": message.sending_client = clients['zephyr_mirror'] else: message.sending_client = clients['populate_db'] message.type = type_hash[message_type] message.content = old_message["content"] message.subject = old_message["subject"] message.pub_date = timestamp_to_datetime(old_message["timestamp"]) if message.type == Recipient.PERSONAL: message.recipient = user_recipients[old_message["recipient"][0]["email"]] elif message.type == Recipient.STREAM: message.recipient = stream_recipients[(realm.id, old_message["recipient"].lower())] elif message.type == Recipient.HUDDLE: huddle_hash = get_huddle_hash([users[u["email"]].id for u in old_message["recipient"]]) message.recipient = huddle_recipients[huddle_hash] else: raise ValueError('Bad message type') messages_to_create.append(message) print(datetime.datetime.now(), "Importing messages, part 2...") Message.objects.bulk_create(messages_to_create) messages_to_create = [] # Finally, create all the UserMessage objects print(datetime.datetime.now(), "Importing usermessages, part 1...") personal_recipients = {} # type: Dict[int, bool] for r in Recipient.objects.filter(type = Recipient.PERSONAL): personal_recipients[r.id] = True all_messages = Message.objects.all() # type: Sequence[Message] user_messages_to_create = [] # type: List[UserMessage] messages_by_id = {} # type: Dict[int, Message] for message in all_messages: messages_by_id[message.id] = message if len(messages_by_id) == 0: print(datetime.datetime.now(), "No old messages to replay") return if first_message_id is None: first_message_id = min(messages_by_id.keys()) tot_user_messages = 0 pending_subs = {} # type: Dict[Tuple[int, int], bool] current_message_id = first_message_id pending_colors = {} # type: Dict[Tuple[text_type, text_type], text_type] for old_message in old_messages: message_type = old_message["type"] if message_type == 'subscription_added': stream_key = (realms[old_message["domain"]].id, old_message["name"].strip().lower()) subscribers.setdefault(stream_recipients[stream_key].id, set()).add(users[old_message["user"]].id) pending_subs[(stream_recipients[stream_key].id, users[old_message["user"]].id)] = True continue elif message_type == "subscription_removed": stream_key = (realms[old_message["domain"]].id, old_message["name"].strip().lower()) user_id = users[old_message["user"]].id subscribers.setdefault(stream_recipients[stream_key].id, set()) try: subscribers[stream_recipients[stream_key].id].remove(user_id) except KeyError: print("Error unsubscribing %s from %s: not subscribed" % ( old_message["user"], old_message["name"])) pending_subs[(stream_recipients[stream_key].id, users[old_message["user"]].id)] = False continue elif message_type == "user_activated" or message_type == "user_created": # These are rare, so just handle them the slow way user_profile = users[old_message["user"]] join_date = timestamp_to_datetime(old_message['timestamp']) do_activate_user(user_profile, log=False, join_date=join_date) # Update the cache of users to show this user as activated users_by_id[user_profile.id] = user_profile users[old_message["user"]] = user_profile continue elif message_type == "user_deactivated": user_profile = users[old_message["user"]] do_deactivate_user(user_profile, log=False) continue elif message_type == "user_change_password": # Just handle these the slow way user_profile = users[old_message["user"]] do_change_password(user_profile, old_message["pwhash"], log=False, hashed_password=True) continue elif message_type == "user_change_full_name": # Just handle these the slow way user_profile = users[old_message["user"]] user_profile.full_name = old_message["full_name"] user_profile.save(update_fields=["full_name"]) continue elif message_type == "enable_desktop_notifications_changed": # Just handle these the slow way user_profile = users[old_message["user"]] user_profile.enable_desktop_notifications = (old_message["enable_desktop_notifications"] != "false") user_profile.save(update_fields=["enable_desktop_notifications"]) continue elif message_type == "enable_sounds_changed": user_profile = users[old_message["user"]] user_profile.enable_sounds = (old_message["enable_sounds"] != "false") user_profile.save(update_fields=["enable_sounds"]) elif message_type == "enable_offline_email_notifications_changed": user_profile = users[old_message["user"]] user_profile.enable_offline_email_notifications = ( old_message["enable_offline_email_notifications"] != "false") user_profile.save(update_fields=["enable_offline_email_notifications"]) continue elif message_type == "enable_offline_push_notifications_changed": user_profile = users[old_message["user"]] user_profile.enable_offline_push_notifications = ( old_message["enable_offline_push_notifications"] != "false") user_profile.save(update_fields=["enable_offline_push_notifications"]) continue elif message_type == "default_streams": set_default_streams(get_realm(old_message["domain"]), old_message["streams"]) continue elif message_type == "subscription_property": property_name = old_message.get("property") if property_name == "stream_color" or property_name == "color": color = old_message.get("color", old_message.get("value")) pending_colors[(old_message["user"], old_message["stream_name"].lower())] = color elif property_name in ["in_home_view", "notifications"]: # TODO: Handle this continue else: raise RuntimeError("Unknown property %s" % (property_name,)) continue elif message_type == "realm_created": # No action required continue elif message_type in ["user_email_changed", "update_onboarding", "update_message"]: # TODO: Handle these continue if message_type not in ["stream", "huddle", "personal"]: raise RuntimeError("Unexpected message type %s" % (message_type,)) message = messages_by_id[current_message_id] current_message_id += 1 if message.recipient_id not in subscribers: # Nobody received this message -- probably due to our # subscriptions being out-of-date. continue recipient_user_ids = set() # type: Set[int] for user_profile_id in subscribers[message.recipient_id]: recipient_user_ids.add(user_profile_id) if message.recipient_id in personal_recipients: # Include the sender in huddle recipients recipient_user_ids.add(message.sender_id) for user_profile_id in recipient_user_ids: if users_by_id[user_profile_id].is_active: um = UserMessage(user_profile_id=user_profile_id, message=message) user_messages_to_create.append(um) if len(user_messages_to_create) > 100000: tot_user_messages += len(user_messages_to_create) UserMessage.objects.bulk_create(user_messages_to_create) user_messages_to_create = [] print(datetime.datetime.now(), "Importing usermessages, part 2...") tot_user_messages += len(user_messages_to_create) UserMessage.objects.bulk_create(user_messages_to_create) print(datetime.datetime.now(), "Finalizing subscriptions...") current_subs = {} # type: Dict[Tuple[int, int], bool] current_subs_obj = {} # type: Dict[Tuple[int, int], Subscription] for s in Subscription.objects.select_related().all(): current_subs[(s.recipient_id, s.user_profile_id)] = s.active current_subs_obj[(s.recipient_id, s.user_profile_id)] = s subscriptions_to_add = [] # type: List[Subscription] subscriptions_to_change = [] # type: List[Tuple[Tuple[int, int], bool]] for pending_sub in pending_subs.keys(): (recipient_id, user_profile_id) = pending_sub current_state = current_subs.get(pending_sub) if pending_subs[pending_sub] == current_state: # Already correct in the database continue elif current_state is not None: subscriptions_to_change.append((pending_sub, pending_subs[pending_sub])) continue s = Subscription(recipient_id=recipient_id, user_profile_id=user_profile_id, active=pending_subs[pending_sub]) subscriptions_to_add.append(s) Subscription.objects.bulk_create(subscriptions_to_add) for (sub_tuple, active) in subscriptions_to_change: current_subs_obj[sub_tuple].active = active current_subs_obj[sub_tuple].save(update_fields=["active"]) subs = {} # type: Dict[Tuple[int, int], Subscription] for sub in Subscription.objects.all(): subs[(sub.user_profile_id, sub.recipient_id)] = sub # TODO: do restore of subscription colors -- we're currently not # logging changes so there's little point in having the code :( print(datetime.datetime.now(), "Finished importing %s messages (%s usermessages)" % \ (len(all_messages), tot_user_messages)) site = Site.objects.get_current() site.domain = 'zulip.com' site.save() print(datetime.datetime.now(), "Filling in user pointers...") # Set restored pointers to the very latest messages for user_profile in UserProfile.objects.all(): try: top = UserMessage.objects.filter( user_profile_id=user_profile.id).order_by("-message")[0] user_profile.pointer = top.message_id except IndexError: user_profile.pointer = -1 user_profile.save(update_fields=["pointer"]) print(datetime.datetime.now(), "Done replaying old messages")