defmodule DaProductApp.Transactions do @moduledoc """ Transactions context module - Public API for transaction operations. This follows Phoenix context patterns and encapsulates all transaction-related business logic. It provides a clean interface for controllers, processors, and other parts of the application. Elixir best practices applied: - Context pattern for domain boundaries - Ecto changesets for data validation - Transaction safety for multi-step operations - Comprehensive error handling """ import Ecto.Query, warn: false alias DaProductApp.Repo # Import schemas for generic gateway approach alias DaProductApp.Acquirer.Schemas.{ PosTempTransaction, PosTransaction, PaymentMethod } @doc """ Creates a new temporary transaction for processing. """ def create_temp_transaction(attrs \\ %{}) do %PosTempTransaction{} |> PosTempTransaction.changeset(attrs) |> Repo.insert() end @doc """ Creates a final/permanent transaction record. """ def create_final_transaction(attrs \\ %{}) do %PosTransaction{} |> PosTransaction.changeset(attrs) |> Repo.insert() end @doc """ Deletes a temporary transaction. """ def delete_temp_transaction(temp_transaction_id) do case Repo.get(PosTempTransaction, temp_transaction_id) do nil -> {:error, :not_found} temp_transaction -> case Repo.delete(temp_transaction) do {:ok, _} -> :ok {:error, changeset} -> {:error, changeset} end end end @doc """ Gets a temporary transaction by switch identifiers. """ def get_temp_transaction_by_switch_ids(s_tid, s_mid, s_tid_stan) do Repo.get_by(PosTempTransaction, s_tid: s_tid, s_mid: s_mid, s_tid_stan: s_tid_stan) end @doc """ Updates temporary transaction processing state. """ def update_temp_transaction_state(temp_transaction, state) do temp_transaction |> PosTempTransaction.state_changeset(state) |> Repo.update() end @doc """ Records retry attempt for temporary transaction. """ def increment_temp_transaction_retry(temp_transaction) do temp_transaction |> PosTempTransaction.retry_changeset() |> Repo.update() end # Gateway-specific query functions for the generic approach @doc """ Lists transactions by gateway type for a specific date range. """ def list_transactions_by_gateway(gateway_type, start_date \\ nil, end_date \\ nil) do query = from(t in PosTransaction, where: t.gateway_type == ^gateway_type) query = if start_date do from(t in query, where: fragment("DATE(?)", t.inserted_at) >= ^start_date) else query end query = if end_date do from(t in query, where: fragment("DATE(?)", t.inserted_at) <= ^end_date) else query end Repo.all(query) end @doc """ Lists failed transactions by gateway for retry processing. """ def list_failed_transactions_by_gateway(gateway_type) do from(t in PosTempTransaction, where: t.gateway_type == ^gateway_type and t.gateway_status == "FAILED", order_by: [desc: t.inserted_at] ) |> Repo.all() end @doc """ Gets gateway volume report for a date range. """ def gateway_volume_report(start_date, end_date) do from(t in PosTransaction, where: t.inserted_at >= ^start_date and t.inserted_at <= ^end_date, group_by: t.gateway_type, select: %{ gateway: t.gateway_type, transaction_count: count(t.id), total_amount: sum(t.total_amount), success_rate: fragment("AVG(CASE WHEN gateway_status = 'SUCCESS' THEN 1.0 ELSE 0.0 END)") } ) |> Repo.all() end @doc """ Gets settlement reconciliation report for a specific date. """ def settlement_reconciliation(settlement_date) do from(t in PosTransaction, where: t.settlement_date == ^settlement_date, group_by: [t.gateway_type, t.settlement_status], select: %{ gateway: t.gateway_type, status: t.settlement_status, count: count(t.id), amount: sum(t.total_amount) } ) |> Repo.all() end @doc """ Updates gateway status for a transaction. """ def update_gateway_status(transaction, gateway_status, metadata \\ nil) do updates = %{gateway_status: gateway_status} updates = if metadata, do: Map.put(updates, :metadata, metadata), else: updates transaction |> PosTempTransaction.gateway_changeset(updates) |> Repo.update() end @doc """ Gets transactions pending settlement for a gateway. """ def get_pending_settlements(gateway_type) do from(t in PosTransaction, where: t.gateway_type == ^gateway_type and t.settlement_status == "PENDING", order_by: [desc: t.inserted_at] ) |> Repo.all() end @doc """ Records upstream request for temporary transaction. """ def record_upstream_request(temp_transaction, iso_message) do temp_transaction |> PosTempTransaction.upstream_request_changeset(iso_message) |> Repo.update() end @doc """ Records upstream response for temporary transaction. """ def record_upstream_response(temp_transaction, iso_response) do temp_transaction |> PosTempTransaction.upstream_response_changeset(iso_response) |> Repo.update() end @doc """ Converts temporary transaction to permanent transaction. This is typically done after successful processing. """ def finalize_transaction(temp_transaction, additional_attrs \\ %{}) do Repo.transaction(fn -> # Create permanent transaction pos_transaction_attrs = temp_transaction |> Map.from_struct() |> Map.drop([:__meta__, :id, :processing_state, :retry_count, :last_retry_at, :processing_timeout_at, :original_request_iso, :upstream_request_iso, :upstream_response_iso]) |> Map.merge(additional_attrs) |> Map.put(:status, determine_final_status(temp_transaction)) case create_pos_transaction(pos_transaction_attrs) do {:ok, pos_transaction} -> # Remove temporary transaction case Repo.delete(temp_transaction) do {:ok, _} -> pos_transaction {:error, reason} -> Repo.rollback(reason) end {:error, reason} -> Repo.rollback(reason) end end) end @doc """ Creates a permanent POS transaction. """ def create_pos_transaction(attrs \\ %{}) do %PosTransaction{} |> PosTransaction.changeset(attrs) |> Repo.insert() end @doc """ Gets a POS transaction by ID. """ def get_pos_transaction!(id), do: Repo.get!(PosTransaction, id) @doc """ Gets a POS transaction by switch identifiers. """ def get_pos_transaction_by_switch_ids(s_tid, s_mid, s_tid_stan) do Repo.get_by(PosTransaction, s_tid: s_tid, s_mid: s_mid, s_tid_stan: s_tid_stan) end @doc """ Updates POS transaction status. """ def update_pos_transaction_status(pos_transaction, status) do pos_transaction |> PosTransaction.status_changeset(status) |> Repo.update() end @doc """ Marks POS transaction as settled. """ def settle_pos_transaction(pos_transaction, attrs \\ %{}) do pos_transaction |> PosTransaction.settle_changeset(attrs) |> Repo.update() end @doc """ Gets the next STAN for an acquirer/terminal combination. """ def next_stan(acquirer_id, terminal_id) do Stan.next_stan(acquirer_id, terminal_id) end @doc """ Formats STAN as 6-digit string. """ def format_stan(stan_value), do: Stan.format_stan(stan_value) @doc """ Lists temporary transactions that are stuck/timed out. """ def list_timed_out_temp_transactions(timeout_minutes \\ 15) do timeout_threshold = DateTime.add(DateTime.utc_now(), -timeout_minutes * 60, :second) from(t in PosTempTransaction, where: t.inserted_at < ^timeout_threshold and t.processing_state in ["SENT_TO_UPSTREAM", "WAITING_RESPONSE"], order_by: [asc: t.inserted_at] ) |> Repo.all() end @doc """ Cleans up old temporary transactions (for maintenance). """ def cleanup_old_temp_transactions(hours_old \\ 24) do cutoff_time = DateTime.add(DateTime.utc_now(), -hours_old * 3600, :second) from(t in PosTempTransaction, where: t.inserted_at < ^cutoff_time ) |> Repo.delete_all() end @doc """ Finds transactions for reversal by original transaction data. """ def find_transaction_for_reversal(s_tid, s_mid, original_stan, original_date) do # Look for the original transaction to reverse from(t in PosTransaction, where: t.s_tid == ^s_tid and t.s_mid == ^s_mid and t.s_tid_stan == ^original_stan and t.b_tid_date == ^original_date and t.status in ["APPROVED", "SETTLED"], order_by: [desc: t.inserted_at], limit: 1 ) |> Repo.one() end # Private helper functions defp determine_final_status(temp_transaction) do case temp_transaction.response_code do "00" -> "APPROVED" nil -> "ERROR" _ -> "DECLINED" end end # MPGS Transaction Management - Phase 4 Enhancement @doc """ Gets an MPGS transaction by ID. """ def get_mpgs_transaction(id) do MpgsTransaction |> Repo.get(id) |> Repo.preload([:pos_transaction, :payment_method, :authorizations]) end @doc """ Gets an MPGS transaction by gateway transaction ID. """ def get_mpgs_transaction_by_gateway_id(gateway_transaction_id) do MpgsTransaction |> where([mt], mt.gateway_transaction_id == ^gateway_transaction_id) |> Repo.one() |> case do nil -> nil transaction -> Repo.preload(transaction, [:pos_transaction, :payment_method, :authorizations]) end end end