defmodule Plausible.Billing.PaddleApi do alias Plausible.HTTPClient @headers [ {"content-type", "application/json"}, {"accept", "application/json"} ] def update_subscription_preview(paddle_subscription_id, new_plan_id) do config = get_config() params = %{ vendor_id: config[:vendor_id], vendor_auth_code: config[:vendor_auth_code], subscription_id: paddle_subscription_id, plan_id: new_plan_id, prorate: true, keep_modifiers: true, bill_immediately: true, quantity: 1 } case HTTPClient.post(preview_update_url(), @headers, params) do {:ok, response} -> if response.body["success"] do {:ok, response.body["response"]} else {:error, response.body["error"]} end {:error, error} -> {:error, error} end end def update_subscription(paddle_subscription_id, params) do config = get_config() params = Map.merge(params, %{ vendor_id: config[:vendor_id], vendor_auth_code: config[:vendor_auth_code], subscription_id: paddle_subscription_id, prorate: true, keep_modifiers: true, bill_immediately: true, quantity: 1 }) case HTTPClient.post(update_subscription_url(), @headers, params) do {:ok, response} -> if response.body["success"] do {:ok, response.body["response"]} else {:error, response.body["error"]} end {:error, %{reason: reason}} -> {:error, reason} end end def get_subscription(paddle_subscription_id) do config = get_config() params = %{ vendor_id: config[:vendor_id], vendor_auth_code: config[:vendor_auth_code], subscription_id: paddle_subscription_id } case HTTPClient.post(get_subscription_url(), @headers, params) do {:ok, response} -> if response.body["success"] do [subscription] = response.body["response"] {:ok, subscription} else {:error, response.body["error"]} end {:error, %{reason: reason}} -> {:error, reason} end end @spec get_invoices(Plausible.Billing.Subscription.t() | nil) :: {:ok, list()} | {:error, :request_failed} | {:error, :no_invoices} def get_invoices(nil), do: {:error, :no_invoices} def get_invoices(%{paddle_subscription_id: nil}), do: {:error, :no_invoices} def get_invoices(subscription) do config = get_config() params = %{ vendor_id: config[:vendor_id], vendor_auth_code: config[:vendor_auth_code], subscription_id: subscription.paddle_subscription_id, is_paid: 1, from: Date.shift(Date.utc_today(), year: -5) |> Calendar.strftime("%Y-%m-%d"), to: Date.shift(Date.utc_today(), day: 1) |> Calendar.strftime("%Y-%m-%d") } with {:ok, %{body: body}} <- HTTPClient.post(invoices_url(), @headers, params), true <- Map.get(body, "success"), [_ | _] = response <- Map.get(body, "response") do Enum.sort(response, fn %{"payout_date" => d1}, %{"payout_date" => d2} -> Date.compare(Date.from_iso8601!(d1), Date.from_iso8601!(d2)) == :gt end) |> then(&{:ok, &1}) else error -> Sentry.capture_message("Failed to retrieve invoices from Paddle", extra: %{extra: inspect(error), params: params, invoices_url: invoices_url()} ) {:error, :request_failed} end end def fetch_prices([_ | _] = product_ids, customer_ip) do params = %{product_ids: Enum.join(product_ids, ","), customer_ip: customer_ip} prices_final_url = prices_url() |> URI.parse() |> Map.replace(:query, URI.encode_query(params)) |> URI.to_string() case HTTPClient.impl().get(prices_final_url, @headers, nil) do {:ok, %{body: %{"success" => true, "response" => %{"products" => products}}}} -> products = Enum.into(products, %{}, fn %{ "currency" => currency, "price" => %{"net" => net_price}, "product_id" => product_id } -> {Integer.to_string(product_id), Money.from_float!(currency, net_price)} end) {:ok, products} {:ok, %{body: body}} -> Sentry.capture_message("Paddle API: Unexpected response when fetching prices", extra: %{api_response: body, product_ids: product_ids} ) {:error, :api_error} {:error, %{reason: reason}} -> Sentry.capture_message("Paddle API: Error when fetching prices", extra: %{reason: reason}) {:error, :api_error} end end def checkout_domain() do if Application.get_env(:plausible, :environment) == "staging" do "https://sandbox-checkout.paddle.com" else "https://checkout.paddle.com" end end def vendors_domain() do if Application.get_env(:plausible, :environment) == "staging" do "https://sandbox-vendors.paddle.com" else "https://vendors.paddle.com" end end defp get_config() do Application.get_env(:plausible, :paddle) end defp invoices_url() do Path.join(vendors_domain(), "/api/2.0/subscription/payments") end defp preview_update_url() do Path.join(vendors_domain(), "/api/2.0/subscription/preview_update") end defp update_subscription_url() do Path.join(vendors_domain(), "/api/2.0/subscription/users/update") end defp get_subscription_url() do Path.join(vendors_domain(), "/api/2.0/subscription/users") end defp prices_url() do Path.join(checkout_domain(), "/api/2.0/prices") end end