Skip to content

Commit

Permalink
Added stripe client and listing products endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
pawelsierant committed Oct 8, 2024
1 parent 04b14da commit e1a1be4
Show file tree
Hide file tree
Showing 8 changed files with 299 additions and 0 deletions.
2 changes: 2 additions & 0 deletions apps/api/config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,5 @@ config :buildel,
System.get_env("REGISTRATION_DISABLED", "false") == "true"

config :buildel, :skip_flame, System.get_env("SKIP_FLAME", "false") == "true"

config :buildel, :stripe_api_key, System.get_env("STRIPE_API_KEY")
2 changes: 2 additions & 0 deletions apps/api/config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ if config_env() == :prod do

config :flame, :backend, FLAME.FlyBackend

config :buildel, :stripe_api_key, System.get_env("STRIPE_API_KEY")

config :flame, FLAME.FlyBackend,
token: System.get_env("FLY_API_TOKEN"),
env: %{
Expand Down
117 changes: 117 additions & 0 deletions apps/api/lib/buildel/clients/stripe.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
defmodule Buildel.Clients.StripeBehaviour do
@type list_products_params :: %{
active: boolean()
}

@callback list_products(list_products_params()) ::
{:ok, [Buildel.Clients.Stripe.Product.t()]} | {:error, term}
end

defmodule Buildel.Clients.Stripe do
@behaviour Buildel.Clients.StripeBehaviour

defmodule Price do
@type t :: %__MODULE__{
id: binary(),
amount: integer(),
currency: binary()
}

defstruct [:id, :currency, :amount]
end

defmodule Product do
@type t :: %__MODULE__{
id: binary(),
name: binary(),
description: binary(),
active: boolean(),
price: Price.t() | nil
}

defstruct [:id, :name, :description, :active, :price]
end

@impl Buildel.Clients.StripeBehaviour
def list_products(attrs \\ %{}) do
url = "/products?expand[]=data.default_price"

url =
Enum.reduce(Map.to_list(attrs), url, fn
{:active, active}, url when is_boolean(active) ->
"#{url}&active=#{active}"

_, url ->
url
end)

with {:ok, %Req.Response{body: body, status: 200}} <-
request(url) do
{:ok, body["data"] |> map_products()}
end
end

def get_price(price_id) do
request(price_id)
end

def new(options \\ []) when is_list(options) do
Req.new(
base_url: "https://api.stripe.com/v1",
auth: {:bearer, Application.fetch_env!(:buildel, :stripe_api_key)}
)
|> Req.Request.append_request_steps(
post: fn req ->
with %{method: :get, body: <<_::binary>>} <- req do
%{req | method: :post}
end
end
)
|> Req.merge(options)
end

def request(url, options \\ []), do: Req.request(new(url: parse_url(url)), options)

def request!(url, options \\ []), do: Req.request!(new(url: parse_url(url)), options)

defp parse_url("prod_" <> _ = id), do: "/products/#{id}"
defp parse_url("price_" <> _ = id), do: "/prices/#{id}"
defp parse_url("sub_" <> _ = id), do: "/subscriptions/#{id}"
defp parse_url("cus_" <> _ = id), do: "/customers/#{id}"
defp parse_url("cs_" <> _ = id), do: "/checkout/sessions/#{id}"
defp parse_url("inv_" <> _ = id), do: "/invoices/#{id}"
defp parse_url("evt_" <> _ = id), do: "/events/#{id}"
defp parse_url(url) when is_binary(url), do: url

defp map_products(products) do
Enum.map(products, fn %{
"id" => id,
"name" => name,
"description" => description,
"active" => active,
"default_price" => price
} ->
%Product{
id: id,
name: name,
description: description,
active: active,
price: map_price(price)
}
end)
end

defp map_price(nil), do: nil

defp map_price(%{
"id" => id,
"currency" => currency,
"unit_amount" => amount
}) do
%Price{
id: id,
currency: currency,
amount: amount
}
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
defmodule BuildelWeb.OrganizationSubscriptionController do
use BuildelWeb, :controller
use OpenApiSpex.ControllerSpecs

import BuildelWeb.UserAuth

alias Buildel.Clients.Stripe

alias Buildel.Organizations

action_fallback(BuildelWeb.FallbackController)

plug(:fetch_current_user)
plug(:require_authenticated_user)

plug OpenApiSpex.Plug.CastAndValidate,
json_render_error_v2: true,
render_error: BuildelWeb.ErrorRendererPlug,
replace_params: false

tags ["subscriptions"]

operation :list_products,
summary: "List products",
parameters: [
organization_id: [
in: :path,
description: "Organization ID",
type: :integer,
required: true
]
],
request_body: nil,
responses: [
ok: {"success", "application/json", BuildelWeb.Schemas.Subscriptions.ListProductsResponse},
unprocessable_entity:
{"unprocessable entity", "application/json",
BuildelWeb.Schemas.Errors.UnprocessableEntity},
unauthorized:
{"unauthorized", "application/json", BuildelWeb.Schemas.Errors.UnauthorizedResponse},
forbidden: {"forbidden", "application/json", BuildelWeb.Schemas.Errors.ForbiddenResponse}
],
security: [%{"authorization" => []}]

def list_products(conn, _params) do
%{"organization_id" => organization_id} = conn.params

user = conn.assigns.current_user

with {:ok, _organization} <- Organizations.get_user_organization(user, organization_id),
{:ok, products} <-
Stripe.list_products(%{
active: true
}) do
render(conn, :list_products, products: products)
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
defmodule BuildelWeb.OrganizationSubscriptionJSON do
alias Buildel.Clients.Stripe

def list_products(%{
products: products
}) do
%{
data: for(product <- products, do: product(product))
}
end

defp product(%Stripe.Product{} = product) do
%{
id: product.id,
name: product.name,
description: product.description,
active: product.active,
price: price(product.price)
}
end

defp price(nil), do: nil

defp price(%Stripe.Price{} = price) do
%{
id: price.id,
amount: price.amount,
currency: price.currency
}
end
end
6 changes: 6 additions & 0 deletions apps/api/lib/buildel_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,12 @@ defmodule BuildelWeb.Router do
resources("/organizations", OrganizationController, only: [:index, :create, :show])
put("/organizations/:id", OrganizationController, :update)

get(
"/organizations/:organization_id/subscriptions/products",
OrganizationSubscriptionController,
:list_products
)

resources("/organizations/:organization_id/costs", OrganizationCostsController,
only: [:index]
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
defmodule BuildelWeb.Schemas.Subscriptions do
alias OpenApiSpex.Schema

defmodule Price do
require OpenApiSpex

OpenApiSpex.schema(%{
title: "SubscriptionPrice",
type: :object,
properties: %{
id: %Schema{type: :string, description: "Price ID"},
amount: %Schema{type: :number, description: "Price amount"},
currency: %Schema{type: :string, description: "Price currency"}
},
required: [:id, :amount, :currency]
})
end

defmodule Product do
require OpenApiSpex

OpenApiSpex.schema(%{
title: "SubscriptionProduct",
type: :object,
properties: %{
id: %Schema{type: :string, description: "Product ID"},
name: %Schema{type: :string, description: "Product name"},
description: %Schema{type: :string, description: "Product description"},
active: %Schema{type: :boolean, description: "Product active status"},
price: Price
},
required: [:id, :name, :description, :active]
})
end

defmodule ListProductsResponse do
require OpenApiSpex

OpenApiSpex.schema(%{
title: "SubscriptionListProductsResponse",
type: :object,
properties: %{
data: %Schema{
type: :array,
items: Product
}
},
required: [:data]
})
end
end
32 changes: 32 additions & 0 deletions apps/api/test/support/client_mocks/stripe.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule Buildel.ClientMocks.Stripe do
@behaviour Buildel.Clients.StripeBehaviour

@impl Buildel.Clients.StripeBehaviour
def list_products(attrs \\ %{}) do
{:ok,
[
%Buildel.Clients.Stripe.Product{
id: "prod_1",
name: "Product 1",
description: "Description 1",
active: true,
price: %Buildel.Clients.Stripe.Price{
id: "price_1",
currency: "usd",
amount: 1000
}
},
%Buildel.Clients.Stripe.Product{
id: "prod_2",
name: "Product 2",
description: "Description 2",
active: true,
price: %Buildel.Clients.Stripe.Price{
id: "price_2",
currency: "usd",
amount: 2000
}
}
]}
end
end

0 comments on commit e1a1be4

Please sign in to comment.