diff --git a/config/test.exs b/config/test.exs index 1e24076e..b3e8b69b 100644 --- a/config/test.exs +++ b/config/test.exs @@ -94,4 +94,6 @@ config :towerops, # Reduce debounce delay in tests for faster test execution (50ms vs 500ms in prod) agent_channel_debounce_ms: 50, # Reduce device deletion delay for faster tests (10ms vs 500ms in prod) - device_deletion_delay_ms: 10 + device_deletion_delay_ms: 10, + # Stripe test configuration + stripe_product_id: "prod_test" diff --git a/lib/towerops/billing/stripe_client.ex b/lib/towerops/billing/stripe_client.ex index f98fbc34..a9b357e3 100644 --- a/lib/towerops/billing/stripe_client.ex +++ b/lib/towerops/billing/stripe_client.ex @@ -125,6 +125,33 @@ defmodule Towerops.Billing.StripeClient do end end + # Price Management + + @doc """ + Create a new Price object for metered billing. + + Returns {:ok, price_object} or {:error, reason}. + """ + def create_price(unit_amount_decimal) when is_binary(unit_amount_decimal) do + params = %{ + product: stripe_product_id(), + currency: "usd", + unit_amount_decimal: unit_amount_decimal, + recurring: %{ + interval: "month", + usage_type: "metered", + aggregate_usage: "max" + }, + billing_scheme: "per_unit", + metadata: %{ + created_by: "towerops_admin_ui", + created_at: DateTime.to_iso8601(DateTime.utc_now()) + } + } + + post("/v1/prices", params) + end + # HTTP Request Helpers defp get(path) do @@ -255,4 +282,8 @@ defmodule Towerops.Billing.StripeClient do defp stripe_price_id do Application.fetch_env!(:towerops, :stripe_price_id) end + + defp stripe_product_id do + Application.fetch_env!(:towerops, :stripe_product_id) + end end diff --git a/test/towerops/billing/stripe_client_test.exs b/test/towerops/billing/stripe_client_test.exs index faf1560c..5141ef74 100644 --- a/test/towerops/billing/stripe_client_test.exs +++ b/test/towerops/billing/stripe_client_test.exs @@ -315,4 +315,49 @@ defmodule Towerops.Billing.StripeClientTest do StripeClient.create_customer(organization) end end + + describe "create_price/1" do + test "creates metered billing price at specified amount" do + Req.Test.stub(StripeClient, fn conn -> + assert conn.method == "POST" + assert conn.request_path == "/v1/prices" + + # Verify form params + {:ok, body, conn} = Plug.Conn.read_body(conn) + params = URI.decode_query(body) + + assert params["unit_amount_decimal"] == "2.50" + assert params["currency"] == "usd" + assert params["recurring[interval]"] == "month" + assert params["recurring[usage_type]"] == "metered" + assert params["recurring[aggregate_usage]"] == "max" + assert params["billing_scheme"] == "per_unit" + + conn + |> Plug.Conn.put_resp_content_type("application/json") + |> Plug.Conn.send_resp( + 200, + Jason.encode!(%{ + "id" => "price_test123", + "object" => "price", + "unit_amount_decimal" => "2.50" + }) + ) + end) + + assert {:ok, %{"id" => "price_test123"}} = StripeClient.create_price("2.50") + end + + test "returns error on API failure" do + Req.Test.stub(StripeClient, fn conn -> + error = %{"error" => %{"message" => "Invalid amount"}} + + conn + |> Plug.Conn.put_resp_content_type("application/json") + |> Plug.Conn.send_resp(400, Jason.encode!(error)) + end) + + assert {:error, {:bad_request, "Invalid amount"}} = StripeClient.create_price("invalid") + end + end end