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

New pricing #308

Merged
merged 22 commits into from
Jun 30, 2024
Merged
Show file tree
Hide file tree
Changes from 15 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
9 changes: 8 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ export FILE_STORAGE_ADAPTER=local
# export SMTP_PASSWORD=
# export SMTP_PORT=

# export SUPPORT_EMAIL=
# export SENDER_EMAIL=
# export CONTACT_EMAIL=
# export SUPPORT_EMAIL=
# export ENTERPRISE_SUPPORT_EMAIL=

# Key features

Expand Down Expand Up @@ -121,6 +123,11 @@ export AUTH_PASSWORD=true
# export STRIPE=true
# export STRIPE_API_KEY=sk_test_abcdef
# export STRIPE_WEBHOOK_SIGNING_SECRET=whsec_1234
# export STRIPE_PRICE_SOLO_MONTHLY=
# export STRIPE_PRICE_SOLO_YEARLY=
# export STRIPE_PRICE_TEAM_MONTHLY=
# export STRIPE_PRICE_TEAM_YEARLY=
# export STRIPE_PRODUCT_ENTERPRISE=
# export STRIPE_PRICE_PRO_MONTHLY=

## Clever Cloud Addon
Expand Down
11 changes: 9 additions & 2 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,10 @@ These are the basic variables you will **need** to set up Azimutt:
- `SMTP_USERNAME` (required)
- `SMTP_PASSWORD` (required)
- `SMTP_PORT` (required)
- `SUPPORT_EMAIL` (optional, default `[email protected]`): email shown in Azimutt when users need support
- `SENDER_EMAIL` (optional, default `[email protected]`): email Azimutt will us to send emails
- `CONTACT_EMAIL` (optional, default `[email protected]`): email shown in Azimutt to reach out
- `SUPPORT_EMAIL` (optional, default `[email protected]`): email shown in Azimutt when users need support
- `ENTERPRISE_SUPPORT_EMAIL` (optional, default `[email protected]`): email shown in Azimutt for high priority support


### Key features
Expand Down Expand Up @@ -207,7 +209,12 @@ At least one of authentication methods should be defined:
- `STRIPE` (optional): if `true`, allow to purchase plans with [Stripe](https://stripe.com), you probably don't need it ^^
- `STRIPE_API_KEY` (required): Stripe api key (ex: `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`)
- `STRIPE_WEBHOOK_SIGNING_SECRET` (required): Stripe webhook secret (ex: `whsec_ayZAyKqOLy34UKNeI3eq4icXVWJam0IW`)
- `STRIPE_PRICE_PRO_MONTHLY` (required): the Stripe price for the pro plan (ex: `price_uJINukB78aAbajUQHy6Ra523`)
- `STRIPE_PRICE_SOLO_MONTHLY` (required): Stripe price for the monthly solo plan (ex: `price_uJINukB78aAbajUQHy6Ra523`)
- `STRIPE_PRICE_SOLO_YEARLY` (required): Stripe price for the yearly solo plan (ex: `price_uJINukB78aAbajUQHy6Ra523`)
- `STRIPE_PRICE_TEAM_MONTHLY` (required): Stripe price for the monthly team plan (ex: `price_uJINukB78aAbajUQHy6Ra523`)
- `STRIPE_PRICE_TEAM_YEARLY` (required): Stripe price for the yearly team plan (ex: `price_uJINukB78aAbajUQHy6Ra523`)
- `STRIPE_PRODUCT_ENTERPRISE` (required): Stripe product for enterprise plan (ex: `prod_eBlQLUZPVprdAo`)
- `STRIPE_PRICE_PRO_MONTHLY` (required): Stripe price for the monthly legacy pro plan (ex: `price_uJINukB78aAbajUQHy6Ra523`)
- `CLEVER_CLOUD` (optional): if `true`, enable auth & hooks for [Clever Cloud Add-on](https://www.clever-cloud.com/doc/extend/add-ons-api)
- `CLEVER_CLOUD_ADDON_ID` (required)
- `CLEVER_CLOUD_PASSWORD` (required)
Expand Down
7 changes: 7 additions & 0 deletions backend/assets/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ module.exports = {

],
safelist: [
'w-1/4',
'w-1/5',
'grid-cols-4',
'grid-cols-5',
'lg:grid-cols-3',
'lg:grid-cols-4',
'lg:grid-cols-5',
{
pattern: /(bg|text|border|from|via|to)-(red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-.*/,
},
Expand Down
17 changes: 2 additions & 15 deletions backend/config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,10 @@ config :azimutt,
azimutt_github: "https://github.com/azimuttapp/azimutt",
azimutt_github_issues: "https://github.com/azimuttapp/azimutt/issues",
azimutt_github_issues_new: "https://github.com/azimuttapp/azimutt/issues/new",
pro_plan_seat_price: 13,
free_plan_seats: 2,
free_plan_projects: 2,
# MUST stay in sync with frontend/src/Conf.elm (`features`)
free_plan_layouts: 2,
free_plan_layout_tables: 10,
free_plan_memos: 3,
free_plan_groups: 1,
free_plan_colors: false,
free_plan_local_save: false,
free_plan_private_links: false,
free_plan_sql_export: false,
free_plan_db_analysis: false,
free_plan_db_access: false,
environment: config_env(),
# TODO: find an automated process to build it
version: "2.0.1",
version: "2.1.0",
version_date: "2024-07-01T00:00:00.000Z",
commit_hash: System.cmd("git", ["log", "-1", "--pretty=format:%h"]) |> elem(0) |> String.trim(),
commit_message: System.cmd("git", ["log", "-1", "--pretty=format:%s"]) |> elem(0) |> String.trim(),
commit_date: System.cmd("git", ["log", "-1", "--pretty=format:%aI"]) |> elem(0) |> String.trim(),
Expand Down
11 changes: 9 additions & 2 deletions backend/config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ config :azimutt,
organization_default_plan: System.get_env("ORGANIZATION_DEFAULT_PLAN"),
global_organization: global_organization,
global_organization_alone: global_organization && System.get_env("GLOBAL_ORGANIZATION_ALONE") == "true",
support_email: System.get_env("SUPPORT_EMAIL") || "[email protected]",
sender_email: System.get_env("SENDER_EMAIL") || "[email protected]",
sender_email: System.get_env("SENDER_EMAIL") || Azimutt.config(:azimutt_email),
contact_email: System.get_env("CONTACT_EMAIL") || Azimutt.config(:azimutt_email),
support_email: System.get_env("SUPPORT_EMAIL") || System.get_env("CONTACT_EMAIL") || Azimutt.config(:azimutt_email),
enterprise_support_email: System.get_env("ENTERPRISE_SUPPORT_EMAIL") || System.get_env("SUPPORT_EMAIL") || System.get_env("CONTACT_EMAIL") || Azimutt.config(:azimutt_email),
server_started: DateTime.utc_now()

config :azimutt, Azimutt.Repo,
Expand Down Expand Up @@ -270,6 +272,11 @@ if System.get_env("STRIPE") == "true" do

config :azimutt,
stripe: true,
stripe_price_solo_monthly: System.fetch_env!("STRIPE_PRICE_SOLO_MONTHLY"),
stripe_price_solo_yearly: System.fetch_env!("STRIPE_PRICE_SOLO_YEARLY"),
stripe_price_team_monthly: System.fetch_env!("STRIPE_PRICE_TEAM_MONTHLY"),
stripe_price_team_yearly: System.fetch_env!("STRIPE_PRICE_TEAM_YEARLY"),
stripe_product_enterprise: System.fetch_env!("STRIPE_PRODUCT_ENTERPRISE"),
stripe_price_pro_monthly: System.fetch_env!("STRIPE_PRICE_PRO_MONTHLY")

config :stripity_stripe,
Expand Down
146 changes: 100 additions & 46 deletions backend/lib/azimutt.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,61 +28,115 @@ defmodule Azimutt do
end

def plans do
# Next ones: Explore ($3), Expand ($13), Extend ($25)
[
%{
%{
free: %{
id: :free,
name: "Explorer",
description: "Design or Explore any kind of database, seamlessly.",
monthly: 0,
annually: 0,
name: "Free",
description: "Quickly explore your db with one command. No long term save.",
monthly: "Free",
yearly: "Free",
features: [
"Schema & Data exploration",
"Database design with AML",
"Unlimited Tables",
"2 projects with 2 layouts of 10 tables",
"2 collaborators"
],
cta: "Select this plan",
buy: "/login?plan=free",
selected: false
"Unlimited tables",
"Schema exploration",
"Data exploration"
]
},
%{
id: :pro,
name: "Pro",
description: "Remove limits, make Azimutt a central space for collaboration.",
monthly: 13,
annually: 130,
solo: %{
id: :solo,
name: "Solo",
description: "Personal usage with one project. Allows design and custom colors.",
monthly: 9,
yearly: 7,
unit: "€ / month",
features: [
"Everything included in Explorer",
"Unlimited projects",
"Unlimited layouts",
"Unlimited notes & memos",
"Full database analysis",
"Premium support"
],
cta: "Try this plan",
buy: "/login?plan=pro",
selected: true
"Free plan features",
"Long term usage",
"Database design",
"Schema export"
]
},
%{
team: %{
id: :team,
name: "Team",
description: "Collaborate on Azimutt with all database features.",
monthly: 42,
yearly: 35,
unit: "€ / user / month",
features: [
"Solo plan features",
"Database analysis",
"Collaboration",
"Documentation",
"AI capabilities",
"Export project"
]
},
enterprise: %{
id: :enterprise,
name: "Enterprise",
description: "Features you only dreamed of to ease database understanding and management.",
monthly: nil,
annually: nil,
description: "Getting serious: higher limits, security, control and automation.",
monthly: "Custom",
yearly: "Custom",
features: [
"Everything included in Pro",
"User roles",
"Schema change alerting",
"Advanced data access",
"AI query generation"
],
cta: "Contact us",
buy: "mailto:#{Azimutt.config(:support_email)}",
selected: false
"Team plan features",
"Unlimited usage",
"User management",
"Custom integrations"
]
},
pro: %{
id: :pro,
name: "Pro",
monthly: 13,
yearly: 13,
features: []
}
]
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tip

Codebase Verification

Incomplete Integration of New Plan Attributes and Features

The new plan attributes and features (:free, :solo, :team, :enterprise, :pro) are only referenced in backend/lib/azimutt.ex and backend/lib/azimutt_web/controllers/user_onboarding_controller.ex. Ensure these attributes and features are correctly integrated and utilized throughout the codebase.

  • backend/lib/azimutt.ex
  • backend/lib/azimutt_web/controllers/user_onboarding_controller.ex
Analysis chain

LGTM!

The new plan attributes and features align with the updated pricing plans. Ensure that these attributes and features are correctly integrated and used throughout the codebase.

Scripts executed

The following scripts were executed for the analysis:


Script:

#!/bin/bash
# Description: Verify the integration of new plan attributes and features.

# Test: Search for the usage of the new attributes and features. Expect: Correct integration and usage.
rg --type elixir 'id: :free|id: :solo|id: :team|id: :enterprise|id: :pro'

Length of output: 406



Script:

#!/bin/bash
# Description: Verify the integration of new plan attributes and features across the entire codebase.

# Test: Search for the usage of the new attributes and features. Expect: Correct integration and usage.
rg --type elixir 'name: "Free"|name: "Solo"|name: "Team"|name: "Enterprise"|name: "Pro"|description: "Quickly explore your db with one command. No long term save."|description: "Personal usage with one project. Allows design and custom colors."|description: "Collaborate on Azimutt with all database features."|description: "Getting serious: higher limits, security, control and automation."|monthly: 9|monthly: 42|monthly: 13|monthly: "Custom"|yearly: 7|yearly: 35|yearly: 13|yearly: "Custom"|features: \["Unlimited tables"|features: \["Free plan features"|features: \["Solo plan features"|features: \["Team plan features"'

Length of output: 1646



Script:

#!/bin/bash
# Description: Verify the usage of new plan keys across the entire codebase.

# Test: Search for the usage of the new plan keys. Expect: Correct integration and usage.
rg --type elixir ':free|:solo|:team|:enterprise|:pro'

Length of output: 10558

end

def active_plans, do: [plans().free, plans().solo, plans().team, plans().enterprise]

# MUST stay in sync with frontend/src/Conf.elm (`features`)
def limits do
%{
# Database features
schema_exploration: %{name: "Schema exploration", free: true, solo: true, team: true, enterprise: true},
data_exploration: %{name: "Data exploration", free: true, solo: true, team: true, enterprise: true},
colors: %{name: "Custom colors", free: false, solo: true, team: true, enterprise: true},
aml: %{name: "Database design (AML)", free: false, solo: true, team: true, enterprise: true},
# saved_queries: %{name: "Saved queries", free: false, solo: false, team: false, enterprise: true, description: "Soon... Save and share useful queries."},
# dashboard: %{name: "Dashboard", free: false, solo: false, team: false, enterprise: true, description: "Soon... Visually see query results."},
# db_stat_history: %{name: "Stats history", free: false, solo: false, team: false, enterprise: true, description: "Soon... Keep evolutions of database stats."},
schema_export: %{name: "Export schema", free: false, solo: true, team: true, enterprise: true, description: "Export your schema as SQL, AML or JSON."},
ai: %{name: "AI features", free: false, solo: false, team: true, enterprise: true},
analysis: %{
name: "Database analysis",
free: "preview",
solo: "preview",
team: "snapshot",
enterprise: "trends",
description: "See top 3 suggestions with preview, all suggestions with snapshot and compute evolution with trends."
},
project_export: %{name: "Export project", free: false, solo: false, team: true, enterprise: true},
# Product quotas
users: %{name: "Max users", free: 1, solo: 1, team: 5, enterprise: nil},
projects: %{name: "Max projects", free: 0, solo: 1, team: 5, enterprise: nil, description: "0 means you can create a project but can't save it."},
project_dbs: %{name: "Max db/project", free: 1, solo: 1, team: 3, enterprise: nil},
project_layouts: %{name: "Max layout/project", free: 1, solo: 3, team: 20, enterprise: nil},
layout_tables: %{name: "Max table/layout", free: 10, solo: 10, team: 40, enterprise: nil},
project_doc: %{name: "Max doc/project", free: 10, solo: 10, team: 1000, enterprise: nil},
# Extended integration
project_share: %{name: "Sharing project", free: false, solo: false, team: false, enterprise: true, description: "Use private links and embed to share with guest users."},
api: %{name: "API access", free: false, solo: false, team: false, enterprise: true, description: "Fetch and update sources and documentation programmatically."},
sso: %{name: "SSO", free: false, solo: false, team: false, enterprise: true, description: "Soon..."},
user_rights: %{name: "User rights", free: false, solo: false, team: false, enterprise: true, description: "Soon... Have read-only users in your organization."},
gateway_custom: %{name: "Custom gateway", free: false, solo: false, team: false, enterprise: true, description: "Soon... Securely connect to your databases."},
billing: %{name: "Flexible billing", free: false, solo: false, team: false, enterprise: true},
support_on_premise: %{name: "On-premise support", free: false, solo: false, team: false, enterprise: true},
support_enterprise: %{name: "Enterprise support", free: false, solo: false, team: false, enterprise: true, description: "Priority email, answer within 48h."},
consulting: %{name: "1h expert consulting", free: false, solo: false, team: false, enterprise: true},
roadmap: %{name: "Roadmap impact", free: "suggestions", solo: "suggestions", team: "suggestions", enterprise: "discussions"}
}
end

def use_cases do
Expand Down
2 changes: 1 addition & 1 deletion backend/lib/azimutt/admin.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ defmodule Azimutt.Admin do
def count_organizations, do: Organization |> Repo.aggregate(:count, :id)
def count_personal_organizations, do: Organization |> where([o], o.is_personal == true) |> Repo.aggregate(:count, :id)
def count_non_personal_organizations, do: Organization |> where([o], o.is_personal == false) |> Repo.aggregate(:count, :id)
def count_stripe_subscriptions, do: Organization |> where([o], not is_nil(o.stripe_subscription_id)) |> Repo.aggregate(:count, :id)
def count_paid_organizations, do: Organization |> where([o], not is_nil(o.plan) and o.plan != "free") |> Repo.aggregate(:count, :id)
def count_clever_cloud_resources, do: CleverCloud.Resource |> Repo.aggregate(:count, :id)
def count_heroku_resources, do: Heroku.Resource |> Repo.aggregate(:count, :id)
def count_projects, do: Project |> Repo.aggregate(:count, :id)
Expand Down
17 changes: 3 additions & 14 deletions backend/lib/azimutt/clever_cloud.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,6 @@ defmodule Azimutt.CleverCloud do
def app_addons_url, do: "#TODO"
def app_settings_url, do: "#TODO"

def allowed_members(plan) do
team_members = Regex.named_captures(~r/pro-(?<members>[0-9]+)/, plan)

if team_members do
String.to_integer(team_members["members"])
else
Azimutt.config(:free_plan_seats)
end
end

# use only for CleverCloudController.index local helper
def all_resources do
Resource
Expand Down Expand Up @@ -63,18 +53,17 @@ defmodule Azimutt.CleverCloud do
end
end

def add_member_if_needed(%Resource{} = resource, %Organization{} = organization, %User{} = current_user) do
slots_in_plan = allowed_members(resource.plan)
def add_member_if_needed(%Organization{} = organization, %User{} = current_user) do
existing_members = Organizations.count_member(organization)

cond do
existing_members > slots_in_plan ->
existing_members > organization.plan_seats ->
{:error, :too_many_members}

Organizations.has_member?(organization, current_user) ->
{:ok, :already_member}

existing_members < slots_in_plan ->
existing_members < organization.plan_seats ->
OrganizationMember.new_member_changeset(organization.id, current_user)
|> Repo.insert()
|> Result.map(fn _ -> :member_added end)
Expand Down
17 changes: 3 additions & 14 deletions backend/lib/azimutt/heroku.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,6 @@ defmodule Azimutt.Heroku do
def app_addons_url(app), do: "https://dashboard.heroku.com/apps/#{app}/resources"
def app_settings_url(app), do: "https://dashboard.heroku.com/apps/#{app}/settings"

def allowed_members(plan) do
team_members = Regex.named_captures(~r/pro-(?<members>[0-9]+)/, plan)

if team_members do
String.to_integer(team_members["members"])
else
Azimutt.config(:free_plan_seats)
end
end

# use only for HerokuController.index local helper
def all_resources do
Resource
Expand Down Expand Up @@ -74,18 +64,17 @@ defmodule Azimutt.Heroku do
end
end

def add_member_if_needed(%Resource{} = resource, %Organization{} = organization, %User{} = current_user) do
slots_in_plan = allowed_members(resource.plan)
def add_member_if_needed(%Organization{} = organization, %User{} = current_user) do
existing_members = Organizations.count_member(organization)

cond do
existing_members > slots_in_plan ->
existing_members > organization.plan_seats ->
{:error, :too_many_members}

Organizations.has_member?(organization, current_user) ->
{:ok, :already_member}

existing_members < slots_in_plan ->
existing_members < organization.plan_seats ->
OrganizationMember.new_member_changeset(organization.id, current_user)
|> Repo.insert()
|> Result.map(fn _ -> :member_added end)
Expand Down
Loading
Loading