From a813f56d1dfac8ef8b2737c27563f62b3ef29f26 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Fri, 6 Mar 2026 13:50:11 -0600 Subject: [PATCH] feat: add StripeClient.update_subscription_price --- lib/towerops/billing/stripe_client.ex | 19 +++++++ test/towerops/billing/stripe_client_test.exs | 55 ++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/lib/towerops/billing/stripe_client.ex b/lib/towerops/billing/stripe_client.ex index a9b357e3..06ed7242 100644 --- a/lib/towerops/billing/stripe_client.ex +++ b/lib/towerops/billing/stripe_client.ex @@ -152,6 +152,25 @@ defmodule Towerops.Billing.StripeClient do post("/v1/prices", params) end + @doc """ + Update subscription to use new price. + + Fetches subscription to get item ID, then updates with proration_behavior: "none" + so price change takes effect at next billing cycle. + """ + def update_subscription_price(subscription_id, new_price_id) do + with {:ok, sub} <- get_subscription(subscription_id) do + item_id = sub |> get_in(["items", "data"]) |> List.first() |> Map.get("id") + + params = %{ + items: [%{id: item_id, price: new_price_id}], + proration_behavior: "none" + } + + post("/v1/subscriptions/#{subscription_id}", params) + end + end + # HTTP Request Helpers defp get(path) do diff --git a/test/towerops/billing/stripe_client_test.exs b/test/towerops/billing/stripe_client_test.exs index 5141ef74..c12908ce 100644 --- a/test/towerops/billing/stripe_client_test.exs +++ b/test/towerops/billing/stripe_client_test.exs @@ -360,4 +360,59 @@ defmodule Towerops.Billing.StripeClientTest do assert {:error, {:bad_request, "Invalid amount"}} = StripeClient.create_price("invalid") end end + + describe "update_subscription_price/2" do + test "updates subscription to new price" do + # First stub: get subscription + Req.Test.stub(StripeClient, fn conn -> + if conn.method == "GET" and String.contains?(conn.request_path, "/v1/subscriptions/sub_test") do + conn + |> Plug.Conn.put_resp_content_type("application/json") + |> Plug.Conn.send_resp( + 200, + Jason.encode!(%{ + "id" => "sub_test", + "items" => %{ + "data" => [%{"id" => "si_test123", "price" => %{"id" => "price_old"}}] + } + }) + ) + else + # Second stub: update subscription + {:ok, body, conn} = Plug.Conn.read_body(conn) + params = URI.decode_query(body) + + assert params["items[0][id]"] == "si_test123" + assert params["items[0][price]"] == "price_new123" + assert params["proration_behavior"] == "none" + + conn + |> Plug.Conn.put_resp_content_type("application/json") + |> Plug.Conn.send_resp( + 200, + Jason.encode!(%{ + "id" => "sub_test", + "items" => %{"data" => [%{"price" => %{"id" => "price_new123"}}]} + }) + ) + end + end) + + assert {:ok, %{"id" => "sub_test"}} = + StripeClient.update_subscription_price("sub_test", "price_new123") + end + + test "returns error when subscription not found" do + Req.Test.stub(StripeClient, fn conn -> + conn + |> Plug.Conn.put_resp_content_type("application/json") + |> Plug.Conn.send_resp( + 404, + Jason.encode!(%{"error" => %{"message" => "No such subscription"}}) + ) + end) + + assert {:error, _} = StripeClient.update_subscription_price("sub_invalid", "price_new") + end + end end