Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/algora/accounts/schemas/user.ex
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ defmodule Algora.Accounts.User do
:need_avatar,
:website_url,
:bio,
:country,
:location,
:timezone
])
Expand Down
140 changes: 140 additions & 0 deletions lib/algora/integrations/stripe/connect_countries.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
defmodule Algora.Stripe.ConnectCountries do
@moduledoc false

@spec list() :: [{String.t(), String.t()}]
def list,
do: [
{"Albania", "AL"},
{"Algeria", "DZ"},
{"Angola", "AO"},
{"Antigua and Barbuda", "AG"},
{"Argentina", "AR"},
{"Armenia", "AM"},
{"Australia", "AU"},
{"Austria", "AT"},
{"Azerbaijan", "AZ"},
{"Bahamas", "BS"},
{"Bahrain", "BH"},
{"Bangladesh", "BD"},
{"Belgium", "BE"},
{"Benin", "BJ"},
{"Bhutan", "BT"},
{"Bolivia", "BO"},
{"Bosnia and Herzegovina", "BA"},
{"Botswana", "BW"},
{"Brazil", "BR"},
{"Brunei", "BN"},
{"Bulgaria", "BG"},
{"Cambodia", "KH"},
{"Canada", "CA"},
{"Chile", "CL"},
{"Colombia", "CO"},
{"Costa Rica", "CR"},
{"Croatia", "HR"},
{"Cyprus", "CY"},
{"Czech Republic", "CZ"},
{"Denmark", "DK"},
{"Dominican Republic", "DO"},
{"Ecuador", "EC"},
{"Egypt", "EG"},
{"El Salvador", "SV"},
{"Estonia", "EE"},
{"Ethiopia", "ET"},
{"Finland", "FI"},
{"France", "FR"},
{"Gabon", "GA"},
{"Gambia", "GM"},
{"Germany", "DE"},
{"Ghana", "GH"},
{"Gibraltar", "GI"},
{"Greece", "GR"},
{"Guatemala", "GT"},
{"Guyana", "GY"},
{"Hong Kong", "HK"},
{"Hungary", "HU"},
{"Iceland", "IS"},
{"India", "IN"},
{"Indonesia", "ID"},
{"Ireland", "IE"},
{"Israel", "IL"},
{"Italy", "IT"},
{"Ivory Coast", "CI"},
{"Jamaica", "JM"},
{"Japan", "JP"},
{"Jordan", "JO"},
{"Kazakhstan", "KZ"},
{"Kenya", "KE"},
{"Kuwait", "KW"},
{"Laos", "LA"},
{"Latvia", "LV"},
{"Liechtenstein", "LI"},
{"Lithuania", "LT"},
{"Luxembourg", "LU"},
{"Macao", "MO"},
{"Macedonia", "MK"},
{"Madagascar", "MG"},
{"Malaysia", "MY"},
{"Malta", "MT"},
{"Mauritius", "MU"},
{"Mexico", "MX"},
{"Moldova", "MD"},
{"Monaco", "MC"},
{"Mongolia", "MN"},
{"Morocco", "MA"},
{"Mozambique", "MZ"},
{"Namibia", "NA"},
{"Netherlands", "NL"},
{"New Zealand", "NZ"},
{"Nigeria", "NG"},
{"Norway", "NO"},
{"Oman", "OM"},
{"Pakistan", "PK"},
{"Panama", "PA"},
{"Paraguay", "PY"},
{"Peru", "PE"},
{"Philippines", "PH"},
{"Poland", "PL"},
{"Portugal", "PT"},
{"Qatar", "QA"},
{"Romania", "RO"},
{"Rwanda", "RW"},
{"Saint Lucia", "LC"},
{"San Marino", "SM"},
{"Saudi Arabia", "SA"},
{"Senegal", "SN"},
{"Serbia", "RS"},
{"Singapore", "SG"},
{"Slovakia", "SK"},
{"Slovenia", "SI"},
{"South Africa", "ZA"},
{"South Korea", "KR"},
{"Spain", "ES"},
{"Sri Lanka", "LK"},
{"Sweden", "SE"},
{"Switzerland", "CH"},
{"Taiwan", "TW"},
{"Tanzania", "TZ"},
{"Thailand", "TH"},
{"Trinidad and Tobago", "TT"},
{"Tunisia", "TN"},
{"Turkey", "TR"},
{"United Arab Emirates", "AE"},
{"United Kingdom", "GB"},
{"United States", "US"},
{"Uruguay", "UY"},
{"Uzbekistan", "UZ"},
{"Vietnam", "VN"}
]

@spec from_code(String.t()) :: String.t()
def from_code(code) do
list() |> Enum.find(&(elem(&1, 1) == code)) |> elem(0) || code
end

@spec list_codes() :: [String.t()]
def list_codes, do: Enum.map(list(), &elem(&1, 1))

@spec account_type(String.t()) :: :standard | :express
def account_type("BR"), do: :standard
def account_type(_), do: :express
end
120 changes: 120 additions & 0 deletions lib/algora/payments/payments.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ defmodule Algora.Payments do
@moduledoc false
import Ecto.Query

alias Algora.Accounts
alias Algora.Accounts.User
alias Algora.Payments.Account
alias Algora.Payments.Customer
alias Algora.Payments.PaymentMethod
alias Algora.Payments.Transaction
alias Algora.Repo
alias Algora.Stripe.ConnectCountries
alias Algora.Util

require Logger

Expand Down Expand Up @@ -128,4 +133,119 @@ defmodule Algora.Payments do
|> order_by([t], desc: t.inserted_at)
|> Repo.all()
end

@spec fetch_or_create_account(user :: User.t(), country :: String.t()) ::
{:ok, Account.t()} | {:error, Ecto.Changeset.t()}
def fetch_or_create_account(user, country) do
case fetch_account(user) do
{:ok, account} -> {:ok, account}
{:error, :not_found} -> create_account(user, country)
end
end

@spec fetch_account(user :: User.t()) ::
{:ok, Account.t()} | {:error, :not_found}
def fetch_account(user) do
Repo.fetch_by(Account, user_id: user.id)
end

@spec create_account(user :: User.t(), country :: String.t()) ::
{:ok, Account.t()} | {:error, Ecto.Changeset.t()}
def create_account(user, country) do
type = ConnectCountries.account_type(country)

with {:ok, stripe_account} <- create_stripe_account(%{country: country, type: type}) do
attrs = %{
provider: "stripe",
provider_id: stripe_account.id,
provider_meta: Util.normalize_struct(stripe_account),
type: type,
user_id: user.id,
country: country
}

%Account{}
|> Account.changeset(attrs)
|> Repo.insert()
end
end

@spec create_stripe_account(attrs :: map()) ::
{:ok, Stripe.Account.t()} | {:error, Stripe.Error.t()}
defp create_stripe_account(%{country: country, type: type}) do
case Stripe.Account.create(%{country: country, type: to_string(type)}) do
{:ok, account} -> {:ok, account}
{:error, _reason} -> Stripe.Account.create(%{type: to_string(type)})
end
end

@spec create_account_link(account :: Account.t(), base_url :: String.t()) ::
{:ok, Stripe.AccountLink.t()} | {:error, Stripe.Error.t()}
def create_account_link(account, base_url) do
Stripe.AccountLink.create(%{
account: account.provider_id,
refresh_url: "#{base_url}/callbacks/stripe/refresh",
return_url: "#{base_url}/callbacks/stripe/return",
type: "account_onboarding"
})
end

@spec create_login_link(account :: Account.t()) ::
{:ok, Stripe.LoginLink.t()} | {:error, Stripe.Error.t()}
def create_login_link(account) do
Stripe.LoginLink.create(account.provider_id, %{})
end

@spec update_account(account :: Account.t(), stripe_account :: Stripe.Account.t()) ::
{:ok, Account.t()} | {:error, Ecto.Changeset.t()}
def update_account(account, stripe_account) do
account
|> Account.changeset(%{
provider: "stripe",
provider_id: stripe_account.id,
provider_meta: Util.normalize_struct(stripe_account),
charges_enabled: stripe_account.charges_enabled,
payouts_enabled: stripe_account.payouts_enabled,
payout_interval: stripe_account.settings.payouts.schedule.interval,
payout_speed: stripe_account.settings.payouts.schedule.delay_days,
default_currency: stripe_account.default_currency,
details_submitted: stripe_account.details_submitted,
country: stripe_account.country,
service_agreement: get_service_agreement(stripe_account)
})
|> Repo.update()
end

@spec refresh_stripe_account(user :: User.t()) ::
{:ok, Account.t()} | {:error, Ecto.Changeset.t()} | {:error, :not_found} | {:error, Stripe.Error.t()}
def refresh_stripe_account(user) do
with {:ok, account} <- fetch_account(user),
{:ok, stripe_account} <- Stripe.Account.retrieve(account.provider_id, []),
{:ok, updated_account} <- update_account(account, stripe_account) do
user = Accounts.get_user(account.user_id)

if user && stripe_account.charges_enabled do
Accounts.update_settings(user, %{country: stripe_account.country})
end

{:ok, updated_account}
end
end

@spec get_service_agreement(account :: Stripe.Account.t()) :: String.t()
defp get_service_agreement(%{tos_acceptance: %{service_agreement: agreement}} = _account) when not is_nil(agreement) do
agreement
end

@spec get_service_agreement(account :: Stripe.Account.t()) :: String.t()
defp get_service_agreement(%{capabilities: capabilities}) do
if is_nil(capabilities[:card_payments]), do: "recipient", else: "full"
end

@spec delete_account(account :: Account.t()) :: {:ok, Account.t()} | {:error, Ecto.Changeset.t()}
def delete_account(account) do
with {:ok, _stripe_account} <- Stripe.Account.delete(account.provider_id) do
Repo.delete(account)
end
end
end
50 changes: 37 additions & 13 deletions lib/algora/payments/schemas/account.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,27 @@ defmodule Algora.Payments.Account do
@moduledoc false
use Algora.Schema

alias Algora.Stripe

@derive {Inspect, except: [:provider_meta]}
typed_schema "accounts" do
field :provider, :string
field :provider_id, :string
field :provider_meta, :map
field :provider, :string, null: false
field :provider_id, :string, null: false
field :provider_meta, :map, null: false

field :name, :string
field :details_submitted, :boolean, default: false
field :charges_enabled, :boolean, default: false
field :details_submitted, :boolean, default: false, null: false
field :charges_enabled, :boolean, default: false, null: false
field :payouts_enabled, :boolean, default: false, null: false
field :payout_interval, :string
field :payout_speed, :integer
field :default_currency, :string
field :service_agreement, :string
field :country, :string
field :type, Ecto.Enum, values: [:standard, :express]
field :region, Ecto.Enum, values: [:US, :EU]
field :stale, :boolean, default: false
field :country, :string, null: false
field :type, Ecto.Enum, values: [:standard, :express], null: false
field :stale, :boolean, default: false, null: false

belongs_to :user, Algora.Accounts.User
belongs_to :user, Algora.Accounts.User, null: false

timestamps()
end
Expand All @@ -30,12 +35,31 @@ defmodule Algora.Payments.Account do
:provider_meta,
:details_submitted,
:charges_enabled,
:payouts_enabled,
:payout_interval,
:payout_speed,
:default_currency,
:service_agreement,
:country,
:type,
:region,
:stale
:stale,
:user_id
])
|> validate_required([
:provider,
:provider_id,
:provider_meta,
:details_submitted,
:charges_enabled,
:payouts_enabled,
:country,
:type,
:stale,
:user_id
])
|> validate_required([:provider, :provider_id, :provider_meta])
|> validate_inclusion(:type, [:standard, :express])
|> validate_inclusion(:country, Stripe.ConnectCountries.list_codes())
|> foreign_key_constraint(:user_id)
|> generate_id()
end
end
5 changes: 2 additions & 3 deletions lib/algora/payments/schemas/customer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ defmodule Algora.Payments.Customer do
field :provider_meta, :map

field :name, :string
field :region, Ecto.Enum, values: [:US, :EU]

belongs_to :user, Algora.Accounts.User

Expand All @@ -22,8 +21,8 @@ defmodule Algora.Payments.Customer do

def changeset(customer, attrs) do
customer
|> cast(attrs, [:user_id, :provider, :provider_id, :provider_meta, :name, :region])
|> validate_required([:user_id, :provider, :provider_id, :provider_meta, :name, :region])
|> cast(attrs, [:user_id, :provider, :provider_id, :provider_meta, :name])
|> validate_required([:user_id, :provider, :provider_id, :provider_meta, :name])
|> unique_constraint(:user_id)
end
end
2 changes: 1 addition & 1 deletion lib/algora/repo.ex
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ defmodule Algora.Repo do
fetch_one(query, opts)
end

@spec fetch_by(Ecto.Queryable.t(), Keyword.t(), Keyword.t()) ::
@spec fetch_by(Ecto.Queryable.t(), Keyword.t() | map(), Keyword.t()) ::
{:ok, struct()} | {:error, :not_found}
def fetch_by(queryable, clauses, opts \\ []) do
query =
Expand Down
2 changes: 1 addition & 1 deletion lib/algora_web/components/ui/drawer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ defmodule AlgoraWeb.Components.UI.Drawer do
"fixed z-50 transform border bg-background transition-transform duration-300 ease-in-out",
case @direction do
"bottom" -> "inset-x-0 bottom-0 rounded-t-xl"
"right" -> "inset-y-0 right-0 h-full max-w-lg"
"right" -> "inset-y-0 right-0 h-full max-w-lg w-full"
end,
case @direction do
"bottom" -> if(@show, do: "translate-y-0", else: "translate-y-full")
Expand Down
Loading
Loading