cover/Elixir.DaProductAppWeb.Api.V1.PartnerMerchantController.html

1
:-(
defmodule DaProductAppWeb.Api.V1.PartnerMerchantController do
2 @moduledoc """
3 API controller for partner merchant management.
4
5 Allows partners to enroll, manage, and configure their merchants
6 for UPI International payment processing.
7 """
8
:-(
use DaProductAppWeb, :controller
9
10 alias DaProductApp.Merchants
11 alias DaProductApp.Partners
12 alias DaProductApp.Merchants.Merchant
13
14 action_fallback DaProductAppWeb.FallbackController
15
16 # ================================
17 # MERCHANT ENROLLMENT & MANAGEMENT
18 # ================================
19
20 @doc """
21 Enroll a new merchant under partner.
22
23 POST /api/v1/partners/:partner_id/merchants
24 """
25 def create(conn, %{"partner_id" => partner_id} = params) do
26
:-(
with {:ok, partner} <- validate_partner(partner_id),
27
:-(
{:ok, attrs} <- validate_merchant_attrs(params),
28
:-(
{:ok, %Merchant{} = merchant} <- Merchants.create_merchant(partner.id, attrs) do
29
30 conn
31 |> put_status(:created)
32
:-(
|> json(%{
33 success: true,
34 data: format_merchant(merchant),
35 message: "Merchant enrolled successfully"
36 })
37 else
38 {:error, :not_found} ->
39 conn
40 |> put_status(:not_found)
41
:-(
|> json(%{success: false, error: "Partner not found"})
42
43 {:error, %Ecto.Changeset{} = changeset} ->
44 conn
45 |> put_status(:unprocessable_entity)
46
:-(
|> json(%{success: false, errors: format_changeset_errors(changeset)})
47
48 {:error, :bad_request, message} ->
49 conn
50 |> put_status(:bad_request)
51
:-(
|> json(%{success: false, error: message})
52 end
53 end
54
55 @doc """
56 List all merchants for a partner.
57
58 GET /api/v1/partners/:partner_id/merchants
59 """
60 def index(conn, %{"partner_id" => partner_id} = params) do
61
:-(
with {:ok, partner} <- validate_partner(partner_id) do
62
:-(
opts = [
63 status: params["status"],
64 corridor: params["corridor"],
65 page: parse_int(params["page"]),
66
:-(
per_page: parse_int(params["per_page"]) || 20
67 ]
68
69
:-(
merchants = Merchants.list_partner_merchants(partner.id, opts)
70
71 conn
72
:-(
|> json(%{
73 success: true,
74 data: Enum.map(merchants, &format_merchant_summary/1),
75 meta: %{total: length(merchants)}
76 })
77 else
78 {:error, :not_found} ->
79 conn
80 |> put_status(:not_found)
81
:-(
|> json(%{success: false, error: "Partner not found"})
82 end
83 end
84
85 @doc """
86 Get specific merchant details.
87
88 GET /api/v1/partners/:partner_id/merchants/:id
89 """
90 def show(conn, %{"partner_id" => partner_id, "id" => merchant_id}) do
91
:-(
with {:ok, _partner} <- validate_partner(partner_id),
92
:-(
%Merchant{} = merchant <- Merchants.get_merchant(merchant_id),
93
:-(
true <- merchant.partner_id == partner_id do
94
95 conn
96
:-(
|> json(%{
97 success: true,
98 data: format_merchant_detail(merchant)
99 })
100 else
101 {:error, :not_found} ->
102 conn
103 |> put_status(:not_found)
104
:-(
|> json(%{success: false, error: "Partner not found"})
105
106 nil ->
107 conn
108 |> put_status(:not_found)
109
:-(
|> json(%{success: false, error: "Merchant not found"})
110
111 false ->
112 conn
113 |> put_status(:forbidden)
114
:-(
|> json(%{success: false, error: "Merchant does not belong to this partner"})
115 end
116 end
117
118 @doc """
119 Update merchant information.
120
121 PUT /api/v1/partners/:partner_id/merchants/:id
122 """
123 def update(conn, %{"partner_id" => partner_id, "id" => merchant_id} = params) do
124
:-(
with {:ok, _partner} <- validate_partner(partner_id),
125
:-(
%Merchant{} = merchant <- Merchants.get_merchant(merchant_id),
126
:-(
true <- merchant.partner_id == partner_id,
127
:-(
{:ok, attrs} <- validate_merchant_update_attrs(params),
128
:-(
{:ok, %Merchant{} = updated_merchant} <- Merchants.update_merchant(merchant, attrs) do
129
130 conn
131
:-(
|> json(%{
132 success: true,
133 data: format_merchant(updated_merchant),
134 message: "Merchant updated successfully"
135 })
136 else
137 {:error, :not_found} ->
138 conn
139 |> put_status(:not_found)
140
:-(
|> json(%{success: false, error: "Partner not found"})
141
142 nil ->
143 conn
144 |> put_status(:not_found)
145
:-(
|> json(%{success: false, error: "Merchant not found"})
146
147 false ->
148 conn
149 |> put_status(:forbidden)
150
:-(
|> json(%{success: false, error: "Merchant does not belong to this partner"})
151
152 {:error, %Ecto.Changeset{} = changeset} ->
153 conn
154 |> put_status(:unprocessable_entity)
155
:-(
|> json(%{success: false, errors: format_changeset_errors(changeset)})
156 end
157 end
158
159 @doc """
160 Update merchant status (ACTIVE, SUSPENDED, INACTIVE).
161
162 PATCH /api/v1/partners/:partner_id/merchants/:id/status
163 """
164 def update_status(conn, %{"partner_id" => partner_id, "id" => merchant_id, "status" => status}) do
165
:-(
with {:ok, _partner} <- validate_partner(partner_id),
166
:-(
%Merchant{} = merchant <- Merchants.get_merchant(merchant_id),
167
:-(
true <- merchant.partner_id == partner_id,
168
:-(
true <- status in ["ACTIVE", "SUSPENDED", "INACTIVE"],
169
:-(
{:ok, %Merchant{} = updated_merchant} <- Merchants.update_merchant_status(merchant_id, status) do
170
171 conn
172
:-(
|> json(%{
173 success: true,
174 data: format_merchant(updated_merchant),
175 message: "Merchant status updated successfully"
176 })
177 else
178 {:error, :not_found} ->
179 conn
180 |> put_status(:not_found)
181
:-(
|> json(%{success: false, error: "Partner not found"})
182
183 nil ->
184 conn
185 |> put_status(:not_found)
186
:-(
|> json(%{success: false, error: "Merchant not found"})
187
188 false ->
189 conn
190 |> put_status(:forbidden)
191
:-(
|> json(%{success: false, error: "Invalid status or merchant access denied"})
192 end
193 end
194
195 # ================================
196 # MERCHANT VALIDATION & LIMITS
197 # ================================
198
199 @doc """
200 Validate merchant for transaction processing.
201
202 GET /api/v1/partners/:partner_id/merchants/:id/validate
203 """
204 def validate_merchant(conn, %{"partner_id" => partner_id, "id" => merchant_id}) do
205
:-(
with {:ok, _partner} <- validate_partner(partner_id),
206
:-(
%Merchant{} = merchant <- Merchants.get_merchant(merchant_id),
207
:-(
true <- merchant.partner_id == partner_id do
208
209
:-(
case Merchants.validate_merchant_for_transaction(merchant_id) do
210 {:ok, _merchant} ->
211 conn
212
:-(
|> json(%{
213 success: true,
214 data: %{valid: true, message: "Merchant is valid for transactions"}
215 })
216
217 {:error, reason} ->
218 conn
219
:-(
|> json(%{
220 success: true,
221 data: %{valid: false, message: reason}
222 })
223 end
224 else
225 {:error, :not_found} ->
226 conn
227 |> put_status(:not_found)
228
:-(
|> json(%{success: false, error: "Partner not found"})
229
230 nil ->
231 conn
232 |> put_status(:not_found)
233
:-(
|> json(%{success: false, error: "Merchant not found"})
234
235 false ->
236 conn
237 |> put_status(:forbidden)
238
:-(
|> json(%{success: false, error: "Merchant access denied"})
239 end
240 end
241
242 @doc """
243 Check transaction limits for merchant.
244
245 POST /api/v1/partners/:partner_id/merchants/:id/check-limits
246 """
247 def check_limits(conn, %{"partner_id" => partner_id, "id" => merchant_id, "amount" => amount}) do
248
:-(
with {:ok, _partner} <- validate_partner(partner_id),
249
:-(
%Merchant{} = merchant <- Merchants.get_merchant(merchant_id),
250
:-(
true <- merchant.partner_id == partner_id,
251
:-(
{:ok, decimal_amount} <- parse_decimal(amount) do
252
253
:-(
case Merchants.check_transaction_limits(merchant, decimal_amount) do
254 :ok ->
255 conn
256
:-(
|> json(%{
257 success: true,
258 data: %{allowed: true, message: "Transaction within limits"}
259 })
260
261 {:error, reason} ->
262 conn
263
:-(
|> json(%{
264 success: true,
265 data: %{allowed: false, message: reason}
266 })
267 end
268 else
269 {:error, :not_found} ->
270 conn
271 |> put_status(:not_found)
272
:-(
|> json(%{success: false, error: "Partner not found"})
273
274 nil ->
275 conn
276 |> put_status(:not_found)
277
:-(
|> json(%{success: false, error: "Merchant not found"})
278
279 false ->
280 conn
281 |> put_status(:forbidden)
282
:-(
|> json(%{success: false, error: "Merchant access denied"})
283
284 {:error, :invalid_amount} ->
285 conn
286 |> put_status(:bad_request)
287
:-(
|> json(%{success: false, error: "Invalid amount format"})
288 end
289 end
290
291 # ================================
292 # MERCHANT SEARCH & ANALYTICS
293 # ================================
294
295 @doc """
296 Search merchants with filters.
297
298 GET /api/v1/partners/:partner_id/merchants/search
299 """
300 def search(conn, %{"partner_id" => partner_id} = params) do
301
:-(
with {:ok, partner} <- validate_partner(partner_id) do
302
:-(
search_params = %{
303
:-(
partner_id: partner.id,
304 status: params["status"],
305 corridor: params["corridor"],
306 business_type: params["business_type"],
307 search: params["q"],
308 sort: params["sort"],
309 order: params["order"],
310 page: parse_int(params["page"]),
311
:-(
per_page: parse_int(params["per_page"]) || 20
312 }
313
314
:-(
merchants = Merchants.search_merchants(search_params)
315
316 conn
317
:-(
|> json(%{
318 success: true,
319 data: Enum.map(merchants, &format_merchant_summary/1),
320 meta: %{total: length(merchants)}
321 })
322 else
323 {:error, :not_found} ->
324 conn
325 |> put_status(:not_found)
326
:-(
|> json(%{success: false, error: "Partner not found"})
327 end
328 end
329
330 @doc """
331 Get merchant statistics for partner.
332
333 GET /api/v1/partners/:partner_id/merchants/stats
334 """
335 def stats(conn, %{"partner_id" => partner_id}) do
336
:-(
with {:ok, partner} <- validate_partner(partner_id) do
337
:-(
stats = %{
338
:-(
total_merchants: Merchants.count_partner_merchants(partner.id),
339
:-(
active_merchants: Merchants.list_partner_merchants(partner.id, status: "ACTIVE") |> length(),
340
:-(
by_corridor: get_corridor_stats(partner.id),
341
:-(
by_business_type: get_business_type_stats(partner.id)
342 }
343
344 conn
345
:-(
|> json(%{
346 success: true,
347 data: stats
348 })
349 else
350 {:error, :not_found} ->
351 conn
352 |> put_status(:not_found)
353
:-(
|> json(%{success: false, error: "Partner not found"})
354 end
355 end
356
357 # ================================
358 # PRIVATE HELPER FUNCTIONS
359 # ================================
360
361 defp validate_partner(partner_id) do
362
:-(
case Partners.get_partner(partner_id) do
363
:-(
%Partners.Partner{} = partner -> {:ok, partner}
364
:-(
nil -> {:error, :not_found}
365 end
366 end
367
368 defp validate_merchant_attrs(params) do
369
:-(
required_fields = ["merchant_code", "brand_name", "merchant_vpa"]
370
371
:-(
case check_required_fields(params, required_fields) do
372 :ok ->
373
:-(
attrs = %{
374 "merchant_code" => params["merchant_code"],
375 "brand_name" => params["brand_name"],
376 "merchant_vpa" => params["merchant_vpa"],
377
:-(
"mid" => params["mid"] || generate_mid(),
378
:-(
"tid" => params["tid"] || generate_tid(),
379
:-(
"business_type" => params["business_type"] || "RETAIL",
380
:-(
"merchant_type" => params["merchant_type"] || "SMALL",
381
:-(
"merchant_genre" => params["merchant_genre"] || "RETAIL",
382
:-(
"corridor" => params["corridor"] || "DOMESTIC",
383 "legal_name" => params["legal_name"],
384 "contact_email" => params["contact_email"],
385 "contact_phone" => params["contact_phone"],
386 "address" => params["address"],
387 "city" => params["city"],
388 "state" => params["state"],
389 "pincode" => params["pincode"],
390 "max_transaction_limit" => params["max_transaction_limit"],
391 "daily_transaction_limit" => params["daily_transaction_limit"],
392
:-(
"settlement_frequency" => params["settlement_frequency"] || "T+1",
393 "settlement_account_number" => params["settlement_account_number"],
394 "settlement_account_ifsc" => params["settlement_account_ifsc"]
395 }
396 {:ok, attrs}
397
398 {:error, missing_fields} ->
399
:-(
{:error, :bad_request, "Missing required fields: #{Enum.join(missing_fields, ", ")}"}
400 end
401 end
402
403 defp validate_merchant_update_attrs(params) do
404 # Allow updating most fields except core identifiers
405
:-(
restricted_fields = ["merchant_code", "mid", "tid", "partner_id"]
406
407
:-(
attrs = params
408 |> Map.drop(restricted_fields)
409 |> Map.drop(["partner_id", "id"])
410
411 {:ok, attrs}
412 end
413
414 defp check_required_fields(params, required_fields) do
415
:-(
missing_fields = Enum.filter(required_fields, fn field ->
416
:-(
is_nil(params[field]) or params[field] == ""
417 end)
418
419
:-(
case missing_fields do
420
:-(
[] -> :ok
421
:-(
missing -> {:error, missing}
422 end
423 end
424
425 # JSON formatting functions
426 defp format_merchant(merchant) do
427
:-(
%{
428
:-(
id: merchant.id,
429
:-(
merchant_code: merchant.merchant_code,
430
:-(
mid: merchant.mid,
431
:-(
tid: merchant.tid,
432
:-(
brand_name: merchant.brand_name,
433
:-(
merchant_vpa: merchant.merchant_vpa,
434
:-(
business_type: merchant.business_type,
435
:-(
corridor: merchant.corridor,
436
:-(
local_currency: merchant.local_currency,
437
:-(
status: merchant.status,
438
:-(
compliance_status: merchant.compliance_status,
439
:-(
qr_enabled: merchant.qr_enabled,
440
:-(
max_transaction_limit: merchant.max_transaction_limit,
441
:-(
contact_email: merchant.contact_email,
442
:-(
contact_phone: merchant.contact_phone,
443
:-(
onboarded_at: merchant.onboarded_at,
444
:-(
updated_at: merchant.updated_at
445 }
446 end
447
448 defp format_merchant_summary(merchant) do
449
:-(
%{
450
:-(
id: merchant.id,
451
:-(
merchant_code: merchant.merchant_code,
452
:-(
brand_name: merchant.brand_name,
453
:-(
merchant_vpa: merchant.merchant_vpa,
454
:-(
corridor: merchant.corridor,
455
:-(
status: merchant.status,
456
:-(
compliance_status: merchant.compliance_status,
457
:-(
onboarded_at: merchant.onboarded_at
458 }
459 end
460
461 defp format_merchant_detail(merchant) do
462
:-(
Map.merge(format_merchant(merchant), %{
463
:-(
legal_name: merchant.legal_name,
464
:-(
address: merchant.address,
465
:-(
city: merchant.city,
466
:-(
state: merchant.state,
467
:-(
pincode: merchant.pincode,
468
:-(
settlement_frequency: merchant.settlement_frequency,
469
:-(
settlement_account_number: merchant.settlement_account_number,
470
:-(
settlement_account_ifsc: merchant.settlement_account_ifsc,
471
:-(
risk_category: merchant.risk_category,
472
:-(
last_transaction_at: merchant.last_transaction_at
473 })
474 end
475
476 defp format_changeset_errors(changeset) do
477
:-(
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
478
:-(
Enum.reduce(opts, msg, fn {key, value}, acc ->
479
:-(
String.replace(acc, "%{#{key}}", to_string(value))
480 end)
481 end)
482 end
483
484
:-(
defp parse_int(nil), do: nil
485
:-(
defp parse_int(value) when is_integer(value), do: value
486 defp parse_int(value) when is_binary(value) do
487
:-(
case Integer.parse(value) do
488
:-(
{int, ""} -> int
489
:-(
_ -> nil
490 end
491 end
492
493 defp parse_decimal(value) when is_binary(value) do
494
:-(
case Decimal.parse(value) do
495
:-(
{decimal, ""} -> {:ok, decimal}
496
:-(
_ -> {:error, :invalid_amount}
497 end
498 end
499
:-(
defp parse_decimal(value) when is_number(value) do
500 {:ok, Decimal.new(value)}
501 end
502
:-(
defp parse_decimal(_), do: {:error, :invalid_amount}
503
504 defp generate_mid do
505
:-(
"MID" <> (:crypto.strong_rand_bytes(6) |> Base.encode16(case: :upper))
506 end
507
508 defp generate_tid do
509
:-(
"TID" <> (:crypto.strong_rand_bytes(3) |> Base.encode16(case: :upper))
510 end
511
512 defp get_corridor_stats(partner_id) do
513 # This would be implemented with proper aggregation queries
514
:-(
%{
515 "SINGAPORE" => 0,
516 "UAE" => 0,
517 "USA" => 0,
518 "DOMESTIC" => 0
519 }
520 end
521
522 defp get_business_type_stats(partner_id) do
523 # This would be implemented with proper aggregation queries
524
:-(
%{
525 "RETAIL" => 0,
526 "ECOMMERCE" => 0,
527 "SERVICES" => 0
528 }
529 end
530 end
Line Hits Source