cover/Elixir.DaProductApp.Transactions.ReqPay.html

1 defmodule DaProductApp.Transactions.ReqPay do
2 @moduledoc """
3 ReqPay aggregate for UPI payment requests.
4
5 Stores normalized data from ReqPay/RespPay with payment transaction information.
6 Handles both domestic and international payment requests.
7 """
8 use Ecto.Schema
9 import Ecto.Changeset
10
11 alias DaProductApp.Transactions.{Transaction, TransactionEvent}
12 alias DaProductApp.Partners.{Partner, Merchant}
13
14 232 schema "req_pays" do
15 # Core UPI fields for payment request
16 field :txn_id, :string
17 field :msg_id, :string
18 field :org_id, :string
19 field :ref_id, :string # Reference ID for the payment
20 field :ref_url, :string # Reference URL if any
21 field :payer_addr, :string
22 field :payee_addr, :string
23 field :payer_name, :string
24 field :payee_name, :string
25 field :network_inst_id, :string
26 field :payment_type, :string # "QR", "INTENT", "COLLECT"
27 field :payment_purpose, :string # Purpose of payment
28
29 # Amount fields
30 field :amount, :decimal
31 field :currency, :string
32
33 # Response fields
34 field :response_code, :string
35 field :response_message, :string
36 field :payment_status, :string # PENDING, SUCCESS, FAILURE, EXPIRED
37 field :settlement_status, :string # SETTLED, PENDING, FAILED
38 field :npci_txn_id, :string # NPCI generated transaction ID
39 field :rrn, :string # Retrieval Reference Number
40
41 # International fields
42 field :corridor, :string # "SINGAPORE", "UAE", "USA"
43 field :partner_txn_id, :string # Partner's transaction reference
44 field :fx_rate, :decimal # Exchange rate if international
45 field :base_amount, :decimal # Amount in merchant's local currency
46 field :base_currency, :string # Merchant's currency
47
48 # QR specific fields
49 field :qr_string, :string # QR code string if QR payment
50 field :qr_medium, :string # DYNAMIC, STATIC
51 field :merchant_category_code, :string
52 field :merchant_vpa, :string
53
54 # Status and tracking
55 field :status, :string # PENDING, PROCESSED, FAILED
56 field :validation_type, :string # DOMESTIC, INTERNATIONAL
57 field :error_code, :string
58 field :error_message, :string
59
60 # Timestamps
61 field :npci_request_received_at, :utc_datetime # When NPCI request was received
62 field :npci_response_sent_at, :utc_datetime # When PSP response was sent
63 field :processing_duration_ms, :integer # Processing time in milliseconds
64 field :paid_at, :utc_datetime # When payment was processed
65
66 # Associations
67 belongs_to :transaction, Transaction, foreign_key: :transaction_id
68 belongs_to :partner, Partner, foreign_key: :partner_id
69 belongs_to :merchant, Merchant, foreign_key: :merchant_id
70 has_many :events, TransactionEvent, foreign_key: :req_pay_id
71
72 timestamps(type: :utc_datetime)
73 end
74
75 @required ~w(msg_id org_id amount currency status validation_type)a
76 @optional ~w(
77 txn_id ref_id ref_url payer_addr payee_addr payer_name payee_name
78 network_inst_id payment_type payment_purpose response_code response_message
79 payment_status settlement_status npci_txn_id rrn corridor partner_txn_id
80 fx_rate base_amount base_currency qr_string qr_medium merchant_category_code
81 merchant_vpa error_code error_message npci_request_received_at
82 npci_response_sent_at processing_duration_ms paid_at transaction_id
83 partner_id merchant_id
84 )a
85
86 def changeset(struct, attrs) do
87 struct
88 |> cast(attrs, @required ++ @optional)
89 |> validate_required(@required)
90 |> validate_inclusion(:status, ~w(PENDING PROCESSED FAILED))
91 |> validate_inclusion(:validation_type, ~w(DOMESTIC INTERNATIONAL))
92 |> validate_inclusion(:payment_status, ~w(PENDING SUCCESS FAILURE EXPIRED DEEMED REVERSED))
93 |> validate_inclusion(:payment_type, ~w(QR INTENT COLLECT))
94 |> validate_length(:msg_id, max: 35)
95 |> validate_length(:txn_id, max: 35)
96 |> validate_format(:payer_addr, ~r/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+$/, message: "Invalid UPI ID format")
97 |> validate_format(:payee_addr, ~r/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+$/, message: "Invalid UPI ID format")
98 |> validate_number(:amount, greater_than: 0)
99 29 |> unique_constraint(:msg_id)
100 end
101
102 @doc """
103 Changeset for international payment requests with FX validation
104 """
105 def international_changeset(struct, attrs) do
106 struct
107 |> changeset(attrs)
108 |> put_change(:validation_type, "INTERNATIONAL")
109
:-(
|> validate_international_fields()
110 end
111
112 @doc """
113 Changeset for domestic payment requests
114 """
115 def domestic_changeset(struct, attrs) do
116 struct
117 |> changeset(attrs)
118 |> put_change(:validation_type, "DOMESTIC")
119 |> put_change(:corridor, nil)
120
:-(
|> put_change(:fx_rate, nil)
121 end
122
123 # Private validation functions
124
125 defp validate_international_fields(changeset) do
126 changeset
127 |> validate_required([:corridor])
128 |> validate_inclusion(:corridor, ~w(SINGAPORE UAE USA))
129
:-(
|> validate_currency_fields()
130 end
131
132 defp validate_currency_fields(changeset) do
133 changeset
134 |> validate_inclusion(:base_currency, ~w(USD SGD AED EUR GBP))
135 |> validate_number(:fx_rate, greater_than: 0)
136
:-(
|> validate_number(:base_amount, greater_than: 0)
137 end
138
139 @doc """
140 Get status counts for analytics
141 """
142 def status_counts do
143
:-(
%{
144 "pending" => 0,
145 "processed" => 0,
146 "failed" => 0
147 }
148 end
149
150 @doc """
151 Get validation type counts
152 """
153 def validation_type_counts do
154
:-(
%{
155 "domestic" => 0,
156 "international" => 0
157 }
158 end
159
160 @doc """
161 Get payment status counts
162 """
163 def payment_status_counts do
164
:-(
%{
165 "pending" => 0,
166 "success" => 0,
167 "failure" => 0,
168 "expired" => 0,
169 "deemed" => 0,
170 "reversed" => 0
171 }
172 end
173 end
Line Hits Source