defmodule DaProductApp.Transactions.ReqPay do @moduledoc """ ReqPay aggregate for UPI payment requests. Stores normalized data from ReqPay/RespPay with payment transaction information. Handles both domestic and international payment requests. """ use Ecto.Schema import Ecto.Changeset alias DaProductApp.Transactions.{Transaction, TransactionEvent} alias DaProductApp.Partners.{Partner, Merchant} schema "req_pays" do # Core UPI fields for payment request field :txn_id, :string field :msg_id, :string field :org_id, :string field :ref_id, :string # Reference ID for the payment field :ref_url, :string # Reference URL if any field :payer_addr, :string field :payee_addr, :string field :payer_name, :string field :payee_name, :string field :network_inst_id, :string field :payment_type, :string # "QR", "INTENT", "COLLECT" field :payment_purpose, :string # Purpose of payment # Amount fields field :amount, :decimal field :currency, :string # Response fields field :response_code, :string field :response_message, :string field :payment_status, :string # PENDING, SUCCESS, FAILURE, EXPIRED field :settlement_status, :string # SETTLED, PENDING, FAILED field :npci_txn_id, :string # NPCI generated transaction ID field :rrn, :string # Retrieval Reference Number # International fields field :corridor, :string # "SINGAPORE", "UAE", "USA" field :partner_txn_id, :string # Partner's transaction reference field :fx_rate, :decimal # Exchange rate if international field :base_amount, :decimal # Amount in merchant's local currency field :base_currency, :string # Merchant's currency # QR specific fields field :qr_string, :string # QR code string if QR payment field :qr_medium, :string # DYNAMIC, STATIC field :merchant_category_code, :string field :merchant_vpa, :string # Payee account details (for RespChkTxn Ref element) field :payee_ifsc, :string # Payee bank IFSC code field :payee_account_number, :string # Payee account number field :payee_account_type, :string # SAVINGS, CURRENT field :invoice_number, :string # Invoice/approval number # Status and tracking field :status, :string # PENDING, PROCESSED, FAILED field :validation_type, :string # DOMESTIC, INTERNATIONAL field :error_code, :string field :error_message, :string # Timestamps field :npci_request_received_at, :utc_datetime # When NPCI request was received field :npci_response_sent_at, :utc_datetime # When PSP response was sent field :processing_duration_ms, :integer # Processing time in milliseconds field :paid_at, :utc_datetime # When payment was processed # Associations belongs_to :transaction, Transaction, foreign_key: :transaction_id belongs_to :partner, Partner, foreign_key: :partner_id belongs_to :merchant, Merchant, foreign_key: :merchant_id has_many :events, TransactionEvent, foreign_key: :req_pay_id timestamps(type: :utc_datetime) end @required ~w(msg_id org_id amount currency status validation_type)a @optional ~w( txn_id ref_id ref_url payer_addr payee_addr payer_name payee_name network_inst_id payment_type payment_purpose response_code response_message payment_status settlement_status npci_txn_id rrn corridor partner_txn_id fx_rate base_amount base_currency qr_string qr_medium merchant_category_code merchant_vpa payee_ifsc payee_account_number payee_account_type invoice_number error_code error_message npci_request_received_at npci_response_sent_at processing_duration_ms paid_at transaction_id partner_id merchant_id )a def changeset(struct, attrs) do struct |> cast(attrs, @required ++ @optional) |> validate_required(@required) |> validate_inclusion(:status, ~w(PENDING PROCESSED FAILED)) |> validate_inclusion(:validation_type, ~w(DOMESTIC INTERNATIONAL)) |> validate_inclusion(:payment_status, ~w(PENDING SUCCESS FAILURE EXPIRED DEEMED REVERSED)) |> validate_inclusion(:payment_type, ~w(QR INTENT COLLECT REVERSAL)) |> validate_length(:msg_id, max: 35) |> validate_length(:txn_id, max: 35) |> validate_format(:payer_addr, ~r/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+$/, message: "Invalid UPI ID format") |> validate_format(:payee_addr, ~r/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+$/, message: "Invalid UPI ID format") |> validate_number(:amount, greater_than_or_equal_to: 0) |> unique_constraint(:msg_id) end @doc """ Changeset for international payment requests with FX validation """ def international_changeset(struct, attrs) do struct |> changeset(attrs) |> put_change(:validation_type, "INTERNATIONAL") |> validate_international_fields() end @doc """ Changeset for domestic payment requests """ def domestic_changeset(struct, attrs) do struct |> changeset(attrs) |> put_change(:validation_type, "DOMESTIC") |> put_change(:corridor, nil) |> put_change(:fx_rate, nil) end # Private validation functions defp validate_international_fields(changeset) do changeset |> validate_required([:corridor]) |> validate_inclusion(:corridor, ~w(SINGAPORE UAE USA)) |> validate_currency_fields() end defp validate_currency_fields(changeset) do changeset |> validate_inclusion(:base_currency, ~w(USD SGD AED EUR GBP)) |> validate_number(:fx_rate, greater_than: 0) |> validate_number(:base_amount, greater_than: 0) end @doc """ Get status counts for analytics """ def status_counts do %{ "pending" => 0, "processed" => 0, "failed" => 0 } end @doc """ Get validation type counts """ def validation_type_counts do %{ "domestic" => 0, "international" => 0 } end @doc """ Get payment status counts """ def payment_status_counts do %{ "pending" => 0, "success" => 0, "failure" => 0, "expired" => 0, "deemed" => 0, "reversed" => 0 } end end