diff --git a/lib/sanbase/stripe/stripe_api.ex b/lib/sanbase/stripe/stripe_api.ex index 145460a2ae..ebdba9b58c 100644 --- a/lib/sanbase/stripe/stripe_api.ex +++ b/lib/sanbase/stripe/stripe_api.ex @@ -200,10 +200,14 @@ defmodule Sanbase.StripeApi do def upgrade_downgrade(db_subscription, plan) do with {:ok, params} <- get_upgrade_downgrade_subscription_params(db_subscription, plan), + {:ok, stripe_subscription} <- retrieve_subscription(db_subscription.stripe_id), # Remove coupon for free basic API subscription - {:ok, params} <- maybe_remove_coupon(params, db_subscription, plan), - {:ok, stripe_subscription} <- update_subscription(db_subscription.stripe_id, params) do - {:ok, stripe_subscription} + {:ok, params} <- maybe_remove_coupon(params, db_subscription, plan, stripe_subscription), + # Add SAN holder coupon if user has 1000+ SAN and no existing discount + {:ok, params} <- maybe_add_san_coupon(params, db_subscription, stripe_subscription), + {:ok, updated_stripe_subscription} <- + update_subscription(db_subscription.stripe_id, params) do + {:ok, updated_stripe_subscription} end end @@ -257,24 +261,34 @@ defmodule Sanbase.StripeApi do # Helpers - defp maybe_remove_coupon(params, db_subscription, _plan) do - with {:ok, stripe_subscription} <- retrieve_subscription(db_subscription.stripe_id) do - percent_off = - get_in(stripe_subscription, [ - Access.key!(:discount), - Access.key!(:coupon), - Access.key!(:percent_off) - ]) - - is_basic_plan? = fn plan_id -> - plan_id in Sanbase.Billing.Subscription.ProPlus.basic_api_plans() - end - - if is_basic_plan?.(db_subscription.plan_id) and percent_off == 100.0 do - {:ok, Map.put(params, :coupon, nil)} - else - {:ok, params} - end + defp maybe_remove_coupon(params, db_subscription, _plan, stripe_subscription) do + percent_off = + get_in(stripe_subscription, [ + Access.key(:discount), + Access.key(:coupon), + Access.key(:percent_off) + ]) + + is_basic_plan? = fn plan_id -> + plan_id in Sanbase.Billing.Subscription.ProPlus.basic_api_plans() + end + + if is_basic_plan?.(db_subscription.plan_id) and percent_off == 100.0 do + {:ok, Map.put(params, :coupon, nil)} + else + {:ok, params} + end + end + + defp maybe_add_san_coupon(params, db_subscription, stripe_subscription) do + has_discount? = not is_nil(stripe_subscription.discount) + user = db_subscription.user + san_balance = User.san_balance_or_zero(user) + + if not has_discount? and san_balance >= 1000 do + {:ok, Map.put(params, :coupon, "SAN_HOLDER_1000")} + else + {:ok, params} end end diff --git a/test/sanbase/billing/subscription_test.exs b/test/sanbase/billing/subscription_test.exs index 87d7d7989a..7e20fb84e6 100644 --- a/test/sanbase/billing/subscription_test.exs +++ b/test/sanbase/billing/subscription_test.exs @@ -126,6 +126,113 @@ defmodule Sanbase.Billing.SubscriptionTest do end end + describe "#update_subscription - SAN discount on upgrade" do + test "applies SAN discount when user has 1000+ SAN and no existing coupon", context do + user = insert(:user, email: "test@example.com", san_balance: Decimal.new(1000)) + subscription = insert(:subscription_pro_sanbase, user: user, stripe_id: "sub_test_upgrade") + new_plan = context.plans.plan_pro_sanbase_yearly + + stripe_sub_no_discount = + StripeApiTestResponse.retrieve_subscription_resp(stripe_id: "sub_test_upgrade") |> elem(1) + + test_pid = self() + + with_mocks([ + {Stripe.Subscription, [], + [ + retrieve: fn _stripe_id, _opts -> {:ok, stripe_sub_no_discount} end, + update: fn _stripe_id, params, _opts -> + send(test_pid, {:update_params, params}) + StripeApiTestResponse.update_subscription_resp() + end + ]} + ]) do + {:ok, _updated} = Subscription.update_subscription(subscription, new_plan) + + assert_receive {:update_params, params} + assert params[:coupon] == "SAN_HOLDER_1000" + end + end + + test "does not apply coupon when subscription already has a discount", context do + user = insert(:staked_user, email: "test@example.com") + subscription = insert(:subscription_pro_sanbase, user: user, stripe_id: "sub_test_upgrade") + new_plan = context.plans.plan_pro_sanbase_yearly + + stripe_sub_with_discount = + StripeApiTestResponse.retrieve_subscription_resp(stripe_id: "sub_test_upgrade") + |> elem(1) + |> Map.put(:discount, %{coupon: %{id: "existing_coupon", percent_off: 10}}) + + test_pid = self() + + with_mocks([ + {Stripe.Subscription, [], + [ + retrieve: fn _stripe_id, _opts -> {:ok, stripe_sub_with_discount} end, + update: fn _stripe_id, params, _opts -> + send(test_pid, {:update_params, params}) + StripeApiTestResponse.update_subscription_resp() + end + ]} + ]) do + {:ok, _updated} = Subscription.update_subscription(subscription, new_plan) + + assert_receive {:update_params, params} + refute Map.has_key?(params, :coupon) + end + end + + test "does not apply coupon when user has less than 1000 SAN", context do + user = insert(:user, email: "test@example.com", san_balance: Decimal.new(500)) + subscription = insert(:subscription_pro_sanbase, user: user, stripe_id: "sub_test_upgrade") + new_plan = context.plans.plan_pro_sanbase_yearly + + stripe_sub_no_discount = + StripeApiTestResponse.retrieve_subscription_resp(stripe_id: "sub_test_upgrade") |> elem(1) + + test_pid = self() + + with_mocks([ + {Stripe.Subscription, [], + [ + retrieve: fn _stripe_id, _opts -> {:ok, stripe_sub_no_discount} end, + update: fn _stripe_id, params, _opts -> + send(test_pid, {:update_params, params}) + StripeApiTestResponse.update_subscription_resp() + end + ]} + ]) do + {:ok, _updated} = Subscription.update_subscription(subscription, new_plan) + + assert_receive {:update_params, params} + refute Map.has_key?(params, :coupon) + end + end + + test "upgrade works for user without SAN balance (no regression)", context do + user = insert(:user, email: "test@example.com") + subscription = insert(:subscription_pro_sanbase, user: user, stripe_id: "sub_test_upgrade") + new_plan = context.plans.plan_pro_sanbase_yearly + + stripe_sub_no_discount = + StripeApiTestResponse.retrieve_subscription_resp(stripe_id: "sub_test_upgrade") |> elem(1) + + with_mocks([ + {Stripe.Subscription, [], + [ + retrieve: fn _stripe_id, _opts -> {:ok, stripe_sub_no_discount} end, + update: fn _stripe_id, _params, _opts -> + StripeApiTestResponse.update_subscription_resp() + end + ]} + ]) do + result = Subscription.update_subscription(subscription, new_plan) + assert {:ok, _updated} = result + end + end + end + describe "#sync_subscription_with_stripe" do setup(context) do stripe_base_subscription =