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