Skip to content

Commit

Permalink
Set current donation goal
Browse files Browse the repository at this point in the history
Add DonationGoalsManager

Integrate manager into controller

Add current donation goal migration and project tests
  • Loading branch information
begedin authored and joshsmith committed Nov 26, 2016
1 parent 755e601 commit 02dc2b2
Show file tree
Hide file tree
Showing 14 changed files with 386 additions and 26 deletions.
115 changes: 115 additions & 0 deletions lib/code_corps/donation_goals_manager.ex
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 priv/repo/migrations/20161126045705_add_current_donation_goal.exs
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
6 changes: 4 additions & 2 deletions test/controllers/donation_goal_controller_test.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule CodeCorps.DonationGoalControllerTest do
use CodeCorps.ApiCase, resource_name: :donation_goal

@valid_attrs %{amount: 200, current: false, description: "A description"}
@valid_attrs %{amount: 200, description: "A description"}
@invalid_attrs %{description: nil}

describe "index" do
Expand Down Expand Up @@ -67,7 +67,9 @@ defmodule CodeCorps.DonationGoalControllerTest do
describe "update" do
@tag authenticated: :admin
test "updates and renders chosen resource when data is valid", %{conn: conn} do
assert conn |> request_update(@valid_attrs) |> json_response(200)
project = insert(:project)
attrs = @valid_attrs |> Map.merge(%{project: project})
assert conn |> request_update(attrs) |> json_response(200)
end

@tag authenticated: :admin
Expand Down
167 changes: 167 additions & 0 deletions test/lib/code_corps/donation_goals_manager_test.exs
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
10 changes: 4 additions & 6 deletions test/models/donation_goal_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,17 @@ defmodule CodeCorps.DonationGoalTest do
alias CodeCorps.DonationGoal

describe "%create_changeset/2" do
test "requires amount, current, description and project_id" do
test "requires amount, description and project_id" do
changeset = DonationGoal.create_changeset(%DonationGoal{}, %{})

refute changeset.valid?
assert changeset.errors[:amount] == {"can't be blank", []}
assert changeset.errors[:current] == {"can't be blank", []}
assert changeset.errors[:description] == {"can't be blank", []}
assert changeset.errors[:project_id] == {"can't be blank", []}
end

test "ensures project with specified id actually exists" do
attrs = %{amount: 100, current: true, description: "Bar", project_id: -1}
attrs = %{amount: 100, description: "Bar", project_id: -1}
{ result, changeset } =
DonationGoal.create_changeset(%DonationGoal{}, attrs)
|> Repo.insert
Expand All @@ -27,14 +26,13 @@ defmodule CodeCorps.DonationGoalTest do
end

describe "&update_changeset/2" do
test "requires amount, current, description" do
attrs = %{amount: nil, current: nil, description: nil}
test "requires amount, description" do
attrs = %{amount: nil, description: nil}
donation_goal = insert(:donation_goal)
changeset = DonationGoal.update_changeset(donation_goal, attrs)

refute changeset.valid?
assert changeset.errors[:amount] == {"can't be blank", []}
assert changeset.errors[:current] == {"can't be blank", []}
assert changeset.errors[:description] == {"can't be blank", []}
end
end
Expand Down

0 comments on commit 02dc2b2

Please sign in to comment.