-
Notifications
You must be signed in to change notification settings - Fork 86
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add DonationGoalsManager Integrate manager into controller Add current donation goal migration and project tests
- Loading branch information
Showing
14 changed files
with
386 additions
and
26 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
defmodule CodeCorps.DonationGoalsManager do | ||
@moduledoc """ | ||
Handles CRUD operations for donation goals. | ||
When operations happen on `CodeCorps.DonationGoal`, we need to set | ||
the current donation goal on `CodeCorps.Project`. | ||
The current donation goal should be the smallest value that is | ||
greater than the project's current total donations, _or_ falls back to | ||
the largest donation goal. | ||
""" | ||
|
||
import Ecto.Query | ||
|
||
alias CodeCorps.{DonationGoal, Project, Repo} | ||
alias Ecto.Multi | ||
|
||
def create(attributes) do | ||
changeset = %DonationGoal{} |> DonationGoal.create_changeset(attributes) | ||
|
||
multi = Multi.new | ||
|> Multi.insert(:donation_goal, changeset) | ||
|> Multi.run(:update_related_goals, &update_related_goals/1) | ||
|
||
case Repo.transaction(multi) do | ||
{:ok, %{donation_goal: donation_goal, update_related_goals: _}} -> | ||
{:ok, donation_goal} | ||
{:error, :donation_goal, %Ecto.Changeset{} = changeset, %{}} -> | ||
{:error, changeset} | ||
{:error, _failed_operation, _failed_value, _changes_so_far} -> | ||
{:error, :unhandled} | ||
end | ||
end | ||
|
||
def update(%DonationGoal{} = donation_goal, attributes) do | ||
changeset = donation_goal |> DonationGoal.create_changeset(attributes) | ||
|
||
multi = Multi.new | ||
|> Multi.update(:donation_goal, changeset) | ||
|> Multi.run(:update_related_goals, &update_related_goals/1) | ||
|
||
case Repo.transaction(multi) do | ||
{:ok, %{donation_goal: donation_goal, update_related_goals: _}} -> | ||
{:ok, Repo.get(DonationGoal, donation_goal.id)} | ||
{:error, :donation_goal, %Ecto.Changeset{} = changeset, %{}} -> | ||
{:error, changeset} | ||
{:error, _failed_operation, _failed_value, _changes_so_far} -> | ||
{:error, :unhandled} | ||
end | ||
end | ||
|
||
def set_current_goal_for_project(%Project{} = project) do | ||
project | ||
|> find_current_goal | ||
|> set_to_current(project) | ||
end | ||
|
||
defp update_related_goals(%{donation_goal: %DonationGoal{project_id: project_id}}) do | ||
Project | ||
|> Repo.get(project_id) | ||
|> set_current_goal_for_project | ||
end | ||
|
||
defp find_current_goal(%Project{} = project) do | ||
amount_donated = get_amount_donated(project) | ||
case find_lowest_not_yet_reached(project, amount_donated) do | ||
nil -> find_largest_goal(project) | ||
%DonationGoal{} = donation_goal -> donation_goal | ||
end | ||
end | ||
|
||
defp get_amount_donated(%Project{id: project_id}) do | ||
# TODO: This should be simplified by having | ||
# subscriptions relate to projects instead of plans | ||
# and by caching the total amount on the project itself | ||
|
||
CodeCorps.StripeConnectPlan | ||
|> Repo.get_by(project_id: project_id) | ||
|> aggregate_donations | ||
|> default_to_zero | ||
end | ||
|
||
defp aggregate_donations(nil), do: 0 | ||
defp aggregate_donations(%CodeCorps.StripeConnectPlan{id: plan_id}) do | ||
CodeCorps.StripeConnectSubscription | ||
|> where([s], s.stripe_connect_plan_id == ^plan_id) | ||
|> Repo.aggregate(:sum, :quantity) | ||
end | ||
|
||
defp default_to_zero(nil), do: 0 | ||
defp default_to_zero(value), do: value | ||
|
||
defp find_lowest_not_yet_reached(%Project{id: project_id}, amount_donated) do | ||
DonationGoal | ||
|> where([d], d.project_id == ^project_id and d.amount > ^amount_donated) | ||
|> order_by(asc: :amount) | ||
|> limit(1) | ||
|> Repo.one | ||
end | ||
|
||
defp find_largest_goal(%Project{id: project_id}) do | ||
DonationGoal | ||
|> where([d], d.project_id == ^project_id) | ||
|> order_by(desc: :amount) | ||
|> limit(1) | ||
|> Repo.one | ||
end | ||
|
||
defp set_to_current(%DonationGoal{} = donation_goal, %Project{} = project) do | ||
attrs = %{current_donation_goal_id: donation_goal.id} | ||
project | ||
|> Project.set_current_donation_goal_changeset(attrs) | ||
|> Repo.update | ||
end | ||
end |
27 changes: 27 additions & 0 deletions
27
priv/repo/migrations/20161126045705_add_current_donation_goal.exs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
defmodule CodeCorps.Repo.Migrations.AddCurrentDonationGoal do | ||
use Ecto.Migration | ||
|
||
def up do | ||
alter table(:projects) do | ||
add :current_donation_goal_id, references(:donation_goals) | ||
end | ||
|
||
create unique_index(:projects, [:current_donation_goal_id]) | ||
|
||
alter table(:donation_goals) do | ||
remove(:current) | ||
end | ||
end | ||
|
||
def down do | ||
drop_if_exists unique_index(:projects, [:current_donation_goal_id]) | ||
|
||
alter table(:projects) do | ||
remove(:current_donation_goal_id) | ||
end | ||
|
||
alter table(:donation_goals) do | ||
add :current, :boolean, default: false | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
defmodule CodeCorps.DonationGoalsManagerTest do | ||
use ExUnit.Case, async: true | ||
|
||
use CodeCorps.ModelCase | ||
|
||
alias CodeCorps.DonationGoal | ||
alias CodeCorps.DonationGoalsManager | ||
alias CodeCorps.Project | ||
|
||
defp assert_current_goal_id(goal_id) do | ||
project = | ||
Project | ||
|> Repo.get_by(current_donation_goal_id: goal_id) | ||
|> Repo.preload([:current_donation_goal]) | ||
assert project.current_donation_goal.id == goal_id | ||
end | ||
|
||
defp donate(plan, amount) do | ||
insert(:stripe_connect_subscription, quantity: amount, stripe_connect_plan: plan) | ||
end | ||
|
||
describe "create/1" do | ||
test "inserts new goal, returns {:ok, record}" do | ||
project = insert(:project) | ||
insert(:stripe_connect_plan, project: project) | ||
|
||
{:ok, %DonationGoal{} = donation_goal} = DonationGoalsManager.create(%{amount: 10, description: "Test", project_id: project.id}) | ||
assert_current_goal_id(donation_goal.id) | ||
end | ||
|
||
test "returns {:error, changeset} if there are validation errors" do | ||
{:error, %Ecto.Changeset{} = changeset} = DonationGoalsManager.create(%{amount: 10}) | ||
refute changeset.valid? | ||
end | ||
|
||
test "sets current goal correctly" do | ||
project = insert(:project) | ||
plan = insert(:stripe_connect_plan, project: project) | ||
|
||
{:ok, first_goal} = DonationGoalsManager.create(%{amount: 10, description: "Test", project_id: project.id}) | ||
|
||
# total donated is 0, | ||
# only goal inserted is the first goal | ||
assert_current_goal_id(first_goal.id) | ||
|
||
{:ok, second_goal} = DonationGoalsManager.create(%{amount: 20, description: "Test", project_id: project.id}) | ||
|
||
# total donated is still 0 | ||
# first goal larger than 0 is the very first goal | ||
assert_current_goal_id(first_goal.id) | ||
|
||
insert(:stripe_connect_subscription, quantity: 15, stripe_connect_plan: plan) | ||
|
||
{:ok, _} = DonationGoalsManager.create(%{amount: 30, description: "Test", project_id: project.id}) | ||
|
||
# total donated is 15. | ||
# first applicable goal is second goal, with an amount of 20 | ||
# third goal has an amount of 30, which is not applicable | ||
assert_current_goal_id(second_goal.id) | ||
|
||
insert(:stripe_connect_subscription, quantity: 30, stripe_connect_plan: plan) | ||
|
||
{:ok, fourth_goal} = DonationGoalsManager.create(%{amount: 40, description: "Test", project_id: project.id}) | ||
|
||
# total donated is 45, which is more than any defined goal | ||
# largest goal inserted after change the fourth goal, with an amount of 40 | ||
assert_current_goal_id(fourth_goal.id) | ||
end | ||
end | ||
|
||
describe "update/2" do | ||
test "updates existing goal, returns {:ok, record}" do | ||
project = insert(:project) | ||
insert(:stripe_connect_plan, project: project) | ||
donation_goal = insert(:donation_goal, amount: 10, project: project) | ||
|
||
{:ok, %DonationGoal{} = updated_goal} = DonationGoalsManager.update(donation_goal, %{amount: 15}) | ||
assert_current_goal_id(updated_goal.id) | ||
assert updated_goal.id == donation_goal.id | ||
end | ||
test "returns {:error, changeset} if there are validation errors" do | ||
project = insert(:project) | ||
insert(:stripe_connect_plan, project: project) | ||
donation_goal = insert(:donation_goal, amount: 10, project: project) | ||
|
||
{:error, %Ecto.Changeset{} = changeset} = DonationGoalsManager.update(donation_goal, %{amount: nil}) | ||
refute changeset.valid? | ||
end | ||
|
||
test "sets current goal correctly" do | ||
project = insert(:project) | ||
plan = insert(:stripe_connect_plan, project: project) | ||
goal_1 = insert(:donation_goal, amount: 10, project: project) | ||
goal_2 = insert(:donation_goal, amount: 15, project: project) | ||
insert(:donation_goal, amount: 20, project: project) | ||
|
||
DonationGoalsManager.update(goal_1, %{amount: 11}) | ||
|
||
# amount donated is 0, first goal above that is still goal 1 | ||
assert_current_goal_id(goal_1.id) | ||
|
||
DonationGoalsManager.update(goal_1, %{amount: 21}) | ||
|
||
# amount donated is still 0, first goal above that is now goal 2 | ||
assert_current_goal_id(goal_2.id) | ||
|
||
insert(:stripe_connect_subscription, quantity: 25, stripe_connect_plan: plan) | ||
|
||
DonationGoalsManager.update(goal_1, %{amount: 21}) | ||
|
||
# amount donated is now 25 | ||
# this is more than any current goal | ||
# largest goal is goal 1, with 21 | ||
assert_current_goal_id(goal_1.id) | ||
|
||
DonationGoalsManager.update(goal_2, %{amount: 22}) | ||
|
||
# amount donated is now 25 | ||
# this is more than any current goal | ||
# largest goal is goal 2, with 22 | ||
assert_current_goal_id(goal_2.id) | ||
|
||
DonationGoalsManager.update(goal_1, %{amount: 27}) | ||
|
||
# amount donated is still 25 | ||
# first goal higher than that is goal 1, with 27 | ||
assert_current_goal_id(goal_1.id) | ||
end | ||
end | ||
|
||
|
||
|
||
describe "set_current_goal_for_project/1" do | ||
test "sets current goal correctly" do | ||
project = insert(:project) | ||
plan = insert(:stripe_connect_plan, project: project) | ||
|
||
goal_1 = insert(:donation_goal, amount: 10, project: project) | ||
goal_2 = insert(:donation_goal, amount: 15, project: project) | ||
goal_3 = insert(:donation_goal, amount: 20, project: project) | ||
|
||
plan |> donate(5) | ||
DonationGoalsManager.set_current_goal_for_project(project) | ||
assert_current_goal_id(goal_1.id) | ||
|
||
plan |> donate(5) # total is now 10 | ||
DonationGoalsManager.set_current_goal_for_project(project) | ||
assert_current_goal_id(goal_2.id) | ||
|
||
plan |> donate(5) # total is now 15 | ||
DonationGoalsManager.set_current_goal_for_project(project) | ||
assert_current_goal_id(goal_3.id) | ||
|
||
plan |> donate(5) # total is now 20 | ||
DonationGoalsManager.set_current_goal_for_project(project) | ||
assert_current_goal_id(goal_3.id) | ||
|
||
plan |> donate(5) # total is now 25 | ||
DonationGoalsManager.set_current_goal_for_project(project) | ||
assert_current_goal_id(goal_3.id) | ||
|
||
goal_4 = insert(:donation_goal, amount: 30, project: project) # 30 is more than the current 25 total | ||
DonationGoalsManager.set_current_goal_for_project(project) | ||
assert_current_goal_id(goal_4.id) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.