defmodule DaProductApp.Acquirer do @moduledoc """ Acquirer context module - Public API for all acquirer operations. This follows Phoenix context patterns and encapsulates all acquirer-related business logic for any acquirer network (YSP, VISA, MasterCard, etc.). Provides a clean, generic interface for: - Transaction processing (sales, reversals, etc.) - Terminal management - STAN generation - Batch management - Settlement operations """ import Ecto.Query, warn: false alias DaProductApp.Repo alias DaProductApp.Acquirer.Schemas.{ PosTransaction, PosTempTransaction, AcquirerTerminal, AcquirerTerminalStan, PosReversal, AcquirerBatch } # ====================== # TRANSACTION OPERATIONS # ====================== @doc """ Creates a new temporary transaction for processing. """ def create_temp_transaction(attrs \\ %{}) do now = DateTime.utc_now() attrs = Map.merge(attrs, %{created_dateTime: now, updated_dateTime: now}) %PosTempTransaction{} |> PosTempTransaction.changeset(attrs) |> Repo.insert() 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 status. """ def update_temp_transaction_status(temp_transaction, status) do temp_transaction |> PosTempTransaction.status_changeset(status) |> Repo.update() end @doc """ Converts temporary transaction to permanent transaction. """ 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, :status, :retry_count, :error_message, :timeout_at, :original_message]) |> Map.merge(additional_attrs) 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 # Remove any datetime fields that don't exist in schema - Ecto timestamps() handles this attrs = Map.drop(attrs, [:created_dateTime, :updated_dateTime]) %PosTransaction{} |> PosTransaction.changeset(attrs) |> Repo.insert() end @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 """ Finds original transaction for reversal. """ def find_original_transaction(s_tid, s_mid, original_stan, original_date) do 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, order_by: [desc: t.created_dateTime], limit: 1 ) |> Repo.one() end # =================== # REVERSAL OPERATIONS # =================== @doc """ Creates a reversal transaction. """ def create_reversal(attrs \\ %{}) do now = DateTime.utc_now() attrs = Map.merge(attrs, %{created_dateTime: now, updated_dateTime: now}) %PosReversal{} |> PosReversal.changeset(attrs) |> Repo.insert() end @doc """ Processes a reversal against an original transaction. """ def process_reversal(original_transaction, reversal_attrs) do Repo.transaction(fn -> # Create reversal record reversal_attrs = reversal_attrs |> Map.put(:original_transaction_id, original_transaction.id) |> Map.put(:original_s_tid, original_transaction.s_tid) |> Map.put(:original_s_mid, original_transaction.s_mid) |> Map.put(:original_s_stan, original_transaction.s_tid_stan) |> Map.put(:original_b_stan, original_transaction.b_tid_stan) |> Map.put(:original_reference_no, original_transaction.reference_no) |> Map.put(:original_approval_code, original_transaction.approval_code) |> Map.put(:original_amount, original_transaction.total_amount) |> Map.put(:original_date, original_transaction.b_tid_date) |> Map.put(:original_time, original_transaction.b_tid_time) case create_reversal(reversal_attrs) do {:ok, reversal} -> reversal {:error, reason} -> Repo.rollback(reason) end end) end # ==================== # TERMINAL OPERATIONS # ==================== @doc """ Gets terminal configuration by switch identifiers. """ def get_terminal_by_switch_ids(switch_tid, switch_mid) do AcquirerTerminal.find_by_switch_ids(switch_tid, switch_mid) end @doc """ Creates or updates terminal configuration. """ def upsert_terminal(attrs) do now = DateTime.utc_now() attrs = attrs |> Map.put_new(:created_dateTime, now) |> Map.put(:updated_dateTime, now) %AcquirerTerminal{} |> AcquirerTerminal.changeset(attrs) |> Repo.insert() end # =============== # STAN OPERATIONS # =============== @doc """ Gets the next STAN for an acquirer/terminal combination. """ def next_stan(acquirer_id, terminal_id) do AcquirerTerminalStan.next_stan(acquirer_id, terminal_id) end @doc """ Formats STAN as 6-digit string. """ def format_stan(stan_value), do: AcquirerTerminalStan.format_stan(stan_value) # ================ # BATCH OPERATIONS # ================ @doc """ Gets current open batch for a terminal. """ def get_current_batch(acquirer_id, terminal_id) do from(b in AcquirerBatch, where: b.acquirer_id == ^acquirer_id and b.tid == ^terminal_id and b.status == "OPEN", order_by: [desc: b.created_dateTime], limit: 1 ) |> Repo.one() end @doc """ Creates a new batch for a terminal. """ def create_batch(acquirer_id, terminal_id, batch_attrs \\ %{}) do today = Date.utc_today() |> Date.to_string() |> String.replace("-", "") time_now = Time.utc_now() |> Time.to_string() |> String.slice(0..5) |> String.replace(":", "") now = DateTime.utc_now() # Generate next batch number (simple increment, could be more sophisticated) next_batch_no = generate_next_batch_number(acquirer_id, terminal_id) attrs = batch_attrs |> Map.merge(%{ batch_no: next_batch_no, acquirer_id: acquirer_id, tid: terminal_id, batch_date: today, batch_time: time_now, open_date: today, open_time: time_now, status: "OPEN", created_dateTime: now, updated_dateTime: now }) %AcquirerBatch{} |> AcquirerBatch.changeset(attrs) |> Repo.insert() end @doc """ Adds a transaction to a batch and updates totals. """ def add_transaction_to_batch(batch, transaction_type, amount, tip_amount \\ 0) do batch |> AcquirerBatch.update_totals_changeset(transaction_type, amount, tip_amount) |> Repo.update() end @doc """ Closes a batch for settlement. """ def close_batch(batch) do today = Date.utc_today() |> Date.to_string() |> String.replace("-", "") time_now = Time.utc_now() |> Time.to_string() |> String.slice(0..5) |> String.replace(":", "") batch |> AcquirerBatch.changeset(%{ status: "CLOSED", close_date: today, close_time: time_now, updated_dateTime: DateTime.utc_now() }) |> Repo.update() end # ================== # UTILITY FUNCTIONS # ================== @doc """ Lists timed out temporary transactions for cleanup. """ 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.created_dateTime < ^timeout_threshold and t.status in ["CREATED", "VALIDATED", "SENT_TO_UPSTREAM", "WAITING_RESPONSE"], order_by: [asc: t.created_dateTime] ) |> Repo.all() end @doc """ Cleans up old temporary transactions. """ 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.created_dateTime < ^cutoff_time ) |> Repo.delete_all() end # Private helper functions defp generate_next_batch_number(acquirer_id, terminal_id) do today = Date.utc_today() |> Date.to_string() |> String.replace("-", "") last_batch = from(b in AcquirerBatch, where: b.acquirer_id == ^acquirer_id and b.tid == ^terminal_id and b.batch_date == ^today, order_by: [desc: b.batch_no], limit: 1, select: b.batch_no ) |> Repo.one() case last_batch do nil -> "000001" batch_no -> next_num = String.to_integer(batch_no) + 1 String.pad_leading(Integer.to_string(next_num), 6, "0") end end # ============================== # ADDITIONAL FUNCTIONS FOR PHASE 3 # ============================== @doc """ Creates a final transaction from attributes (used by TransactionProcessor). """ def create_transaction(attrs \\ %{}) do create_pos_transaction(attrs) end @doc """ Gets a transaction by switch IDs (used by TransactionProcessor). """ def get_transaction_by_switch_ids(s_tid, s_mid, s_tid_stan) do get_pos_transaction_by_switch_ids(s_tid, s_mid, s_tid_stan) end @doc """ Finds terminal by switch IDs (used by TransactionProcessor). """ def find_terminal_by_switch_ids(switch_tid, switch_mid) do from(t in AcquirerTerminal, where: t.tid == ^switch_tid and t.mid == ^switch_mid and t.status == "ACTIVE" ) |> Repo.one() end @doc """ Updates reversal status. """ def update_reversal_status(%PosReversal{} = reversal, new_status) do reversal |> PosReversal.changeset(%{ status: new_status, updated_dateTime: DateTime.utc_now(), updated_by: "SYSTEM" }) |> Repo.update() end @doc """ Updates transaction response code (final transactions don't have a separate status field). """ def update_transaction_status(%PosTransaction{} = transaction, new_response_code) do transaction |> PosTransaction.changeset(%{ response_code: new_response_code, updated_dateTime: DateTime.utc_now(), updated_by: "SYSTEM" }) |> Repo.update() end @doc """ Gets terminal configuration by terminal ID and acquirer ID. Used by event listeners to get terminal configuration. """ def get_acquirer_terminal(terminal_id, acquirer_id) do from(t in AcquirerTerminal, where: t.tid == ^terminal_id and t.acquirer_id == ^acquirer_id and t.status == "ACTIVE" ) |> Repo.one() end end