File size: 5,084 Bytes
c27e67a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
defmodule Plausible.Workers.CheckUsage do
  use Plausible.Repo
  use Oban.Worker, queue: :check_usage
  require Plausible.Billing.Subscription.Status
  alias Plausible.Billing.{Subscription, Quota}
  alias Plausible.Teams

  defmacro yesterday() do
    quote do
      fragment("now() - INTERVAL '1 day'")
    end
  end

  defmacro last_day_of_month(day) do
    quote do
      fragment(
        "(date_trunc('month', ?::date) + interval '1 month' - interval '1 day')::date",
        unquote(day)
      )
    end
  end

  defmacro day_of_month(date) do
    quote do
      fragment("EXTRACT(day from ?::date)", unquote(date))
    end
  end

  defmacro least(left, right) do
    quote do
      fragment("least(?, ?)", unquote(left), unquote(right))
    end
  end

  @impl Oban.Worker
  def perform(_job, usage_mod \\ Teams.Billing, today \\ Date.utc_today()) do
    yesterday = today |> Date.shift(day: -1)

    active_subscribers =
      Repo.all(
        from(t in Teams.Team,
          as: :team,
          inner_join: o in assoc(t, :owners),
          left_join: bm in assoc(t, :billing_members),
          inner_lateral_join: s in subquery(Teams.last_subscription_join_query()),
          on: true,
          left_join: ep in Plausible.Billing.EnterprisePlan,
          on: ep.team_id == t.id and ep.paddle_plan_id == s.paddle_plan_id,
          where:
            s.status in [
              ^Subscription.Status.active(),
              ^Subscription.Status.past_due(),
              ^Subscription.Status.deleted()
            ],
          where: not is_nil(s.last_bill_date),
          # Accounts for situations like last_bill_date==2021-01-31 AND today==2021-03-01. Since February never reaches the 31st day, the account is checked on 2021-03-01.
          where: s.next_bill_date >= ^today,
          where:
            least(day_of_month(s.last_bill_date), day_of_month(last_day_of_month(^yesterday))) ==
              day_of_month(^yesterday),
          order_by: t.id,
          preload: [subscription: s, enterprise_plan: ep, owners: o, billing_members: bm]
        )
      )

    for subscriber <- active_subscribers do
      case {subscriber.grace_period, subscriber.enterprise_plan} do
        {nil, nil} ->
          check_regular_subscriber(subscriber, usage_mod)

        {nil, _} ->
          check_enterprise_subscriber(subscriber, usage_mod)

        {_, nil} ->
          maybe_remove_grace_period(subscriber, usage_mod)

        _ ->
          :skip
      end
    end

    :ok
  end

  defp check_site_usage_for_enterprise(subscriber) do
    limit = subscriber.enterprise_plan.site_limit

    usage = Teams.Billing.site_usage(subscriber)

    if Quota.below_limit?(usage, limit) do
      {:below_limit, {usage, limit}}
    else
      {:over_limit, {usage, limit}}
    end
  end

  def maybe_remove_grace_period(subscriber, usage_mod) do
    case check_pageview_usage_last_cycle(subscriber, usage_mod) do
      {:below_limit, _} ->
        Plausible.Teams.remove_grace_period(subscriber)
        :ok

      _ ->
        :skip
    end
  end

  defp check_regular_subscriber(subscriber, usage_mod) do
    case check_pageview_usage_two_cycles(subscriber, usage_mod) do
      {:over_limit, pageview_usage} ->
        suggested_volume =
          Plausible.Billing.Plans.suggest_volume(subscriber, pageview_usage.last_cycle.total)

        for owner <- subscriber.owners ++ subscriber.billing_members do
          PlausibleWeb.Email.over_limit_email(owner, subscriber, pageview_usage, suggested_volume)
          |> Plausible.Mailer.send()
        end

        Plausible.Teams.start_grace_period(subscriber)

      _ ->
        nil
    end
  end

  def check_enterprise_subscriber(subscriber, usage_mod) do
    pageview_usage = check_pageview_usage_two_cycles(subscriber, usage_mod)
    site_usage = check_site_usage_for_enterprise(subscriber)

    case {pageview_usage, site_usage} do
      {{:below_limit, _}, {:below_limit, _}} ->
        nil

      {{_, pageview_usage}, {_, {site_usage, site_allowance}}} ->
        for owner <- subscriber.owners ++ subscriber.billing_members do
          PlausibleWeb.Email.enterprise_over_limit_internal_email(
            owner,
            pageview_usage,
            site_usage,
            site_allowance
          )
          |> Plausible.Mailer.send()
        end

        Plausible.Teams.start_manual_lock_grace_period(subscriber)
    end
  end

  def check_pageview_usage_two_cycles(subscriber, usage_mod) do
    usage = usage_mod.monthly_pageview_usage(subscriber)
    limit = Teams.Billing.monthly_pageview_limit(subscriber.subscription)

    if Quota.exceeds_last_two_usage_cycles?(usage, limit) do
      {:over_limit, usage}
    else
      {:below_limit, usage}
    end
  end

  defp check_pageview_usage_last_cycle(subscriber, usage_mod) do
    usage = usage_mod.monthly_pageview_usage(subscriber)
    limit = Teams.Billing.monthly_pageview_limit(subscriber.subscription)

    if :last_cycle in Quota.exceeded_cycles(usage, limit) do
      {:over_limit, usage}
    else
      {:below_limit, usage}
    end
  end
end