مستندات ریختم
ریختم هیچ نقش واسطهای ندارد: پول مستقیم به حساب شما واریز میشود. اپلیکیشن پیامک واریز بانک را میخواند و سرور ما با یک درخواست ساده به سایت یا ربات شما خبر میدهد تا صورتحساب همان لحظه تایید شود.
🧩 افزونهی ووکامرس
یک درگاه پرداخت «کارت به کارت با تایید خودکار» به ووکامرس اضافه میکند. با تسویه حساب کلاسیک و بلوکی ووکامرس و ذخیرهسازی جدید سفارشها (HPOS) سازگار است.
⬇️ دانلود افزونهنصب در ۵ قدم
- پیشخوان وردپرس ← افزونهها ← افزودن ← بارگذاری افزونه ← فایل zip را انتخاب و فعال کنید.
- ووکامرس ← پیکربندی ← پرداختها ← کارت به کارت خودکار را باز کنید.
- کارتها را وارد کنید؛ هر خط:
شماره کارت | نام صاحب کارت | بانک | شبا. - آدرسی که همان صفحه نشان میدهد (
/wp-json/rikhtam/v1/callback) را خودتان در پنل ریختم (بخش «درگاهها») بهعنوان «آدرس دریافتکننده» ثبت کنید، بعد «کلید امنیتی» همان درگاه را از پنل بردارید و در فیلد Secret افزونه بگذارید. - در اپلیکیشن ریختم، حساب بانکی را اضافه و به همین درگاه وصل کنید. تمام! یک سفارش تستی ارزان بزنید و واریز کنید.
⚙️ تنظیمات افزونه
| تنظیم | توضیح |
|---|---|
| عنوان و توضیحات | متنی که مشتری در صفحهی پرداخت، صفحهی تشکر و ایمیل میبیند؛ کاملاً قابل تغییر. |
| کارتها | یک یا چند کارت. حالت «یک کارت تصادفی برای هر سفارش» واریزها را بین حسابها پخش میکند. |
| واحد پول فروشگاه | خودکار از ووکامرس (تومان، ریال، هزار تومان، هزار ریال) یا دستی. |
| عدد یکتا | واحد (تومان/ریال)، اضافه یا کم شدن از مبلغ (تخفیف کوچک برای مشتری) و بازهی عدد (پیشفرض ۱ تا ۹۹۹). |
| وضعیت سفارش | قبل از واریز «در انتظار بررسی» یا «در انتظار پرداخت»؛ بعد از تایید «پیشفرض»، «در حال انجام» یا «تکمیل شده». |
| مهلت واریز | سفارش پرداختنشده بعد از N دقیقه لغو میشود (۰ = هرگز). اگر واریز دیر برسد، سفارش لغوشده دوباره فعال و پرداخت میشود (قابل خاموش کردن). |
| حداقل / حداکثر مبلغ | درگاه فقط برای سبدهای در این بازه نمایش داده میشود. |
| ایمیل، بروزرسانی خودکار صفحه، نمایش شبا، گزارش اشکالیابی | هر کدام جدا روشن/خاموش میشود. گزارشها: ووکامرس ← وضعیت ← گزارشها ← rikhtam. |
برای توسعهدهندهها: اکشن rikhtam_order_paid ($order, $deposit) بعد از هر تایید اجرا میشود.
🔔 بروزرسانی افزونه
افزونه هر ۶ ساعت نسخهی جدید را از سرور ریختم چک میکند. وقتی نسخهی جدید منتشر شود:
- در پیشخوان وردپرس اعلانی با فهرست تغییرات و دکمهی «بروزرسانی همین حالا» نمایش داده میشود.
- در صفحهی افزونهها و پیشخوان ← بروزرسانیها هم مثل بقیهی افزونهها قابل نصب با یک کلیک است (بروزرسانی خودکار وردپرس هم پشتیبانی میشود).
- برای چک فوری: افزونهها ← زیر «ریختم» ← بررسی بروزرسانی.
تنظیمات و سفارشها با بروزرسانی پاک نمیشوند.
🛠 طرز کار (اتصال اختصاصی)
- سایت شما برای هر صورتحساب یک مبلغ یکتا میسازد و همراه شماره کارت به مشتری نشان میدهد.
- مشتری کارت به کارت میکند؛ پیامک واریز به گوشیای میرسد که اپلیکیشن ریختم روی آن است.
- اپلیکیشن مبلغ را از پیامک جدا میکند و به سرور ریختم میفرستد.
- اپلیکیشن خودش، مستقیم از روی گوشی، واریز را با یک درخواست
POSTبه آدرس درگاه شما میفرستد؛ تایید پرداخت به سایت یا سرور ریختم وابسته نیست و حتی اگر سایت اصلی ما در دسترس نباشد کار میکند. ثبت سابقه و کارمزد هم بعداً و خودکار در سرور انجام میشود. - شما فقط
keyرا با کلید امنیتی خودتان مقایسه میکنید، صورتحساب هممبلغ را پیدا و تایید میکنید و{"ok":true}برمیگردانید.
خودتان در پنل کاربری سایت (بخش «درگاهها») برای هر درگاه «آدرس دریافت» (کالبک، فقط https و با دامنهی عمومی) را ثبت میکنید و «کلید امنیتی» همانجا برایتان ساخته میشود. چند درگاه میتوانند یک حساب مشترک داشته باشند؛ واریز به همهی درگاههای آن حساب عرضه میشود تا درگاهای که صورتحساب هممبلغ دارد آن را بپذیرد. به محض اینکه یکی از درگاهها واریز را تایید کند، همان واریز برای بقیهی درگاهها باطل میشود و در اپ و پنل «فاکتور پیدا نشد» یا «در صف» نشان داده نمیشود.
🔢 مبلغ یکتا
تنها اطلاعات قابل اعتماد پیامک بانک «مبلغ» است؛ پس مبلغ صورتحسابهای باز نباید تکراری باشد. روش پیشنهادی:
// مبلغ اصلی 125,000 تومان → 125,317 تومان (عدد ۳ رقمی تصادفی که بین صورتحسابهای باز تکراری نباشد)
$unique = $price + random_int(1, 999);
// اگر با ریال کار میکنید: 1,250,000 → 1,250,317 ریال
📥 درخواستی که به آدرس شما میرسد
روش POST با بدنهی فرم (application/x-www-form-urlencoded)، User-Agent: Rikhtam/1.0. درخواست از روی گوشیای که اپلیکیشن روی آن نصب است فرستاده میشود، پس IP ثابتی ندارد؛ آدرس دریافتکننده را بر اساس IP فیلتر نکنید و فقط با key احراز کنید. اگر گوشی اینترنت نداشته باشد یا درگاه شما در دسترس نباشد، واریز در صف میماند و دوباره تلاش میشود.
| فیلد | توضیح |
|---|---|
project_id | شناسهی درگاه در ریختم |
uid | شناسهی یکتای واریز؛ برای جلوگیری از تایید تکراری ذخیرهاش کنید |
amount | مبلغ در واحدی که برای درگاه تنظیم شده (ریال، یا تومان = ریال ÷ ۱۰) |
amount_rial | مبلغ خام پیامک به ریال |
account | شماره حسابی که کاربر در اپلیکیشن ثبت کرده (ممکن است خالی باشد) |
sms_time / time | زمان پیامک: ثانیهی یونیکس / متن به وقت تهران |
ts | زمان ارسال درخواست (ثانیهی یونیکس)؛ اختیاری |
event_id | شمارهی واریز در سرور ریختم؛ وقتی اپلیکیشن مستقیم خبر میدهد 0 است، پس به آن تکیه نکنید و از uid استفاده کنید |
invoice, manual | فقط در پیوست دستی: شمارهی صورتحسابی که صاحب درگاه در اپلیکیشن وارد کرده |
key | کلید امنیتی درگاه شما؛ فقط با کلید خودتان مقایسه کنید |
test | فقط در «تست سلامت درگاه» (1)؛ توضیح |
🔐 احراز درخواست
فقط یک شرط: فیلد key باید دقیقاً با «کلید امنیتی» درگاه شما در پنل برابر باشد؛ وگرنه 403 برگردانید. آدرس دریافت فقط https است و کلید فقط دست شما و ماست، پس بدون آن کسی نمیتواند واریز جعلی بفرستد. تکراری بودن را هم همان uid میگیرد.
if (!hash_equals(SECRET, $_POST['key'] ?? '')) { http_response_code(403); exit; }
📤 پاسخ شما
| وضعیت | پاسخ | کار سرور |
|---|---|---|
| تایید شد (یا همین واریز قبلاً تایید شده) | 200 و {"ok":true,"invoice":"4821"} | تمام؛ شمارهی invoice کنار واریز در اپلیکیشن و پنل نمایش داده میشود |
| صورتحسابی با این مبلغ/شماره نیست | 404 | درگاه بعدیِ همان حساب امتحان میشود و بعداً دوباره تلاش میشود |
| صورتحساب قبلاً پرداخت شده | 409 | دوباره تایید نمیشود؛ پیام مناسب به صاحب درگاه نشان داده میشود |
| صورتحساب لغو یا منقضی شده | 410 | مثل بالا |
| خطای موقت | 5xx یا تایماوت | تلاش مجدد با فاصلهی افزایشی |
invoice در پاسخ اختیاری ولی بسیار پیشنهادی است: شمارهی صورتحساب/سفارشی که تایید کردید. با آن در اپلیکیشن میبینید هر واریز برای کدام صورتحساب بوده.
مهلت پاسخ تا ۳۰ ثانیه است (در تست سلامت و پیوست دستی که از سرور ما فرستاده میشود تا ۲۰ ثانیه)؛ کارهای سنگین (ارسال پیام، ایمیل) را بعد از پاسخ یا در صف انجام دهید.
🔗 پیوست دستی به صورتحساب
اگر مشتری بهجای مبلغ یکتا یک عدد رند (مثلاً ۱۲۵,۰۰۰ بهجای ۱۲۵,۳۱۷) واریز کند، هیچ صورتحسابی خودکار تایید نمیشود. صاحب درگاه در اپلیکیشن روی همان واریز «پیوست به صورتحساب» را میزند و شمارهی صورتحساب را وارد میکند. سرور همان واریز را دوباره به آدرس شما میفرستد، این بار با:
| فیلد | توضیح |
|---|---|
invoice | شمارهی صورتحسابی که صاحب درگاه وارد کرده |
manual | 1 |
در این حالت صورتحساب را با شماره پیدا کنید، نه مبلغ:
- صورتحساب نیست →
404 - قبلاً پرداخت شده →
409(یک صورتحساب هرگز دو بار تایید نمیشود) - لغو/منقضی →
410(یا اگر میخواهید، دوباره فعالش کنید و تایید کنید) - در غیر این صورت پرداختشده کنید و
{"ok":true,"invoice":"…"}برگردانید.
invoice را بگذارید؛ نمونهکدهای پایین آن را دارند.🔁 تلاش مجدد و درخواست تکراری
اگر پاسخ موفق نگیرد، سرور با فاصلهی ۱، ۲، ۵، ۱۰، ۳۰، ۶۰، ۱۲۰ و سپس هر ۳۶۰ دقیقه دوباره میفرستد (تا ۳۰ بار). پس یک واریز ممکن است بیش از یک بار برسد:
uid را ذخیره کنید و برای درخواست تکراری هم {"ok":true} (همراه شمارهی صورتحساب) برگردانید، بدون تایید دوباره.
💻 کد آماده
همهی نمونهها کاملاند: بررسی کلید، تکراری، پیدا کردن با مبلغ یا شماره، جلوگیری از تایید دوباره و برگرداندن شمارهی صورتحساب. فقط سه تابع دیتابیس را به سیستم خودتان وصل کنید.
<?php
// rikhtam.php — آدرس همین فایل را در پنل ریختم ثبت کنید
const SECRET = 'کلید امنیتی درگاه';
header('Content-Type: application/json; charset=utf-8');
$f = fn($k) => (string)($_POST[$k] ?? '');
if (!hash_equals(SECRET, $f('key'))) {
http_response_code(403); exit(json_encode(['ok' => false, 'error' => 'bad key']));
}
$invoice = trim($f('invoice'));
$pdo = new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', 'user', 'pass', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
// همین واریز قبلاً ثبت شده؟
$st = $pdo->prepare('SELECT id FROM invoices WHERE paid_uid = ?'); $st->execute([$f('uid')]);
if ($id = $st->fetchColumn()) exit(json_encode(['ok' => true, 'duplicate' => true, 'invoice' => (string)$id]));
if ($invoice !== '') { // پیوست دستی: با شماره
$st = $pdo->prepare('SELECT id, status FROM invoices WHERE id = ?'); $st->execute([$invoice]);
} else { // عادی: با مبلغ یکتا (صورتحسابهای باز ۲۴ ساعت اخیر)
$st = $pdo->prepare('SELECT id, status FROM invoices WHERE amount = ? AND status = "unpaid" AND created_at > NOW() - INTERVAL 1 DAY ORDER BY id DESC LIMIT 1');
$st->execute([(int)$f('amount')]);
}
$inv = $st->fetch(PDO::FETCH_ASSOC);
if (!$inv) { http_response_code(404); exit(json_encode(['ok' => false, 'error' => 'invoice not found'])); }
if ($inv['status'] === 'paid') { http_response_code(409); exit(json_encode(['ok' => false, 'error' => 'invoice already paid'])); }
if ($inv['status'] === 'cancelled') { http_response_code(410); exit(json_encode(['ok' => false, 'error' => 'invoice cancelled'])); }
// تایید اتمی: فقط اگر هنوز پرداخت نشده
$up = $pdo->prepare('UPDATE invoices SET status = "paid", paid_uid = ?, paid_at = NOW() WHERE id = ? AND status <> "paid"');
$up->execute([$f('uid'), $inv['id']]);
if (!$up->rowCount()) { http_response_code(409); exit(json_encode(['ok' => false, 'error' => 'invoice already paid'])); }
echo json_encode(['ok' => true, 'invoice' => (string)$inv['id']]);
// بعد از پاسخ: فعالسازی سرویس، پیام به مشتری و ...// routes/api.php
Route::post('/rikhtam', [RikhtamController::class, 'callback']); // مسیر api است؛ CSRF ندارد
// app/Http/Controllers/RikhtamController.php
class RikhtamController extends Controller
{
public function callback(Request $r)
{
$invoice = trim((string) $r->input('invoice', ''));
if (!hash_equals(config('services.rikhtam.secret'), (string) $r->input('key'))) {
return response()->json(['ok' => false, 'error' => 'bad key'], 403);
}
if ($dup = Invoice::where('paid_uid', $r->input('uid'))->first()) {
return ['ok' => true, 'duplicate' => true, 'invoice' => (string) $dup->id];
}
$inv = $invoice !== ''
? Invoice::find($invoice)
: Invoice::where('amount', (int) $r->input('amount'))->where('status', 'unpaid')->where('created_at', '>', now()->subDay())->latest('id')->first();
if (!$inv) return response()->json(['ok' => false, 'error' => 'invoice not found'], 404);
if ($inv->status === 'cancelled') return response()->json(['ok' => false, 'error' => 'invoice cancelled'], 410);
$updated = Invoice::whereKey($inv->id)->where('status', '!=', 'paid')
->update(['status' => 'paid', 'paid_uid' => $r->input('uid'), 'paid_at' => now()]);
if (!$updated) return response()->json(['ok' => false, 'error' => 'invoice already paid'], 409);
dispatch(new ActivateOrder($inv->id)); // کار سنگین در صف
return ['ok' => true, 'invoice' => (string) $inv->id];
}
}// Express
const express = require('express'), crypto = require('crypto');
const app = express();
const SECRET = process.env.RIKHTAM_SECRET;
app.post('/rikhtam', express.urlencoded({ extended: false }), async (req, res) => {
const b = req.body, invoice = (b.invoice || '').trim();
const given = String(b.key || '');
const ok = given.length === SECRET.length && crypto.timingSafeEqual(Buffer.from(SECRET), Buffer.from(given));
if (!ok) return res.status(403).json({ ok: false, error: 'bad key' });
const dup = await db.invoice.findFirst({ where: { paidUid: b.uid } });
if (dup) return res.json({ ok: true, duplicate: true, invoice: String(dup.id) });
const inv = invoice
? await db.invoice.findUnique({ where: { id: Number(invoice) } })
: await db.invoice.findFirst({ where: { amount: Number(b.amount), status: 'unpaid', createdAt: { gt: new Date(Date.now() - 864e5) } }, orderBy: { id: 'desc' } });
if (!inv) return res.status(404).json({ ok: false, error: 'invoice not found' });
if (inv.status === 'cancelled') return res.status(410).json({ ok: false, error: 'invoice cancelled' });
const r = await db.invoice.updateMany({ where: { id: inv.id, status: { not: 'paid' } }, data: { status: 'paid', paidUid: b.uid, paidAt: new Date() } });
if (!r.count) return res.status(409).json({ ok: false, error: 'invoice already paid' });
res.json({ ok: true, invoice: String(inv.id) });
});# Flask
import hmac
from flask import Flask, request
app = Flask(__name__)
SECRET = b'project secret'
@app.post('/rikhtam')
def rikhtam():
f = request.form
invoice = f.get('invoice', '').strip()
if not hmac.compare_digest(SECRET, f.get('key', '').encode()):
return {'ok': False, 'error': 'bad key'}, 403
dup = db.one('SELECT id FROM invoices WHERE paid_uid = %s', f['uid'])
if dup:
return {'ok': True, 'duplicate': True, 'invoice': str(dup.id)}
inv = (db.one('SELECT id, status FROM invoices WHERE id = %s', invoice) if invoice else
db.one("SELECT id, status FROM invoices WHERE amount = %s AND status = 'unpaid' AND created_at > NOW() - INTERVAL '1 day' ORDER BY id DESC LIMIT 1", int(f['amount'])))
if not inv:
return {'ok': False, 'error': 'invoice not found'}, 404
if inv.status == 'cancelled':
return {'ok': False, 'error': 'invoice cancelled'}, 410
if not db.execute("UPDATE invoices SET status = 'paid', paid_uid = %s, paid_at = NOW() WHERE id = %s AND status <> 'paid'", f['uid'], inv.id):
return {'ok': False, 'error': 'invoice already paid'}, 409
return {'ok': True, 'invoice': str(inv.id)}# views.py — path('rikhtam/', views.rikhtam) در urls.py
import hmac
from django.conf import settings
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from django.utils import timezone
from .models import Invoice
@csrf_exempt
@require_POST
def rikhtam(request):
f = request.POST
invoice = f.get('invoice', '').strip()
if not hmac.compare_digest(settings.RIKHTAM_SECRET.encode(), f.get('key', '').encode()):
return JsonResponse({'ok': False, 'error': 'bad key'}, status=403)
if (dup := Invoice.objects.filter(paid_uid=f['uid']).first()):
return JsonResponse({'ok': True, 'duplicate': True, 'invoice': str(dup.pk)})
qs = Invoice.objects
inv = (qs.filter(pk=invoice).first() if invoice else
qs.filter(amount=int(f['amount']), status='unpaid', created_at__gt=timezone.now() - timezone.timedelta(days=1)).order_by('-pk').first())
if not inv:
return JsonResponse({'ok': False, 'error': 'invoice not found'}, status=404)
if inv.status == 'cancelled':
return JsonResponse({'ok': False, 'error': 'invoice cancelled'}, status=410)
if not Invoice.objects.filter(pk=inv.pk).exclude(status='paid').update(status='paid', paid_uid=f['uid'], paid_at=timezone.now()):
return JsonResponse({'ok': False, 'error': 'invoice already paid'}, status=409)
return JsonResponse({'ok': True, 'invoice': str(inv.pk)})package main
import (
"crypto/subtle"
"encoding/json"
"net/http"
"strconv"
"strings"
)
var secret = []byte("project secret")
func reply(w http.ResponseWriter, code int, body map[string]any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(body)
}
func rikhtam(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
f := r.PostForm.Get
invoice := strings.TrimSpace(f("invoice"))
if subtle.ConstantTimeCompare(secret, []byte(f("key"))) != 1 {
reply(w, 403, map[string]any{"ok": false, "error": "bad key"})
return
}
if id, ok := findByPaidUID(f("uid")); ok {
reply(w, 200, map[string]any{"ok": true, "duplicate": true, "invoice": id})
return
}
var inv *Invoice
if invoice != "" {
inv = findInvoiceByID(invoice)
} else {
amount, _ := strconv.ParseInt(f("amount"), 10, 64)
inv = findOpenInvoiceByAmount(amount)
}
switch {
case inv == nil:
reply(w, 404, map[string]any{"ok": false, "error": "invoice not found"})
case inv.Status == "cancelled":
reply(w, 410, map[string]any{"ok": false, "error": "invoice cancelled"})
case !markPaidIfUnpaid(inv.ID, f("uid")): // UPDATE ... WHERE id = ? AND status <> 'paid'
reply(w, 409, map[string]any{"ok": false, "error": "invoice already paid"})
default:
reply(w, 200, map[string]any{"ok": true, "invoice": inv.ID})
}
}
func main() { http.HandleFunc("/rikhtam", rikhtam); http.ListenAndServe(":8080", nil) }// ASP.NET Core Minimal API (Program.cs)
using System.Security.Cryptography;
using System.Text;
app.MapPost("/rikhtam", async (HttpRequest req, AppDb db) =>
{
var f = await req.ReadFormAsync();
string F(string k) => f[k].ToString();
var invoice = F("invoice").Trim();
var a = Encoding.UTF8.GetBytes(app.Configuration["Rikhtam:Secret"]!);
var b = Encoding.UTF8.GetBytes(F("key"));
if (a.Length != b.Length || !CryptographicOperations.FixedTimeEquals(a, b))
return Results.Json(new { ok = false, error = "bad key" }, statusCode: 403);
var dup = await db.Invoices.FirstOrDefaultAsync(i => i.PaidUid == F("uid"));
if (dup != null) return Results.Json(new { ok = true, duplicate = true, invoice = dup.Id.ToString() });
var amount = long.Parse(F("amount"));
var inv = invoice != ""
? await db.Invoices.FindAsync(long.Parse(invoice))
: await db.Invoices.Where(i => i.Amount == amount && i.Status == "unpaid" && i.CreatedAt > DateTime.UtcNow.AddDays(-1))
.OrderByDescending(i => i.Id).FirstOrDefaultAsync();
if (inv == null) return Results.Json(new { ok = false, error = "invoice not found" }, statusCode: 404);
if (inv.Status == "cancelled") return Results.Json(new { ok = false, error = "invoice cancelled" }, statusCode: 410);
var n = await db.Invoices.Where(i => i.Id == inv.Id && i.Status != "paid")
.ExecuteUpdateAsync(s => s.SetProperty(i => i.Status, "paid").SetProperty(i => i.PaidUid, F("uid")).SetProperty(i => i.PaidAt, DateTime.UtcNow));
if (n == 0) return Results.Json(new { ok = false, error = "invoice already paid" }, statusCode: 409);
return Results.Json(new { ok = true, invoice = inv.Id.ToString() });
}).DisableAntiforgery();// Spring Boot
@RestController
public class RikhtamController {
@Value("${rikhtam.secret}") private String secret;
private final InvoiceRepository invoices;
public RikhtamController(InvoiceRepository invoices) { this.invoices = invoices; }
@PostMapping(value = "/rikhtam", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public ResponseEntity<Map<String, Object>> callback(@RequestParam MultiValueMap<String, String> f) throws Exception {
Function<String, String> g = k -> Optional.ofNullable(f.getFirst(k)).orElse("");
String invoice = g.apply("invoice").trim();
if (!MessageDigest.isEqual(secret.getBytes(StandardCharsets.UTF_8), g.apply("key").getBytes(StandardCharsets.UTF_8)))
return ResponseEntity.status(403).body(Map.of("ok", false, "error", "bad key"));
var dup = invoices.findByPaidUid(g.apply("uid"));
if (dup.isPresent()) return ResponseEntity.ok(Map.of("ok", true, "duplicate", true, "invoice", dup.get().getId().toString()));
Optional<Invoice> inv = invoice.isEmpty()
? invoices.findFirstByAmountAndStatusAndCreatedAtAfterOrderByIdDesc(Long.parseLong(g.apply("amount")), "unpaid", LocalDateTime.now().minusDays(1))
: invoices.findById(Long.parseLong(invoice));
if (inv.isEmpty()) return ResponseEntity.status(404).body(Map.of("ok", false, "error", "invoice not found"));
if ("cancelled".equals(inv.get().getStatus())) return ResponseEntity.status(410).body(Map.of("ok", false, "error", "invoice cancelled"));
// @Modifying @Query("update Invoice i set i.status='paid', i.paidUid=:uid where i.id=:id and i.status <> 'paid'")
if (invoices.markPaidIfUnpaid(inv.get().getId(), g.apply("uid")) == 0)
return ResponseEntity.status(409).body(Map.of("ok", false, "error", "invoice already paid"));
return ResponseEntity.ok(Map.of("ok", true, "invoice", inv.get().getId().toString()));
}
}🩺 تست سلامت درگاه
در پنل سایت (بخش «درگاهها») و در اپلیکیشن (بخش درگاهها)، کنار هر درگاه دکمهی «تست سلامت درگاه» هست. سرور ریختم دو درخواست به آدرس شما میفرستد و نتیجه را موردبهمورد نشان میدهد:
- یک درخواست با کلید درست و فیلد
test=1(مبلغ ۱ وuidتستی که باtestشروع میشود). هیچ واریزی ثبت نمیشود. - یک درخواست با کلید اشتباه؛ کد درست باید آن را با
403رد کند.
| بررسی | یعنی |
|---|---|
| آدرس و اتصال | https بودن، دامنهی عمومی، DNS، گواهی SSL، پورت و تایماوت (سرعت پاسخ) |
| پاسخ درگاه | 2xx سالم؛ ریدایرکت (3xx) و 5xx خطا حساب میشود |
| کلید | اگر با کلید درست 401/403 گرفتید، کلید کد شما با کلید درگاه یکی نیست |
| تشخیص حالت تست | پاسخ {"ok":true,"test":true} یعنی کد شما حالت تست را میشناسد |
| رد کلید اشتباه | اگر کلید غلط هم پذیرفته شود، نتیجهی قرمز و هشدار امنیتی میگیرید (هر کسی میتواند پرداخت جعلی بفرستد) |
برای اینکه کد شما به تست درست جواب دهد، بعد از بررسی کلید و قبل از هر کار دیگر این را اضافه کنید (هیچ صورتحسابی را عوض نکنید):
if (!hash_equals(SECRET, $_POST['key'] ?? '')) { http_response_code(403); exit; }
if (($_POST['test'] ?? '') === '1') { header('Content-Type: application/json'); echo json_encode(['ok' => true, 'test' => true]); exit; }
test را نمیشناسد، تست «هشدار» میدهد (نه خطا)، چون ممکن است برای مبلغ ۱ تومان دنبال صورتحساب بگردد. بهتر است همین چند خط را اضافه کنید. هر ۱۰ دقیقه حداکثر ۱۲ تست مجاز است.🧪 تست اتصال بدون واریز واقعی
این دستور یک واریز ساختگی به آدرس شما میفرستد (کلید، آدرس و مبلغ یک صورتحساب باز را عوض کنید):
curl -s -X POST 'https://your-site.com/wp-json/rikhtam/v1/callback' \
-d 'key=کلید_امنیتی' -d 'project_id=1' -d "uid=test$(date +%s)" -d 'amount=1250317' -d 'amount_rial=1250317'
پاسخ {"ok":true} یعنی صورتحساب تایید شد؛ 404 یعنی صورتحسابی با این مبلغ نبود؛ 403 یعنی کلید امنیتی اشتباه است.
🏦 حساب، کارت و پیامک
- به مشتری شماره کارت نشان میدهید، ولی پیامک واریز بانک معمولاً شماره حساب را دارد. مشکلی نیست: صورتحساب فقط با مبلغ یکتا شناسایی میشود و شماره کارت در تطبیق نقشی ندارد.
- هنگام ساخت درگاه شماره حساب نمیپرسیم. بعد از ساخت درگاه، در اپلیکیشن از بخش «حسابها ← افزودن حساب» بانک و شمارهی حساب را بنویسید و همان درگاه را تیک بزنید؛ تا حسابی پیوست نشود هیچ واریزی به درگاه نمیرسد. فقط شماره حساب (همان که در پیامک بانک میآید) را بنویسید، نه شماره کارت و نه شبا. بعضی بانکها (مثل ملی) فقط چند رقم آخر حساب را در پیامک مینویسند؛ باز هم شمارهی کامل را بنویسید.
- حساب لازم نیست به نام خودتان باشد؛ فقط اپلیکیشن ریختم باید روی گوشیای باشد که سیمکارتش پیامک واریز همان حساب را میگیرد.
- چند حساب از چند بانک را میتوانید همزمان در اپلیکیشن اضافه کنید.
🧯 رفع مشکل
| مشکل | راهحل |
|---|---|
| چرا فاکتور تسویهی کارمزد نیامده؟ | تا وقتی بدهی کارمزد کمتر از ۱۰٬۰۰۰ تومان باشد فاکتور تسویه ساخته نمیشود؛ با رسیدن بدهی به این مقدار، فاکتور خودکار ساخته میشود. |
| واریز انجام شد ولی هیچ واریزی در اپ ثبت نشد | بعضی بانکها برای مبالغ پایین پیامک نمیفرستند. صورتحسابهایتان نباید زیر حداقل مبلغی باشد که بانک برایش پیامک میدهد، وگرنه سیستم عمل نمیکند. |
| واریز در پنل «فاکتور پیدا نشد» است | مبلغ واریزی با مبلغ یکتای صورتحساب یکی نیست، یا صورتحساب منقضی شده، یا واحد درگاه (ریال/تومان) با کد شما نمیخواند. |
| پاسخ 403 | کلید امنیتی در سایت با پنل یکی نیست. |
در وردپرس آدرس /wp-json/ باز نمیشود | تنظیمات ← پیوندهای یکتا را یک بار ذخیره کنید؛ افزونهی امنیتی یا فایروال REST API را مسدود نکند. |
| واریز به سرور نمیرسد | در اپ، بخش گزارش و «وضعیت سلامت برنامه» را ببینید: دسترسی پیامک، معافیت باتری و قالب بانک. |