Skip to content

Commit

Permalink
add support for tracking and displaying taxes, and improve total repo…
Browse files Browse the repository at this point in the history
…rting for invoices (#722)

Fixes: #536
  • Loading branch information
zkat authored Sep 5, 2023
1 parent bca5a61 commit fd593c6
Show file tree
Hide file tree
Showing 12 changed files with 226 additions and 21 deletions.
103 changes: 103 additions & 0 deletions lib/banchan/commissions/commissions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,109 @@ defmodule Banchan.Commissions do
end
end

@doc """
Calculates total taxes so far for a commission.
"""
def taxed_amount(
%User{id: user_id, roles: roles} = actor,
%Commission{client_id: client_id} = comm,
current_user_member?
)
when user_id != client_id and current_user_member? == false do
if :admin in roles || :mod in roles do
taxed_amount(actor, comm, true)
else
{:error, :unauthorized}
end
end

def taxed_amount(_, %Commission{} = commission, _) 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
Money.add(acc, event.invoice.tax || Money.new(0, currency))
else
acc
end
end
)
else
taxes =
from(
i in Invoice,
where:
i.commission_id == ^commission.id and
i.status in [:succeeded, :released],
select: i.tax
)
|> Repo.all()

taxes
|> Enum.filter(&(!is_nil(&1)))
|> Enum.reduce(
Money.new(0, currency),
&Money.add/2
)
end
end

@doc """
Calculates total charged amount so far for a commission.
"""
def charged_amount(
%User{id: user_id, roles: roles} = actor,
%Commission{client_id: client_id} = comm,
current_user_member?
)
when user_id != client_id and current_user_member? == false do
if :admin in roles || :mod in roles do
charged_amount(actor, comm, true)
else
{:error, :unauthorized}
end
end

def charged_amount(_, %Commission{} = commission, _) 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
Money.add(acc, event.invoice.total_charged)
else
acc
end
end
)
else
charged =
from(
i in Invoice,
where:
i.commission_id == ^commission.id and
i.status in [:succeeded, :released],
select: i.total_charged
)
|> Repo.all()

Enum.reduce(
charged,
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.
Expand Down
12 changes: 9 additions & 3 deletions lib/banchan/payments/currency.ex
Original file line number Diff line number Diff line change
Expand Up @@ -265,10 +265,16 @@ defmodule Banchan.Payments.Currency do
"""
def print_money(%Money{} = money, symbol \\ true) do
if symbol do
{amt, prefix} =
case Money.to_string(money, symbol: false) do
"-" <> amt -> {amt, "-"}
other -> {other, ""}
end

case Money.Currency.symbol(money) do
"$" -> currency_symbol(money.currency) <> Money.to_string(money, symbol: false)
"" -> currency_symbol(money.currency) <> " " <> Money.to_string(money, symbol: false)
" " -> currency_symbol(money.currency) <> " " <> Money.to_string(money, symbol: false)
"$" -> prefix <> currency_symbol(money.currency) <> amt
"" -> prefix <> currency_symbol(money.currency) <> " " <> amt
" " -> prefix <> currency_symbol(money.currency) <> " " <> amt
_ -> Money.to_string(money, symbol: true)
end
else
Expand Down
3 changes: 3 additions & 0 deletions lib/banchan/payments/invoice.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ defmodule Banchan.Payments.Invoice do
field(:platform_fee, Money.Ecto.Composite.Type)
field(:total_charged, Money.Ecto.Composite.Type)
field(:total_transferred, Money.Ecto.Composite.Type)
field(:tax, Money.Ecto.Composite.Type)
field(:discounts, Money.Ecto.Composite.Type)
field(:shipping, Money.Ecto.Composite.Type)
field(:payout_available_on, :utc_datetime)
field(:paid_on, :utc_datetime)
field(:required, :boolean)
Expand Down
4 changes: 3 additions & 1 deletion lib/banchan/payments/notifications.ex
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,9 @@ defmodule Banchan.Payments.Notifications do
:receipt,
invoice: invoice |> Repo.preload([:event]),
commission: commission,
tipped: Commissions.tipped_amount(client, commission, false)
tipped: Commissions.tipped_amount(client, commission, false),
taxes: Commissions.taxed_amount(client, commission, false),
charged: Commissions.charged_amount(client, commission, false)
)
|> Mailer.deliver()
end
Expand Down
28 changes: 25 additions & 3 deletions lib/banchan/payments/payments.ex
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,11 @@ defmodule Banchan.Payments do
platform_fee: Money.new(0, invoice.amount.currency),
status: :succeeded
})
|> Ecto.Changeset.put_change(:total_charged, Money.new(0, invoice.amount.currency))
|> Ecto.Changeset.put_change(:total_transferred, Money.new(0, invoice.amount.currency))
|> Ecto.Changeset.put_change(:tax, Money.new(0, invoice.amount.currency))
|> Ecto.Changeset.put_change(:discounts, Money.new(0, invoice.amount.currency))
|> Ecto.Changeset.put_change(:shipping, Money.new(0, invoice.amount.currency))
end)
|> Ecto.Multi.run(:finalize, fn _, %{updated_invoice: invoice} ->
client = Repo.reload!(%User{id: invoice.client_id})
Expand Down Expand Up @@ -962,16 +967,30 @@ defmodule Banchan.Payments do
Webhook handler for when a payment has been successfully processed by Stripe.
"""
def process_payment_succeeded!(session) do
%{
total_details: %{
amount_tax: tax_amt,
amount_discount: discount_amt,
amount_shipping: shipping_amt
},
amount_total: amt
} = session

{:ok,
%{charges: %{data: [%{id: charge_id, balance_transaction: txn_id, transfer: transfer}]}}} =
stripe_mod().retrieve_payment_intent(session.payment_intent, %{}, [])
%{
currency: curr,
charges: %{data: [%{id: charge_id, balance_transaction: txn_id, transfer: transfer}]}
}} = stripe_mod().retrieve_payment_intent(session.payment_intent, %{}, [])

{:ok, %{created: paid_on, available_on: available_on, amount: amt, currency: curr}} =
{:ok, %{created: paid_on, available_on: available_on}} =
stripe_mod().retrieve_balance_transaction(txn_id, [])

{:ok, transfer} = stripe_mod().retrieve_transfer(transfer)

total_charged = Money.new(amt, String.to_atom(String.upcase(curr)))
tax = Money.new(tax_amt, String.to_atom(String.upcase(curr)))
discounts = Money.new(discount_amt, String.to_atom(String.upcase(curr)))
shipping = Money.new(shipping_amt, String.to_atom(String.upcase(curr)))
final_transfer_txn = transfer.destination_payment.balance_transaction

total_transferred =
Expand Down Expand Up @@ -999,6 +1018,9 @@ defmodule Banchan.Payments do
paid_on: paid_on,
total_charged: total_charged,
total_transferred: total_transferred,
tax: tax,
discounts: discounts,
shipping: shipping,
stripe_charge_id: charge_id
]
)
Expand Down
15 changes: 13 additions & 2 deletions lib/banchan_web/controllers/email/commissions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,21 @@ defmodule BanchanWeb.Email.Commissions do
</tr>
{/for}
<tr>
<td colspan="2">Total Invoiced</td>
<td colspan="2">Subtotal</td>
<td>{Payments.print_money(estimate)}</td>
</tr>
<tr>
<td colspan="2">Additional Tip</td>
<td>{Payments.print_money(tipped)}</td>
</tr>
<tr>
<td colspan="2">Tax/VAT</td>
<td>{Payments.print_money(@taxes)}</td>
</tr>
<tr>
<td colspan="2">Total</td>
<td>{Payments.print_money(@charged)}</td>
</tr>
</table>
"""
end
Expand Down Expand Up @@ -72,8 +80,11 @@ defmodule BanchanWeb.Email.Commissions do
end} - #{Payments.print_money(Money.multiply(item.amount, item.count))}
#{item.description}
""" end)}
Total Invoiced: #{Payments.print_money(estimate)}
Subtotal: #{Payments.print_money(estimate)}
Additional Tip: #{Payments.print_money(tipped)}
Tax/VAT: #{Payments.print_money(assigns.taxes)}
----------------------------
Total: #{Payments.print_money(assigns.charged)}
"""
end
end
27 changes: 23 additions & 4 deletions lib/banchan_web/live/commission_live/components/balance_box.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ defmodule BanchanWeb.CommissionLive.Components.BalanceBox do
prop deposited, :struct
prop invoiced, :struct
prop tipped, :struct
prop tax, :struct
prop total, :struct

data estimate_amt, :list
data deposited_amt, :list
Expand Down Expand Up @@ -44,11 +46,19 @@ defmodule BanchanWeb.CommissionLive.Components.BalanceBox do
</div>
</div>
<div class="flex flex-row items-center">
<div class="font-medium grow">Deposited:</div>
<div class="font-medium grow">Previously Deposited:</div>
<div class="font-medium">
{Payments.print_money(@deposited_amt)}
{Payments.print_money(@deposited_amt |> Money.multiply(-1))}
</div>
</div>
{#if @tax}
<div class="flex flex-row items-center">
<div class="font-medium grow">Tax:</div>
<div class="font-medium">
{Payments.print_money(@tax)}
</div>
</div>
{/if}
{#if @tipped}
<div class="flex flex-row items-center">
<div class="font-medium grow">Tipped:</div>
Expand All @@ -67,12 +77,21 @@ defmodule BanchanWeb.CommissionLive.Components.BalanceBox do
</div>
<div class={
"font-bold",
"text-primary": @remaining_amt.amount > 0,
"text-error": @remaining_amt.amount < 0
"text-primary": !@total && @remaining_amt.amount > 0,
"text-error": !@total && @remaining_amt.amount < 0
}>
{Payments.print_money(@remaining_amt)}
</div>
</div>
{#if @total}
<div class="divider" />
<div class="flex flex-row items-center">
<div class="font-bold grow">Total Paid:</div>
<div class="font-bold text-primary">
{Payments.print_money(@total)}
</div>
</div>
{/if}
</div>
{#else}
<div class="flex flex-row">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,11 +266,14 @@ defmodule BanchanWeb.CommissionLive.Components.InvoiceBox do
{!-- # NOTE: Older invoices don't have these fields, so we need to check for them here. --}
{#if @event.invoice.line_items && @event.invoice.deposited}
<Summary line_items={@event.invoice.line_items} />
<div class="divider" />
<BalanceBox
id={@id <> "-balance-box"}
line_items={@event.invoice.line_items}
deposited={@event.invoice.deposited}
invoiced={@event.invoice.amount}
tax={@event.invoice.tax}
total={@event.invoice.total_charged}
tipped={@event.invoice.final && @event.invoice.tip}
/>
<div class="divider" />
Expand Down Expand Up @@ -319,7 +322,7 @@ defmodule BanchanWeb.CommissionLive.Components.InvoiceBox do
{#else}
<div class="stat-title">Payment Requested</div>
<div class="stat-value">{Payments.print_money(@event.invoice.amount)}</div>
<div class="stat-desc">Waiting for Payment</div>
<div class="stat-desc">Waiting for Payment (Subtotal)</div>
{#if @current_user_member?}
<div class="stat-actions">
<Button
Expand Down Expand Up @@ -360,7 +363,7 @@ defmodule BanchanWeb.CommissionLive.Components.InvoiceBox do
<div class="stat-desc">You'll need to submit a new invoice.</div>
{#match :succeeded}
<div class="stat-title">Payment Succeeded</div>
<div class="stat-value">{Payments.print_money(@event.invoice.amount)}</div>
<div class="stat-value">{Payments.print_money(@event.invoice.total_charged)}</div>
{#if @event.invoice.tip.amount > 0}
<div class="stat-desc">Tip: +{Payments.print_money(@event.invoice.tip)}
({estimate = Commissions.line_item_estimate(@commission.line_items)
Expand All @@ -386,7 +389,7 @@ defmodule BanchanWeb.CommissionLive.Components.InvoiceBox do
</div>
{#match :released}
<div class="stat-title">Payment Released to Studio</div>
<div class="stat-value">{Payments.print_money(@event.invoice.amount)}</div>
<div class="stat-value">{Payments.print_money(@event.invoice.total_charged)}</div>
{#if @event.invoice.tip.amount > 0}
<div class="stat-desc">Tip: +{Payments.print_money(@event.invoice.tip)}
({estimate = Commissions.line_item_estimate(@commission.line_items)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
defmodule Banchan.Repo.Migrations.AddTaxToInvoice do
use Ecto.Migration

def change do
alter table(:commission_invoices) do
add :tax, :money_with_currency
add :discounts, :money_with_currency
add :shipping, :money_with_currency
end
end
end
13 changes: 11 additions & 2 deletions test/banchan/commissions/commissions_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,10 @@ defmodule Banchan.CommissionsTest do
assert payment_intent_id == id

{:ok,
%{charges: %{data: [%{id: charge_id, balance_transaction: txn_id, transfer: trans_id}]}}}
%{
currency: "usd",
charges: %{data: [%{id: charge_id, balance_transaction: txn_id, transfer: trans_id}]}
}}
end)
|> expect(:retrieve_transfer, fn id ->
assert trans_id == id
Expand Down Expand Up @@ -345,7 +348,13 @@ defmodule Banchan.CommissionsTest do
assert :ok ==
Payments.process_payment_succeeded!(%Stripe.Session{
id: sess_id,
payment_intent: payment_intent_id
payment_intent: payment_intent_id,
total_details: %{
amount_tax: 0,
amount_discount: 0,
amount_shipping: 0
},
amount_total: amount.amount + tip.amount
})

assert_enqueued(worker: ExpiredInvoicePurger, args: %{invoice_id: invoice.id})
Expand Down
Loading

0 comments on commit fd593c6

Please sign in to comment.