From b1732c5151c86091da5bbbfac3177b11e6509b17 Mon Sep 17 00:00:00 2001 From: Tsvetozar Penov Date: Fri, 30 Jan 2026 10:20:14 +0100 Subject: [PATCH 1/2] Add SAN discount for upgrades --- lib/sanbase/stripe/stripe_api.ex | 16 +++ test/sanbase/billing/subscription_test.exs | 107 +++++++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/lib/sanbase/stripe/stripe_api.ex b/lib/sanbase/stripe/stripe_api.ex index 145460a2ae..3109307e33 100644 --- a/lib/sanbase/stripe/stripe_api.ex +++ b/lib/sanbase/stripe/stripe_api.ex @@ -202,6 +202,8 @@ defmodule Sanbase.StripeApi do with {:ok, params} <- get_upgrade_downgrade_subscription_params(db_subscription, plan), # Remove coupon for free basic API subscription {:ok, params} <- maybe_remove_coupon(params, db_subscription, plan), + # Add SAN holder coupon if user has 1000+ SAN and no existing discount + {:ok, params} <- maybe_add_san_coupon(params, db_subscription), {:ok, stripe_subscription} <- update_subscription(db_subscription.stripe_id, params) do {:ok, stripe_subscription} end @@ -278,6 +280,20 @@ defmodule Sanbase.StripeApi do end end + defp maybe_add_san_coupon(params, db_subscription) do + with {:ok, stripe_subscription} <- retrieve_subscription(db_subscription.stripe_id) 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 + end + defp get_subscription_first_item_id(stripe_id) do stripe_id |> retrieve_subscription() 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 = From 63ff14746e8d8acde98fc85737e1a76c401a7c33 Mon Sep 17 00:00:00 2001 From: Tsvetozar Penov Date: Mon, 2 Feb 2026 09:35:11 +0100 Subject: [PATCH 2/2] remove duplicate stripe api call --- lib/sanbase/stripe/stripe_api.ex | 62 ++++++++++++++++---------------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/lib/sanbase/stripe/stripe_api.ex b/lib/sanbase/stripe/stripe_api.ex index 3109307e33..ebdba9b58c 100644 --- a/lib/sanbase/stripe/stripe_api.ex +++ b/lib/sanbase/stripe/stripe_api.ex @@ -200,12 +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, 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), - {:ok, stripe_subscription} <- update_subscription(db_subscription.stripe_id, params) do - {:ok, stripe_subscription} + {: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 @@ -259,38 +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) do - with {:ok, stripe_subscription} <- retrieve_subscription(db_subscription.stripe_id) do - has_discount? = not is_nil(stripe_subscription.discount) - user = db_subscription.user - san_balance = User.san_balance_or_zero(user) + 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 + if not has_discount? and san_balance >= 1000 do + {:ok, Map.put(params, :coupon, "SAN_HOLDER_1000")} + else + {:ok, params} end end