defmodule DaProductApp.Settlements.Settlement do @moduledoc """ Schema for settlement records supporting financial reconciliation. Settlements represent the process of transferring aggregated transaction amounts to partners and merchants on a scheduled basis. """ use Ecto.Schema import Ecto.Changeset alias DaProductApp.Partners.{Partner, Merchant} alias DaProductApp.Transactions.Transaction @settlement_types ~w(net merchant partner) @settlement_statuses ~w(pending scheduled processing approved bank_processing completed failed cancelled) @frequencies ~w(daily weekly monthly) @priorities ~w(low normal high urgent) @reconciliation_statuses ~w(pending reconciled disputed failed) schema "settlements" do field :reference_id, :string field :batch_id, :string field :type, :string field :status, :string, default: "pending" field :frequency, :string, default: "daily" field :priority, :string, default: "normal" # Amount details field :settlement_amount, :decimal field :fee_amount, :decimal field :tax_amount, :decimal field :net_amount, :decimal field :transaction_count, :integer, default: 0 field :currency, :string, default: "INR" # Partner/merchant details field :partner_account_number, :string field :partner_ifsc, :string field :partner_bank_name, :string # Timing details field :scheduled_at, :utc_datetime field :started_at, :utc_datetime field :approved_at, :utc_datetime field :bank_processed_at, :utc_datetime field :completed_at, :utc_datetime field :settlement_window_start, :utc_datetime field :settlement_window_end, :utc_datetime # Error handling field :failure_code, :string field :failure_reason, :string field :retry_count, :integer, default: 0 # Reconciliation field :reconciliation_status, :string, default: "pending" field :reconciled_at, :utc_datetime field :reconciliation_reference, :string # Relationships belongs_to :partner, Partner belongs_to :merchant, Merchant has_many :transactions, Transaction, foreign_key: :settlement_id timestamps(type: :utc_datetime) end @required_fields ~w(reference_id type settlement_amount net_amount)a @optional_fields ~w( batch_id status frequency priority fee_amount tax_amount transaction_count currency partner_id merchant_id partner_account_number partner_ifsc partner_bank_name scheduled_at started_at approved_at bank_processed_at completed_at settlement_window_start settlement_window_end failure_code failure_reason retry_count reconciliation_status reconciled_at reconciliation_reference )a def changeset(settlement, attrs) do settlement |> cast(attrs, @required_fields ++ @optional_fields) |> validate_required(@required_fields) |> validate_inclusion(:type, @settlement_types) |> validate_inclusion(:status, @settlement_statuses) |> validate_inclusion(:frequency, @frequencies) |> validate_inclusion(:priority, @priorities) |> validate_inclusion(:reconciliation_status, @reconciliation_statuses) |> validate_number(:settlement_amount, greater_than: 0) |> validate_number(:net_amount, greater_than: 0) |> validate_number(:retry_count, greater_than_or_equal_to: 0) |> validate_length(:currency, is: 3) |> unique_constraint(:reference_id) |> foreign_key_constraint(:partner_id) |> foreign_key_constraint(:merchant_id) |> validate_partner_or_merchant_required() |> calculate_net_amount() end def create_changeset(attrs) do %__MODULE__{} |> changeset(attrs) |> put_change(:reference_id, generate_reference_id()) end defp validate_partner_or_merchant_required(changeset) do partner_id = get_field(changeset, :partner_id) merchant_id = get_field(changeset, :merchant_id) if is_nil(partner_id) and is_nil(merchant_id) do add_error(changeset, :base, "Either partner_id or merchant_id must be present") else changeset end end defp calculate_net_amount(changeset) do settlement_amount = get_field(changeset, :settlement_amount) fee_amount = get_field(changeset, :fee_amount) || Decimal.new(0) tax_amount = get_field(changeset, :tax_amount) || Decimal.new(0) if settlement_amount do net_amount = settlement_amount |> Decimal.sub(fee_amount) |> Decimal.sub(tax_amount) put_change(changeset, :net_amount, net_amount) else changeset end end defp generate_reference_id do "SET" <> (DateTime.utc_now() |> DateTime.to_unix() |> to_string()) <> (:rand.uniform(9999) |> to_string() |> String.pad_leading(4, "0")) end end