defmodule UpiSettlement.Fixtures.RealQrTransactions do @moduledoc """ Real transaction fixtures from Mercury PSP platform (May 2026). Captures actual transaction patterns: - Static QR: Mercury Static Restaurant (0.00 AED initial, actual amounts entered at payment) - Dynamic QR: Dubai Fine Dining (AED 2.22 → ₹50.00) - Corridor: UAE international payments """ # ============================================================================ # MERCHANT CREATION FUNCTIONS (Create fresh test merchants) # ============================================================================ @doc """ Creates a test merchant with static QR enabled and returns its ID. """ def get_merchant_for_static_qr do unique_id = :erlang.unique_integer([:positive]) |> to_string() attrs = %{ mid: "STQR#{String.slice(unique_id, 0, 11)}", tid: "T001", status: "ACTIVE", qr_enabled: true } merchant = %UpiCore.Merchants.Merchant{} |> UpiCore.Merchants.Merchant.changeset(attrs) |> UpiCore.Repo.insert!() merchant.id end @doc """ Creates a test merchant with dynamic QR enabled and returns its ID. """ def get_merchant_for_dynamic_qr do unique_id = :erlang.unique_integer([:positive]) |> to_string() attrs = %{ mid: "DYQR#{String.slice(unique_id, 0, 11)}", tid: "T001", status: "ACTIVE", dynamic_qr_enabled: true } merchant = %UpiCore.Merchants.Merchant{} |> UpiCore.Merchants.Merchant.changeset(attrs) |> UpiCore.Repo.insert!() merchant.id end # ============================================================================ # MERCHANT ATTRIBUTE MAPS (converted to keyword lists for insertion) # ============================================================================ def static_qr_merchant_attrs do [ mid: "MERC_STATIC_001", tid: "T001", status: "active" ] end def dynamic_qr_merchant_attrs do [ mid: "MERC_DYN_001", tid: "T001", status: "active" ] end # Legacy map format for reference/testing def static_qr_merchant do %{ name: "Mercury Static Restaurant", vpa: "mercury.static@upi", merchant_id: "MERC_STATIC_001", status: "active", settlement_frequency: "T+1", fund_transfer_days: 2, qr_mode: "02" # STATIC_SECURE } end def dynamic_qr_merchant do %{ name: "Dubai Fine Dining", vpa: "dubai.dining@upi", merchant_id: "MERC_DYN_001", status: "active", settlement_frequency: "T+1", fund_transfer_days: 2, qr_mode: "16" # DYNAMIC_SECURE } end # ============================================================================ # STATIC QR TRANSACTIONS (Mercury Static Restaurant) # Real transaction IDs from May 14-20, 2026 # ============================================================================ # Raw map format for reference def static_qr_txn_1_map do %{ utxn_id: "MER7964aa502ecb078f444cb84c0d5e9e49", payer_vpa: "tester2@upi", payee_vpa: "mercury.static@upi", rrn: "000000000001", amount: Decimal.new("0"), status: "success", inserted_at: DateTime.from_naive!(NaiveDateTime.new!(~D[2026-05-20], ~T[12:49:00]), "Etc/UTC"), corridor: "UAE", initiation_mode: "02", # STATIC_SECURE merchant_id: "MERC_STATIC_001" } end # Keyword list format for insert_transaction def static_qr_txn_1(merchant_id \\ "MERC_STATIC_001") do [ merchant_id: merchant_id, inr_amount: Decimal.new("0"), status: "success", inserted_at: DateTime.from_naive!(NaiveDateTime.new!(~D[2026-05-20], ~T[12:49:00]), "Etc/UTC") ] end def static_qr_txn_2(merchant_id) do [ merchant_id: merchant_id, inr_amount: Decimal.new("0"), status: "success", inserted_at: DateTime.from_naive!(NaiveDateTime.new!(~D[2026-05-14], ~T[01:19:00]), "Etc/UTC") ] end def static_qr_txn_3(merchant_id) do [ merchant_id: merchant_id, inr_amount: Decimal.new("0"), status: "success", inserted_at: DateTime.from_naive!(NaiveDateTime.new!(~D[2026-05-14], ~T[12:40:00]), "Etc/UTC") ] end def static_qr_txn_4(merchant_id) do [ merchant_id: merchant_id, inr_amount: Decimal.new("0"), status: "success", inserted_at: DateTime.from_naive!(NaiveDateTime.new!(~D[2026-05-14], ~T[11:04:00]), "Etc/UTC") ] end def static_qr_txn_5(merchant_id) do [ merchant_id: merchant_id, inr_amount: Decimal.new("0"), status: "success", inserted_at: DateTime.from_naive!(NaiveDateTime.new!(~D[2026-05-14], ~T[10:59:00]), "Etc/UTC") ] end # ============================================================================ # DYNAMIC QR TRANSACTIONS (Dubai Fine Dining) # Real transaction IDs from May 14-15, 2026 # AED 2.22 → ₹50.00 conversion # ============================================================================ def dynamic_qr_txn_1(merchant_id) do [ merchant_id: merchant_id, inr_amount: Decimal.new("50.00"), status: "success", inserted_at: DateTime.from_naive!(NaiveDateTime.new!(~D[2026-05-14], ~T[10:41:00]), "Etc/UTC") ] end def dynamic_qr_txn_2(merchant_id) do [ merchant_id: merchant_id, inr_amount: Decimal.new("50.00"), status: "success", inserted_at: DateTime.from_naive!(NaiveDateTime.new!(~D[2026-05-14], ~T[10:32:00]), "Etc/UTC") ] end def dynamic_qr_txn_3(merchant_id) do [ merchant_id: merchant_id, inr_amount: Decimal.new("50.00"), status: "success", inserted_at: DateTime.from_naive!(NaiveDateTime.new!(~D[2026-05-15], ~T[05:28:00]), "Etc/UTC") ] end def dynamic_qr_txn_4(merchant_id) do [ merchant_id: merchant_id, inr_amount: Decimal.new("50.00"), status: "success", inserted_at: DateTime.from_naive!(NaiveDateTime.new!(~D[2026-05-14], ~T[10:08:00]), "Etc/UTC") ] end # ============================================================================ # STATIC QR WITH VALIDATIONS (for test scenarios) # ============================================================================ def static_qr_with_expiry_valid do %{ qr_id: "QR_STATIC_VALID_001", merchant_id: "MERC_STATIC_001", initiation_mode: "02", status: "active", max_usage_count: 10000, usage_count: 5000, expires_at: DateTime.add(DateTime.utc_now() |> DateTime.truncate(:second), 86400 * 30), # 30 days from now created_at: DateTime.utc_now() |> DateTime.truncate(:second) } end def static_qr_with_expiry_expired do %{ qr_id: "QR_STATIC_EXPIRED_001", merchant_id: "MERC_STATIC_001", initiation_mode: "02", status: "active", max_usage_count: 10000, usage_count: 5000, expires_at: DateTime.add(DateTime.utc_now() |> DateTime.truncate(:second), -86400), # 1 day ago created_at: DateTime.utc_now() |> DateTime.truncate(:second) } end def static_qr_with_usage_limit_exceeded do %{ qr_id: "QR_STATIC_LIMIT_001", merchant_id: "MERC_STATIC_001", initiation_mode: "02", status: "active", max_usage_count: 100, usage_count: 100, # At limit expires_at: DateTime.add(DateTime.utc_now() |> DateTime.truncate(:second), 86400 * 30), created_at: DateTime.utc_now() |> DateTime.truncate(:second) } end def static_qr_inactive do %{ qr_id: "QR_STATIC_INACTIVE_001", merchant_id: "MERC_STATIC_001", initiation_mode: "02", status: "inactive", # Deactivated max_usage_count: 10000, usage_count: 5000, expires_at: DateTime.add(DateTime.utc_now() |> DateTime.truncate(:second), 86400 * 30), created_at: DateTime.utc_now() |> DateTime.truncate(:second) } end # ============================================================================ # FEE CONFIGURATION # ============================================================================ def fee_config_standard do %{ interchange_rate: Decimal.new("0.0015"), # 0.15% switching_flat: Decimal.new("0.25"), # ₹0.25 per transaction psp_fee_rate: Decimal.new("0.005"), # 0.50% gst_rate: Decimal.new("0.18") # 18% on PSP fee } end def fee_config_static_qr do %{ interchange_rate: Decimal.new("0.0010"), # 0.10% (lower) switching_flat: Decimal.new("0.15"), # ₹0.15 (lower) psp_fee_rate: Decimal.new("0.0045"), # 0.45% (margin increase) gst_rate: Decimal.new("0.18") # 18% on PSP fee } end # ============================================================================ # FX RATES # ============================================================================ def fx_rate_uae_inr do %{ from_currency: "AED", to_currency: "INR", rate: Decimal.new("22.5"), # 1 AED = ₹22.50 effective_date: ~D[2026-05-14], expires_at: ~D[2026-06-14] } end # ============================================================================ # HELPER FUNCTIONS FOR NTSL CALCULATION # ============================================================================ @doc "Calculate NTSL with given fee config" def calculate_net_amount(gross_amount, txn_count, fee_config \\ nil) do config = fee_config || fee_config_standard() gross = Decimal.new(to_string(gross_amount)) interchange = Decimal.mult(gross, config.interchange_rate) |> Decimal.round(4) switching = Decimal.mult(config.switching_flat, Decimal.new(txn_count)) |> Decimal.round(4) psp_fee = Decimal.mult(gross, config.psp_fee_rate) |> Decimal.round(4) gst = Decimal.mult(psp_fee, config.gst_rate) |> Decimal.round(4) net = gross |> Decimal.sub(interchange) |> Decimal.sub(switching) |> Decimal.sub(psp_fee) |> Decimal.sub(gst) |> Decimal.round(2) %{ gross: gross, interchange_fee: interchange, switching_fee: switching, psp_fee: psp_fee, gst: gst, net: net } end @doc "Example: Static QR batch from Mercury (5 txns, ₹0 each)" def static_qr_batch_mercury(merchant_id) do [ static_qr_txn_1(merchant_id), static_qr_txn_2(merchant_id), static_qr_txn_3(merchant_id), static_qr_txn_4(merchant_id), static_qr_txn_5(merchant_id) ] end @doc "Example: Dynamic QR batch from Dubai Fine Dining (4 txns, ₹50 each)" def dynamic_qr_batch_dubai(merchant_id) do [ dynamic_qr_txn_1(merchant_id), dynamic_qr_txn_2(merchant_id), dynamic_qr_txn_3(merchant_id), dynamic_qr_txn_4(merchant_id) ] end end