defmodule UpiSettlement.Fixtures do @moduledoc """ Test data helpers for UpiSettlement tests. All functions insert directly via `UpiCore.Repo` and allow precise control over timestamps — which is critical for NPCI T-day window boundary tests. """ alias UpiCore.Repo alias UpiCore.Merchants.Merchant alias UpiCore.Transactions.Transaction alias UpiSettlement.{Settlement, HolidayCalendar, Dispute} @doc """ Inserts a minimal `Merchant` record. `mid` and `tid` are required by DB. ## Options - `:mid` — merchant ID string (default: unique random) - `:tid` — terminal ID (default: "T001") - `:status` — "active" | "inactive" (default: "active") """ def insert_merchant(opts \\ []) do unique_id = :erlang.unique_integer([:positive]) |> abs() |> rem(100_000_000) |> to_string() |> String.pad_leading(8, "0") attrs = %{ mid: Keyword.get(opts, :mid, "MER#{unique_id}"), tid: Keyword.get(opts, :tid, "T001"), status: Keyword.get(opts, :status, "ACTIVE") } %Merchant{} |> Merchant.changeset(attrs) |> Repo.insert!() end @doc """ Inserts a `Transaction` record with full control over `inserted_at`. ## Options - `:merchant_id` — required - `:inr_amount` — Decimal (default: Decimal.new("1000.00")) - `:status` — "success" | "failure" | "pending" (default: "success") - `:deemed` — boolean (default: false) - `:settlement_id`— integer | nil (default: nil) - `:inserted_at` — `DateTime.t()` (default: 12 hours before now, within today's window) """ def insert_transaction(opts \\ []) do merchant_id = Keyword.fetch!(opts, :merchant_id) unique_id = :erlang.unique_integer([:positive]) |> to_string() now = DateTime.utc_now() |> DateTime.truncate(:second) default_ts = DateTime.add(now, -12 * 3600, :second) inserted_at = Keyword.get(opts, :inserted_at, default_ts) attrs = %{ current_state: "success", status: Keyword.get(opts, :status, "success"), transaction_type: "DOMESTIC", merchant_id: merchant_id, inr_amount: Keyword.get(opts, :inr_amount, Decimal.new("1000.00")), deemed: Keyword.get(opts, :deemed, false), settlement_id: Keyword.get(opts, :settlement_id, nil), org_txn_id: "TXN#{unique_id}", payer_addr: "user@upi", payee_addr: "merchant@upi" } %Transaction{} |> Transaction.changeset(attrs) |> Ecto.Changeset.put_change(:inserted_at, inserted_at) |> Repo.insert!() end @doc """ Inserts a `Settlement` record directly. ## Options - `:merchant_id` — required (unless `:partner_id` given) - `:partner_id` — alternative to merchant_id - `:status` — default "pending" - `:settlement_amount` — Decimal (default: Decimal.new("5000.00")) - `:window_start` — DateTime - `:window_end` — DateTime """ def insert_settlement(opts \\ []) do unique_id = :erlang.unique_integer([:positive]) |> to_string() attrs = %{ reference_id: "SET#{unique_id}", type: Keyword.get(opts, :type, "merchant"), status: Keyword.get(opts, :status, "pending"), settlement_amount: Keyword.get(opts, :settlement_amount, Decimal.new("5000.00")), currency: "INR", merchant_id: Keyword.get(opts, :merchant_id), partner_id: Keyword.get(opts, :partner_id), settlement_window_start: Keyword.get(opts, :window_start), settlement_window_end: Keyword.get(opts, :window_end), retry_count: Keyword.get(opts, :retry_count, 0) } %Settlement{} |> Settlement.changeset(attrs) |> Repo.insert!() end @doc """ Inserts a `HolidayCalendar` record. ## Options - `:date` — `Date.t()`, required - `:country` — "IN" | "WEEKLY_OFF" | "SG" | "AE" (default: "IN") - `:name` — holiday name (default: "Test Holiday") """ def insert_holiday(opts \\ []) do attrs = %{ date: Keyword.fetch!(opts, :date), country: Keyword.get(opts, :country, "IN"), name: Keyword.get(opts, :name, "Test Holiday") } %HolidayCalendar{} |> HolidayCalendar.changeset(attrs) |> Repo.insert!() end @doc """ Inserts a `Dispute` record. ## Options - `:merchant_id` — required - `:dispute_type` — "chargeback" | "representment" | "refund" (default: "chargeback") - `:amount` — Decimal (default: Decimal.new("500.00")) - `:status` — default "open" - `:window_start` — DateTime - `:window_end` — DateTime - `:deadline_at` — `Date.t()` """ def insert_dispute(opts \\ []) do unique_id = :erlang.unique_integer([:positive]) |> to_string() attrs = %{ dispute_type: Keyword.get(opts, :dispute_type, "chargeback"), status: Keyword.get(opts, :status, "open"), amount: Keyword.get(opts, :amount, Decimal.new("500.00")), merchant_id: Keyword.fetch!(opts, :merchant_id), dispute_ref: Keyword.get(opts, :dispute_ref, "DREF#{unique_id}"), utxn_id: Keyword.get(opts, :utxn_id, "UTXN#{unique_id}"), window_start: Keyword.get(opts, :window_start), window_end: Keyword.get(opts, :window_end), deadline_at: Keyword.get(opts, :deadline_at) } %Dispute{} |> Dispute.changeset(attrs) |> Repo.insert!() end @doc """ Returns a `DateTime` that falls within the NPCI T-day window for the given date. T-day window: (date - 1) 17:30:00 UTC → date 17:29:59 UTC Returns a point 4 hours after the window opens (safe midpoint). """ def within_window(date) do prev_day = Date.add(date, -1) DateTime.new!(prev_day, ~T[21:30:00], "Etc/UTC") end @doc """ Returns a `DateTime` that is BEFORE the T-day window start (i.e. old midnight-UTC). Useful for verifying old midnight-UTC transactions are excluded. """ def before_window(date) do # At midnight on the date itself — before window open of T-1 17:30 UTC DateTime.new!(Date.add(date, -1), ~T[10:00:00], "Etc/UTC") end @doc """ Returns a `DateTime` that is AFTER the T-day window end. """ def after_window(date) do DateTime.new!(date, ~T[18:00:00], "Etc/UTC") end end