Packages
ledger_tp2
0.1.0
Sistema de libro contable para transacciones multi-moneda desarrollado como trabajo práctico para la materia Taller de Programación.
Current section
Files
Jump to
Current section
Files
lib/ledger/transaccion.ex
defmodule Ledger.Transaccion do
@moduledoc """
Módulo para manejar transacciones en el sistema Ledger.
Proporciona funciones para crear, listar y gestionar transacciones.
"""
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias Ledger.{Repo, Usuario, Moneda, FileHandler}
schema "transacciones" do
field(:tipo, :string)
field(:monto, :float)
belongs_to(:cuenta_origen, Usuario)
belongs_to(:cuenta_destino, Usuario)
belongs_to(:moneda_origen, Moneda)
belongs_to(:moneda_destino, Moneda)
field(:precio_moneda_origen, :float)
field(:precio_moneda_destino, :float)
timestamps()
end
@doc """
Da de alta una cuenta para un usuario con una moneda y un monto inicial.
## Parámetros
- `usuario`: ID del usuario.
- `moneda`: ID de la moneda.
- `monto`: Monto inicial para la cuenta (debe ser positivo).
## Retorno
- `{:ok, transaccion}` si la cuenta fue dada de alta exitosamente.
- `{:error, razón}` si ocurrió algún error durante el alta.
"""
def alta_cuenta(usuario, moneda, monto) do
cond do
monto <= 0 ->
{:error, "El monto debe ser un número positivo"}
monto > 0 ->
alta_cuenta_interna(usuario, moneda, monto)
end
end
@doc """
Realiza una transferencia entre dos cuentas de usuarios.
## Parámetros
- `cuenta_origen_id`: ID de la cuenta de origen (usuario).
- `cuenta_destino_id`: ID de la cuenta de destino (usuario).
- `moneda_id`: ID de la moneda utilizada en la transferencia.
- `monto`: Monto a transferir (debe ser positivo).
## Retorno
- `{:ok, transaccion}` si la transferencia fue realizada exitosamente.
- `{:error, razón}` si ocurrió algún error durante la transferencia.
"""
def realizar_transferencia(cuenta_origen_id, cuenta_destino_id, moneda_id, monto) do
with :ok <- validar_transferencia(cuenta_origen_id, cuenta_destino_id, moneda_id) do
changeset =
%__MODULE__{}
|> changeset_crear(%{
tipo: "transferencia",
cuenta_origen_id: cuenta_origen_id,
cuenta_destino_id: cuenta_destino_id,
moneda_origen_id: moneda_id,
monto: monto,
precio_moneda_origen: Moneda.obtener_precio(moneda_id)
})
case Repo.insert(changeset) do
{:ok, transaccion} ->
{:ok, transaccion}
{:error, _razon} ->
{:error, FileHandler.extraer_error(changeset)}
end
end
end
@doc """
Realiza un swap entre dos monedas en la misma cuenta de un usuario.
## Parámetros
- `cuenta_id`: ID de la cuenta (usuario).
- `moneda_origen_id`: ID de la moneda de origen.
- `moneda_destino_id`: ID de la moneda de destino.
- `monto`: Monto a swapear (debe ser positivo).
## Retorno
- `{:ok, transaccion}` si el swap fue realizado exitosamente.
- `{:error, razón}` si ocurrió algún error durante el swap.
"""
def realizar_swap(
cuenta_id,
moneda_origen_id,
moneda_destino_id,
monto
) do
with {:ok, _usuario} <- Usuario.obtener_usuario(cuenta_id),
{:ok, _moneda_origen} <- Moneda.obtener_moneda(moneda_origen_id),
{:ok, _moneda_destino} <- Moneda.obtener_moneda(moneda_destino_id),
:ok <- existe_cuenta?(cuenta_id, moneda_origen_id) do
changeset =
%__MODULE__{}
|> changeset_crear(%{
tipo: "swap",
cuenta_origen_id: cuenta_id,
moneda_origen_id: moneda_origen_id,
moneda_destino_id: moneda_destino_id,
monto: monto,
precio_moneda_origen: Moneda.obtener_precio(moneda_origen_id),
precio_moneda_destino: Moneda.obtener_precio(moneda_destino_id)
})
case Repo.insert(changeset) do
{:ok, transaccion} ->
{:ok, transaccion}
{:error, _razon} ->
{:error, FileHandler.extraer_error(changeset)}
end
end
end
@doc """
Muestra la información de una transacción.
## Parámetros
- `id`: ID de la transacción.
- `archivo`: Archivo donde se mostrará la información o "stdout" para mostrar por salida estándar.
## Retorno
- `{:ok, informacion}` si la información fue mostrada exitosamente.
- `{:error, razón}` si ocurrió algún error al obtener la transacción.
"""
def ver_transaccion(id, archivo) do
with {:ok, transaccion} <- obtener_transaccion(id) do
{:ok, FileHandler.mostrar_transaccion(transaccion, archivo)}
end
end
@doc """
Deshace una transacción.
## Parámetros
- `transaccion_id`: ID de la transacción a deshacer.
## Retorno
- `{:ok, transaccion}` si la transacción fue deshecha exitosamente.
- `{:error, razón}` si ocurrió algún error al deshacer la transacción.
"""
def deshacer_transaccion(transaccion_id) do
with {:ok, t} <- obtener_transaccion(transaccion_id),
:ok <- es_la_ultima_transaccion_de_usuario?(transaccion_id, t.cuenta_origen_id) do
case t.tipo do
"alta" ->
{:error, "No se puede deshacer un alta de cuenta"}
"swap" ->
case existe_cuenta?(t.cuenta_origen_id, t.moneda_destino_id) do
{:error, _} -> alta_cuenta_interna(t.cuenta_origen_id, t.moneda_destino_id, 0)
:ok -> nil
end
realizar_swap(t.cuenta_origen_id, t.moneda_destino_id, t.moneda_origen_id, t.monto)
"transferencia" ->
with :ok <-
es_la_ultima_transaccion_de_usuario?(transaccion_id, t.cuenta_destino_id) do
case existe_cuenta?(t.cuenta_destino_id, t.moneda_origen_id) do
{:error, _} -> alta_cuenta_interna(t.cuenta_destino_id, t.moneda_id, 0)
:ok -> nil
end
realizar_transferencia(
t.cuenta_destino_id,
t.cuenta_origen_id,
t.moneda_origen_id,
t.monto
)
end
end
end
end
@doc """
Obtiene las transacciones entre dos cuentas.
## Parámetros
- `cuenta_origen_id`: ID de la cuenta de origen.
- `cuenta_destino_id`: ID de la cuenta de destino.
## Comportamiento
- Ambos "" → Todas las transacciones
- Solo origen → Transacciones donde la cuenta es origen
- Solo destino → Transacciones donde la cuenta es destino
- Ambos IDs → Transacciones específicas entre esas cuentas
## Retorno
- `{:ok, transacciones}` si se obtuvieron las transacciones exitosamente.
- `{:error, razón}` si ocurrió algún error al obtener las transacciones.
"""
def obtener_transacciones(cuenta_origen_id, cuenta_destino_id) do
with :ok <- validar_cuenta(cuenta_origen_id),
:ok <- validar_cuenta(cuenta_destino_id) do
query =
from(t in Ledger.Transaccion,
preload: [:cuenta_origen, :cuenta_destino]
)
query =
case {cuenta_origen_id, cuenta_destino_id} do
{"", ""} ->
query
{co, ""} ->
from(t in query,
where: t.cuenta_origen_id == ^co
)
{"", cd} ->
from(t in query,
where: t.cuenta_destino_id == ^cd
)
{co, cd} ->
from(t in query,
where:
t.cuenta_origen_id == ^co and
t.cuenta_destino_id == ^cd
)
end
transacciones = Ledger.Repo.all(query) || []
{:ok, transacciones}
end
end
@doc """
Lista las transacciones entre dos cuentas.
## Parámetros
- `cuenta_origen_id`: ID de la cuenta de origen.
- `cuenta_destino_id`: ID de la cuenta de destino.
- `archivo`: Archivo donde se mostrarán las transacciones.
## Retorno
- `{:ok, transacciones}` si se obtuvieron las transacciones exitosamente.
- `{:error, razón}` si ocurrió algún error al obtener las transacciones.
"""
def listar_transacciones(c1, c2, o) do
with {:ok, transacciones} <- obtener_transacciones(c1, c2) do
Enum.each(transacciones, fn t ->
FileHandler.mostrar_transaccion(t, o)
end)
{:ok, transacciones}
end
end
@doc """
Lista el balance de una cuenta.
## Parámetros
- `cuenta_id`: ID de la cuenta.
- `moneda_id`: ID de la moneda.
- `archivo`: Archivo donde se mostrará el balance.
## Comportamiento
- moneda_id "" → Balance en todas las monedas
- moneda_id específico → Balance solo en esa moneda
## Retorno
- `{:ok, balance}` si se obtuvo el balance exitosamente.
- `{:error, razón}` si ocurrió algún error al obtener el balance.
"""
def listar_balance(cuenta_id, moneda_id, archivo) do
balance = %{}
with {:ok, transacciones_salientes} <- obtener_transacciones(cuenta_id, ""),
{:ok, transacciones_entrantes} <- obtener_transacciones("", cuenta_id) do
balance_actualizado =
(transacciones_salientes ++ transacciones_entrantes)
|> Enum.sort_by(& &1.inserted_at, {:asc, NaiveDateTime})
|> Enum.reduce(balance, fn t, acc -> calcular_balance(acc, t, cuenta_id) end)
if moneda_id != "" do
with {:ok, _moneda} <- Moneda.obtener_moneda(moneda_id) do
balance_en_moneda = cambiar_balance_a_moneda(balance_actualizado, moneda_id)
FileHandler.mostrar_balance(balance_en_moneda, archivo)
{:ok, balance_en_moneda}
end
else
FileHandler.mostrar_balance(balance_actualizado, archivo)
{:ok, balance_actualizado}
end
end
end
defp changeset_crear(transaccion, attrs) do
transaccion
|> cast(attrs, [
:tipo,
:cuenta_origen_id,
:moneda_origen_id,
:monto,
:precio_moneda_origen
])
|> validate_required(
:tipo,
message: "El tipo es obligatorio"
)
|> validate_required(:cuenta_origen_id, message: "La cuenta de origen es obligatoria")
|> validate_required(:moneda_origen_id, message: "La moneda es obligatoria")
|> validate_required(:monto, message: "El monto es obligatorio")
|> validar_segun_tipo(attrs)
end
defp validar_segun_tipo(changeset, attrs) do
tipo = get_field(changeset, :tipo)
validar_segun_tipo(tipo, changeset, attrs)
end
defp validar_segun_tipo("alta", changeset, _attrs) do
changeset
|> validate_number(:monto,
greater_than_or_equal_to: 0,
message: "El monto debe ser un número positivo"
)
|> foreign_key_constraint(:cuenta_origen_id)
|> foreign_key_constraint(:moneda_origen_id)
|> validar_cuenta_no_existe()
end
defp validar_segun_tipo("transferencia", changeset, attrs) do
changeset
|> cast(attrs, [:cuenta_destino_id, :moneda_destino_id])
|> validate_required(:cuenta_destino_id)
|> validate_number(:monto,
greater_than: 0,
message: "El monto debe ser un número positivo"
)
|> foreign_key_constraint(:cuenta_origen_id)
|> foreign_key_constraint(:cuenta_destino_id)
|> foreign_key_constraint(:moneda_origen_id)
|> validar_saldo_sufieciente()
end
defp validar_segun_tipo("swap", changeset, attrs) do
changeset
|> cast(attrs, [:moneda_destino_id, :precio_moneda_destino])
|> validate_number(:monto,
greater_than: 0,
message: "El monto debe ser un número positivo"
)
|> foreign_key_constraint(:cuenta_origen_id)
|> foreign_key_constraint(:moneda_origen_id)
|> foreign_key_constraint(:moneda_destino_id)
|> validar_saldo_sufieciente()
# |> changeset_hasta_seis_decimales([:precio_moneda_destino])
end
defp validar_segun_tipo(_, changeset, _attrs) do
add_error(changeset, :tipo, "Tipo de transacción inválido")
end
defp validar_cuenta_no_existe(changeset) do
usuario_id = get_field(changeset, :cuenta_origen_id)
moneda_id = get_field(changeset, :moneda_origen_id)
if existe_cuenta?(usuario_id, moneda_id) == :ok do
add_error(changeset, :cuenta_origen_id, "Ya existe una cuenta para este usuario y moneda")
else
changeset
end
end
defp validar_saldo_sufieciente(changeset) do
usuario_id = get_field(changeset, :cuenta_origen_id)
moneda_id = get_field(changeset, :moneda_origen_id)
monto = get_field(changeset, :monto)
saldo = calcular_saldo_para_moneda(usuario_id, moneda_id)
if saldo < monto do
add_error(changeset, :monto, "Saldo insuficiente")
else
changeset
end
end
defp validar_transferencia(cuenta_origen_id, cuenta_destino_id, moneda_id) do
with {:ok, _usuario_origen} <- Usuario.obtener_usuario(cuenta_origen_id),
{:ok, _usuario_destino} <- Usuario.obtener_usuario(cuenta_destino_id),
{:ok, _moneda} <- Moneda.obtener_moneda(moneda_id),
:ok <- existe_cuenta?(cuenta_origen_id, moneda_id) do
if cuenta_origen_id == cuenta_destino_id do
{:error, "No se puede realizar una transferencia a la misma cuenta"}
else
case existe_cuenta?(cuenta_destino_id, moneda_id) do
{:error, _} -> alta_cuenta_interna(cuenta_destino_id, moneda_id, 0)
:ok -> nil
end
:ok
end
end
end
defp alta_cuenta_interna(usuario, moneda, monto) do
with {:ok, _usuario} <- Usuario.obtener_usuario(usuario),
{:ok, _moneda} <- Moneda.obtener_moneda(moneda) do
changeset =
%__MODULE__{}
|> changeset_crear(%{
tipo: "alta",
cuenta_origen_id: usuario,
moneda_origen_id: moneda,
monto: monto,
precio_moneda_origen: Moneda.obtener_precio(moneda)
})
case Repo.insert(changeset) do
{:ok, transaccion} ->
{:ok, transaccion}
{:error, _razon} ->
{:error, FileHandler.extraer_error(changeset)}
end
end
end
defp es_la_ultima_transaccion_de_usuario?(transaccion_id, usuario_id) do
id =
Repo.one(
from(t in Ledger.Transaccion,
where: t.cuenta_origen_id == ^usuario_id or t.cuenta_destino_id == ^usuario_id,
order_by: [desc: t.inserted_at, desc: t.id],
limit: 1,
select: t.id
)
)
cond do
# Id es nil si el usuario aún no tiene transacciones
id == nil or id == transaccion_id -> :ok
true -> {:error, "La transacción no es la última del usuario #{usuario_id}"}
end
end
defp existe_cuenta?(usuario_id, moneda_id) do
existe =
Repo.exists?(
from(t in Ledger.Transaccion,
where:
t.tipo == "alta" and t.cuenta_origen_id == ^usuario_id and
t.moneda_origen_id == ^moneda_id
)
)
if existe do
:ok
else
{:error, "No hay una cuenta asociada con esa moneda"}
end
end
defp validar_cuenta(""), do: :ok
defp validar_cuenta(cuenta_id) do
with {:ok, _} <- Usuario.obtener_usuario(cuenta_id) do
:ok
end
end
defp calcular_saldo_para_moneda(cuenta_id, moneda_id) do
with {:ok, transacciones_salientes} <- obtener_transacciones(cuenta_id, ""),
{:ok, transacciones_entrantes} <- obtener_transacciones("", cuenta_id) do
saldo = %{}
(transacciones_salientes ++ transacciones_entrantes)
|> Enum.reduce(saldo, fn t, acc -> calcular_balance(acc, t, cuenta_id) end)
|> Map.get(moneda_id, 0.0)
end
end
defp calcular_balance(balance, t, cuenta_id) do
monto_a_sumar = t.monto * t.precio_moneda_origen / Moneda.obtener_precio(t.moneda_origen_id)
cond do
t.tipo == "alta" ->
balance_actualizado =
Map.update(balance, t.moneda_origen_id, monto_a_sumar, fn monto_actual ->
monto_actual + monto_a_sumar
end)
balance_actualizado
t.tipo == "transferencia" and t.cuenta_origen_id == cuenta_id ->
balance_actualizado =
Map.update(balance, t.moneda_origen_id, -monto_a_sumar, fn monto_actual ->
monto_actual - monto_a_sumar
end)
balance_actualizado
t.tipo == "transferencia" and t.cuenta_destino_id == cuenta_id ->
balance_actualizado =
Map.update(balance, t.moneda_origen_id, monto_a_sumar, fn monto_actual ->
monto_actual + monto_a_sumar
end)
balance_actualizado
t.tipo == "swap" ->
balance_actualizado =
Map.update(balance, t.moneda_origen_id, -t.monto, fn monto_actual ->
monto_actual - monto_a_sumar
end)
|> Map.update(
t.moneda_destino_id,
t.monto * t.precio_moneda_origen / t.precio_moneda_destino,
fn monto_actual ->
monto_actual + t.monto * t.precio_moneda_origen / t.precio_moneda_destino
end
)
balance_actualizado
true ->
balance
end
end
defp cambiar_balance_a_moneda(balance, moneda_id) do
total =
Enum.reduce(balance, 0.0, fn {moneda_actual_id, monto}, acc ->
with {:ok, monto_cambiado} <-
Moneda.cambiar_a_moneda(
monto,
Moneda.obtener_precio(moneda_actual_id),
Moneda.obtener_precio(moneda_id)
) do
acc + monto_cambiado
end
end)
%{moneda_id => total}
end
defp obtener_transaccion(id) do
case Repo.get(Ledger.Transaccion, id) do
nil -> {:error, "Transaccion no encontrada"}
transaccion -> {:ok, transaccion}
end
end
end