Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Organizations #158

Closed
wants to merge 10 commits into from
4 changes: 2 additions & 2 deletions assets/elm/src/Icons.elm
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,11 @@ closed toggle =
case toggle of
Off ->
iconView <|
svg [ width "17px", height "17px", viewBox "0 0 17 17", version "1.1" ] [ g [ id "Page-1", stroke "none", strokeWidth "1", fill "none", fillRule "evenodd" ] [ g [ id "closed-avatar-2-copy-6", transform "translate(0.500000, 0.500000)" ] [ circle [ id "Oval-2", stroke "#979797", cx "8", cy "8", r "8" ] [], g [ id "check", transform "translate(4.444444, 5.777778)", stroke "#8A98A5", strokeLinecap "round", strokeLinejoin "round" ] [ polyline [ id "Shape", points "7.11111111 0 2.22222222 4.88888889 0 2.66666667" ] [] ] ] ] ]
svg [ width "17px", height "17px", viewBox "0 0 17 17", version "1.1" ] [ g [ id "Page-1", stroke "none", strokeWidth "1", fill "none", fillRule "evenodd" ] [ g [ id "closed-avatar-2-copy-6", transform "translate(0.500000, 0.500000)", stroke "#8A98A5" ] [ circle [ id "Oval-2", cx "8", cy "8", r "8" ] [], g [ id "check", transform "translate(4.444444, 5.777778)", strokeLinecap "round", strokeLinejoin "round" ] [ polyline [ id "Shape", points "7.11111111 0 2.22222222 4.88888889 0 2.66666667" ] [] ] ] ] ]

On ->
iconView <|
svg [ width "16px", height "16px", viewBox "0 0 16 16", version "1.1" ] [ g [ id "Page-1", stroke "none", strokeWidth "1", fill "none", fillRule "evenodd" ] [ g [ id "closed-avatar-2-copy-4" ] [ circle [ id "Oval-2", fill "#38C172", cx "8", cy "8", r "8" ] [], g [ id "check", transform "translate(4.444444, 5.777778)", stroke "#FFFFFF", strokeLinecap "round", strokeLinejoin "round", strokeWidth "2" ] [ polyline [ id "Shape", points "7.11111111 0 2.22222222 4.88888889 0 2.66666667" ] [] ] ] ] ]
svg [ width "18px", height "18px", viewBox "0 0 18 18", version "1.1" ] [ g [ id "Page-1", stroke "none", strokeWidth "1", fill "none", fillRule "evenodd" ] [ g [ id "closed-avatar-2-copy-4", transform "translate(1.000000, 1.000000)" ] [ circle [ id "Oval-2", stroke "#38C172", fill "#38C172", cx "8", cy "8", r "8" ] [], g [ id "check", transform "translate(4.444444, 5.777778)", stroke "#FFFFFF", strokeLinecap "round", strokeLinejoin "round", strokeWidth "2" ] [ polyline [ id "Shape", points "7.11111111 0 2.22222222 4.88888889 0 2.66666667" ] [] ] ] ] ]


commentWhite : Html msg
Expand Down
2 changes: 1 addition & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ config :level, :honeybadger_js, api_key: System.get_env("HONEYBADGER_JS_API_KEY"

config :level, :stripe,
public_key: System.get_env("STRIPE_PUBLIC_KEY"),
preorder_sku: System.get_env("STRIPE_PREORDER_SKU")
private_key: System.get_env("STRIPE_PRIVATE_KEY")

# Configure the scheduler
config :level, Level.Scheduler,
Expand Down
3 changes: 3 additions & 0 deletions config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ config :level, Level.WebPush,
# Analytics
config :level, Level.Analytics, adapter: Level.Analytics.LogAdapter

# Billing
config :level, Level.Billing, adapter: Level.Billing.LiveAdapter, enabled: false

# Signup-related configuration
config :level, :signups,
enabled: true,
Expand Down
6 changes: 6 additions & 0 deletions config/prod.exs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ config :level, Level.WebPush,
# Analytics
config :level, Level.Analytics, adapter: Level.Analytics.LiveAdapter

# Billing
config :level, Level.Billing,
adapter: Level.Billing.LiveAdapter,
plan_id: System.get_env("LEVEL_BILLING_PLAN_ID"),
enabled: System.get_env("LEVEL_BILLING_ENABLED") == "1"

# Signup-related configuration
config :level, :signups,
enabled: System.get_env("LEVEL_SIGNUPS_ENABLED") == "1",
Expand Down
22 changes: 22 additions & 0 deletions config/secret_template.exs
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,25 @@ config :level, Level.Repo,
config :ex_aws,
access_key_id: "REPLACE ME",
secret_access_key: "REPLACE ME"

# Configuration for Stripe.
#
# In production, use the following environment variables:
#
# STRIPE_PUBLIC_KEY
# STRIPE_PRIVATE_KEY
#
config :level, :stripe,
public_key: "REPLACE ME WITH TEST PUBLIC KEY",
private_key: "REPLACE ME WITH TEST PRIVATE KEY"

# Configuration for Billing.
#
# In production, use the following environment variables:
#
# LEVEL_BILLING_ENABLED
# LEVEL_BILLING_PLAN_ID
#
config :level, Level.Billing,
enabled: false,
plan_id: "REPLACE ME"
3 changes: 3 additions & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ config :level, Level.WebPush,
# Analytics
config :level, Level.Analytics, adapter: Level.Analytics.LogAdapter

# Billing
config :level, Level.Billing, adapter: Level.Billing.TestAdapter, enabled: false, plan_id: ""

# Signup-related configuration
config :level, :signups,
enabled: true,
Expand Down
15 changes: 15 additions & 0 deletions lib/level/billing.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule Level.Billing do
@moduledoc """
The billing engine.
"""

@adapter Application.get_env(:level, __MODULE__)[:adapter]

def create_customer(email) do
@adapter.create_customer(email)
end

def create_subscription(customer_id, plan_id, quantity) do
@adapter.create_subscription(customer_id, plan_id, quantity)
end
end
8 changes: 8 additions & 0 deletions lib/level/billing/adapter.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defmodule Level.Billing.Adapter do
@moduledoc """
A behaviour for the Billing adapter.
"""

@callback create_customer(String.t()) :: {:ok, map()} | :error
@callback create_subscription(String.t(), String.t(), integer()) :: {:ok, map()} | :error
end
71 changes: 71 additions & 0 deletions lib/level/billing/live_adapter.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
defmodule Level.Billing.LiveAdapter do
@moduledoc false

require Logger

@behaviour Level.Billing.Adapter
@stripe_config Application.get_env(:level, :stripe)

@impl Level.Billing.Adapter
def create_customer(email) do
params = %{"email" => email}

stripe_client()
|> Stripe.create_customer(params)
|> after_create_customer(email)
end

defp after_create_customer({:ok, %Tesla.Env{status: 200, body: body}}, _email) do
{:ok, body}
end

defp after_create_customer({:ok, %Tesla.Env{status: status, body: body}}, email) do
message =
"[stripe] create customer failed " <>
"email=#{email} status=#{status} body=#{Jason.encode!(body)}"

Logger.error(message)
:error
end

defp after_create_customer(_, email) do
Logger.error("[stripe] create customer failed email=#{email}")
:error
end

@impl Level.Billing.Adapter
def create_subscription(customer_id, plan_id, quantity) do
params = %{
"customer" => customer_id,
"items[0][plan]" => plan_id,
"items[0][quantity]" => quantity,
"trial_from_plan" => true
}

stripe_client()
|> Stripe.create_subscription(params)
|> after_create_subscription(customer_id)
end

defp after_create_subscription({:ok, %Tesla.Env{status: 200, body: body}}, _customer_id) do
{:ok, body}
end

defp after_create_subscription({:ok, %Tesla.Env{status: status, body: body}}, customer_id) do
message =
"[stripe] create subscription failed " <>
"customer_id=#{customer_id} status=#{status} body=#{Jason.encode!(body)}"

Logger.error(message)
:error
end

defp after_create_subscription(_, customer_id) do
Logger.error("[stripe] create subscription failed customer_id=#{customer_id}")
:error
end

defp stripe_client do
Stripe.client(@stripe_config[:private_key])
end
end
3 changes: 3 additions & 0 deletions lib/level/mutations.ex
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ defmodule Level.Mutations do
{:ok, %{space: space}} ->
%{success: true, space: space, errors: []}

{:error, :org, changeset, _} ->
%{success: false, space: nil, errors: format_errors(changeset)}

{:error, :space, changeset, _} ->
%{success: false, space: nil, errors: format_errors(changeset)}

Expand Down
39 changes: 39 additions & 0 deletions lib/level/schemas/org.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
defmodule Level.Schemas.Org do
@moduledoc """
The Org schema.
"""

use Ecto.Schema
import Ecto.Changeset

alias Level.Schemas.Space

@type t :: %__MODULE__{}
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id

schema "orgs" do
field :subscription_state, :string, read_after_writes: true
field :name, :string
field :stripe_customer_id, :string
field :stripe_subscription_id, :string
field :seat_quantity, :integer

has_many :spaces, Space

timestamps()
end

@doc false
def create_changeset(%__MODULE__{} = org, attrs) do
org
|> cast(attrs, [
:subscription_state,
:name,
:stripe_customer_id,
:stripe_subscription_id,
:seat_quantity
])
|> validate_required([:name, :seat_quantity])
end
end
24 changes: 24 additions & 0 deletions lib/level/schemas/org_user.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
defmodule Level.Schemas.OrgUser do
@moduledoc """
The OrgUser schema.
"""

use Ecto.Schema

alias Level.Schemas.Org
alias Level.Schemas.User

@type t :: %__MODULE__{}
@primary_key {:id, :binary_id, autogenerate: true}
@foreign_key_type :binary_id

schema "org_users" do
field :state, :string, read_after_writes: true
field :role, :string, default: "OWNER"

belongs_to :org, Org
belongs_to :user, User

timestamps()
end
end
4 changes: 3 additions & 1 deletion lib/level/schemas/space.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule Level.Schemas.Space do
import Level.Gettext

alias Level.Schemas.Group
alias Level.Schemas.Org
alias Level.Schemas.SpaceUser

@type t :: %__MODULE__{}
Expand All @@ -22,6 +23,7 @@ defmodule Level.Schemas.Space do
field :avatar, :string
field :postbot_key, :string

belongs_to :org, Org
has_many :space_users, SpaceUser
has_many :groups, Group

Expand All @@ -31,7 +33,7 @@ defmodule Level.Schemas.Space do
@doc false
def create_changeset(struct, attrs \\ %{}) do
struct
|> cast(attrs, [:name, :slug, :avatar, :is_demo])
|> cast(attrs, [:org_id, :name, :slug, :avatar, :is_demo])
|> validate_required([:name, :slug])
|> validate_format(
:slug,
Expand Down
69 changes: 3 additions & 66 deletions lib/level/spaces.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,8 @@ defmodule Level.Spaces do
import Level.Gettext

alias Ecto.Changeset
alias Ecto.Multi
alias Level.AssetStore
alias Level.Events
alias Level.Groups
alias Level.Levelbot
alias Level.Postbot
alias Level.Repo
alias Level.Schemas.Bot
alias Level.Schemas.OpenInvitation
Expand All @@ -22,33 +18,8 @@ defmodule Level.Spaces do
alias Level.Schemas.SpaceUser
alias Level.Schemas.User
alias Level.Spaces.CreateDemo
alias Level.Spaces.CreateSpace
alias Level.Spaces.JoinSpace
alias Level.Users

@typedoc "The result of creating a space"
@type create_space_result ::
{:ok,
%{
space: Space.t(),
space_user: SpaceUser.t(),
open_invitation: OpenInvitation.t(),
levelbot: SpaceBot.t(),
postbot: SpaceBot.t(),
default_group: Group.t()
}}
| {:error,
:space | :space_user | :open_invitation | :levelbot | :postbot | :default_group,
any(),
%{
optional(
:space
| :space_user
| :open_invitation
| :levelbot
| :postbot
| :default_group
) => any()
}}

@typedoc "The result of getting a space"
@type get_space_result ::
Expand Down Expand Up @@ -133,45 +104,11 @@ defmodule Level.Spaces do
@doc """
Creates a new space.
"""
@spec create_space(User.t(), map(), list()) :: create_space_result()
@spec create_space(User.t(), map(), list()) :: CreateSpace.result()
def create_space(user, params, opts \\ []) do
Multi.new()
|> Multi.insert(:space, Space.create_changeset(%Space{}, params))
|> Multi.run(:levelbot, fn %{space: space} -> Levelbot.install_bot(space) end)
|> Multi.run(:postbot, fn %{space: space} -> Postbot.install_bot(space) end)
|> Multi.run(:open_invitation, fn %{space: space} -> create_open_invitation(space) end)
|> Repo.transaction()
|> after_create_space(user, opts)
end

defp create_everyone_group(space_user) do
case Groups.create_group(space_user, %{name: "everyone", is_default: true}) do
{:ok, %{group: group}} ->
{:ok, group}

err ->
err
end
CreateSpace.perform(user, params, opts)
end

defp after_create_space({:ok, %{space: space} = data}, user, opts) do
{:ok, owner} = create_owner(user, space, opts)
{:ok, default_group} = create_everyone_group(owner)
Events.space_joined(user.id, space, owner)

if !space.is_demo do
Users.track_analytics_event(user, "Created a team", %{
team_id: space.id,
team_name: space.name,
team_slug: space.slug
})
end

{:ok, Map.merge(data, %{space_user: owner, default_group: default_group})}
end

defp after_create_space(err, _, _), do: err

@doc """
Creates a demo space.
"""
Expand Down
Loading