Skip to content

Commit

Permalink
commission payment workflow overhaul (#448)
Browse files Browse the repository at this point in the history
* commission payment workflow overhaul

* fix deposited_amount not taking :released into account

* reorganize the BalanceBox a bit

* wip new status dropdown

* new status box done

* make `mix quality` check formatted instead of formatting

* make sure commission terms are displayed in both offering and commission pages

* get current_user, current_user_member? and commission from context in StatusItem

* all accounts should use the 'recipient' service agreement

chatted with Stripe support about this. Docs were a little confusing and made it sound like I needed the full agreement here.

* Enforce having a single currency per offering

* final commission form working I think

* more work around invoices

* receipt fixes

* styling

* partial deposits

* deposit release + related events and messaging

* missed some spots with tip % calc

* fix weird multi-currency stuff

* disable Release Deposits properly

* almost got tests passing

* another test fixed

* test stuff
  • Loading branch information
zkat authored Jul 6, 2023
1 parent 4d82b4f commit 16854c3
Show file tree
Hide file tree
Showing 52 changed files with 1,383 additions and 955 deletions.
6 changes: 4 additions & 2 deletions lib/banchan/commissions/commission.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ defmodule Banchan.Commissions.Commission do
import Ecto.Changeset

alias Banchan.Commissions.Common
alias Banchan.Studios

schema "commissions" do
field :public_id, :string, autogenerate: {Common, :gen_public_id, []}
field :title, :string
field :description, :string
field :tos_ok, :boolean, virtual: true
field :terms, :string
field :currency, Ecto.Enum, values: Studios.Common.supported_currencies()

field :status, Ecto.Enum,
values: Common.status_values(),
Expand Down Expand Up @@ -41,8 +43,8 @@ defmodule Banchan.Commissions.Commission do
@doc false
def creation_changeset(commission, attrs) do
commission
|> cast(attrs, [:title, :description, :tos_ok])
|> validate_required([:title, :description, :tos_ok])
|> cast(attrs, [:title, :description, :tos_ok, :currency])
|> validate_required([:title, :description, :tos_ok, :currency])
|> validate_length(:title, max: 50)
|> validate_length(:description, max: 160)
|> cast_assoc(:line_items)
Expand Down
152 changes: 69 additions & 83 deletions lib/banchan/commissions/commissions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -373,20 +373,16 @@ defmodule Banchan.Commissions do
end

def deposited_amount(_, %Commission{} = commission, _) do
if Ecto.assoc_loaded?(commission.events) do
currency = commission_currency(commission)

if Ecto.assoc_loaded?(commission.events) &&
Enum.all?(commission.events, &Ecto.assoc_loaded?(&1.invoice)) do
Enum.reduce(
commission.events,
%{},
Money.new(0, currency),
fn event, acc ->
if event.invoice && event.invoice.status in [:succeeded, :released] do
current =
Map.get(
acc,
event.invoice.amount.currency,
Money.new(0, event.invoice.amount.currency)
)

Map.put(acc, event.invoice.amount.currency, Money.add(current, event.invoice.amount))
Money.add(acc, event.invoice.amount)
else
acc
end
Expand All @@ -398,18 +394,15 @@ defmodule Banchan.Commissions do
i in Invoice,
where:
i.commission_id == ^commission.id and
i.status == :succeeded,
i.status in [:succeeded, :released],
select: i.amount
)
|> Repo.all()

Enum.reduce(
deposits,
%{},
fn dep, acc ->
current = Map.get(acc, dep.currency, Money.new(0, dep.currency))
Map.put(acc, dep.currency, Money.add(current, dep))
end
Money.new(0, currency),
&Money.add/2
)
end
end
Expand All @@ -431,20 +424,16 @@ defmodule Banchan.Commissions do
end

def tipped_amount(_, %Commission{} = commission, _) do
if Ecto.assoc_loaded?(commission.events) do
currency = commission_currency(commission)

if Ecto.assoc_loaded?(commission.events) &&
Enum.all?(commission.events, &Ecto.assoc_loaded?(&1.invoice)) do
Enum.reduce(
commission.events,
%{},
Money.new(0, currency),
fn event, acc ->
if event.invoice && event.invoice.status in [:succeeded, :released] do
current =
Map.get(
acc,
event.invoice.tip.currency,
Money.new(0, event.invoice.tip.currency)
)

Map.put(acc, event.invoice.tip.currency, Money.add(current, event.invoice.tip.amount))
Money.add(acc, event.invoice.tip)
else
acc
end
Expand All @@ -463,31 +452,43 @@ defmodule Banchan.Commissions do

Enum.reduce(
deposits,
%{},
fn dep, acc ->
current = Map.get(acc, dep.currency, Money.new(0, dep.currency))
Map.put(acc, dep.currency, Money.add(current, dep))
end
Money.new(0, currency),
&Money.add/2
)
end
end

@doc """
Gets the currency used for the commission, taking into account legacy
commissions before the currency field existed.
"""
def commission_currency(%Commission{} = commission) do
commission.currency ||
(!Enum.empty?(commission.line_items) && Enum.at(commission.line_items, 0).amount.currency) ||
from(
s in Studio,
where: s.id == ^commission.studio_id,
select: s.default_currency
)
|> Repo.one!()
end

@doc """
Calculates the total commission cost based on the current line items.
"""
def line_item_estimate(line_items) do
currency =
if Enum.empty?(line_items) do
:USD
else
Enum.at(line_items, 0).amount.currency
end

Enum.reduce(
line_items,
%{},
Money.new(0, currency),
fn item, acc ->
current =
Map.get(
acc,
item.amount.currency,
Money.new(0, item.amount.currency)
)

Map.put(acc, item.amount.currency, Money.add(current, item.amount))
Money.add(acc, item.amount)
end
)
end
Expand Down Expand Up @@ -586,6 +587,7 @@ defmodule Banchan.Commissions do
%Commission{
studio: studio,
offering: offering,
currency: Offerings.offering_currency(offering),
client: actor,
line_items: line_items,
terms: offering.terms || studio.default_terms
Expand Down Expand Up @@ -769,7 +771,11 @@ defmodule Banchan.Commissions do
with {:ok, actor} <- check_actor_edit_access(actor, commission) do
changeset = Repo.reload(commission) |> Commission.status_changeset(%{status: status})

check_status_transition!(actor, commission, changeset.changes.status)
check_status_transition!(
actor,
commission,
Ecto.Changeset.fetch_field!(changeset, :status)
)

with {:ok, commission} <- changeset |> Repo.update() do
if commission.status == :accepted do
Expand All @@ -785,30 +791,15 @@ defmodule Banchan.Commissions do
end
end

if commission.status == :approved do
# Release any successful deposits.
from(i in Invoice,
where: i.commission_id == ^commission.id and i.status == :succeeded
)
|> Repo.update_all(set: [status: :released])

from(e in Event,
join: i in assoc(e, :invoice),
where: i.commission_id == ^commission.id and i.status == :released,
select: e,
preload: [:actor, invoice: [], attachments: [:upload, :thumbnail, :preview]]
)
|> Repo.all()
|> Enum.each(fn ev ->
Notifications.commission_event_updated(commission, ev, actor)
end)
end

# current_user_member? is checked as part of check_status_transition!
with {:ok, _event} <-
create_event(:status, actor, commission, true, [], %{status: status}) do
Notifications.commission_status_changed(commission, actor)

if commission.status == :approved do
Notifications.commission_approved(commission)
end

{:ok, commission}
end
end
Expand Down Expand Up @@ -841,35 +832,30 @@ defmodule Banchan.Commissions do
end

# Transition changes studios can make
defp status_transition_allowed?(artist?, client?, from, to)

defp status_transition_allowed?(true, _, :submitted, :accepted), do: true
defp status_transition_allowed?(true, _, :submitted, :rejected), do: true
defp status_transition_allowed?(true, _, :accepted, :in_progress), do: true
defp status_transition_allowed?(true, _, :accepted, :paused), do: true
defp status_transition_allowed?(true, _, :accepted, :ready_for_review), do: true
defp status_transition_allowed?(true, _, :in_progress, :paused), do: true
defp status_transition_allowed?(true, _, :in_progress, :waiting), do: true
defp status_transition_allowed?(true, _, :in_progress, :ready_for_review), do: true
defp status_transition_allowed?(true, _, :paused, :in_progress), do: true
defp status_transition_allowed?(true, _, :paused, :waiting), do: true
defp status_transition_allowed?(true, _, :waiting, :in_progress), do: true
defp status_transition_allowed?(true, _, :waiting, :paused), do: true
defp status_transition_allowed?(true, _, :waiting, :ready_for_review), do: true
defp status_transition_allowed?(true, _, :ready_for_review, :in_progress), do: true
defp status_transition_allowed?(true, _, :approved, :accepted), do: true
defp status_transition_allowed?(true, _, :withdrawn, :accepted), do: true
defp status_transition_allowed?(true, _, :rejected, :accepted), do: true
def status_transition_allowed?(artist?, client?, from, to)

def status_transition_allowed?(true, _, :submitted, :accepted), do: true
def status_transition_allowed?(true, _, :submitted, :rejected), do: true
def status_transition_allowed?(true, _, :accepted, :in_progress), do: true
def status_transition_allowed?(true, _, :accepted, :paused), do: true
def status_transition_allowed?(true, _, :in_progress, :paused), do: true
def status_transition_allowed?(true, _, :in_progress, :waiting), do: true
def status_transition_allowed?(true, _, :paused, :in_progress), do: true
def status_transition_allowed?(true, _, :paused, :waiting), do: true
def status_transition_allowed?(true, _, :waiting, :in_progress), do: true
def status_transition_allowed?(true, _, :waiting, :paused), do: true
def status_transition_allowed?(true, _, :withdrawn, :accepted), do: true
def status_transition_allowed?(true, _, :rejected, :accepted), do: true

# Transition changes clients can make
defp status_transition_allowed?(_, true, :ready_for_review, :approved), do: true
defp status_transition_allowed?(_, true, :withdrawn, :submitted), do: true
def status_transition_allowed?(_, true, _, :approved), do: true
def status_transition_allowed?(_, true, :withdrawn, :submitted), do: true

# Either party can withdraw a commission
defp status_transition_allowed?(_, _, _, :withdrawn), do: true
def status_transition_allowed?(_, _, _, :withdrawn), do: true

# Everything else is a no from me, Bob.
defp status_transition_allowed?(_, _, _, _), do: false
def status_transition_allowed?(_, _, _, _), do: false

@doc """
Adds a new line item to an existing commission. Only studio members and
Expand Down
33 changes: 32 additions & 1 deletion lib/banchan/commissions/common.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ defmodule Banchan.Commissions.Common do
:payment_processed,
:refund_processed,
:status,
:title_changed
:title_changed,
:invoice_released,
:all_invoices_released
]

@doc """
Expand All @@ -54,6 +56,35 @@ defmodule Banchan.Commissions.Common do
def humanize_status(:approved), do: "Approved"
def humanize_status(:withdrawn), do: "Withdrawn"

@doc """
Description of a commission status.
"""
def status_description(:submitted),
do: "The commission has been submitted to the studio and is awaiting acceptance."

def status_description(:accepted),
do: "The commission has been accepted by the studio and work will begin soon."

def status_description(:rejected),
do: "The studio has decided to reject this commission request."

def status_description(:paused),
do: "The studio has paused work on this commission temporarily."

def status_description(:in_progress), do: "The studio is actively working on this commission."

def status_description(:waiting),
do: "The studio is waiting for a response from the client before continuing work."

def status_description(:ready_for_review), do: "The studio is waiting for a final review."

def status_description(:approved),
do:
"The commission has received final approval and all payments and attachments have been released."

def status_description(:withdrawn),
do: "The client has withdrawn this commission. It is now closed."

@doc """
Generates a new public_id for a commission.
"""
Expand Down
4 changes: 2 additions & 2 deletions lib/banchan/commissions/event.ex
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ defmodule Banchan.Commissions.Event do
|> validate_text()
end

def invoice_changeset(event, attrs) do
def invoice_changeset(event, attrs, remaining \\ 99_999) do
event
|> cast(attrs, [:text, :amount])
|> validate_money(:amount)
|> validate_money(:amount, remaining)
|> validate_required([:amount])
|> validate_text()
end
Expand Down
24 changes: 16 additions & 8 deletions lib/banchan/commissions/notifications.ex
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,14 @@ defmodule Banchan.Commissions.Notifications do
"The commission status has been changed to #{Common.humanize_status(status)}."
end

defp new_event_notification_body(%Event{type: :all_invoices_released}) do
"All deposits for the commission have been released."
end

defp new_event_notification_body(%Event{type: :invoice_released}) do
"An invoice deposit has been released."
end

@doc """
Broadcasts commission title changes so commission pages can live-update.
"""
Expand Down Expand Up @@ -297,28 +305,28 @@ defmodule Banchan.Commissions.Notifications do
end

@doc """
Sends out web/email notifications only, when an invoice is released by the
client before a commission has been approved.
Sends out web/email notifications only, when a commission has been finalized
and all invoices have been released.
"""
def invoice_released(%Commission{} = commission, %Event{} = event, actor \\ nil) do
def commission_approved(%Commission{} = commission, actor \\ nil) do
# No need for a broadcast. That's already being handled by commission_event_updated
Notifications.with_task(fn ->
{:ok, _} =
Repo.transaction(fn ->
subs = subscribers(commission)

url =
url(~p"/commissions/#{commission.public_id}")
|> replace_fragment(event)
url = url(~p"/commissions/#{commission.public_id}")

body =
"The commission has been approved. All deposits and attachments have been released."

body = "An invoice has been released before commission approval."
{:safe, safe_url} = Phoenix.HTML.html_escape(url)

Notifications.notify_subscribers!(
actor,
subs,
%Notifications.UserNotification{
type: "invoice_released",
type: "commission_approved",
title: commission.title,
short_body: body,
text_body: "#{body}\n\n#{url}",
Expand Down
Loading

0 comments on commit 16854c3

Please sign in to comment.