Implement example of sep
This commit is contained in:
commit
a63fe09f26
|
|
@ -0,0 +1 @@
|
||||||
|
.idea
|
||||||
|
|
@ -0,0 +1,240 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fa" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>پرداخت آنلاین - بانک سامان</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: Tahoma, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 450px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-header {
|
||||||
|
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-icon {
|
||||||
|
font-size: 50px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-header h2 {
|
||||||
|
margin: 10px 0 5px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-header p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.payment-body {
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #38ef7d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group .hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-pay {
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
padding: 16px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 50px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.3s, box-shadow 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-pay:hover:not(:disabled) {
|
||||||
|
transform: scale(1.02);
|
||||||
|
box-shadow: 0 5px 20px rgba(56, 239, 125, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-pay:disabled {
|
||||||
|
opacity: 0.7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert {
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-top: 20px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-success {
|
||||||
|
background: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-danger {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top-color: white;
|
||||||
|
animation: spin 1s ease-in-out infinite;
|
||||||
|
margin-right: 10px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="payment-card">
|
||||||
|
<div class="payment-header">
|
||||||
|
<div class="payment-icon">💳</div>
|
||||||
|
<h2>پرداخت آنلاین</h2>
|
||||||
|
<p>بانک سامان - IPG_SEP</p>
|
||||||
|
</div>
|
||||||
|
<div class="payment-body">
|
||||||
|
<form id="paymentForm">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>مبلغ (تومان)</label>
|
||||||
|
<input type="number" id="amount" placeholder="مبلغ را وارد کنید" required min="1000">
|
||||||
|
<div class="hint">حداقل مبلغ: ۱۰۰۰ تومان</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>شماره تلفن همراه</label>
|
||||||
|
<input type="tel" id="phone" placeholder="۰۹۱۲۳۴۵۶۷۸۹" required pattern="09[0-9]{9}">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn-pay" id="payBtn">
|
||||||
|
پرداخت امن 🔒
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div id="message" class="alert"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('paymentForm').addEventListener('submit', async function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const amount = document.getElementById('amount').value;
|
||||||
|
const phone = document.getElementById('phone').value;
|
||||||
|
const btn = document.getElementById('payBtn');
|
||||||
|
const message = document.getElementById('message');
|
||||||
|
|
||||||
|
if (parseInt(amount) < 1000) {
|
||||||
|
showMessage('حداقل مبلغ ۱۰۰۰ تومان است', 'danger');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!phone.match(/^09[0-9]{9}$/)) {
|
||||||
|
showMessage('شماره تلفن معتبر وارد کنید', 'danger');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<span class="spinner"></span>در حال انتقال به درگاه...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/init-payment', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
amount: parseInt(amount),
|
||||||
|
phone: phone
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success && data.payment_url) {
|
||||||
|
window.location.href = data.payment_url;
|
||||||
|
} else {
|
||||||
|
showMessage(data.message || 'خطا در ایجاد پرداخت', 'danger');
|
||||||
|
resetButton();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showMessage('خطا در ارتباط با سرور', 'danger');
|
||||||
|
resetButton();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function resetButton() {
|
||||||
|
const btn = document.getElementById('payBtn');
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = 'پرداخت امن 🔒';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMessage(text, type) {
|
||||||
|
const message = document.getElementById('message');
|
||||||
|
message.textContent = text;
|
||||||
|
message.className = 'alert alert-' + type;
|
||||||
|
message.style.display = 'block';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,172 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fa" dir="rtl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>نتیجه پرداخت</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: Tahoma, Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-icon {
|
||||||
|
font-size: 80px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success .result-icon {
|
||||||
|
animation: bounce 1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 20%, 50%, 80%, 100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: translateY(-30px);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
transform: translateY(-15px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-title {
|
||||||
|
font-size: 28px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success .result-title {
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.failed .result-title {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-message {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-box {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-label {
|
||||||
|
color: #888;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction-id {
|
||||||
|
color: #667eea;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-home {
|
||||||
|
display: inline-block;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 14px 30px;
|
||||||
|
border-radius: 50px;
|
||||||
|
text-decoration: none;
|
||||||
|
margin-top: 25px;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-home:hover {
|
||||||
|
transform: scale(1.05);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="result-card" id="resultCard">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function showResult() {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
const state = urlParams.get('state') || '';
|
||||||
|
const status = urlParams.get('status') || '';
|
||||||
|
const transactionId = urlParams.get('transactionId') || '';
|
||||||
|
const refNum = urlParams.get('refNum') || '';
|
||||||
|
const amount = urlParams.get('amount') || '';
|
||||||
|
|
||||||
|
const resultCard = document.getElementById('resultCard');
|
||||||
|
|
||||||
|
const isSuccess = (state === 'OK' || status === 'success');
|
||||||
|
|
||||||
|
if (isSuccess) {
|
||||||
|
resultCard.className = 'result-card success';
|
||||||
|
resultCard.innerHTML = `
|
||||||
|
<div class="result-icon">✅</div>
|
||||||
|
<h2 class="result-title">پرداخت موفق</h2>
|
||||||
|
<p class="result-message">پرداخت شما با موفقیت انجام شد.</p>
|
||||||
|
<div class="transaction-box">
|
||||||
|
<div class="transaction-label">شماره تراکنش:</div>
|
||||||
|
<div class="transaction-id">${transactionId || 'نامشخص'}</div>
|
||||||
|
</div>
|
||||||
|
${refNum ? `
|
||||||
|
<div class="transaction-box" style="margin-top: 10px;">
|
||||||
|
<div class="transaction-label">شماره سفارش:</div>
|
||||||
|
<div class="transaction-id" style="font-size: 16px;">${refNum}</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
${amount ? `
|
||||||
|
<div class="transaction-box" style="margin-top: 10px;">
|
||||||
|
<div class="transaction-label">مبلغ:</div>
|
||||||
|
<div class="transaction-id" style="font-size: 16px;">${parseInt(amount).toLocaleString()} ریال</div>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
<a href="/" class="btn-home">🏠 بازگشت به صفحه اصلی</a>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
let message = 'پرداخت شما ناموفق بود.';
|
||||||
|
if (status === 'verify_failed') {
|
||||||
|
message = 'پرداخت موفق بود اما در تأیید تراکنش خطایی رخ داد.';
|
||||||
|
}
|
||||||
|
|
||||||
|
resultCard.className = 'result-card failed';
|
||||||
|
resultCard.innerHTML = `
|
||||||
|
<div class="result-icon">❌</div>
|
||||||
|
<h2 class="result-title">پرداخت ناموفق</h2>
|
||||||
|
<p class="result-message">${message}</p>
|
||||||
|
<a href="/" class="btn-home">🔄 تلاش مجدد</a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showResult();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,320 @@
|
||||||
|
package httphandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.project/gocasts/ipg-example/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
service service.PaymentService
|
||||||
|
templatesPath string
|
||||||
|
staticPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandler(service service.PaymentService) *Handler {
|
||||||
|
return &Handler{
|
||||||
|
service: service,
|
||||||
|
templatesPath: "./delivery/html/",
|
||||||
|
staticPath: "./delivery/static/",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) RegisterRoutes() *http.ServeMux {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
|
mux.HandleFunc("/", h.homeHandler)
|
||||||
|
mux.HandleFunc("/api/init-payment", h.requestPaymentHandler)
|
||||||
|
mux.HandleFunc("/api/callback", h.callbackHandler)
|
||||||
|
mux.HandleFunc("/result", h.resultHandler)
|
||||||
|
|
||||||
|
//mux.HandleFunc("/api/verify-payment", h.verifyPaymentHandler)
|
||||||
|
|
||||||
|
mux.Handle("/static/", http.StripPrefix("/static/",
|
||||||
|
http.FileServer(http.Dir(h.staticPath))))
|
||||||
|
|
||||||
|
return mux
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) StartServer(addr string) {
|
||||||
|
mux := h.RegisterRoutes()
|
||||||
|
log.Printf("Server running on http://localhost%s", addr)
|
||||||
|
log.Fatal(http.ListenAndServe(addr, mux))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Params
|
||||||
|
type PaymentRequest struct {
|
||||||
|
Amount int64 `json:"amount"`
|
||||||
|
Phone string `json:"phone"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaymentResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Token string `json:"token,omitempty"`
|
||||||
|
PaymentURL string `json:"payment_url,omitempty"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VerifyResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
TransactionID string `json:"transaction_id,omitempty"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BankCallbackResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
TransactionID string `json:"transaction_id,omitempty"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
func (h *Handler) homeHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl, err := template.ParseFiles(h.templatesPath + "payment.html")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Template error: %v", err)
|
||||||
|
http.Error(w, "Template parsing error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tmpl.Execute(w, nil); err != nil {
|
||||||
|
log.Printf("Template execute error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) requestPaymentHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
var req PaymentRequest
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
if err := decoder.Decode(&req); err != nil {
|
||||||
|
json.NewEncoder(w).Encode(PaymentResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: "خطا در پارس کردن دادهها",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Amount < 1000 {
|
||||||
|
json.NewEncoder(w).Encode(PaymentResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: "حداقل مبلغ ۱۰۰۰ تومان",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Phone == "" {
|
||||||
|
json.NewEncoder(w).Encode(PaymentResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: "شماره تلفن الزامی است",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token, paymentURL, err := h.service.InitPayment(r.Context(), req.Amount, req.Phone)
|
||||||
|
if err != nil {
|
||||||
|
json.NewEncoder(w).Encode(PaymentResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: "خطا در ایجاد پرداخت: " + err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(PaymentResponse{
|
||||||
|
Success: true,
|
||||||
|
Token: token,
|
||||||
|
PaymentURL: paymentURL,
|
||||||
|
Message: "پرداخت ایجاد شد",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) callbackHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var state, status, refNum, resNum, traceNo, amount string
|
||||||
|
|
||||||
|
contentType := r.Header.Get("Content-Type")
|
||||||
|
|
||||||
|
if strings.Contains(contentType, "application/json") {
|
||||||
|
var data map[string]interface{}
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
if err := decoder.Decode(&data); err != nil {
|
||||||
|
log.Printf("Failed to parse JSON: %v", err)
|
||||||
|
h.redirectToResult(w, r, "", "NOK", "failed", "", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := data["State"].(string); ok {
|
||||||
|
state = v
|
||||||
|
}
|
||||||
|
if v, ok := data["Status"]; ok {
|
||||||
|
status = fmt.Sprintf("%v", v)
|
||||||
|
}
|
||||||
|
if v, ok := data["RefNum"].(string); ok {
|
||||||
|
refNum = v
|
||||||
|
}
|
||||||
|
if v, ok := data["ResNum"].(string); ok {
|
||||||
|
resNum = v
|
||||||
|
}
|
||||||
|
if v, ok := data["TraceNo"].(string); ok {
|
||||||
|
traceNo = v
|
||||||
|
}
|
||||||
|
if v, ok := data["Amount"]; ok {
|
||||||
|
amount = fmt.Sprintf("%v", v)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := r.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to parse form: %v", err)
|
||||||
|
h.redirectToResult(w, r, "", "NOK", "failed", "", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state = r.FormValue("State")
|
||||||
|
status = r.FormValue("Status")
|
||||||
|
refNum = r.FormValue("RefNum")
|
||||||
|
resNum = r.FormValue("ResNum")
|
||||||
|
traceNo = r.FormValue("TraceNo")
|
||||||
|
amount = r.FormValue("Amount")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Bank Callback Received:")
|
||||||
|
log.Printf(" State: %s", state)
|
||||||
|
log.Printf(" Status: %s", status)
|
||||||
|
log.Printf(" RefNum: %s", refNum)
|
||||||
|
log.Printf(" ResNum: %s", resNum)
|
||||||
|
log.Printf(" TraceNo: %s", traceNo)
|
||||||
|
log.Printf(" Amount: %s", amount)
|
||||||
|
|
||||||
|
isSuccess := (state == "OK" || status == "0")
|
||||||
|
|
||||||
|
if !isSuccess {
|
||||||
|
// payment failed
|
||||||
|
h.redirectToResult(w, r, "", state, "failed", refNum, amount)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify transaction
|
||||||
|
transactionID, err := h.service.VerifyPayment(r.Context(), resNum, refNum, traceNo, amount)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("❌ Verify failed: %v", err)
|
||||||
|
h.redirectToResult(w, r, "", state, "verify_failed", refNum, amount)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Payment verified successfully. TransactionID: %s", transactionID)
|
||||||
|
|
||||||
|
h.redirectToResult(w, r, transactionID, state, "success", refNum, amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) resultHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/result" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
http.Error(w, "خطا در پارس کردن دادهها", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state := r.FormValue("state")
|
||||||
|
status := r.FormValue("status")
|
||||||
|
refNum := r.FormValue("refNum")
|
||||||
|
amount := r.FormValue("amount")
|
||||||
|
transactionID := r.FormValue("transactionId")
|
||||||
|
|
||||||
|
isSuccess := (state == "OK" || status == "success")
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"State": state,
|
||||||
|
"Status": status,
|
||||||
|
"IsSuccess": isSuccess,
|
||||||
|
"RefNum": refNum,
|
||||||
|
"Amount": amount,
|
||||||
|
"TransactionID": transactionID,
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl, err := template.ParseFiles(h.templatesPath + "result.html")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Template error: %v", err)
|
||||||
|
http.Error(w, "Template parsing error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tmpl.Execute(w, data); err != nil {
|
||||||
|
log.Printf("Template execute error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) redirectToResult(w http.ResponseWriter, r *http.Request, transactionID, state, status, refNum, amount string) {
|
||||||
|
query := fmt.Sprintf("state=%s&status=%s", state, status)
|
||||||
|
|
||||||
|
if transactionID != "" {
|
||||||
|
query += "&transactionId=" + url.QueryEscape(transactionID)
|
||||||
|
}
|
||||||
|
if refNum != "" {
|
||||||
|
query += "&refNum=" + url.QueryEscape(refNum)
|
||||||
|
}
|
||||||
|
if amount != "" {
|
||||||
|
query += "&amount=" + url.QueryEscape(amount)
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectURL := "/result?" + query
|
||||||
|
http.Redirect(w, r, redirectURL, http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*func (h *Handler) verifyPaymentHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
token := r.URL.Query().Get("token")
|
||||||
|
status := r.URL.Query().Get("status")
|
||||||
|
|
||||||
|
if token == "" || status == "" {
|
||||||
|
json.NewEncoder(w).Encode(VerifyResponse{
|
||||||
|
Success: false,
|
||||||
|
Status: "failed",
|
||||||
|
Message: "پارامترهای نامعتبر",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionID, err := h.service.VerifyPayment(r.Context(), token, status, "", "")
|
||||||
|
if err != nil || status != "OK" {
|
||||||
|
json.NewEncoder(w).Encode(VerifyResponse{
|
||||||
|
Success: false,
|
||||||
|
Status: "failed",
|
||||||
|
Message: "پرداخت ناموفق",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(VerifyResponse{
|
||||||
|
Success: true,
|
||||||
|
Status: "success",
|
||||||
|
TransactionID: transactionID,
|
||||||
|
Message: "پرداخت موفق",
|
||||||
|
})
|
||||||
|
}*/
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
module golang.project/gocasts/ipg-example
|
||||||
|
|
||||||
|
go 1.25.4
|
||||||
|
|
||||||
|
replace golang.project/gocasts/ipg-example/service => ./service
|
||||||
|
|
||||||
|
replace golang.project/gocasts/ipg-example/delivery/httphandler => ./delivery/httphandler
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.project/gocasts/ipg-example/delivery/httphandler"
|
||||||
|
"golang.project/gocasts/ipg-example/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config := service.Config{
|
||||||
|
PaymentGetToken: "https://sep.shaparak.ir/OnlinePG/OnlinePG",
|
||||||
|
PaymentVerifyURL: "https://sep.shaparak.ir/verifyTxnRandomSessionkey/ipg/VerifyTransaction",
|
||||||
|
CallbackURL: "http://localhost:8070/api/callback",
|
||||||
|
Credential: "15144433",
|
||||||
|
}
|
||||||
|
|
||||||
|
paymentSvc := service.NewPaymentService(config)
|
||||||
|
handler := httphandler.NewHandler(paymentSvc)
|
||||||
|
|
||||||
|
handler.StartServer(":8070")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,182 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SepTokenResponse struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
ErrorCode string `json:"errorCode"`
|
||||||
|
ErrorDesc string `json:"errorDesc"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
}
|
||||||
|
type VerifyTransactionResponse struct {
|
||||||
|
TransactionDetail TransactionDetail `json:"TransactionDetail"`
|
||||||
|
ResultCode int `json:"ResultCode"`
|
||||||
|
ResultDescription string `json:"ResultDescription"`
|
||||||
|
Success bool `json:"Success"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TransactionDetail struct {
|
||||||
|
RRN string `json:"RRN"`
|
||||||
|
RefNum string `json:"RefNum"`
|
||||||
|
MaskedPan string `json:"MaskedPan"`
|
||||||
|
HashedPan string `json:"HashedPan"`
|
||||||
|
TerminalNumber int `json:"TerminalNumber"`
|
||||||
|
OrginalAmount int64 `json:"OrginalAmount"`
|
||||||
|
AffectiveAmount int64 `json:"AffectiveAmount"`
|
||||||
|
StraceDate string `json:"StraceDate"`
|
||||||
|
StraceNo string `json:"StraceNo"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReverseTransactionResponse struct {
|
||||||
|
TransactionDetail TransactionDetail `json:"TransactionDetail"`
|
||||||
|
ResultCode int `json:"ResultCode"`
|
||||||
|
ResultDescription string `json:"ResultDescription"`
|
||||||
|
Success bool `json:"Success"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PaymentService) RealInitialize(ctx context.Context, credential, phone, resNum string, amount int64) (*SepTokenResponse, error) {
|
||||||
|
description := p.generateDescription()
|
||||||
|
|
||||||
|
message := SepTokenRequest{
|
||||||
|
Action: "token",
|
||||||
|
TerminalId: credential,
|
||||||
|
Amount: amount,
|
||||||
|
ResNum: resNum,
|
||||||
|
RedirectUrl: p.Cfg.CallbackURL,
|
||||||
|
CellNumber: phone,
|
||||||
|
TokenExpiryInMin: 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(description) > 0 {
|
||||||
|
if len(description) <= 50 {
|
||||||
|
message.ResNum1 = description
|
||||||
|
} else if len(description) <= 100 {
|
||||||
|
message.ResNum1 = description[:50]
|
||||||
|
message.ResNum2 = description[50:]
|
||||||
|
} else if len(description) <= 150 {
|
||||||
|
message.ResNum1 = description[:50]
|
||||||
|
message.ResNum2 = description[50:100]
|
||||||
|
message.ResNum3 = description[100:]
|
||||||
|
} else {
|
||||||
|
message.ResNum1 = description[:50]
|
||||||
|
message.ResNum2 = description[50:100]
|
||||||
|
message.ResNum3 = description[100:150]
|
||||||
|
message.ResNum4 = description[150:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err := json.Marshal(message)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", p.Cfg.PaymentGetToken, bytes.NewReader(bodyBytes))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("request creation error: %v", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
slog.Info("SEP Token Request", "endpoint", p.Cfg.PaymentGetToken, "body", message)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("request error: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
responseBody, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read body error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("SEP Token Response", "status_code", resp.StatusCode, "body", string(responseBody))
|
||||||
|
|
||||||
|
var parsed SepTokenResponse
|
||||||
|
if err := json.Unmarshal(responseBody, &parsed); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsed.Status == 1 && parsed.Token != "" {
|
||||||
|
slog.Info("sep payment initialized", "token", parsed.Token)
|
||||||
|
return &parsed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("sep init error (code %s): %s", parsed.ErrorCode, parsed.ErrorDesc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PaymentService) RealVerifyTransaction(ctx context.Context, refNum string) (*VerifyTransactionResponse, error) {
|
||||||
|
terminalNumber, err := strconv.Atoi(p.Cfg.Credential)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("خطا در تبدیل TerminalId: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
requestBody := map[string]interface{}{
|
||||||
|
"RefNum": refNum,
|
||||||
|
"TerminalNumber": terminalNumber,
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err := json.Marshal(requestBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyURL := p.Cfg.PaymentVerifyURL
|
||||||
|
if verifyURL == "" {
|
||||||
|
verifyURL = "https://sep.shaparak.ir/verifyTxnRandomSessionkey/ipg/VerifyTransaction"
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", verifyURL, bytes.NewReader(bodyBytes))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("request creation error: %v", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
slog.Info("SEP VerifyTransaction Request",
|
||||||
|
"endpoint", verifyURL,
|
||||||
|
"refNum", refNum,
|
||||||
|
"terminalNumber", terminalNumber)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("request error: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBytes, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read body error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("SEP VerifyTransaction Response",
|
||||||
|
"status_code", resp.StatusCode,
|
||||||
|
"body", string(respBytes))
|
||||||
|
|
||||||
|
var verifyResp VerifyTransactionResponse
|
||||||
|
if err := json.Unmarshal(respBytes, &verifyResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("VerifyTransaction Result",
|
||||||
|
"success", verifyResp.Success,
|
||||||
|
"resultCode", verifyResp.ResultCode,
|
||||||
|
"resultDescription", verifyResp.ResultDescription)
|
||||||
|
|
||||||
|
return &verifyResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PaymentService) generateDescription() string {
|
||||||
|
description := "سفارش از درگاه"
|
||||||
|
if p.IPGTransaction != nil {
|
||||||
|
description += " : " + p.IPGTransaction.ID
|
||||||
|
}
|
||||||
|
return description
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
PaymentGetToken string // آدرس دریافت توکن
|
||||||
|
PaymentVerifyURL string // آدرس تأیید تراکنش
|
||||||
|
PaymentReverseURL string // آدرس برگشت تراکنش
|
||||||
|
CallbackURL string // آدرس بازگشت
|
||||||
|
Credential string // TerminalId
|
||||||
|
}
|
||||||
|
|
||||||
|
type IPGTransaction struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
IPGType string `json:"ipg_type"`
|
||||||
|
GatewayMeta map[string]string `json:"gateway_meta,omitempty"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaymentService struct {
|
||||||
|
IPGTransaction *IPGTransaction
|
||||||
|
Cfg Config
|
||||||
|
}
|
||||||
|
|
||||||
|
type InitPaymentResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
GatewayToken string `json:"gateway_token"`
|
||||||
|
RedirectUrl string `json:"redirect_url"`
|
||||||
|
IPGTransactionId string `json:"ipg_transaction_id"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Time time.Time `json:"time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SEP Request/Response
|
||||||
|
type SepTokenRequest struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
TerminalId string `json:"TerminalId"`
|
||||||
|
Amount int64 `json:"Amount"`
|
||||||
|
ResNum string `json:"ResNum"`
|
||||||
|
RedirectUrl string `json:"RedirectUrl"`
|
||||||
|
CellNumber string `json:"CellNumber"`
|
||||||
|
TokenExpiryInMin int `json:"TokenExpiryInMin,omitempty"`
|
||||||
|
ResNum1 string `json:"ResNum1,omitempty"`
|
||||||
|
ResNum2 string `json:"ResNum2,omitempty"`
|
||||||
|
ResNum3 string `json:"ResNum3,omitempty"`
|
||||||
|
ResNum4 string `json:"ResNum4,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPaymentService(cfg Config) PaymentService {
|
||||||
|
return PaymentService{
|
||||||
|
Cfg: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PaymentService) InitPayment(ctx context.Context, amount int64, phone string) (string, string, error) {
|
||||||
|
res, err := p.startPayment(ctx, amount, phone)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
return res.GatewayToken, res.RedirectUrl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PaymentService) startPayment(ctx context.Context, amount int64, phone string) (*InitPaymentResponse, error) {
|
||||||
|
if amount < 1000 {
|
||||||
|
return nil, fmt.Errorf("حداقل مبلغ ۱۰۰۰ تومان است")
|
||||||
|
}
|
||||||
|
|
||||||
|
resNum := GenerateUniqueString()
|
||||||
|
|
||||||
|
ipgTx := &IPGTransaction{
|
||||||
|
ID: resNum,
|
||||||
|
IPGType: "sep",
|
||||||
|
GatewayMeta: map[string]string{
|
||||||
|
"callback_url": p.Cfg.CallbackURL,
|
||||||
|
},
|
||||||
|
Amount: float64(amount),
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
p.IPGTransaction = ipgTx
|
||||||
|
|
||||||
|
initRes, err := p.RealInitialize(ctx, p.Cfg.Credential, phone, resNum, amount)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("failed to init payment", "error", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectUrl := fmt.Sprintf("https://sep.shaparak.ir/OnlinePG/SendToken?token=%s", initRes.Token)
|
||||||
|
|
||||||
|
return &InitPaymentResponse{
|
||||||
|
Success: true,
|
||||||
|
GatewayToken: initRes.Token,
|
||||||
|
RedirectUrl: redirectUrl,
|
||||||
|
IPGTransactionId: ipgTx.ID,
|
||||||
|
Time: time.Now(),
|
||||||
|
Message: "پرداخت ایجاد شد",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PaymentService) VerifyPayment(ctx context.Context, resNum, refNum, traceNo, amount string) (string, error) {
|
||||||
|
if resNum == "" {
|
||||||
|
return "", fmt.Errorf("شماره سفارش نامعتبر")
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedAmount, err := strconv.ParseInt(amount, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("مبلغ نامعتبر: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyResp, err := p.RealVerifyTransaction(ctx, refNum)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("خطا در تأیید تراکنش: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !verifyResp.Success {
|
||||||
|
return "", fmt.Errorf("تأیید تراکنش ناموفق: %s", verifyResp.ResultDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
actualAmount := verifyResp.TransactionDetail.OrginalAmount
|
||||||
|
if actualAmount != expectedAmount {
|
||||||
|
slog.Error("amount mismatch",
|
||||||
|
"expected", expectedAmount,
|
||||||
|
"actual", actualAmount)
|
||||||
|
return "", fmt.Errorf("مبلغ تراکنش مغایرت دارد")
|
||||||
|
}
|
||||||
|
|
||||||
|
trackingCode := verifyResp.TransactionDetail.StraceNo
|
||||||
|
if trackingCode == "" {
|
||||||
|
trackingCode = verifyResp.TransactionDetail.RRN
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("payment verified successfully",
|
||||||
|
"refNum", refNum,
|
||||||
|
"trackingCode", trackingCode,
|
||||||
|
"amount", actualAmount)
|
||||||
|
|
||||||
|
return trackingCode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateUniqueString() string {
|
||||||
|
timestamp := time.Now().UnixNano()
|
||||||
|
randomBytes := make([]byte, 8)
|
||||||
|
rand.Read(randomBytes)
|
||||||
|
return fmt.Sprintf("%d-%s", timestamp, hex.EncodeToString(randomBytes))
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue