Skip to content

Commit e2685a9

Browse files
Implement delinquency enforcement (#848)
1 parent b8e5642 commit e2685a9

File tree

9 files changed

+142
-8
lines changed

9 files changed

+142
-8
lines changed

apps/core/lib/core/schema/account.ex

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ defmodule Core.Schema.Account do
1010
field :icon, Core.Storage.Type
1111
field :billing_customer_id, :string
1212
field :delinquent_at, :utc_datetime_usec
13+
field :grandfathered_until, :utc_datetime_usec
1314
field :user_count, :integer, default: 0
1415
field :cluster_count, :integer, default: 0
1516
field :usage_updated, :boolean

apps/core/lib/core/services/payments.ex

+25-3
Original file line numberDiff line numberDiff line change
@@ -107,14 +107,36 @@ defmodule Core.Services.Payments do
107107

108108
def preload(%User{} = user), do: Core.Repo.preload(user, @preloads)
109109

110+
@doc """
111+
determine if an account (or a user's account) is currently delinquent
112+
"""
113+
@spec delinquent?(User.t | Account.t) :: boolean
114+
def delinquent?(%Account{delinquent_at: at}) when not is_nil(at) do
115+
Timex.shift(at, days: 14)
116+
|> Timex.before?(Timex.now())
117+
end
118+
def delinquent?(%User{account: account}), do: delinquent?(account)
119+
def delinquent?(_), do: false
120+
121+
@doc """
122+
determine if an account (or a user's account) should be grandfathered into old features
123+
"""
124+
@spec grandfathered?(User.t | Account.t) :: boolean
125+
def grandfathered?(%User{account: account}), do: grandfathered?(account)
126+
def grandfathered?(%Account{grandfathered_until: at}) when not is_nil(at), do: Timex.after?(at, Timex.now())
127+
def grandfathered?(_), do: false
128+
110129
@doc """
111130
Determine's if a user's account has access to the given feature. Returns `true` if enforcement is not enabled yet.
112131
"""
113132
@spec has_feature?(User.t, atom) :: boolean
114133
def has_feature?(%User{} = user, feature) do
115-
case {enforce?(), preload(user)} do
116-
{false, _} -> true
117-
{_, %User{account: %Account{subscription: %PlatformSubscription{plan: %PlatformPlan{features: %{^feature => true}}}}}} -> true
134+
user = preload(user)
135+
case {enforce?(), delinquent?(user), grandfathered?(user), user} do
136+
{false, _, _, _} -> true
137+
{_, true, _, _} -> false
138+
{_, _, true, _} -> true
139+
{_, _, _, %User{account: %Account{subscription: %PlatformSubscription{plan: %PlatformPlan{features: %{^feature => true}}}}}} -> true
118140
_ -> false
119141
end
120142
end

apps/core/lib/core/services/rollouts/rollable/versions.ex

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defimpl Core.Rollouts.Rollable, for: [Core.PubSub.VersionCreated, Core.PubSub.VersionUpdated] do
22
use Core.Rollable.Base
33
import Core.Rollable.Utils
4-
alias Core.Services.{Dependencies, Upgrades, Rollouts}
4+
alias Core.Services.{Dependencies, Upgrades, Rollouts, Payments}
55
alias Core.Schema.{ChartInstallation, TerraformInstallation}
66

77
def name(%Core.PubSub.VersionCreated{}), do: "version:created"
@@ -13,24 +13,24 @@ defimpl Core.Rollouts.Rollable, for: [Core.PubSub.VersionCreated, Core.PubSub.Ve
1313
ChartInstallation.for_chart(chart_id)
1414
|> ChartInstallation.with_auto_upgrade(version.tags)
1515
|> maybe_ignore_version(@for, ChartInstallation, version.id)
16-
|> ChartInstallation.preload(installation: [:repository, :user])
16+
|> ChartInstallation.preload(installation: [:repository, user: :account])
1717
|> ChartInstallation.ordered()
1818
end
1919

2020
def query(%{item: %{terraform_id: tf_id} = version}) when is_binary(tf_id) do
2121
TerraformInstallation.for_terraform(tf_id)
2222
|> TerraformInstallation.with_auto_upgrade(version.tags)
2323
|> maybe_ignore_version(@for, TerraformInstallation, version.id)
24-
|> TerraformInstallation.preload(installation: [:repository, :user])
24+
|> TerraformInstallation.preload(installation: [:repository, user: :account])
2525
|> TerraformInstallation.ordered()
2626
end
2727

2828
# defp maybe_ignore_version(q, Core.PubSub.VersionUpdated, _, _), do: q
2929
defp maybe_ignore_version(q, _, mod, id), do: mod.ignore_version(q, id)
3030

3131
def process(%{item: version}, %{installation: %{user: user}} = inst) do
32-
case Dependencies.valid?(version.dependencies, user) do
33-
true -> directly_install(version, inst)
32+
case {Dependencies.valid?(version.dependencies, user), Payments.delinquent?(user)} do
33+
{true, false} -> directly_install(version, inst)
3434
_ -> Upgrades.create_deferred_update(version.id, inst, user)
3535
end
3636
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
defmodule Core.Repo.Migrations.AddGrandfatheredUntil do
2+
use Ecto.Migration
3+
4+
def change do
5+
alter table(:accounts) do
6+
add :grandfathered_until, :utc_datetime_usec
7+
end
8+
end
9+
end

apps/core/test/services/payments_test.exs

+46
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,52 @@ defmodule Core.Services.PaymentsTest do
538538
end
539539
end
540540

541+
describe "#has_feature?/2" do
542+
test "if a user's plan has a feature, then it returns true" do
543+
account = insert(:account)
544+
insert(:platform_subscription, account: account, plan: build(:platform_plan, features: %{user_management: true}))
545+
user = insert(:user, account: account)
546+
assert Payments.has_feature?(user, :user_management)
547+
end
548+
549+
test "if a user's account is grandfathered, then it returns true" do
550+
account = insert(:account, grandfathered_until: Timex.now() |> Timex.shift(days: 1))
551+
insert(:platform_subscription, account: account, plan: build(:platform_plan, features: %{user_management: false}))
552+
user = insert(:user, account: account)
553+
assert Payments.has_feature?(user, :user_management)
554+
555+
account = insert(:account, grandfathered_until: Timex.now() |> Timex.shift(days: -1))
556+
insert(:platform_subscription, account: account, plan: build(:platform_plan, features: %{user_management: false}))
557+
user = insert(:user, account: account)
558+
refute Payments.has_feature?(user, :user_management)
559+
end
560+
561+
test "if a user's account is delinquent then it returns false" do
562+
account = insert(:account, delinquent_at: Timex.now() |> Timex.shift(days: -100))
563+
insert(:platform_subscription, account: account, plan: build(:platform_plan, features: %{user_management: true}))
564+
user = insert(:user, account: account)
565+
refute Payments.has_feature?(user, :user_management)
566+
567+
account = insert(:account, delinquent_at: Timex.now())
568+
insert(:platform_subscription, account: account, plan: build(:platform_plan, features: %{user_management: true}))
569+
user = insert(:user, account: account)
570+
assert Payments.has_feature?(user, :user_management)
571+
end
572+
573+
test "if a user's account has no plan it returns false" do
574+
account = insert(:account)
575+
user = insert(:user, account: account)
576+
refute Payments.has_feature?(user, :user_management)
577+
end
578+
579+
test "if a user's account is doesn't have the feature, then it returns false" do
580+
account = insert(:account)
581+
insert(:platform_subscription, account: account, plan: build(:platform_plan, features: %{user_management: false}))
582+
user = insert(:user, account: account)
583+
refute Payments.has_feature?(user, :user_management)
584+
end
585+
end
586+
541587
describe "#update_plan/3" do
542588
test "Users can change plans" do
543589
expect(Stripe.Subscription, :update, fn

apps/core/test/services/rollable/versions_test.exs

+26
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,32 @@ defmodule Core.Rollable.VersionsTest do
8383
end
8484
end
8585

86+
test "it will defer updates if a user is delinquent" do
87+
user = insert(:user, account: build(:account, delinquent_at: Timex.now() |> Timex.shift(days: -100)))
88+
%{chart: chart} = chart_version = insert(:version, version: "0.1.0")
89+
inst = insert(:chart_installation,
90+
installation: insert(:installation, auto_upgrade: true, user: user),
91+
chart: chart,
92+
version: chart_version
93+
)
94+
95+
version = insert(:version, version: "0.1.1", chart: chart)
96+
insert(:version_tag, version: version, chart: chart, tag: "latest")
97+
98+
event = %PubSub.VersionCreated{item: version}
99+
{:ok, rollout} = Rollouts.create_rollout(chart.repository_id, event)
100+
101+
{:ok, rolled} = Rollouts.execute(rollout)
102+
103+
assert rolled.status == :finished
104+
assert rolled.count == 1
105+
106+
[deferred] = Core.Repo.all(Core.Schema.DeferredUpdate)
107+
108+
assert deferred.chart_installation_id == inst.id
109+
assert deferred.version_id == version.id
110+
end
111+
86112
test "it will defer updates if a version's dependencies aren't satisfied" do
87113
dep_chart = insert(:chart)
88114
%{chart: chart} = chart_version = insert(:version, version: "0.1.0")

apps/core/test/support/test_helpers.ex

+5
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,10 @@ defmodule Core.TestHelpers do
2727

2828
def refetch(%{__struct__: schema, id: id}), do: Core.Repo.get(schema, id)
2929

30+
def update_record(struct, attrs) do
31+
Ecto.Changeset.change(struct, attrs)
32+
|> Core.Repo.update()
33+
end
34+
3035
def priv_file(app, path), do: Path.join(:code.priv_dir(app), path)
3136
end

apps/graphql/lib/graphql/schema/account.ex

+1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ defmodule GraphQl.Schema.Account do
8282
field :billing_customer_id, :string
8383
field :workos_connection_id, :string
8484
field :delinquent_at, :datetime
85+
field :grandfathered_unitl, :datetime
8586

8687
field :icon, :string, resolve: fn
8788
account, _, _ -> {:ok, Core.Storage.url({account.icon, account}, :original)}

apps/graphql/test/mutations/account_mutations_test.exs

+24
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,30 @@ defmodule GraphQl.AccountMutationTest do
3232
assert hd(svc["impersonationPolicy"]["bindings"])["group"]["id"] == group.id
3333
end
3434

35+
test "delinquent accounts cannot create service accounts", %{user: user, account: account} do
36+
{:ok, account} = update_record(account, %{delinquent_at: Timex.now() |> Timex.shift(days: -100)})
37+
insert(:platform_subscription, account: account, plan: build(:platform_plan, features: %{user_management: true}))
38+
group = insert(:group, account: user.account)
39+
{:ok, %{errors: [_ | _]}} = run_query("""
40+
mutation createSvcAccount($attributes: ServiceAccountAttributes!) {
41+
createServiceAccount(attributes: $attributes) {
42+
id
43+
serviceAccount
44+
impersonationPolicy {
45+
bindings { group { id } }
46+
}
47+
}
48+
}
49+
""",
50+
%{
51+
"attributes" => %{
52+
"name" => "svc",
53+
"impersonationPolicy" => %{"bindings" => [%{"groupId" => group.id}]}
54+
}
55+
},
56+
%{current_user: Core.Services.Rbac.preload(refetch(user))})
57+
end
58+
3559
test "it will fail to create service accounts if feature doesn't exist", %{user: user} do
3660
group = insert(:group, account: user.account)
3761
{:ok, %{errors: [_ | _]}} = run_query("""

0 commit comments

Comments
 (0)