diff --git a/lib/plausible/billing/billing.ex b/lib/plausible/billing/billing.ex index 245d78f53f7d..031c2d3543fc 100644 --- a/lib/plausible/billing/billing.ex +++ b/lib/plausible/billing/billing.ex @@ -89,7 +89,7 @@ defmodule Plausible.Billing do subscription = Subscription |> Repo.get_by(paddle_subscription_id: params["subscription_id"]) - |> Repo.preload(team: :owner) + |> Repo.preload(team: :owners) if subscription do changeset = @@ -99,9 +99,11 @@ defmodule Plausible.Billing do updated = Repo.update!(changeset) - subscription.team.owner - |> PlausibleWeb.Email.cancellation_email() - |> Plausible.Mailer.send() + for owner <- subscription.team.owners do + owner + |> PlausibleWeb.Email.cancellation_email() + |> Plausible.Mailer.send() + end updated end @@ -212,7 +214,7 @@ defmodule Plausible.Billing do Teams.Team |> Repo.get!(subscription.team_id) |> Teams.with_subscription() - |> Repo.preload(:owner) + |> Repo.preload(:owners) if subscription.id != team.subscription.id do Sentry.capture_message("Susbscription ID mismatch", @@ -236,7 +238,8 @@ defmodule Plausible.Billing do ) if plan do - api_keys = from(key in Plausible.Auth.ApiKey, where: key.user_id == ^team.owner.id) + owner_ids = Enum.map(team.owners, & &1.id) + api_keys = from(key in Plausible.Auth.ApiKey, where: key.user_id in ^owner_ids) Repo.update_all(api_keys, set: [hourly_request_limit: plan.hourly_api_request_limit]) end diff --git a/lib/plausible/billing/enterprise_plan_admin.ex b/lib/plausible/billing/enterprise_plan_admin.ex index e0b56ef5a802..582ddf4731f1 100644 --- a/lib/plausible/billing/enterprise_plan_admin.ex +++ b/lib/plausible/billing/enterprise_plan_admin.ex @@ -40,17 +40,17 @@ defmodule Plausible.Billing.EnterprisePlanAdmin do from(r in query, inner_join: t in assoc(r, :team), - inner_join: o in assoc(t, :owner), + inner_join: o in assoc(t, :owners), or_where: ilike(r.paddle_plan_id, ^search_term), or_where: ilike(o.email, ^search_term) or ilike(o.name, ^search_term), - preload: [team: {t, owner: o}] + preload: [team: {t, owners: o}] ) end def custom_show_query(_conn, _schema, query) do from(ep in query, inner_join: t in assoc(ep, :team), - inner_join: o in assoc(t, :owner), + inner_join: o in assoc(t, :owners), select: %{ep | user_id: o.id} ) end @@ -68,7 +68,7 @@ defmodule Plausible.Billing.EnterprisePlanAdmin do ] end - defp get_user_email(plan), do: plan.team.owner.email + defp get_user_email(plan), do: List.first(plan.team.owners).email def create_changeset(schema, attrs) do attrs = sanitize_attrs(attrs) diff --git a/lib/plausible/billing/site_locker.ex b/lib/plausible/billing/site_locker.ex index 666bb0e04849..df871331e2e4 100644 --- a/lib/plausible/billing/site_locker.ex +++ b/lib/plausible/billing/site_locker.ex @@ -26,7 +26,7 @@ defmodule Plausible.Billing.SiteLocker do Plausible.Teams.end_grace_period(team) if send_email? do - team = Repo.preload(team, :owner) + team = Repo.preload(team, :owners) send_grace_period_end_email(team) end @@ -64,8 +64,10 @@ defmodule Plausible.Billing.SiteLocker do usage = Teams.Billing.monthly_pageview_usage(team) suggested_plan = Plausible.Billing.Plans.suggest(team, usage.last_cycle.total) - team.owner - |> PlausibleWeb.Email.dashboard_locked(usage, suggested_plan) - |> Plausible.Mailer.send() + for owner <- team.owners do + owner + |> PlausibleWeb.Email.dashboard_locked(usage, suggested_plan) + |> Plausible.Mailer.send() + end end end diff --git a/lib/plausible/site.ex b/lib/plausible/site.ex index 11d89f4401fa..340031602471 100644 --- a/lib/plausible/site.ex +++ b/lib/plausible/site.ex @@ -47,8 +47,8 @@ defmodule Plausible.Site do has_one :google_auth, GoogleAuth has_one :weekly_report, Plausible.Site.WeeklyReport has_one :monthly_report, Plausible.Site.MonthlyReport - has_one :ownership, through: [:team, :ownership] - has_one :owner, through: [:team, :owner] + has_many :ownerships, through: [:team, :ownerships] + has_many :owners, through: [:team, :owners] # If `from_cache?` is set, the struct might be incomplete - see `Plausible.Site.Cache`. # Use `Plausible.Repo.reload!(cached_site)` to pre-fill missing fields if diff --git a/lib/plausible/site/admin.ex b/lib/plausible/site/admin.ex index 2c89b615930b..67a798968588 100644 --- a/lib/plausible/site/admin.ex +++ b/lib/plausible/site/admin.ex @@ -33,9 +33,9 @@ defmodule Plausible.SiteAdmin do from(r in query, as: :site, - inner_join: o in assoc(r, :owner), + inner_join: o in assoc(r, :owners), inner_join: t in assoc(r, :team), - preload: [owner: o, team: t, guest_memberships: [team_membership: :user]], + preload: [owners: o, team: t, guest_memberships: [team_membership: :user]], or_where: ilike(r.domain, ^search_term), or_where: ilike(o.email, ^search_term), or_where: ilike(o.name, ^search_term), @@ -78,7 +78,7 @@ defmodule Plausible.SiteAdmin do inserted_at: %{name: "Created at", value: &format_date(&1.inserted_at)}, timezone: nil, public: nil, - owner: %{value: &get_owner/1}, + owners: %{value: &get_owners/1}, other_members: %{value: &get_other_members/1}, limits: %{ value: fn site -> @@ -186,20 +186,22 @@ defmodule Plausible.SiteAdmin do Calendar.strftime(date, "%b %-d, %Y") end - defp get_owner(site) do - owner = site.owner + defp get_owners(site) do + owners = Repo.preload(site, :owners).owners - if owner do - escaped_name = Phoenix.HTML.html_escape(owner.name) |> Phoenix.HTML.safe_to_string() - escaped_email = Phoenix.HTML.html_escape(owner.email) |> Phoenix.HTML.safe_to_string() + owners_html = + Enum.map(owners, fn owner -> + escaped_name = Phoenix.HTML.html_escape(owner.name) |> Phoenix.HTML.safe_to_string() + escaped_email = Phoenix.HTML.html_escape(owner.email) |> Phoenix.HTML.safe_to_string() - {:safe, - """ - #{escaped_name} -

- #{escaped_email} - """} - end + """ + #{escaped_name} +

+ #{escaped_email} + """ + end) + + {:safe, Enum.join(owners_html, "

")} end defp get_other_members(site) do diff --git a/lib/plausible/site/memberships/accept_invitation.ex b/lib/plausible/site/memberships/accept_invitation.ex index 2a569f9f7123..6d64aaf9064a 100644 --- a/lib/plausible/site/memberships/accept_invitation.ex +++ b/lib/plausible/site/memberships/accept_invitation.ex @@ -80,9 +80,9 @@ defmodule Plausible.Site.Memberships.AcceptInvitation do :ok <- check_can_transfer_site(new_team, new_owner), :ok <- Teams.Invitations.ensure_can_take_ownership(site, new_team), :ok <- Teams.Invitations.transfer_site(site, new_team) do - site = site |> Repo.reload!() |> Repo.preload(ownership: :user) + site = site |> Repo.reload!() |> Repo.preload(ownerships: :user) - {:ok, site.ownership} + {:ok, site.ownerships} end end @@ -96,9 +96,9 @@ defmodule Plausible.Site.Memberships.AcceptInvitation do :ok <- Teams.Invitations.accept_site_transfer(site_transfer, new_team) do Teams.Invitations.send_transfer_accepted_email(site_transfer) - site = site |> Repo.reload!() |> Repo.preload(ownership: :user) + site = site |> Repo.reload!() |> Repo.preload(ownerships: :user) - {:ok, %{team: new_team, team_membership: site.ownership, site: site}} + {:ok, %{team: new_team, team_memberships: site.ownerships, site: site}} end end diff --git a/lib/plausible/site/memberships/create_invitation.ex b/lib/plausible/site/memberships/create_invitation.ex index 418085ba69f8..4d31af79de6b 100644 --- a/lib/plausible/site/memberships/create_invitation.ex +++ b/lib/plausible/site/memberships/create_invitation.ex @@ -50,7 +50,7 @@ defmodule Plausible.Site.Memberships.CreateInvitation do end defp do_invite(site, inviter, invitee_email, role, opts \\ []) do - with site <- Repo.preload(site, [:owner, :team]), + with site <- Repo.preload(site, [:owners, :team]), :ok <- Teams.Invitations.check_invitation_permissions( site, diff --git a/lib/plausible/teams.ex b/lib/plausible/teams.ex index 27368b42cddd..0b7a8b50097b 100644 --- a/lib/plausible/teams.ex +++ b/lib/plausible/teams.ex @@ -36,15 +36,6 @@ defmodule Plausible.Teams do Repo.get_by!(Teams.Team, identifier: team_identifier) end - @spec get_owner(Teams.Team.t()) :: - {:ok, Auth.User.t()} | {:error, :no_owner | :multiple_owners} - def get_owner(team) do - case Repo.preload(team, :owner).owner do - nil -> {:error, :no_owner} - owner_user -> {:ok, owner_user} - end - end - @spec on_trial?(Teams.Team.t() | nil) :: boolean() on_ee do def on_trial?(nil), do: false @@ -264,7 +255,7 @@ defmodule Plausible.Teams do end def setup_team(team, candidates) do - inviter = Repo.preload(team, :owner).owner + [inviter | _] = Repo.preload(team, :owners).owners setup_team_fn = fn {{email, _name}, role} -> case Teams.Invitations.InviteToTeam.invite(team, inviter, email, role, send_email?: false) do diff --git a/lib/plausible/teams/billing.ex b/lib/plausible/teams/billing.ex index 44cdb9227424..ba356601d498 100644 --- a/lib/plausible/teams/billing.ex +++ b/lib/plausible/teams/billing.ex @@ -382,7 +382,7 @@ defmodule Plausible.Teams.Billing do def team_member_usage(nil, _), do: 0 def team_member_usage(team, opts) do - {:ok, owner} = Teams.get_owner(team) + [owner | _] = Repo.preload(team, :owners).owners exclude_emails = Keyword.get(opts, :exclude_emails, []) ++ [owner.email] pending_site_ids = Keyword.get(opts, :pending_ownership_site_ids, []) diff --git a/lib/plausible/teams/invitations.ex b/lib/plausible/teams/invitations.ex index 10c8b4a3c343..96de6c822362 100644 --- a/lib/plausible/teams/invitations.ex +++ b/lib/plausible/teams/invitations.ex @@ -324,7 +324,7 @@ defmodule Plausible.Teams.Invitations do site = Repo.preload(site, [ :team, - :owner, + :owners, guest_memberships: [team_membership: :user], guest_invitations: [team_invitation: :inviter] ]) @@ -381,19 +381,21 @@ defmodule Plausible.Teams.Invitations do Repo.delete_all(from gm in Teams.GuestMembership, where: gm.id in ^old_guest_ids) :ok = Teams.Memberships.prune_guests(prior_team) - {:ok, prior_owner} = Teams.get_owner(prior_team) + prior_owners = Repo.preload(prior_team, :owners).owners - {:ok, prior_owner_team_membership} = create_team_membership(team, :guest, prior_owner, now) + for prior_owner <- prior_owners do + {:ok, prior_owner_team_membership} = create_team_membership(team, :guest, prior_owner, now) - if prior_owner_team_membership.role == :guest do - {:ok, _} = - prior_owner_team_membership - |> Teams.GuestMembership.changeset(site, :editor) - |> Repo.insert( - on_conflict: [set: [updated_at: now, role: :editor]], - conflict_target: [:team_membership_id, :site_id], - returning: true - ) + if prior_owner_team_membership.role == :guest do + {:ok, _} = + prior_owner_team_membership + |> Teams.GuestMembership.changeset(site, :editor) + |> Repo.insert( + on_conflict: [set: [updated_at: now, role: :editor]], + conflict_target: [:team_membership_id, :site_id], + returning: true + ) + end end on_ee do diff --git a/lib/plausible/teams/invitations/invite_to_team.ex b/lib/plausible/teams/invitations/invite_to_team.ex index f8ce8eddb173..ea2bfe277a2e 100644 --- a/lib/plausible/teams/invitations/invite_to_team.ex +++ b/lib/plausible/teams/invitations/invite_to_team.ex @@ -12,7 +12,7 @@ defmodule Plausible.Teams.Invitations.InviteToTeam do def invite(team, inviter, invitee_email, role, opts \\ []) def invite(team, inviter, invitee_email, role, opts) when role in @valid_roles do - with team <- Repo.preload(team, [:owner]), + with team <- Repo.preload(team, [:owners]), :ok <- Teams.Invitations.check_invitation_permissions( team, diff --git a/lib/plausible/teams/team.ex b/lib/plausible/teams/team.ex index 6871152a3199..ece39d1e440f 100644 --- a/lib/plausible/teams/team.ex +++ b/lib/plausible/teams/team.ex @@ -37,8 +37,6 @@ defmodule Plausible.Teams.Team do has_one :subscription, Plausible.Billing.Subscription has_one :enterprise_plan, Plausible.Billing.EnterprisePlan - has_one :ownership, Plausible.Teams.Membership, where: [role: :owner] - has_one :owner, through: [:ownership, :user] has_many :ownerships, Plausible.Teams.Membership, where: [role: :owner] has_many :owners, through: [:ownerships, :user] diff --git a/lib/plausible_web/controllers/admin_controller.ex b/lib/plausible_web/controllers/admin_controller.ex index 69c6ef5f1cbd..e7424d543db3 100644 --- a/lib/plausible_web/controllers/admin_controller.ex +++ b/lib/plausible_web/controllers/admin_controller.ex @@ -15,7 +15,7 @@ defmodule PlausibleWeb.AdminController do {:ok, team} -> team |> Teams.with_subscription() - |> Plausible.Repo.preload(:owner) + |> Plausible.Repo.preload(:owners) {:error, :no_team} -> nil @@ -177,7 +177,7 @@ defmodule PlausibleWeb.AdminController do sites_link = Routes.kaffy_resource_url(PlausibleWeb.Endpoint, :index, :sites, :site, - custom_search: team.owner.email + custom_search: List.first(team.owners).email ) """ diff --git a/lib/plausible_web/controllers/api/external_query_api_controller.ex b/lib/plausible_web/controllers/api/external_query_api_controller.ex index f64414ee7ef0..7fc17e0e64ee 100644 --- a/lib/plausible_web/controllers/api/external_query_api_controller.ex +++ b/lib/plausible_web/controllers/api/external_query_api_controller.ex @@ -7,7 +7,7 @@ defmodule PlausibleWeb.Api.ExternalQueryApiController do alias Plausible.Stats.Query def query(conn, params) do - site = Repo.preload(conn.assigns.site, :owner) + site = Repo.preload(conn.assigns.site, :owners) case Query.build(site, conn.assigns.schema_type, params, debug_metadata(conn)) do {:ok, query} -> diff --git a/lib/plausible_web/controllers/api/external_stats_controller.ex b/lib/plausible_web/controllers/api/external_stats_controller.ex index a585a71bd215..82b5c976d982 100644 --- a/lib/plausible_web/controllers/api/external_stats_controller.ex +++ b/lib/plausible_web/controllers/api/external_stats_controller.ex @@ -10,7 +10,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController do end def aggregate(conn, params) do - site = Repo.preload(conn.assigns.site, :owner) + site = Repo.preload(conn.assigns.site, :owners) params = Map.put(params, "property", nil) @@ -38,7 +38,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController do end def breakdown(conn, params) do - site = Repo.preload(conn.assigns.site, :owner) + site = Repo.preload(conn.assigns.site, :owners) with :ok <- validate_period(params), :ok <- validate_date(params), @@ -246,7 +246,7 @@ defmodule PlausibleWeb.Api.ExternalStatsController do defp event_only_property?(_), do: false def timeseries(conn, params) do - site = Repo.preload(conn.assigns.site, :owner) + site = Repo.preload(conn.assigns.site, :owners) params = Map.put(params, "property", nil) diff --git a/lib/plausible_web/controllers/site/membership_controller.ex b/lib/plausible_web/controllers/site/membership_controller.ex index 9476373128dd..af55049aeae4 100644 --- a/lib/plausible_web/controllers/site/membership_controller.ex +++ b/lib/plausible_web/controllers/site/membership_controller.ex @@ -27,7 +27,7 @@ defmodule PlausibleWeb.Site.MembershipController do site = conn.assigns.current_user |> Plausible.Sites.get_for_user!(conn.assigns.site.domain) - |> Plausible.Repo.preload(:owner) + |> Plausible.Repo.preload(:owners) limit = Plausible.Teams.Billing.team_member_limit(site.team) usage = Plausible.Teams.Billing.team_member_usage(site.team) @@ -48,7 +48,7 @@ defmodule PlausibleWeb.Site.MembershipController do site = Plausible.Sites.get_for_user!(conn.assigns.current_user, site_domain) - |> Plausible.Repo.preload(:owner) + |> Plausible.Repo.preload(:owners) case Memberships.create_invitation(site, conn.assigns.current_user, email, role) do {:ok, invitation} -> diff --git a/lib/plausible_web/controllers/stats_controller.ex b/lib/plausible_web/controllers/stats_controller.ex index 8d5dc4b8cbb3..e7a5c8fa670e 100644 --- a/lib/plausible_web/controllers/stats_controller.ex +++ b/lib/plausible_web/controllers/stats_controller.ex @@ -51,7 +51,7 @@ defmodule PlausibleWeb.StatsController do plug(PlausibleWeb.Plugs.AuthorizeSiteAccess when action in [:stats, :csv_export]) def stats(%{assigns: %{site: site}} = conn, _params) do - site = Plausible.Repo.preload(site, :owner) + site = Plausible.Repo.preload(site, :owners) current_user = conn.assigns[:current_user] stats_start_date = Plausible.Sites.stats_start_date(site) can_see_stats? = not Sites.locked?(site) or conn.assigns[:site_role] == :super_admin @@ -87,7 +87,7 @@ defmodule PlausibleWeb.StatsController do redirect(conn, external: Routes.site_path(conn, :verification, site.domain)) Sites.locked?(site) -> - site = Plausible.Repo.preload(site, :owner) + site = Plausible.Repo.preload(site, :owners) render(conn, "site_locked.html", site: site, dogfood_page_path: dogfood_page_path) end end @@ -112,7 +112,7 @@ defmodule PlausibleWeb.StatsController do """ def csv_export(conn, params) do if is_nil(params["interval"]) or Plausible.Stats.Interval.valid?(params["interval"]) do - site = Plausible.Repo.preload(conn.assigns.site, :owner) + site = Plausible.Repo.preload(conn.assigns.site, :owners) query = Query.from(site, params, debug_metadata(conn)) date_range = Query.date_range(query) @@ -347,7 +347,7 @@ defmodule PlausibleWeb.StatsController do cond do !shared_link.site.locked -> current_user = conn.assigns[:current_user] - shared_link = Plausible.Repo.preload(shared_link, site: :owner) + shared_link = Plausible.Repo.preload(shared_link, site: :owners) stats_start_date = Plausible.Sites.stats_start_date(shared_link.site) scroll_depth_visible? = @@ -378,10 +378,10 @@ defmodule PlausibleWeb.StatsController do ) Sites.locked?(shared_link.site) -> - owner = Plausible.Repo.preload(shared_link.site, :owner) + owners = Plausible.Repo.preload(shared_link.site, :owners) render(conn, "site_locked.html", - owner: owner, + owners: owners, site: shared_link.site, dogfood_page_path: "/share/:dashboard" ) diff --git a/lib/plausible_web/email.ex b/lib/plausible_web/email.ex index 5623c3c3b191..326b36749a8f 100644 --- a/lib/plausible_web/email.ex +++ b/lib/plausible_web/email.ex @@ -190,22 +190,22 @@ defmodule PlausibleWeb.Email do }) end - def yearly_renewal_notification(team) do + def yearly_renewal_notification(team, owner) do date = Calendar.strftime(team.subscription.next_bill_date, "%B %-d, %Y") priority_email() - |> to(team.owner) + |> to(owner) |> tag("yearly-renewal") |> subject("Your Plausible subscription is up for renewal") |> render("yearly_renewal_notification.html", %{ - user: team.owner, + user: owner, date: date, next_bill_amount: team.subscription.next_bill_amount, currency: team.subscription.currency_code }) end - def yearly_expiration_notification(team) do + def yearly_expiration_notification(team, owner) do next_bill_date = Calendar.strftime(team.subscription.next_bill_date, "%B %-d, %Y") accept_traffic_until = @@ -214,11 +214,11 @@ defmodule PlausibleWeb.Email do |> Calendar.strftime("%B %-d, %Y") priority_email() - |> to(team.owner) + |> to(owner) |> tag("yearly-expiration") |> subject("Your Plausible subscription is about to expire") |> render("yearly_expiration_notification.html", %{ - user: team.owner, + user: owner, next_bill_date: next_bill_date, accept_traffic_until: accept_traffic_until }) diff --git a/lib/plausible_web/live/goal_settings/form.ex b/lib/plausible_web/live/goal_settings/form.ex index 669ceee695e8..64db8656a96a 100644 --- a/lib/plausible_web/live/goal_settings/form.ex +++ b/lib/plausible_web/live/goal_settings/form.ex @@ -9,7 +9,7 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do alias Plausible.Repo def update(assigns, socket) do - site = Repo.preload(assigns.site, [:team, :owner]) + site = Repo.preload(assigns.site, [:team, :owners]) has_access_to_revenue_goals? = Plausible.Billing.Feature.RevenueGoals.check_availability(site.team) == :ok @@ -283,7 +283,7 @@ defmodule PlausibleWeb.Live.GoalSettings.Form do ~H"""
Repo.preload([ - :owner, + :owners, :completed_imports, team: [subscription: Plausible.Teams.last_subscription_query()] ]) diff --git a/lib/plausible_web/templates/site/membership/invite_member_form.html.heex b/lib/plausible_web/templates/site/membership/invite_member_form.html.heex index 34a2ca02f8a6..5c9775f7ca9a 100644 --- a/lib/plausible_web/templates/site/membership/invite_member_form.html.heex +++ b/lib/plausible_web/templates/site/membership/invite_member_form.html.heex @@ -12,7 +12,7 @@

This dashboard is currently locked and cannot be accessed. The site owner - {@site.owner.email} + {List.first(@site.owners).email} must upgrade their subscription plan in order to unlock the stats.

Want to pay for this site with the account you're logged in with?

- Contact {@site.owner.email} and ask them to + Contact {List.first(@site.owners).email} and ask them to <.styled_link href="https://plausible.io/docs/transfer-ownership" new_tab={true}> transfer the ownership diff --git a/lib/workers/accept_traffic_until_notification.ex b/lib/workers/accept_traffic_until_notification.ex index 585c9c2aff56..ae28beae07ad 100644 --- a/lib/workers/accept_traffic_until_notification.ex +++ b/lib/workers/accept_traffic_until_notification.ex @@ -26,14 +26,14 @@ defmodule Plausible.Workers.AcceptTrafficUntil do # send at most one notification per user, per day sent_today_query = from s in "sent_accept_traffic_until_notifications", - where: s.user_id == parent_as(:user).id and s.sent_on == ^today, + where: s.user_id == parent_as(:users).id and s.sent_on == ^today, select: true notifications = Repo.all( from t in Plausible.Teams.Team, - inner_join: u in assoc(t, :owner), - as: :user, + inner_join: u in assoc(t, :owners), + as: :users, inner_join: s in assoc(t, :sites), where: t.accept_traffic_until == ^tomorrow or t.accept_traffic_until == ^next_week, where: not exists(sent_today_query), diff --git a/lib/workers/check_usage.ex b/lib/workers/check_usage.ex index b9fddcfab0be..a6e72e12cc08 100644 --- a/lib/workers/check_usage.ex +++ b/lib/workers/check_usage.ex @@ -40,7 +40,7 @@ defmodule Plausible.Workers.CheckUsage do Repo.all( from(t in Teams.Team, as: :team, - inner_join: o in assoc(t, :owner), + inner_join: o in assoc(t, :owners), inner_lateral_join: s in subquery(Teams.last_subscription_join_query()), on: true, left_join: ep in Plausible.Billing.EnterprisePlan, @@ -58,7 +58,7 @@ defmodule Plausible.Workers.CheckUsage do 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, owner: o] + preload: [subscription: s, enterprise_plan: ep, owners: o] ) ) @@ -110,8 +110,10 @@ defmodule Plausible.Workers.CheckUsage do suggested_plan = Plausible.Billing.Plans.suggest(subscriber, pageview_usage.last_cycle.total) - PlausibleWeb.Email.over_limit_email(subscriber.owner, pageview_usage, suggested_plan) - |> Plausible.Mailer.send() + for owner <- subscriber.owners do + PlausibleWeb.Email.over_limit_email(owner, pageview_usage, suggested_plan) + |> Plausible.Mailer.send() + end Plausible.Teams.start_grace_period(subscriber) @@ -129,13 +131,15 @@ defmodule Plausible.Workers.CheckUsage do nil {{_, pageview_usage}, {_, {site_usage, site_allowance}}} -> - PlausibleWeb.Email.enterprise_over_limit_internal_email( - subscriber.owner, - pageview_usage, - site_usage, - site_allowance - ) - |> Plausible.Mailer.send() + for owner <- subscriber.owners 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 diff --git a/lib/workers/notify_annual_renewal.ex b/lib/workers/notify_annual_renewal.ex index e959e22abdb1..aeb0f8385c0b 100644 --- a/lib/workers/notify_annual_renewal.ex +++ b/lib/workers/notify_annual_renewal.ex @@ -25,7 +25,7 @@ defmodule Plausible.Workers.NotifyAnnualRenewal do Repo.all( from t in Teams.Team, as: :team, - inner_join: o in assoc(t, :owner), + inner_join: o in assoc(t, :owners), inner_lateral_join: s in subquery(Teams.last_subscription_join_query()), on: true, left_join: sent in ^sent_notification, @@ -35,29 +35,38 @@ defmodule Plausible.Workers.NotifyAnnualRenewal do where: s.next_bill_date > fragment("now()::date") and s.next_bill_date <= fragment("now()::date + INTERVAL '7 days'"), - preload: [owner: o, subscription: s] + preload: [owners: o, subscription: s] ) for team <- teams do case team.subscription.status do Subscription.Status.active() -> - template = PlausibleWeb.Email.yearly_renewal_notification(team) - Plausible.Mailer.send(template) + for owner <- team.owners do + template = PlausibleWeb.Email.yearly_renewal_notification(team, owner) + Plausible.Mailer.send(template) + end Subscription.Status.deleted() -> - template = PlausibleWeb.Email.yearly_expiration_notification(team) - Plausible.Mailer.send(template) + for owner <- team.owners do + template = PlausibleWeb.Email.yearly_expiration_notification(team, owner) + Plausible.Mailer.send(template) + end _ -> - Sentry.capture_message("Invalid subscription for renewal", team: team, user: team.owner) + Sentry.capture_message("Invalid subscription for renewal", + team: team, + user: List.first(team.owner) + ) end - Repo.insert_all("sent_renewal_notifications", [ - %{ - user_id: team.owner.id, - timestamp: NaiveDateTime.utc_now() - } - ]) + for owner <- team.owners do + Repo.insert_all("sent_renewal_notifications", [ + %{ + user_id: owner.id, + timestamp: NaiveDateTime.utc_now() + } + ]) + end end :ok diff --git a/lib/workers/send_site_setup_emails.ex b/lib/workers/send_site_setup_emails.ex index db34d9366dec..02678022a194 100644 --- a/lib/workers/send_site_setup_emails.ex +++ b/lib/workers/send_site_setup_emails.ex @@ -44,16 +44,16 @@ defmodule Plausible.Workers.SendSiteSetupEmails do on: se.site_id == s.id, where: is_nil(se.id), where: s.inserted_at > fragment("(now() at time zone 'utc') - '72 hours'::interval"), - preload: [:owner, :team] + preload: [:owners, :team] ) for site <- Repo.all(q) do - owner = site.owner + owners = site.owners setup_completed = Plausible.Sites.has_stats?(site) hours_passed = NaiveDateTime.diff(DateTime.utc_now(), site.inserted_at, :hour) if !setup_completed && hours_passed > 47 do - send_setup_help_email(owner, site) + send_setup_help_email(owners, site) end end end @@ -66,7 +66,7 @@ defmodule Plausible.Workers.SendSiteSetupEmails do where: is_nil(se.id), inner_join: t in assoc(s, :team), where: s.inserted_at > fragment("(now() at time zone 'utc') - '72 hours'::interval"), - preload: [:owner, team: t] + preload: [:owners, team: t] ) for site <- Repo.all(q) do @@ -89,8 +89,10 @@ defmodule Plausible.Workers.SendSiteSetupEmails do end defp send_setup_success_email(site) do - PlausibleWeb.Email.site_setup_success(site.owner, site.team, site) - |> Plausible.Mailer.send() + for owner <- site.owners do + PlausibleWeb.Email.site_setup_success(owner, site.team, site) + |> Plausible.Mailer.send() + end Repo.insert_all("setup_success_emails", [ %{ @@ -100,9 +102,11 @@ defmodule Plausible.Workers.SendSiteSetupEmails do ]) end - defp send_setup_help_email(user, site) do - PlausibleWeb.Email.site_setup_help(user, site) - |> Plausible.Mailer.send() + defp send_setup_help_email(users, site) do + for user <- users do + PlausibleWeb.Email.site_setup_help(user, site) + |> Plausible.Mailer.send() + end Repo.insert_all("setup_help_emails", [ %{ diff --git a/test/plausible_web/controllers/stats_controller_test.exs b/test/plausible_web/controllers/stats_controller_test.exs index 5fd9f620a66d..a9a0febfd095 100644 --- a/test/plausible_web/controllers/stats_controller_test.exs +++ b/test/plausible_web/controllers/stats_controller_test.exs @@ -287,8 +287,8 @@ defmodule PlausibleWeb.StatsControllerTest do } do {:ok, site} = Plausible.Props.allow(site, ["author"]) - site = Repo.preload(site, :owner) - subscribe_to_growth_plan(site.owner) + [owner | _] = Repo.preload(site, :owners).owners + subscribe_to_growth_plan(owner) populate_stats(site, [ build(:pageview, "meta.key": ["author"], "meta.value": ["a"]), @@ -315,8 +315,8 @@ defmodule PlausibleWeb.StatsControllerTest do } do {:ok, site} = Plausible.Props.allow(site, ["author"]) - site = Repo.preload(site, :owner) - subscribe_to_growth_plan(site.owner) + [owner | _] = Repo.preload(site, :owners).owners + subscribe_to_growth_plan(owner) populate_stats(site, [ build(:pageview, "meta.key": ["author"], "meta.value": ["a"]) diff --git a/test/plausible_web/plugins/api/controllers/capabilities_test.exs b/test/plausible_web/plugins/api/controllers/capabilities_test.exs index 4c46621ba306..dc32d73d03f9 100644 --- a/test/plausible_web/plugins/api/controllers/capabilities_test.exs +++ b/test/plausible_web/plugins/api/controllers/capabilities_test.exs @@ -88,8 +88,8 @@ defmodule PlausibleWeb.Plugins.API.Controllers.CapabilitiesTest do @tag :ee_only test "growth", %{conn: conn, site: site, token: token} do - site = Plausible.Repo.preload(site, :owner) - subscribe_to_growth_plan(site.owner) + [owner | _] = Plausible.Repo.preload(site, :owners).owners + subscribe_to_growth_plan(owner) resp = conn diff --git a/test/plausible_web/plugins/api/controllers/custom_props_test.exs b/test/plausible_web/plugins/api/controllers/custom_props_test.exs index e7b461e597b5..12589c5dd17e 100644 --- a/test/plausible_web/plugins/api/controllers/custom_props_test.exs +++ b/test/plausible_web/plugins/api/controllers/custom_props_test.exs @@ -42,8 +42,8 @@ defmodule PlausibleWeb.Plugins.API.Controllers.CustomPropsTest do token: token, conn: conn } do - site = Plausible.Repo.preload(site, :owner) - subscribe_to_growth_plan(site.owner) + [owner | _] = Plausible.Repo.preload(site, :owners).owners + subscribe_to_growth_plan(owner) url = Routes.plugins_api_custom_props_url(PlausibleWeb.Endpoint, :enable) @@ -66,8 +66,8 @@ defmodule PlausibleWeb.Plugins.API.Controllers.CustomPropsTest do token: token, conn: conn } do - site = Plausible.Repo.preload(site, :owner) - subscribe_to_growth_plan(site.owner) + [owner | _] = Plausible.Repo.preload(site, :owners).owners + subscribe_to_growth_plan(owner) url = Routes.plugins_api_custom_props_url(PlausibleWeb.Endpoint, :enable) diff --git a/test/plausible_web/plugins/api/controllers/funnels_test.exs b/test/plausible_web/plugins/api/controllers/funnels_test.exs index cba0f6267427..8ed2d40be78e 100644 --- a/test/plausible_web/plugins/api/controllers/funnels_test.exs +++ b/test/plausible_web/plugins/api/controllers/funnels_test.exs @@ -249,8 +249,8 @@ defmodule PlausibleWeb.Plugins.API.Controllers.FunnelsTest do end test "fails for insufficient plan", %{conn: conn, token: token, site: site} do - site = Plausible.Repo.preload(site, :owner) - subscribe_to_growth_plan(site.owner) + [owner | _] = Plausible.Repo.preload(site, :owners).owners + subscribe_to_growth_plan(owner) url = Routes.plugins_api_funnels_url(PlausibleWeb.Endpoint, :create) diff --git a/test/plausible_web/plugins/api/controllers/goals_test.exs b/test/plausible_web/plugins/api/controllers/goals_test.exs index aea1053dd78e..129d374f0409 100644 --- a/test/plausible_web/plugins/api/controllers/goals_test.exs +++ b/test/plausible_web/plugins/api/controllers/goals_test.exs @@ -53,8 +53,8 @@ defmodule PlausibleWeb.Plugins.API.Controllers.GoalsTest do token: token, conn: conn } do - site = Plausible.Repo.preload(site, :owner) - subscribe_to_growth_plan(site.owner) + [owner | _] = Plausible.Repo.preload(site, :owners).owners + subscribe_to_growth_plan(owner) url = Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :create) @@ -79,8 +79,8 @@ defmodule PlausibleWeb.Plugins.API.Controllers.GoalsTest do token: token, conn: conn } do - site = Plausible.Repo.preload(site, :owner) - subscribe_to_growth_plan(site.owner) + [owner | _] = Plausible.Repo.preload(site, :owners).owners + subscribe_to_growth_plan(owner) url = Routes.plugins_api_goals_url(PlausibleWeb.Endpoint, :create) diff --git a/test/support/teams/test.ex b/test/support/teams/test.ex index 3ac8c6d553c4..13fff5475cef 100644 --- a/test/support/teams/test.ex +++ b/test/support/teams/test.ex @@ -20,9 +20,11 @@ defmodule Plausible.Teams.Test do def new_site(args \\ []) do args = if user = args[:owner] do + {owner, args} = Keyword.pop(args, :owner) {:ok, team} = Teams.get_or_create(user) args + |> Keyword.put(:owners, [owner]) |> Keyword.put(:team, team) else user = new_user() @@ -299,11 +301,13 @@ defmodule Plausible.Teams.Test do end def assert_team_attached(site, team_id \\ nil) do - assert site = %{team: team} = site |> Repo.reload!() |> Repo.preload([:team, :owner]) + assert site = %{team: team} = site |> Repo.reload!() |> Repo.preload([:team, :owners]) - assert membership = assert_team_membership(site.owner, team) + for owner <- site.owners do + assert membership = assert_team_membership(owner, team) - assert membership.team_id == team.id + assert membership.team_id == team.id + end if team_id do assert team.id == team_id