Search by

taha20 / iran-payment

khoshkhu95

پکیج جامع درگاه‌های پرداخت ایرانی برای لاراول؛ سوئیچ آسان بین درگاه‌ها + کامپوننت آماده <x-iran-payment />

Package info

github.com/khoshkhu95/iran-payment

pkg:composer/taha20/iran-payment

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.1.0 2026-11-11 18:57 UTC

This package is auto-updated.

Last update: 2026-11-11 19:03:27 UTC


README

tests License: MIT

پکیج جامع درگاه‌های پرداخت ایرانی برای لاراول.

  • ۱۳ درگاه واقعی + یک درگاه آزمایشی، همه با یک اینترفیس یکسان
  • سوئیچ بین درگاه‌ها فقط با یک خط config (یا حتی یک متغیر .env)
  • کامپوننت آمادهٔ <x-iran-payment /> که لیست درگاه‌های فعال را نشان می‌دهد
  • بدون وابستگی به Tailwind، Vite یا بیلد فرانت‌اند
  • سازگار با لاراول ۱۰، ۱۱، ۱۲ و ۱۳ / PHP 8.2+

پیش‌نمایش کامپوننت

سازگاری تست‌شده

PHP 8.2 PHP 8.3 PHP 8.4
لاراول ۱۰ ✅ ✅ —
لاراول ۱۱ ✅ ✅ ✅
لاراول ۱۲ ✅ ✅ ✅
لاراول ۱۳ — ✅ ✅

این ماتریس در هر push روی GitHub Actions اجرا می‌شود (workflow). «—» یعنی آن ترکیب توسط خود لاراول پشتیبانی نمی‌شود (لاراول ۱۰ با PHP 8.4 و لاراول ۱۳ با PHP 8.2).

چرا این پکیج

اکثر پروژه‌ها یک درگاه را hard-code می‌کنند و روزی که درگاه قطع شود یا کارمزدش عوض شود، باید کل کد پرداخت را بازنویسی کنند. این پکیج لایهٔ پرداخت را از منطق دامنه جدا می‌کند:

┌──────────────┐   Payment    ┌──────────────────┐   Driver   ┌──────────┐
│  کد شما      │ ───────────► │  PaymentProcessor │ ─────────► │  درگاه   │
│ (سفارش/اشتراک)│ ◄─────────── │  + Transaction    │ ◄───────── │ (زرین‌پال،│
└──────────────┘   Event      └──────────────────┘   Result   │  زیبال…) │

کد شما فقط Payment می‌سازد و به رویداد PaymentSucceeded گوش می‌دهد. هیچ‌جا نام درگاه نوشته نمی‌شود.

نصب

۱) افزودن پکیج

composer require taha20/iran-payment

یا اگر پکیج را به‌صورت محلی دارید، در composer.json پروژه:

"repositories": [
    { "type": "path", "url": "../packages/taha20/iran-payment" }
],
"require": {
    "taha20/iran-payment": "*"
}

سپس:

composer update taha20/iran-payment

۲) نصب سریع

php artisan iran-payment:install

این دستور فایل config، ویوها و هر دو مایگریشن (تراکنش‌ها و تنظیمات درگاه‌ها) را منتشر و (با تأیید شما) اجرا می‌کند.

۳) تنظیم درگاه

در .env:

IRAN_PAYMENT_GATEWAY=zarinpal

ZARINPAL_MERCHANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

همین. حالا کافی است کامپوننت را در هر ویویی بگذارید:

<x-iran-payment :amount="1490000" order-id="ORD-1042" />

مبلغ به واحد پیش‌فرض پروژه تفسیر می‌شود (پیش‌فرض: ریال). برای تومان، IRAN_PAYMENT_CURRENCY=IRT را ست کنید یا unit="IRT" را روی کامپوننت بدهید.

کامپوننت <x-iran-payment />

لیست درگاه‌های فعال را به‌صورت کارت‌های قابل انتخاب نشان می‌دهد و کاربر با انتخاب هرکدام به صفحهٔ بانک منتقل می‌شود.

<x-iran-payment
    :amount="1490000"
    order-id="ORD-1042"
    description="خرید اشتراک طلایی"
    :payable="$order"
    return-url="{{ route('orders.show', $order) }}"
    mobile="{{ $order->user->mobile }}"
/>
ویژگی نوع توضیح
amount int اجباری — مبلغ به واحد پیش‌فرض پروژه
order-id string شمارهٔ سفارش؛ اگر ندهید از کلید payable یا یک شناسهٔ تصادفی ساخته می‌شود
description string توضیحی که در صفحهٔ بانک و پنل درگاه دیده می‌شود
payable Model اتصال morph به مدل دامنه (سفارش، اشتراک، …)
return-url string آدرس بازگشت پس از پرداخت
gateway string درگاه پیش‌انتخاب‌شده
mobile / email string برای پیش‌فرض‌های درگاه (مثل زرین‌پال)
unit IRR | IRT واحد مبلغ همین کامپوننت
only array فقط این درگاه‌ها نمایش داده شوند
exclude array این درگاه‌ها نمایش داده نشوند
layout grid | list چیدمان کارت‌ها
theme auto | light | dark تم
button-label string متن دکمه
show-amount bool نمایش/عدم نمایش مبلغ

امنیت مبلغ

مبلغ و شمارهٔ سفارش هرگز به‌صورت خام در HTML قرار نمی‌گیرند. کامپوننت آن‌ها را با APP_KEY پروژه رمزنگاری می‌کند و سرور فقط توکن معتبر را می‌پذیرد. اگر کاربر مبلغ را در DevTools عوض کند، درخواست رد می‌شود.

کامپوننت <x-iran-payment-settings />

فرم کامل تنظیمات همهٔ درگاه‌ها. یک خط در صفحهٔ تنظیمات پنل مدیریت می‌گذارید و برنامه‌نویس لازم نیست برای هر درگاه فیلد، اعتبارسنجی و ذخیره‌سازی بنویسد.

{{-- همهٔ درگاه‌ها --}}
<x-iran-payment-settings />

{{-- فقط چند درگاه --}}
<x-iran-payment-settings :only="['zarinpal', 'zibal', 'mellat']" />

{{-- ارسال به روت اختصاصی پروژه (اگر روت آمادهٔ پکیج را نمی‌خواهید) --}}
<x-iran-payment-settings :action="route('admin.payments.settings')" />

پیش‌نمایش فرم تنظیمات

چه چیزی می‌سازد:

  • لیست کنار صفحه برای همهٔ درگاه‌ها، با نشان تنظیم‌شده یا ناقص
  • سوییچ فعال/غیرفعال، و حالت تست برای درگاه‌هایی که sandbox دارند
  • فیلدهای مخصوص هر درگاه با برچسب فارسی و راهنمای کوتاه (مرچنت آیدی، کلید API، ترمینال، رمز، کلید خصوصی RSA و…)
  • انتخاب درگاه پیش‌فرض در بالای فرم
  • هشدار «این فیلدها خالی‌اند» برای هر درگاه ناقص
  • ماسک‌کردن مقادیر حساس: کلید یا رمزی که ذخیره شده، هرگز در HTML نمی‌آید
ویژگی نوع توضیح
only array فقط این درگاه‌ها در فرم باشند
action string آدرس مقصد فرم؛ پیش‌فرض روت آمادهٔ پکیج
values array مقادیر جایگزین برای پیش‌پر کردن (اگر خودتان ذخیره می‌کنید)
theme auto | light | dark تم
title string عنوان فرم
show-advanced bool نمایش فیلدهای پیشرفته (آدرس‌های endpoint) — پیش‌فرض true

محل ذخیرهٔ تنظیمات

سه درایور آماده است. اولویت خواندن: مقدار ذخیره‌شده ← .env ← config.

// config/iran-payment.php
'settings' => [
    'store' => 'database', // database | env | array
],
درایور مناسب برای نکته
database (پیش‌فرض) اکثر پروژه‌ها با config:cache سازگار؛ مقادیر رمزنگاری‌شده ذخیره می‌شوند
env پروژه‌های بدون جدول اضافه فایل .env را می‌نویسد؛ نیازمند دسترسی نوشتن
array تست خودکار بین درخواست‌ها باقی نمی‌ماند

برای درایور env، نام متغیرها همان قرارداد فعلی است (ZARINPAL_MERCHANT_ID، MELLAT_PASSWORD و…)، پس فایل config بدون تغییر همان مقدارها را می‌خواند.

⚠️ امنیت فرم تنظیمات

روت آماده (POST iran-payment/settings) به‌طور پیش‌فرض فقط میدل‌ور web دارد. اگر کامپوننت را در پنل مدیریت می‌گذارید، حتماً آن را محدود کنید:

'settings' => [
    'middleware' => ['web', 'auth', 'can:manage-payments'],
],

یا settings.routes را false کنید و فرم را به روت اختصاصی خودتان بفرستید.

استفادهٔ برنامه‌نویسی

use Taha20\IranPayment\Facades\IranPayment;

IranPayment::settings()->for('zarinpal');        // مقادیر ذخیره‌شدهٔ یک درگاه
IranPayment::settings()->isConfigured('mellat'); // آیا همهٔ فیلدهای اجباری پر است؟
IranPayment::settings()->missingFields('mellat');
IranPayment::settings()->forget('zibal');        // بازگشت به مقادیر config و .env

اگر فرم را به روت خودتان می‌فرستید، بعد از اعتبارسنجی:

IranPayment::settings()->save('zarinpal', [
    'enabled' => true,
    'merchant_id' => $request->input('merchant_id'),
]);

فقط کلیدهایی ذخیره می‌شوند که در شناسنامهٔ فیلدهای همان درگاه تعریف شده‌اند؛ حتی اگر کسی در DevTools فیلد driver اضافه کند، نادیده گرفته می‌شود.

استفادهٔ برنامه‌نویسی

use Taha20\IranPayment\Facades\IranPayment;
use Taha20\IranPayment\Support\Payment;

$payment = Payment::make(1490000, 'ORD-1042')
    ->withDescription('خرید اشتراک طلایی')
    ->withMobile('09120000000')
    ->withReturnUrl(route('orders.show', $order))
    ->for($order);            // اتصال به مدل دامنه

$response = IranPayment::pay($payment, 'zibal');

return match ($response->type) {
    'redirect' => redirect()->away($response->url),
    'form'     => view('iran-payment::redirect', [
        'action' => $response->formAction,
        'data'   => $response->formData,
    ]),
};

تغییر درگاه در زمان اجرا

IranPayment::use('idpay')->pay($payment);   // این درخواست با آیدی‌پی

بازگشت وجه

$result = IranPayment::refund($transaction);

if ($result->success) {
    // ...
}

رویدادها

رویداد زمان
PaymentInitiated تراکنش در درگاه ساخته شد (قبل از رفتن کاربر به بانک)
PaymentSucceeded پرداخت تأیید شد
PaymentFailed پرداخت ناموفق/لغو/خطای ارتباط
PaymentRefunded بازگشت وجه انجام شد

نمونهٔ Listener:

use Taha20\IranPayment\Events\PaymentSucceeded;

class MarkOrderAsPaid
{
    public function handle(PaymentSucceeded $event): void
    {
        $event->transaction->payable?->markAsPaid();

        // $event->transaction->tracking_code
        // $event->transaction->card_number
    }
}

ثبت در AppServiceProvider:

Event::listen(PaymentSucceeded::class, MarkOrderAsPaid::class);

درگاه‌های پشتیبانی‌شده

کلید درگاه نوع نوع اتصال
zarinpal زرین‌پال واسط REST v4
zibal زیبال واسط REST v1
idpay آیدی‌پی واسط REST v1.1
payir پی‌آی‌آر واسط REST
nextpay نکست‌پی واسط REST
payping پی‌پینگ واسط REST v2
aqayepardakht آقای پرداخت واسط REST v2 (تومان)
paystar پی‌استار واسط REST
mellat به‌پرداخت ملت مستقیم SOAP
sadad سداد (بانک ملی) مستقیم REST + امضای RSA
saman سامان (سپ) مستقیم REST + SOAP
parsian پارسیان مستقیم SOAP
pasargad پاسارگاد مستقیم RSA
fake درگاه آزمایشی — محلی

درگاه‌های بانکی مستقیم به کلید/ترمینال واقعی نیاز دارند. قبل از استفادهٔ عملیاتی، یک تراکنش واقعی در محیط تست بانک انجام دهید؛ برخی بانک‌ها پارامترهای اضافی (مثل AdditionalData) را متفاوت تفسیر می‌کنند و می‌توانید آن‌ها را از config همان درگاه تنظیم کنید.

افزودن درگاه سفارشی

فقط یک کلاس و یک آیتم config:

namespace App\Payments;

use Taha20\IranPayment\Gateways\AbstractGateway;
use Taha20\IranPayment\Models\Transaction;
use Taha20\IranPayment\Support\PaymentResponse;
use Taha20\IranPayment\Support\VerificationResult;

class MyGateway extends AbstractGateway
{
    public function purchase(Transaction $transaction): PaymentResponse
    {
        $response = $this->postJson('https://api.example.com/pay', [
            'amount'      => $transaction->amount,
            'callback'    => $this->callbackUrl($transaction),
            'order_id'    => $transaction->order_id,
            'merchant_id' => $this->require('merchant_id'),
        ]);

        return PaymentResponse::redirect($response['url'], $response['token'], $response);
    }

    public function verify(Transaction $transaction, array $callback): VerificationResult
    {
        $response = $this->postJson('https://api.example.com/verify', [
            'token' => $transaction->reference,
        ]);

        return ($response['ok'] ?? false)
            ? VerificationResult::success(referenceId: $response['ref_id'], raw: $response)
            : VerificationResult::failure($response['message'] ?? null, $response);
    }
}
// config/iran-payment.php
'my-gateway' => [
    'driver' => App\Payments\MyGateway::class,
    'enabled' => true,
    'title' => 'درگاه من',
    'description' => 'توضیح کوتاه',
    'color' => '#0ea5e9',
    'merchant_id' => env('MY_GATEWAY_MERCHANT_ID'),
],

همین. کامپوننت خودش آن را نشان می‌دهد و IranPayment::use('my-gateway') کار می‌کند.

اگر نمی‌خواهید کلاس جدا بسازید:

IranPayment::manager()->extend('dynamic', fn () => new MyGateway([...]));

تست

use Taha20\IranPayment\Facades\IranPayment;

IranPayment::fake();          // پرداخت موفق
IranPayment::fake('failed');  // پرداخت ناموفق

$this->post(route('iran-payment.pay'), ['token' => $token, 'gateway' => 'fake']);

برای تست‌های بدون دیتابیس:

IRAN_PAYMENT_DB_ENABLED=false

تنظیمات مهم

کلید پیش‌فرض توضیح
default zarinpal درگاه پیش‌فرض
currency IRR واحد ورودی مبالغ
routes.enabled true اگر false، خودتان روت/کنترلر می‌سازید
routes.prefix iran-payment پیشوند آدرس‌ها
transactions.enabled true ذخیره در دیتابیس؛ false = حالت بی‌حالت
security.sign_payments true امضای توکن مبلغ
http.retry 0 تکرار خودکار درخواست — برای پرداخت عمداً صفر
ui.only / ui.except [] محدود کردن لیست نمایشی درگاه‌ها

ساختار تراکنش

هر پرداخت یک رکورد در جدول iran_payments می‌سازد:

$transaction->amount;         // به ریال
$transaction->formattedAmount(); // ۱۴۹,۰۰۰ تومان
$transaction->status;         // TransactionStatus enum
$transaction->tracking_code;  // کد رهگیری
$transaction->card_number;    // شمارهٔ کارت ماسک‌شده
$transaction->payable;        // مدل دامنه

برای طراحی کامل و دلایل هر انتخاب، docs/ARCHITECTURE.md را ببینید.

مجوز

MIT