Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
defmodule CodeCorps.StripeService.Events.CustomerSubscriptionDeleted do
@api Application.get_env(:code_corps, :stripe)

def handle(%{"data" => %{"object" => %{"id" => stripe_sub_id, "customer" => connect_customer_id}}}) do
CodeCorps.StripeService.StripeConnectSubscriptionService.update_from_stripe(stripe_sub_id, connect_customer_id)
end
end
Original file line number Diff line number Diff line change
@@ -1,70 +1,7 @@
defmodule CodeCorps.StripeService.Events.CustomerSubscriptionUpdated do
import Ecto.Query

alias CodeCorps.Project
alias CodeCorps.Repo
alias CodeCorps.Services.DonationGoalsService
alias CodeCorps.Services.ProjectService
alias CodeCorps.StripeConnectAccount
alias CodeCorps.StripeConnectCustomer
alias CodeCorps.StripeConnectPlan
alias CodeCorps.StripeConnectSubscription
alias CodeCorps.StripeService.Adapters.StripeConnectSubscriptionAdapter

@api Application.get_env(:code_corps, :stripe)

def handle(%{"data" => %{"object" => %{"id" => stripe_sub_id, "customer" => connect_customer_id}}}) do
with %StripeConnectCustomer{stripe_connect_account: %StripeConnectAccount{id_from_stripe: connect_account_id}} <-
retrieve_connect_customer(connect_customer_id),

{:ok, %Stripe.Subscription{} = stripe_subscription} <-
@api.Subscription.retrieve(stripe_sub_id, connect_account: connect_account_id),

subscription <-
load_subscription(stripe_sub_id),

{:ok, params} <-
stripe_subscription |> StripeConnectSubscriptionAdapter.to_params(%{}),

_subscription <-
update_subscription(subscription, params),

project <-
get_project(subscription),

{:ok, project} <-
ProjectService.update_project_totals(project)
do
DonationGoalsService.update_project_goals(project)
else
{:error, %Stripe.APIErrorResponse{}} -> {:error, :stripe_error}
nil -> {:error, :not_found}
_ -> {:error, :unexpected}
end
end

defp retrieve_connect_customer(connect_customer_id) do
StripeConnectCustomer
|> Repo.get_by(id_from_stripe: connect_customer_id)
|> Repo.preload(:stripe_connect_account)
end

defp load_subscription(id_from_stripe) do
StripeConnectSubscription
|> Repo.get_by(id_from_stripe: id_from_stripe)
end

defp update_subscription(%StripeConnectSubscription{} = record, params) do
record
|> StripeConnectSubscription.webhook_update_changeset(params)
|> Repo.update
end

defp get_project(%StripeConnectSubscription{stripe_connect_plan_id: stripe_connect_plan_id}) do
plan =
StripeConnectPlan
|> Repo.get(stripe_connect_plan_id)

Project |> Repo.get(plan.project_id) |> Repo.preload(:stripe_connect_plan)
CodeCorps.StripeService.StripeConnectSubscriptionService.update_from_stripe(stripe_sub_id, connect_customer_id)
end
end
174 changes: 99 additions & 75 deletions lib/code_corps/stripe_service/stripe_connect_subscription.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,108 +2,111 @@ defmodule CodeCorps.StripeService.StripeConnectSubscriptionService do
import Ecto.Query

alias CodeCorps.{
Organization, Project, Repo, StripeConnectAccount,StripeConnectCard,
StripeConnectCustomer, StripeConnectPlan, StripeConnectSubscription,
StripePlatformCard, StripePlatformCustomer, User
}
alias CodeCorps.Services.ProjectService
alias CodeCorps.StripeService.{
StripeConnectCardService, StripeConnectCustomerService
Project, Repo, StripeConnectCustomer, StripeConnectAccount,
StripeConnectPlan, StripeConnectSubscription, User
}
alias CodeCorps.Services.{DonationGoalsService, ProjectService}
alias CodeCorps.StripeService.{StripeConnectCardService, StripeConnectCustomerService}
alias CodeCorps.StripeService.Adapters.StripeConnectSubscriptionAdapter
alias CodeCorps.StripeService.Validators.{ProjectSubscribable, UserCanSubscribe}

@api Application.get_env(:code_corps, :stripe)

def find_or_create(%{"project_id" => project_id, "quantity" => quantity, "user_id" => user_id} = attributes) do
with %Project{
stripe_connect_plan: %StripeConnectPlan{} = plan,
organization: %Organization{
stripe_connect_account: %StripeConnectAccount{} = connect_account
}
} = project <-
get_project(project_id),
%User{
stripe_platform_card: %StripePlatformCard{} = platform_card,
stripe_platform_customer: %StripePlatformCustomer{} = platform_customer
} = user <-
get_user(user_id),
{:ok, %StripeConnectSubscription{} = stripe_connect_subscription} <-
do_find_or_create(attributes, connect_account, plan, project, platform_card, platform_customer, quantity, user),
_project <-
ProjectService.update_project_totals(project)
def find_or_create(%{"project_id" => project_id, "quantity" => _, "user_id" => user_id} = attributes) do
with {:ok, %Project{} = project} <- get_project(project_id) |> ProjectSubscribable.validate,
{:ok, %User{} = user} <- get_user(user_id) |> UserCanSubscribe.validate
do
{:ok, stripe_connect_subscription}
{:ok, %StripeConnectSubscription{} = subscription} = do_find_or_create(project, user, attributes)

ProjectService.update_project_totals(project)
DonationGoalsService.update_project_goals(project)

{:ok, subscription}
else
# possible errors
# {:error, :project_not_ready} - `CodeCorps.ProjectSubscribable.validate/1` failed
# {:error, :user_not_ready} - `CodeCorps.UserCanSubscribe.validate/1` failed
# {:error, %Ecto.Changeset{}} - Record creation failed due to validation errors
# {:error, %Stripe.APIError{}} - Stripe request failed
# {:error, :not_found} - One of the associated records was not found
{:error, error} -> {:error, error}
nil -> {:error, :not_found}
_ -> {:error, :unexpected}
end
end

def update_from_stripe(stripe_id, connect_customer_id) do
with {:ok, %StripeConnectAccount{} = connect_account} <- retrieve_connect_account(connect_customer_id),
{:ok, %Stripe.Subscription{} = stripe_subscription} <- @api.Subscription.retrieve(stripe_id, connect_account: connect_account.id),
{:ok, %StripeConnectSubscription{} = subscription} <- load_subscription(stripe_id),
{:ok, params} <- stripe_subscription |> StripeConnectSubscriptionAdapter.to_params(%{}),
{:ok, %Project{} = project} <- get_project(subscription)
do
{:ok, %StripeConnectSubscription{} = subscription} = update_subscription(subscription, params)

ProjectService.update_project_totals(project)
DonationGoalsService.update_project_goals(project)

{:ok, subscription}
else
# possible errors
# {:error, %Ecto.Changeset{}} - Record creation failed due to validation errors
# {:error, %Stripe.APIError{}} - Stripe request failed
# {:error, :not_found} - One of the associated records was not found
{:error, error} -> {:error, error}
nil -> {:error, :not_found}
_ -> {:error, :unexpected}
end
end

defp do_find_or_create(
%{"project_id" => _, "quantity" => _, "user_id" => _} = attributes,
%StripeConnectAccount{} = connect_account,
%StripeConnectPlan{} = plan,
%Project{} = project,
%StripePlatformCard{} = platform_card,
%StripePlatformCustomer{} = platform_customer,
quantity,
%User{} = user
) do
case find(plan, user) do
nil -> create(attributes, connect_account, plan, project, platform_card, platform_customer, quantity, user)
defp do_find_or_create(%Project{} = project, %User{} = user, %{} = attributes) do
case find(project, user) do
nil -> create(project, user, attributes)
%StripeConnectSubscription{} = subscription -> {:ok, subscription}
end
end

defp find(%StripeConnectPlan{} = plan, %User{} = user) do
defp find(%Project{} = project, %User{} = user) do
StripeConnectSubscription
|> where([s], s.stripe_connect_plan_id == ^plan.id and s.user_id == ^user.id)
|> where([s], s.stripe_connect_plan_id == ^project.stripe_connect_plan.id and s.user_id == ^user.id)
|> Repo.one
end

defp create(
%{"project_id" => _, "quantity" => _, "user_id" => _} = attributes,
%StripeConnectAccount{} = connect_account,
%StripeConnectPlan{} = plan,
%Project{} = project,
%StripePlatformCard{} = platform_card,
%StripePlatformCustomer{} = platform_customer,
quantity,
%User{} = user
) do
with {:ok, connect_customer} <-
StripeConnectCustomerService.find_or_create(platform_customer, connect_account),
{:ok, connect_card} <-
StripeConnectCardService.find_or_create(platform_card, connect_customer, platform_customer, connect_account),
create_attributes <-
to_create_attributes(connect_card, connect_customer, plan, quantity),
{:ok, subscription} <-
@api.Subscription.create(create_attributes, connect_account: connect_account.id_from_stripe),
insert_attributes <-
to_insert_attributes(attributes, plan),
{:ok, params} <-
StripeConnectSubscriptionAdapter.to_params(subscription, insert_attributes),
{:ok, %StripeConnectSubscription{} = stripe_connect_subscription} <-
insert_subscription(params)
defp create(%Project{} = project, %User{} = user, attributes) do
with platform_card <- user.stripe_platform_card,
platform_customer <- user.stripe_platform_customer,
connect_account <- project.organization.stripe_connect_account,
plan <- project.stripe_connect_plan,
{:ok, connect_customer} <- StripeConnectCustomerService.find_or_create(platform_customer, connect_account),
{:ok, connect_card} <- StripeConnectCardService.find_or_create(platform_card, connect_customer, platform_customer, connect_account),
create_attributes <- to_create_attributes(connect_card, connect_customer, plan, attributes),
{:ok, subscription} <- @api.Subscription.create(create_attributes, connect_account: connect_account.id_from_stripe),
insert_attributes <- to_insert_attributes(attributes, plan),
{:ok, params} <- StripeConnectSubscriptionAdapter.to_params(subscription, insert_attributes),
{:ok, %StripeConnectSubscription{} = stripe_connect_subscription} <- insert_subscription(params)
do
{:ok, stripe_connect_subscription}
else
{:error, error} -> {:error, error}
nil -> {:error, :not_found}
# just pass failure to caller
failure -> failure
end
end

defp get_project(project_id) do
Project
|> Repo.get(project_id)
|> Repo.preload([:stripe_connect_plan, [{:organization, :stripe_connect_account}]])
defp get_project(%StripeConnectSubscription{stripe_connect_plan_id: stripe_connect_plan_id}) do
%StripeConnectPlan{project_id: project_id} = Repo.get(StripeConnectPlan, stripe_connect_plan_id)
{:ok, get_project(project_id, [:stripe_connect_plan])}
end

defp get_user(user_id) do
User
|> Repo.get(user_id)
|> Repo.preload([stripe_platform_card: :stripe_connect_cards])
|> Repo.preload(:stripe_platform_customer)
@default_project_preloads [:stripe_connect_plan, [{:organization, :stripe_connect_account}]]

defp get_project(project_id, preloads \\ @default_project_preloads) do
Repo.get(Project, project_id) |> Repo.preload(preloads)
end

@default_user_preloads [:stripe_platform_customer, [{:stripe_platform_card, :stripe_connect_cards}]]

defp get_user(user_id, preloads \\ @default_user_preloads) do
Repo.get(User, user_id) |> Repo.preload(preloads)
end

defp insert_subscription(params) do
Expand All @@ -112,7 +115,7 @@ defmodule CodeCorps.StripeService.StripeConnectSubscriptionService do
|> Repo.insert
end

defp to_create_attributes(%StripeConnectCard{} = card, %StripeConnectCustomer{} = customer, %StripeConnectPlan{} = plan, quantity) do
defp to_create_attributes(card, customer, plan, %{"quantity" => quantity}) do
%{
application_fee_percent: 5,
customer: customer.id_from_stripe,
Expand All @@ -125,4 +128,25 @@ defmodule CodeCorps.StripeService.StripeConnectSubscriptionService do
defp to_insert_attributes(attrs, %StripeConnectPlan{id: stripe_connect_plan_id}) do
attrs |> Map.merge(%{"stripe_connect_plan_id" => stripe_connect_plan_id})
end

defp retrieve_connect_account(connect_customer_id) do
customer =
StripeConnectCustomer
|> Repo.get_by(id_from_stripe: connect_customer_id)
|> Repo.preload(:stripe_connect_account)

{:ok, customer.stripe_connect_account}
end

defp load_subscription(id_from_stripe) do
subscription = Repo.get_by(StripeConnectSubscription, id_from_stripe: id_from_stripe)

{:ok, subscription}
end

defp update_subscription(%StripeConnectSubscription{} = record, params) do
record
|> StripeConnectSubscription.webhook_update_changeset(params)
|> Repo.update
end
end
33 changes: 33 additions & 0 deletions lib/code_corps/stripe_service/validators/project_subscribable.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
defmodule CodeCorps.StripeService.Validators.ProjectSubscribable do
@moduledoc """
Ensures a `CodeCorps.Project` is able to receive subscriptions.
"""

alias CodeCorps.{Organization, Project, StripeConnectAccount, StripeConnectPlan}

@doc """
Determines if the provided `CodeCorps.Project` is able to
get a subscription by a `CodeCorps.User`

For a project to be able to receive subscriptions,
it needs to have proper associations set up.

These are:

* `StripeConnectPlan`
* `Organization` with a `StripeConnectAccount`

If the project has these relationships set up, it returns `{:ok, project}`

In any other case, it returns {:error, :project_not_ready}
"""
def validate(%Project{} = project), do: do_validate(project)

@invalid {:error, :project_not_ready}

defp do_validate(%Project{
stripe_connect_plan: %StripeConnectPlan{},
organization: %Organization{stripe_connect_account: %StripeConnectAccount{}}
} = project), do: {:ok, project}
defp do_validate(_), do: @invalid
end
33 changes: 33 additions & 0 deletions lib/code_corps/stripe_service/validators/user_can_subscribe.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
defmodule CodeCorps.StripeService.Validators.UserCanSubscribe do
@moduledoc """
Ensures a `CodeCorps.User` is able to subscribe to a `CodeCorps.Project`.
"""

alias CodeCorps.{User, StripePlatformCard, StripePlatformCustomer}

@doc """
Determines if the provided `CodeCorps.User` is able to
subscribe to a `CodeCorps.Project`

For a user to be able to create subscriptions, they need to have
their associated records properly set up

These are:

* `StripePlatformCard`
* `StripePlatformCustomer`

If the user has these relationships set up, it returns `{:ok, user}`

In any other case, it returns {:error, :user_not_ready}
"""
def validate(%User{} = user), do: do_validate(user)

@invalid {:error, :user_not_ready}

defp do_validate(%User{
stripe_platform_card: %StripePlatformCard{},
stripe_platform_customer: %StripePlatformCustomer{}
} = user), do: {:ok, user}
defp do_validate(_), do: @invalid
end
Loading