diff --git a/lib/towerops/billing/stripe_client.ex b/lib/towerops/billing/stripe_client.ex index 06ed7242..f29351e3 100644 --- a/lib/towerops/billing/stripe_client.ex +++ b/lib/towerops/billing/stripe_client.ex @@ -130,13 +130,24 @@ defmodule Towerops.Billing.StripeClient do @doc """ Create a new Price object for metered billing. + Accepts price in dollars (e.g. "2.00") and converts to cents for Stripe API. + Stripe's unit_amount_decimal expects cents, not dollars. + Returns {:ok, price_object} or {:error, reason}. """ - def create_price(unit_amount_decimal) when is_binary(unit_amount_decimal) do + def create_price(unit_amount_dollars) when is_binary(unit_amount_dollars) do + # Convert dollars to cents (Stripe expects cents) + # "2.00" -> Decimal.new("2.00") -> Decimal.mult(100) -> "200.00" + unit_amount_cents = + unit_amount_dollars + |> Decimal.new() + |> Decimal.mult(Decimal.new("100")) + |> Decimal.to_string() + params = %{ product: stripe_product_id(), currency: "usd", - unit_amount_decimal: unit_amount_decimal, + unit_amount_decimal: unit_amount_cents, recurring: %{ interval: "month", usage_type: "metered", diff --git a/lib/towerops/visp/sync.ex b/lib/towerops/visp/sync.ex index 213431b9..a059aeea 100644 --- a/lib/towerops/visp/sync.ex +++ b/lib/towerops/visp/sync.ex @@ -31,10 +31,10 @@ defmodule Towerops.Visp.Sync do message = "Synced #{subscribers_count} subscribers, #{sites_count} sites, #{equipment_count} equipment" <> - if(total_mrr > 0, do: " (MRR: $#{:erlang.float_to_binary(total_mrr / 1, decimals: 2)})", else: "") + if(Decimal.compare(total_mrr, "0") == :gt, do: " (MRR: $#{Decimal.round(total_mrr, 2)})", else: "") Integrations.update_sync_status(integration, "success", message) - Integrations.update_billing_totals(integration, subscribers_count, Decimal.from_float(total_mrr)) + Integrations.update_billing_totals(integration, subscribers_count, total_mrr) {:ok, %{ @@ -50,12 +50,22 @@ defmodule Towerops.Visp.Sync do end defp calculate_total_mrr(subscribers) do - Enum.reduce(subscribers, 0.0, fn sub, acc -> - mrr = sub["mrr"] - if is_number(mrr), do: acc + mrr, else: acc + Enum.reduce(subscribers, Decimal.new("0"), fn sub, acc -> + add_subscriber_mrr(sub["mrr"], acc) end) end + defp add_subscriber_mrr(mrr, acc) when is_float(mrr) do + # Convert float to Decimal to avoid floating point math + Decimal.add(acc, Decimal.from_float(mrr)) + end + + defp add_subscriber_mrr(mrr, acc) when is_integer(mrr) do + Decimal.add(acc, Decimal.new(mrr)) + end + + defp add_subscriber_mrr(_mrr, acc), do: acc + defp humanize_sync_error(:unauthorized), do: "Authentication failed — check your API key" defp humanize_sync_error(:forbidden), do: "Access denied — your API key may not have sufficient permissions" defp humanize_sync_error({:rate_limited, retry_after}), do: "Rate limited by VISP — retry after #{retry_after}s"