Implement example of sep

This commit is contained in:
mohammadreza javid 2026-04-12 17:09:32 +03:30
commit a63fe09f26
9 changed files with 1096 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.idea

240
delivery/html/payment.html Normal file
View File

@ -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>

172
delivery/html/result.html Normal file
View File

@ -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>

View File

@ -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: "پرداخت موفق",
})
}*/

7
go.mod Normal file
View File

@ -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
go.sum Normal file
View File

20
main.go Normal file
View File

@ -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")
}

182
service/sep.go Normal file
View File

@ -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
}

154
service/service.go Normal file
View File

@ -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))
}