| 1 |
|
defmodule DaProductApp.Adapters.SandboxPartner do |
| 2 |
|
@moduledoc """ |
| 3 |
|
Sandbox UPI International partner returning simulated responses. |
| 4 |
|
|
| 5 |
|
Simulates an international partner in Singapore corridor: |
| 6 |
|
- Receives SGD after PSP converts INR → SGD |
| 7 |
|
- Credits local merchant in SGD |
| 8 |
|
- Handles FX-aware QR generation and transactions |
| 9 |
|
""" |
| 10 |
|
@behaviour DaProductApp.Adapters.InternationalPartnerBehaviour |
| 11 |
|
|
| 12 |
|
@impl true |
| 13 |
:-( |
def credit_merchant(params) do |
| 14 |
|
# Simulate crediting international merchant in local currency |
| 15 |
|
# PSP has already received INR from NPCI and converted to SGD |
| 16 |
|
{:ok, %{ |
| 17 |
|
code: "CS", |
| 18 |
|
payload: %{ |
| 19 |
|
partner_status: "PENDING", |
| 20 |
:-( |
partner_txn_id: "SGP_#{:rand.uniform(999999)}", |
| 21 |
:-( |
local_amount: params.base_amount, |
| 22 |
:-( |
local_currency: params.base_currency, |
| 23 |
:-( |
inr_equivalent: params.inr_amount, |
| 24 |
:-( |
fx_rate_applied: params.fx_rate, |
| 25 |
|
merchant_account_credited: true, |
| 26 |
|
settlement_status: "PROCESSING" |
| 27 |
|
} |
| 28 |
|
}} |
| 29 |
|
end |
| 30 |
|
|
| 31 |
|
@impl true |
| 32 |
:-( |
def check_transaction_status(params) do |
| 33 |
|
# Simulate checking status with international partner |
| 34 |
|
{:ok, %{ |
| 35 |
|
code: "CS", |
| 36 |
|
payload: %{ |
| 37 |
|
partner_status: "SUCCESS", |
| 38 |
|
settlement_status: "COMPLETED", |
| 39 |
|
merchant_balance_updated: true, |
| 40 |
:-( |
local_amount_settled: params.base_amount, |
| 41 |
:-( |
partner_txn_id: params.partner_txn_id |
| 42 |
|
} |
| 43 |
|
}} |
| 44 |
|
end |
| 45 |
|
|
| 46 |
|
@impl true |
| 47 |
:-( |
def reverse_payment(params) do |
| 48 |
|
# Simulate reversing payment from international merchant |
| 49 |
|
# Return foreign currency back to PSP for INR reversal to customer |
| 50 |
|
{:ok, %{ |
| 51 |
|
code: "00", |
| 52 |
|
payload: %{ |
| 53 |
|
partner_status: "REVERSED", |
| 54 |
|
settlement_status: "REVERSED", |
| 55 |
|
reversal_completed: true, |
| 56 |
:-( |
foreign_amount_reversed: params.base_amount, |
| 57 |
:-( |
inr_reversal_required: params.inr_amount |
| 58 |
|
} |
| 59 |
|
}} |
| 60 |
|
end |
| 61 |
|
|
| 62 |
|
@impl true |
| 63 |
|
def generate_dynamic_qr_with_fx(params) do |
| 64 |
|
# Simulate generating international QR with FX details embedded |
| 65 |
|
# This would be called by merchant via partner's system |
| 66 |
:-( |
base_amount = params.base_amount || simulate_fx_conversion(params.inr_amount) |
| 67 |
|
|
| 68 |
|
# Build UPI International QR string with FX parameters |
| 69 |
:-( |
qr_data = build_international_qr(%{ |
| 70 |
:-( |
merchant_vpa: params.merchant_vpa, |
| 71 |
:-( |
merchant_name: params.merchant_name, |
| 72 |
|
base_amount: base_amount, |
| 73 |
|
base_currency: "SGD", |
| 74 |
:-( |
fx_rate: params.fx_rate || "0.0165", |
| 75 |
:-( |
markup: params.markup || "2.50", |
| 76 |
:-( |
inr_amount: params.inr_amount |
| 77 |
|
}) |
| 78 |
|
|
| 79 |
|
{:ok, %{ |
| 80 |
|
code: "00", |
| 81 |
|
payload: %{ |
| 82 |
|
qr_code: qr_data, |
| 83 |
:-( |
qr_image_url: "https://api.qrserver.com/v1/create-qr-code/?data=#{URI.encode(qr_data)}", |
| 84 |
|
expires_at: DateTime.add(DateTime.utc_now(), 15, :minute), |
| 85 |
|
fx_details: %{ |
| 86 |
|
base_amount: base_amount, |
| 87 |
|
base_currency: "SGD", |
| 88 |
:-( |
fx_rate: params.fx_rate || "0.0165", |
| 89 |
:-( |
markup_rate: params.markup || "2.50", |
| 90 |
:-( |
inr_equivalent: params.inr_amount |
| 91 |
|
}, |
| 92 |
|
corridor: "SINGAPORE", |
| 93 |
:-( |
partner_ref: "QR_#{:rand.uniform(999999)}" |
| 94 |
|
} |
| 95 |
|
}} |
| 96 |
|
end |
| 97 |
|
|
| 98 |
|
@impl true |
| 99 |
:-( |
def validate_fx_rates(_params) do |
| 100 |
|
# Simulate FX rate validation for the corridor |
| 101 |
|
{:ok, %{ |
| 102 |
|
code: "00", |
| 103 |
|
payload: %{ |
| 104 |
|
rates_valid: true, |
| 105 |
|
current_rate: "0.0165", |
| 106 |
|
markup_rate: "2.50", |
| 107 |
|
rate_expires_at: DateTime.add(DateTime.utc_now(), 5, :minute), |
| 108 |
|
corridor: "SINGAPORE" |
| 109 |
|
} |
| 110 |
|
}} |
| 111 |
|
end |
| 112 |
|
|
| 113 |
|
@impl true |
| 114 |
|
def get_corridor_info() do |
| 115 |
:-( |
%{ |
| 116 |
|
currency: "SGD", |
| 117 |
|
country: "Singapore", |
| 118 |
|
corridor_code: "SGP", |
| 119 |
|
supported_features: ["dynamic_qr", "fx_conversion", "real_time_settlement"] |
| 120 |
|
} |
| 121 |
|
end |
| 122 |
|
|
| 123 |
|
# Private helper functions |
| 124 |
|
defp simulate_fx_conversion(inr_amount) when is_binary(inr_amount) do |
| 125 |
:-( |
{inr_float, _} = Float.parse(inr_amount) |
| 126 |
:-( |
simulate_fx_conversion(inr_float) |
| 127 |
|
end |
| 128 |
|
|
| 129 |
|
defp simulate_fx_conversion(inr_amount) when is_number(inr_amount) do |
| 130 |
|
# Simulate INR to SGD conversion: 1 INR = 0.0165 SGD + 2.5% markup |
| 131 |
:-( |
sgd_rate = 0.0165 |
| 132 |
:-( |
markup = 0.025 |
| 133 |
:-( |
base_sgd = inr_amount * sgd_rate |
| 134 |
:-( |
sgd_with_markup = base_sgd * (1 + markup) |
| 135 |
:-( |
Float.round(sgd_with_markup, 2) |
| 136 |
|
end |
| 137 |
|
|
| 138 |
|
defp build_international_qr(fx_details) do |
| 139 |
|
# Build UPI International QR with embedded FX parameters per UPI spec |
| 140 |
:-( |
base_qr = "upi://pay?pa=#{fx_details.merchant_vpa}&pn=#{URI.encode(fx_details.merchant_name)}" |
| 141 |
|
|
| 142 |
:-( |
fx_params = [ |
| 143 |
:-( |
"am=#{fx_details.base_amount}", # Base amount in local currency |
| 144 |
:-( |
"cu=#{fx_details.base_currency}", # Base currency (SGD/USD/AED) |
| 145 |
:-( |
"fx=#{fx_details.fx_rate}", # FX rate |
| 146 |
:-( |
"mkup=#{fx_details.markup}", # Markup percentage |
| 147 |
:-( |
"inr=#{fx_details.inr_amount}", # INR equivalent for customer |
| 148 |
|
"mode=17", # UPI International mode |
| 149 |
|
"purpose=10" # International merchant payment |
| 150 |
|
] |
| 151 |
|
|
| 152 |
:-( |
"#{base_qr}&#{Enum.join(fx_params, "&")}" |
| 153 |
|
end |
| 154 |
|
end |