cover/Elixir.DaProductAppWeb.InternationalTransactionController.html

1
:-(
defmodule DaProductAppWeb.InternationalTransactionController do
2 @moduledoc """
3 Controller for UPI International transactions.
4
5 Handles the NPCI credit request flow where:
6 1. NPCI has already debited customer in INR
7 2. Your PSP receives the credit request from NPCI
8 3. Your PSP processes FX conversion and credits international partner
9 """
10
:-(
use DaProductAppWeb, :controller
11
12 alias DaProductApp.Transactions.UpiInternationalService
13 alias DaProductApp.ForeignExchange.FxRateService
14 alias DaProductApp.Adapters.SandboxPartner
15
16 action_fallback DaProductAppWeb.FallbackController
17
18 @doc """
19 Process incoming NPCI credit request (ReqPay) for international merchant
20 This is called by NPCI when customer has scanned international QR and paid
21 """
22 def process_npci_request(conn, %{"npci_req_pay" => req_pay_params}) do
23
:-(
case UpiInternationalService.process_npci_credit_request(req_pay_params) do
24 {:ok, transaction} ->
25 conn
26 |> put_status(:created)
27
:-(
|> render(:show, transaction: transaction)
28
29 {:error, {step, reason}} ->
30 conn
31 |> put_status(:unprocessable_entity)
32
:-(
|> json(%{
33 success: false,
34 error: "npci_processing_failed",
35 step: step,
36 reason: reason,
37 message: "Failed to process NPCI credit request"
38 })
39 end
40 end
41
42 @doc """
43 Generate dynamic international QR with FX rates
44 Called by partners when merchants request QR codes
45 """
46 def generate_international_qr(conn, %{"qr_request" => qr_params}) do
47
:-(
with {:ok, fx_data} <- get_fx_conversion(qr_params),
48
:-(
{:ok, qr_response} <- generate_qr_with_partner(qr_params, fx_data) do
49
50 conn
51 |> put_status(:created)
52
:-(
|> json(%{
53 success: true,
54 data: %{
55
:-(
qr_code: qr_response.payload.qr_code,
56
:-(
qr_image_url: qr_response.payload.qr_image_url,
57
:-(
fx_details: qr_response.payload.fx_details,
58
:-(
expires_at: qr_response.payload.expires_at,
59
:-(
corridor: qr_response.payload.corridor
60 }
61 })
62 else
63 {:error, reason} ->
64 conn
65 |> put_status(:unprocessable_entity)
66
:-(
|> json(%{
67 success: false,
68 error: "qr_generation_failed",
69 reason: reason,
70 message: "Failed to generate international QR"
71 })
72 end
73 end
74
75 @doc """
76 Get current FX rates for a corridor
77 """
78 def get_fx_rates(conn, %{"corridor" => corridor, "currency" => currency}) do
79
:-(
case FxRateService.get_current_rate("INR", currency, String.upcase(corridor)) do
80 {:ok, fx_rate} ->
81 conn
82
:-(
|> json(%{
83 success: true,
84 data: %{
85 corridor: corridor,
86 base_currency: "INR",
87 target_currency: currency,
88
:-(
fx_rate: fx_rate.fx_rate,
89
:-(
markup_rate: fx_rate.markup_rate,
90
:-(
effective_from: fx_rate.effective_from,
91
:-(
last_modified: fx_rate.last_modified_ts,
92
:-(
source: fx_rate.source
93 }
94 })
95
96 {:error, reason} ->
97 conn
98 |> put_status(:not_found)
99
:-(
|> json(%{
100 success: false,
101 error: "fx_rate_not_found",
102 reason: reason,
103
:-(
message: "FX rate not available for #{corridor}/#{currency}"
104 })
105 end
106 end
107
108 @doc """
109 Get supported international corridors
110 """
111 def get_corridors(conn, _params) do
112
:-(
corridors = FxRateService.get_supported_corridors()
113
114 conn
115
:-(
|> json(%{
116 success: true,
117 data: %{
118 corridors: corridors,
119 total: map_size(corridors)
120 }
121 })
122 end
123
124 @doc """
125 Check transaction status (used by partners)
126 """
127 def check_status(conn, %{"org_txn_id" => org_txn_id}) do
128
:-(
case UpiInternationalService.get_transaction_by_org_id(org_txn_id) do
129 nil ->
130 conn
131 |> put_status(:not_found)
132
:-(
|> json(%{
133 success: false,
134 error: "transaction_not_found",
135 message: "Transaction not found"
136 })
137
138 transaction ->
139 conn
140
:-(
|> json(%{
141 success: true,
142 data: %{
143
:-(
org_txn_id: transaction.org_txn_id,
144
:-(
current_state: transaction.current_state,
145
:-(
status: transaction.status,
146
:-(
inr_amount: transaction.inr_amount,
147
:-(
foreign_amount: transaction.foreign_amount,
148
:-(
foreign_currency: transaction.foreign_currency,
149
:-(
corridor: transaction.corridor,
150
:-(
npci_received_at: transaction.npci_received_at,
151
:-(
partner_credited_at: transaction.partner_credited_at,
152
:-(
completed_at: transaction.completed_at
153 }
154 })
155 end
156 end
157
158 @doc """
159 Handle partner timeout and initiate check/reversal flow
160 """
161 def handle_timeout(conn, %{"org_txn_id" => org_txn_id}) do
162
:-(
case UpiInternationalService.get_transaction_by_org_id(org_txn_id) do
163 nil ->
164 conn
165 |> put_status(:not_found)
166
:-(
|> json(%{success: false, error: "transaction_not_found"})
167
168 transaction ->
169
:-(
case UpiInternationalService.handle_partner_timeout(transaction) do
170 {:ok, updated_transaction} ->
171 conn
172
:-(
|> json(%{
173 success: true,
174 data: %{
175
:-(
org_txn_id: updated_transaction.org_txn_id,
176
:-(
current_state: updated_transaction.current_state,
177
:-(
status: updated_transaction.status,
178 action_taken: "timeout_processed"
179 }
180 })
181
182 {:error, reason} ->
183 conn
184 |> put_status(:unprocessable_entity)
185
:-(
|> json(%{
186 success: false,
187 error: "timeout_handling_failed",
188 reason: reason
189 })
190 end
191 end
192 end
193
194 # Private helper functions
195
196 defp get_fx_conversion(%{"inr_amount" => inr_amount, "corridor" => corridor}) do
197
:-(
target_currency = get_corridor_currency(corridor)
198
:-(
FxRateService.convert_inr_to_foreign(inr_amount, target_currency, String.upcase(corridor))
199 end
200
201 defp generate_qr_with_partner(qr_params, fx_data) do
202
:-(
partner_adapter = get_partner_adapter(fx_data.corridor)
203
204
:-(
qr_request = %{
205 merchant_vpa: qr_params["merchant_vpa"],
206 merchant_name: qr_params["merchant_name"],
207
:-(
inr_amount: fx_data.inr_amount,
208
:-(
base_amount: fx_data.foreign_amount,
209
:-(
base_currency: fx_data.foreign_currency,
210
:-(
fx_rate: fx_data.fx_rate,
211
:-(
markup: fx_data.markup_rate
212 }
213
214
:-(
apply(partner_adapter, :generate_dynamic_qr_with_fx, [qr_request])
215 end
216
217 defp get_corridor_currency(corridor) do
218
:-(
case String.upcase(corridor) do
219
:-(
"SINGAPORE" -> "SGD"
220
:-(
"UAE" -> "AED"
221
:-(
"USA" -> "USD"
222
:-(
_ -> raise "Unsupported corridor: #{corridor}"
223 end
224 end
225
226 defp get_partner_adapter(corridor) do
227
:-(
case String.upcase(corridor) do
228
:-(
"SINGAPORE" -> SandboxPartner
229
:-(
"UAE" -> SandboxPartner
230
:-(
"USA" -> SandboxPartner
231
:-(
_ -> raise "No adapter for corridor: #{corridor}"
232 end
233 end
234
235 # Render functions
236 def render("show.json", %{transaction: transaction}) do
237
:-(
%{
238 success: true,
239 data: %{
240
:-(
id: transaction.id,
241
:-(
org_txn_id: transaction.org_txn_id,
242
:-(
current_state: transaction.current_state,
243
:-(
status: transaction.status,
244
:-(
transaction_type: transaction.transaction_type,
245
:-(
payer_addr: transaction.payer_addr,
246
:-(
payee_addr: transaction.payee_addr,
247
:-(
inr_amount: transaction.inr_amount,
248
:-(
foreign_amount: transaction.foreign_amount,
249
:-(
foreign_currency: transaction.foreign_currency,
250
:-(
fx_rate: transaction.fx_rate,
251
:-(
markup_rate: transaction.markup_rate,
252
:-(
corridor: transaction.corridor,
253
:-(
npci_received_at: transaction.npci_received_at,
254
:-(
partner_credited_at: transaction.partner_credited_at,
255
:-(
completed_at: transaction.completed_at,
256
:-(
inserted_at: transaction.inserted_at
257 }
258 }
259 end
260 end
Line Hits Source