Current section
Files
Jump to
Current section
Files
lib/invoicex/document/document.ex
defmodule Invoicex.Document do
@moduledoc """
A helper module for `Document` context.
"""
alias __MODULE__.{Group, Invoice}
alias Ecto.{Changeset, Schema}
@total_sql_part """
, group_data as (
select
array_agg(group_discount || jsonb_build_object('__type__', 'discount')) ||
array_prepend(
invoice_group->'item' || jsonb_build_object('__type__', 'item', 'order', 0),
array_agg(gigroup_item || jsonb_build_object('__type__', 'item'))
) || array_agg(group_tax || jsonb_build_object('__type__', 'tax'))
as items,
invoice_group->'id' as group_id,
invoice.id
from INVOICES_SOURCE invoice
left join lateral unnest(invoice.groups) invoice_group on true
left join lateral jsonb_array_elements(invoice_group->'discounts') group_discount on true
left join lateral jsonb_array_elements(invoice_group->'items') group_item on true
left join lateral jsonb_array_elements(invoice_group->'taxes') group_tax on true
group by invoice.id, invoice_group.invoice_group
), group_total as (
select group_id, id, total.total
from group_data data
left join lateral (
with recursive count_group_total(total, item_order) as (
select 0::numeric, -1
union all
select
case
when (item->>'__type__') = 'discount' and (item->>'is_percent')::boolean = true then cgt.total - cgt.total::decimal / 100 * (item->>'value')::decimal
when (item->>'__type__') = 'discount' then cgt.total - (item->>'value')::decimal
when (item->>'__type__') = 'item' then cgt.total + (item->>'price')::decimal * (item->>'quantity')::integer
when (item->>'__type__') = 'tax' and (item->>'is_percent')::boolean = true then cgt.total + cgt.total::decimal / 100 * (item->>'rate')::decimal
when (item->>'__type__') = 'tax' then cgt.total + (item->>'rate')::decimal
end,
(item->>'order')::integer
from
count_group_total cgt,
group_data data2,
unnest(data2.items) item
where
data.id = data2.id and
data.group_id = data2.group_id and
cgt.item_order + 1 = (item->>'order')::integer
)
select cgt.total
from count_group_total cgt
order by cgt.item_order desc limit 1
) total on true
), sum_group_total as (
select id, sum(total) as groups_total from group_total gt group by id
), invoice_total as (
select sgt.groups_total + item_total.total as total
from sum_group_total sgt
left join lateral (
with recursive count_item_total(total, item_order) as (
select 0::numeric, 0::bigint
union all
select
case
when item.data->>'tax' is null then discounted.value
when (item.data#>>'{tax,is_percent}')::boolean = true then discounted.value + discounted.value::decimal / 100 * (item.data#>>'{tax,value}')::decimal
else discounted.value + (item.data#>>'{tax,value}')::decimal
end,
item.ordinality
from
count_item_total cit,
limited_invoices data,
unnest(data.items) with ordinality as item(data, ordinality)
left join lateral (
select (item.data->>'price')::decimal * (item.data->>'quantity')::integer as value
) full_price on true
left join lateral (
select case
when item.data->>'discount' is null then full_price.value
when (item.data#>>'{discount,is_percent}')::boolean = true then full_price.value - full_price.value::decimal / 100 * (item.data#>>'{discount,value}')::decimal
else full_price.value - (item.data#>>'{discount,value}')::decimal
end as value
) discounted on true
where data.id = sgt.id and cit.item_order + 1 = item.ordinality
)
select total from count_item_total order by item_order desc limit 1
) item_total on true
)
"""
@doc """
Returns a sorted list of discounts, items and taxes for specified `Invoicex.Document.Group`.
"""
@spec build_group_list(Changeset.t() | Schema.t()) :: [Schema.t()]
def build_group_list(%Changeset{} = changeset) do
discounts = Changeset.get_field(changeset, :discounts)
items = Changeset.get_field(changeset, :items)
taxes = Changeset.get_field(changeset, :taxes)
sort_by_order(discounts ++ items ++ taxes)
end
def build_group_list(%Group{} = group) do
sort_by_order(group.discounts ++ group.items ++ group.taxes)
end
@doc """
Returns a sorted list of discounts, items and taxes for specified `Invoicex.Document.Invoice`.
"""
@spec build_list(Changeset.t() | Schema.t()) :: [Schema.t()]
def build_list(%Changeset{} = changeset) do
groups = Changeset.get_field(changeset, :groups)
items = Changeset.get_field(changeset, :items)
sort_by_order(groups ++ items)
end
def build_list(%Invoice{} = invoice) do
sort_by_order(invoice.groups ++ invoice.items)
end
defp sort_by_order(list), do: Enum.sort_by(list, & &1.order)
def total_sql_part(invoices_source \\ nil, type \\ nil) do
if validate_source(invoices_source) do
do_total_sql_part(invoices_source, type)
else
{:error, :invoices_source_invalid}
end
end
defp do_total_sql_part(source, type) when source in ["", nil] and type in [:prefix, nil],
do: String.replace(@total_sql_part, "INVOICES_SOURCE", "invoices")
defp do_total_sql_part(source, :prefix),
do: String.replace(@total_sql_part, "INVOICES_SOURCE", source <> ".invoices")
defp do_total_sql_part(source, :with),
do: String.replace(@total_sql_part, "INVOICES_SOURCE", source)
for char <- [?_ | Enum.to_list(?a..?z)] do
defp validate_source(<<unquote(char)::utf8, rest::binary>>), do: validate_source(rest)
end
defp validate_source(<<>>), do: true
defp validate_source(nil), do: true
defp validate_source(_char), do: false
@doc """
Checks if every item in list from changeset have properly set order.
"""
@spec validate_orders(Changeset.t(), list, map) :: Changeset.t()
def validate_orders(changeset, [], %{empty: empty}),
do: Changeset.add_error(changeset, :list, empty)
def validate_orders(changeset, list, messages) do
orders = Enum.map(list, & &1.order)
count = Enum.count(orders)
{min, max} = Enum.min_max(orders)
do_validate_orders(changeset, count, min, max, messages)
end
defp do_validate_orders(changeset, count, 1, max, _messages) when count == max, do: changeset
defp do_validate_orders(changeset, _count, _min, _max, %{invalid: invalid}),
do: Changeset.add_error(changeset, :list, invalid)
end