Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support: Set discounted price instead percentage for customers. #29957

Merged
merged 1 commit into from May 16, 2024

Conversation

amanagr
Copy link
Member

@amanagr amanagr commented May 6, 2024

First we need to run #29890 for exisitng customers (not required for testing this PR though)

cloud discount

Screenshot 2024-05-06 at 8 33 40 AM Screenshot 2024-05-06 at 8 32 59 AM Screenshot 2024-05-06 at 8 36 11 AM Screenshot 2024-05-06 at 8 40 01 AM Screenshot 2024-05-06 at 8 40 22 AM

basic free trial discount

Screenshot 2024-05-06 at 8 37 44 AM Screenshot 2024-05-06 at 8 39 27 AM

business discount

Screenshot 2024-05-06 at 8 40 53 AM Screenshot 2024-05-06 at 8 41 12 AM Screenshot 2024-05-06 at 8 41 52 AM Screenshot 2024-05-06 at 8 42 13 AM

next plan discount

Screenshot 2024-05-06 at 9 00 56 AM Screenshot 2024-05-06 at 8 48 53 AM Screenshot 2024-05-06 at 8 49 25 AM Screenshot 2024-05-06 at 9 05 06 AM

@alya
Copy link
Contributor

alya commented May 6, 2024

Nice! Just to check, since I don't see it in the screenshots -- is there a confirmation banner?

@amanagr
Copy link
Member Author

amanagr commented May 6, 2024

Nice! Just to check, since I don't see it in the screenshots -- is there a confirmation banner?

No, I can add that as a separate PR since we don't have confirmation banners on this page.

@alya
Copy link
Contributor

alya commented May 6, 2024

Hm, I just meant these banners, which we currently have:

Screenshot 2024-05-06 at 14 43 54@2x

@amanagr
Copy link
Member Author

amanagr commented May 10, 2024

Updated the PR as discussed here

Screenshot 2024-05-10 at 3 28 15 PM Screenshot 2024-05-10 at 3 28 11 PM

@alya alya added the integration review Added by maintainers when a PR may be ready for integration. label May 14, 2024
@alya
Copy link
Contributor

alya commented May 14, 2024

Looks great to me!

def estimate_annual_recurring_revenue_by_realm() -> Dict[str, int]: # nocoverage
def get_estimated_arr_and_rate_by_realm() -> Tuple[Dict[str, int], Dict[str, str]]: # nocoverage
# NOTE: Customers without a plan might still have a discount attached to them which
# are not included in `plan_rate`.
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be basically a stale discount percent display value? I guess we could do a migration to clear those if it was a priority.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we don't want to do anything here. I just wanted to note that even if there is no discount displayed for the customer in the support table, that doesn't mean there is no discount applied to them. I am not sure where to best reflect that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I guess that won't be valid after #29957 (comment)

)
self.assert_in_success_response(
["Discount for realm-name-4 changed to 50% from 0%."], result
[
"Monthly price for realm-name-4 changed to 50 from 0. Annual price changed to 500 from 0."
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "from 0" feels odd here; probably that suggests the success message code should be saying "from the default price" for this case.

row["plan_type"] == Realm.PLAN_TYPE_LIMITED
and string_id in realms_with_default_discount
):
row["effective_rate"] = 100 - int(realms_with_default_discount[string_id])
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to still display an effective rate for this case based on their required_plan_tier configuration?

Could be a follow-up, but that seems to make sense to me.

}
}
},
);
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@laurynmm can you also review the support panel code as its maintainer?

plan.discount = format_discount_percentage(
Decimal((original_plan_price - plan.price_per_license) / original_plan_price * 100)
)
plan.save(update_fields=["price_per_license", "discount"])
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to rework plan.discount to be a property, rather than a database field? Just thinking about how if we change the base pricing, we'll need to run a migration to update these cached values.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, especially since annual and monthy discounted prices could be different and hence the plan.discount.

@@ -1276,49 +1278,65 @@ def create_card_update_session(self) -> Dict[str, Any]:
def apply_discount_to_plan(
self,
plan: CustomerPlan,
discount: Decimal,
customer: Customer,
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this just self.get_customer()? I think we generally try to avoid passing that as a parameter.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I can just modify it to use plan.customer.

@timabbott
Copy link
Sponsor Member

This looks good to me, merged with the below minor changes. Several notes above about potential follow-ups may be well worth pursuing, but I think they all are mostly invisible technical tweaks and would be easier to review as follow-ups anyway, so there's no reason to block on finishing them. Thanks for doing this @amanagr!

diff --git a/corporate/migrations/0043_remove_customer_default_discount_and_more.py b/corporate/migrations/0043_remove_customer_default_discount_and_more.py
index 9e0b402144..21e7d2c4af 100644
--- a/corporate/migrations/0043_remove_customer_default_discount_and_more.py
+++ b/corporate/migrations/0043_remove_customer_default_discount_and_more.py
@@ -6,6 +6,10 @@ from django.db import migrations, models
 from django.db.backends.base.schema import BaseDatabaseSchemaEditor
 from django.db.migrations.state import StateApps
 
+# It's generally unsafe to import product code from migrations,
+# because the migration will be run with the **current** version of
+# that code, not the version at the time of the migration. But because
+# this migration will only be run for Zulip Cloud, this is OK.
 from corporate.lib.stripe import get_price_per_license
 
 
@@ -17,6 +21,12 @@ def calculate_discounted_price_per_license(
 
 
 def calculate_discounted_price(apps: StateApps, schema_editor: BaseDatabaseSchemaEditor) -> None:
+    """Migrates existing customers with a configured default discount to
+    instead have discounted prices for their configured required_plan_tier.
+
+    Does not operate on CustomerPlan fields, since those are already
+    resolved into a price.
+    """
     BILLING_SCHEDULE_ANNUAL = 1
     BILLING_SCHEDULE_MONTHLY = 2
 
@@ -39,6 +49,9 @@ def calculate_discounted_price(apps: StateApps, schema_editor: BaseDatabaseSchem
             monthly_price_per_license, customer.default_discount
         )
         customers_to_update.append(customer)
+        print(
+            f"\nChanging price for {customer.id}: {customer.default_discount} => {customer.annual_discounted_price}/{customer.monthly_discounted_price}"
+        )
 
     Customer.objects.bulk_update(
         customers_to_update, ["annual_discounted_price", "monthly_discounted_price"]

@timabbott timabbott enabled auto-merge (rebase) May 15, 2024 21:15
This allows us to set the price of a plan exactly as discussed with
the customer.
@amanagr
Copy link
Member Author

amanagr commented May 16, 2024

Ci failed due to this, fixed in 1d52d08

2024-05-15 21:21:51.923 WARN [django.server] "GET /api/v1/get_stream_id?stream=nonexistent HTTP/1.1" 400 83
2024-05-15 21:21:52.336 WARN [django.server] "POST /api/v1/invites/multiuse HTTP/1.1" 400 92
2024-05-15 21:21:52.394 WARN [django.server] "DELETE /api/v1/invites/multiuse/2 HTTP/1.1" 400 67
Waiting for children to stop...
2024-05-15 21:21:52.439 INFO [:9983] Tornado 9983 dumped 1 event queues in 0.000s
Traceback (most recent call last):
Running API tests...
  File "/__w/zulip/zulip/./tools/test-api", line 94, in <module>
    test_the_api(client, nonadmin_client, owner_client)
  File "/__w/zulip/zulip/zerver/openapi/python_examples.py", line 1807, in test_the_api
    test_invitations(client)
  File "/__w/zulip/zulip/zerver/openapi/python_examples.py", line 1795, in test_invitations
    revoke_reusable_invitation_link(client)
  File "/__w/zulip/zulip/zerver/openapi/python_examples.py", line 53, in _record_calls_wrapper
    return test_func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/__w/zulip/zulip/zerver/openapi/python_examples.py", line 381, in revoke_reusable_invitation_link
    validate_against_openapi_schema(result, "/invites/multiuse/{invite_id}", "delete", "200")
  File "/__w/zulip/zulip/zerver/openapi/openapi.py", line 432, in validate_against_openapi_schema
    return validate_test_response(mock_request, mock_response)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/__w/zulip/zulip/zerver/openapi/openapi.py", line 484, in validate_test_response
    raise SchemaError(message) from None
zerver.openapi.openapi.SchemaError: Response validation error at delete /api/v1/invites/multiuse/{invite_id} (200):

InvalidData: InvalidData: Value {'result': 'error', 'msg': 'No such invitation', 'code': 'BAD_REQUEST'} not valid for schema of type any: (<ValidationError: "'error' is not one of ['success']">, <ValidationError: "Additional properties are not allowed ('code' was unexpected)">)

For help debugging these errors see: https://zulip.readthedocs.io/en/latest/documentation/api.html#debugging-schema-validation-errors

@timabbott timabbott merged commit 7203661 into zulip:main May 16, 2024
13 checks passed
@amanagr amanagr deleted the review_discount_price branch May 16, 2024 09:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
integration review Added by maintainers when a PR may be ready for integration. size: XL
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants