Ödeme Geçidi Yazma

1.8k görüntülenme Markdown

Ödeme modülü bir sağlayıcıyı ödeme seçeneğine dönüştürür: ödeme ekranını gösterir, kartı çeker ya da müşteriyi yönlendirir. Sonucu çekirdeğin mahsup ettiği dizi olarak verir.

Genel Bakış

Bir geçit modülü PaymentGatewayModule'ü genişletir ve coremio/modules/Payment/{Ad}/{Ad}.php altında yaşar. 164 modül gelir, dördü aşağıdaki sandbox örneğidir.

Ödeme hattının dört ayağı vardır; modül ikisine sahiptir.

hat
anlık görüntü       core, müşterinin gördüğü tam rakamları taşıyan bir `checkouts` satırı yazar
      |
ödeme yüzeyi        MODULE  payment_screen() müşterinin neyle karşılaşacağına karar verir
      |
capture / callback  MODULE  capture($params) çeker ya da callback() sağlayıcının dönüşünü alır
      |
settle              core    settle_checkout() ödemeyi faturalara işler

Modül bir faturayı asla ödendi yapmaz. ['status' => 'successful', ...] döner, kaydı çekirdek işler. Kendiniz yaparsanız çift kayıt oluşur: yinelenen bildirim kodu iki kez çalıştırır.

Ön Koşullar

  • Modül iskeleti: dizin, sınıf dosyası, config.php, lang/. Bkz. Modül Anatomisi.
  • Dört Sample* geçidi: çalışan modüller, her biri bir arketip.
  • Sağlayıcının test hesabı bilgileri; bildirim gönderiyorsa dışarıdan erişilebilir bir alan adı.
  • Örnek Modules::getInstance('Payment', 'Acme') ile alınır, new Acme() ile değil.

Yapı

coremio/modules/Payment/Acme/
Acme.php        sınıf: extends PaymentGatewayModule
config.php      ['meta' => [...], 'settings' => [...]] döndürür
lang/en.php     düz bir key => string haritası döndürür, $this->lang['key'] ile okunur
lang/tr.php
logo.png        admin modül listesinde ve ödeme yöntemi satırında görünür

Arketipi önce seçin; hangi metotları bildireceğinizi o belirler. Çekirdek metotları method_exists ile bulur.

ArketipBildirdiğinizMüşteri deneyimiKopyalanacak örnek
Kendi kart formucapture() ve standard_card = trueÖdeme sayfasında kart alanları, çekimi siz yaparsınızSampleMerchant
Yönlendirme / barındırılan sayfaarea() ve callback()Yönlendirme paneli, ardından sağlayıcının kendi sayfasıSampleThirdParty
Kart kasasıcapture(), card_setup_result(), meta card-storage-supportedKayıtlı kartlar ve oturum dışı otomatik ödemeSampleTokenized
Yinelenen abonelikpayment_screen() ezmesi, callback(), cancel_subscription()Tek seferlik öde ya da abone ol, yenilemeleri sağlayıcı çekerSampleSubscription

PayTR ezilmiş bir payment_screen()'den iframe gösterir. Iyzico area()'lı bir yönlendirme geçididir. Stripe kart saklar; PayPal tek çekim ile abonelik sunar.

Adım Adım

Modül

Taban kurucu şunları doldurur: $this->config, $this->lang, $this->dir, $this->url, geri dönüş bağlantıları, yetenek ayarları. parent::__construct() çağırın.

Acme.php
class Acme extends PaymentGatewayModule
{
    public function __construct()
    {
        parent::__construct();

        // $this->links['callback'] artık bu modülün herkese açık dönüş adresidir,
        // get_auth_token() ile imzalanmıştır; onu sağlayıcıya verin.
    }
}

Yetenekler

Yetenek ayarları config.php meta bloğundan gelir, taban kurucu okur.

config.php
<?php
return [
    'meta' => [
        'name'                   => 'Acme Pay',
        'version'                => '1.0',
        'icon_type'              => 'font',
        'icon'                   => 'bi bi-credit-card',
        'card-storage-supported' => false,  // kart jetonunu kasaya alabilir
        'auto-payment-supported' => false,  // oturum dışı çekim yapabilir (otomatik ödeme)
        'standard-card-form'     => false,  // ortak kart giriş formunu basar
        'embedded-pane'          => true,   // panel ödeme sayfasının içine gömülebilir
        'subscription-mode'      => 'amount',  // items | amount | fixed, aşağıya bakın
        'subscription-anchor'    => false,  // ileri bir tarihte sözleşme başlatabilir
    ],
    'settings' => [
        'commission_rate'      => 0,
        'force_convert_to'     => 0,
        'min_amount'           => 0,   // kabul edilen tutar aralığı; 0 sınırı kapatır
        'max_amount'           => 0,
        'amount_limit_cid'     => 0,   // iki sınırın yazıldığı para birimi
        'accepted_countries'   => [],
        'unaccepted_countries' => [],
    ],
];

Ayar Formu

config_fields() bildirin; ayar ekranı kendini kurar. controller_settings() bildirilen her anahtarı ortak alanlarla config['settings'] içine kaydeder: durum, komisyon oranı, zorunlu para birimi, tutar aralığı, ülke listeleri.

config_fields()
public function config_fields()
{
    return [
        'merchant_id' => [
            'name'        => $this->lang['merchant-id'] ?? 'Merchant ID',
            'description' => $this->lang['merchant-id-desc'] ?? '',
            'type'        => 'text',
            'value'       => $this->config['settings']['merchant_id'] ?? '',
            'placeholder' => 'acme_12345',
        ],
        'secret_key' => [
            'name'  => $this->lang['secret-key'] ?? 'Secret Key',
            'type'  => 'password',
            'value' => $this->config['settings']['secret_key'] ?? '',
        ],
    ];
}

Ödeme Ekranı

Yönlendirmeli geçitte area($params) bildirip işaretleme döndürürsünüz. pre_area() $params'ı toplar; payment_screen() sonucu ['mode' => 'html', ...] olarak bildirir. embedded-pane true iken ödeme sayfasına gömülür, false iken ayrı sayfada görünür.

Dönüş

callback() bildirin. Dağıtıcı payments/{Modul}/{auth_token}/callback adresini buraya yönlendirir, sağlayıcının gönderdiğini olduğu gibi bırakır. Checkout'u bulun, imzayı doğrulayın, sonucu tarif edin; dağıtıcı settle_checkout()'u çağırır.

Geri dönüş ayağında oturum yoktur

Checkout kimliğini kendiniz taşıyın, genelde LinkGenerator::wQS() ile kurulan dönüş adresine sorgu parametresi olarak. Sahibi checkout kaydından okuyun.

Kart Alıyorsanız Kartı Çekin

capture($params) bildirin. Kartı önce pre_capture() alır; CSRF anahtarını ve kart alanlarını doğrular. Ayrıca checkout'un işlem yapan hesaba ait olduğunu denetler. Sonra taksit planını çözer, kart üst verisini (CVC hariç) saklar. Durum dizisi döndürün; başarıda mahsup hemen yapılır.

Referans

Taban Sınıfın Verdikleri

PaymentGatewayModule, tam imzalar
// İşlemdeki checkout
public function set_checkout($checkout): void;
public function get_checkout($id = 0, $status = '', $type = '', $uid = 0);
public function save_checkout($id = 0, $fields = []): bool;
public function checkout_total(): float;      // çekilecek tek doğru tutar
public function checkout_currency(): int;     // para birimi id'si, ISO kodu DEĞİL
public function getItems(): array;

// Para birimi
public function cid_convert_code($id = 0);    // 4 => "USD"
public function currency($id = 0);            // idempotent: girdi id ya da kod, çıktı satır

// Para ve ücretler
public function commission_fee_calculator($amount): float;
public function get_commission_rate();
public function installment_plans(array $bin, float $base, int $cid): array;
public function installment_plan_total(float $base, float $rate): float;

// Kartlar
public function get_stored_card($id = 0, $user_id = 0): array;
public function checkSaveCard(): bool;
public function checkAutoPay(): bool;
public function card_setup_result(): array;
public function generate_card_identification_checkout($pmethod = ''): int;

// Abonelikler
public function checkout_subscribable(): array;
public function set_subscribed_items($arg = []): void;
public function set_subscribed_sources(array $sources = []): void;

// Altyapı
public function get_auth_token(): string;
public function define_function($name = '', $function_name = ''): void;
public function controller_settings($extraFields = []): array;
public function isEnabled(): bool;
public function save_custom_data($data, $checkout_id = 0): void;
public function get_custom_data($checkout_id = 0);

// Çekirdeğin çağırdığı üçlü, ezilebilir
public function payment_screen(): array;
public function pre_area(): string|false;
public function pre_capture(): string;

// Mahsup, sizin için çağrılır. Modül kodundan asla çağırmayın.
public static function settle_checkout(PaymentGatewayModule $module, array $checkout, array $result): array;
checkout_total() Sağlayıcıya gönderilecek tek doğru tutar. Vergiyi, komisyonu ve varsa taksit vade farkını zaten içerir.
checkout_currency() İç para birimi id'si. Sağlayıcılar ISO kodu ister, cid_convert_code()'dan geçirin.
get_auth_token() Geri dönüş adresinin içindeki imza parçası. Anahtarı tutmayan çağrı reddedilir, adresi $this->links'ten kurun.
define_function() Çok adımlı panel için payments/{Modul}/function/{ad} adresinde tek bir ek uç yayınlar. Alt çizgi kullanın; tire ve nokta alt çizgiye katlanır.
settle_checkout() Tasarımı gereği idempotenttir: satırı yeniden okur, checkout zaten ödendiyse kısa devre yapar ve işlem numarasına göre tekilleştirir. İkiz bildirim zararsızdır.

Sizin Bildirdikleriniz

Çekirdek bunları method_exists ile yoklar. Tabanda yalnız card_setup_result() vardır, varsayılanı ['status' => 'unsupported']: o bir ezme, kalanı eklemedir.

isteğe bağlı modül metotları
public function capture($params = []);                 // kart çekimi, durum dizisi döner
public function area($params = []);                    // yönlendirme geçidi, işaretleme döner
public function callback();                            // sağlayıcı dönüşü, durum dizisi döner
public function bin_check($number);                    // yerel BIN tablosu, kart üst verisi döner
public function config_fields();                       // yönetici ayar alanları
public function refundInvoice($invoice = []);          // iade, bool döner
public function card_setup_result(): array;            // EZME: kasaya alma koşusunun 3-D dönüşü

// Sanal test modülleri tek parametre bildirir, ama çekirdek bunu İKİ parametreyle
// çağırır: planları tutara bağlı olan bir sağlayıcı sorabilsin diye çekim tabanı da geçilir.
public function installment_rates($bin = [], $base = 0);   // [adet => vade farkı yüzdesi]

// Abonelikler
public function cancel_subscription($params = []): bool;
public function get_subscription($params = []): array|false;
public function change_subscription_fee($params = [], $value = 0, $currency = 0): bool;
public function remove_subscription_item($params = []): bool;   // 'items' modu: tek satırı düşür

$params İçindeki Anahtarlar

İkisi de V3 sözleşmesinin üst kümesini alır. area() altı anahtar alır: checkout_id, amount, currency, currency_id, clientInfo, items. Kalanı, id aynası dahil, yalnız capture'dadır.

checkout_id Checkout satırının id'si. V3 modülleri için id olarak da aynalanır.
amount Float. Çekilecek tam tutar; bir taksit planı çözüldüyse vade farkıyla büyütülmüştür.
currency Dize olarak ISO kodu, örneğin "TRY". Bunu int'e cast etmek taşımanın en sık hatasıdır.
currency_id Fiyatlama ya da kur çevirisi yapan modüller için int para birimi id'si.
clientInfo Ödeyenin adı, e-postası ve adresi; checkout'un dondurulmuş müşteri verisinden.
items Kalem satırları. Toplamları nettir; amount'a tamamlanmazlar.
data Dondurulmuş user_data dahil checkout veri bloğu. Yalnız capture'da.
type Kart şeması ya da tipi; kendi bin_check()'inizden veya saklı kart satırından. Yalnız capture'da.
installment Tarayıcının istediği değil, sunucunun onayladığı taksit adedi. Sıfır tek çekimdir. Yalnız capture'da.
num, holder_name, expiry_m, expiry_y, cvc Doğrulanmış yeni kart. Yalnız saklı kart seçilmediğinde bulunur.
card_storage Saklı kart çekilmiyorsa hiç yoktur. Çözülmüş kasa satırı; token ve ln4 dahil.
save_card, auto_pay Boolean. Ödeyen, başkasının hesabında işlem yapan bir alt kullanıcıysa ikisi de zorla kapatılır.

Ne Döndürürsünüz

MetotAnahtarAnlamı
capture()statusMahsup edilenler: successful, success, paid, pending, papproval. Tarayıcıya geri verilenler: redirect, 3d, output. Gerisi error sayılır
redirect3-D ya da ek doğrulama için tarayıcının gideceği adres. Yalnız redirect ve 3d durumlarında okunur
messageBaşarıda etiket => değer haritası, hatada bir cümle
cardToken'lanmış kart paketi; müşteri saklamak istediyse kasaya verilir
outputTam sayfa basılacak ham işaretleme; kendini gönderen banka formları için. Yalnız output ve 3d durumlarında okunur
callback()statussuccessful, pending ya da error
messageSağlayıcı referansını Transaction ID altına koyun; mahsup tekilleştirmeyi bununla yapar
paid['amount' => float, 'currency' => int]; farklıysa gerçekten çekilen tutar
callback_messageYönlendirme yerine aynen yazılır; onay dizesi bekleyen sağlayıcılar için
payment_screen()modecard, html, redirect, choices, legacy, none ya da error
htmlSunucuda üretilen işaretleme; kaçışsız gösterilir
redirectTek yönlendirme butonunun hedefi
choices['label' => string, 'url' => string, 'image' => string] satırları; PayPal tek seferlik ödemeyi aboneliğin yanında böyle sunar. note üstlerinde görünür, error modu ise onun yerine message taşır

Tabandaki payment_screen() modu bildirdiğiniz metotlardan türetir. Sağlayıcı kendi işaretlemesini gerektiriyorsa ezin.

Komisyon

Komisyon faturanın başlık alanıdır, kalem değildir. Operatör geçit başına commission_rate belirler, çekirdek yeniden hesaplamada fiyatlar. checkout_total() ücreti içerir, commission_fee_calculator() gösterim içindir.

ücret, gösterim için
$base = $this->checkout_total();
$fee  = $this->commission_fee_calculator($base);   // oran config['settings']['commission_rate']'ten okunur
$note = Money::formatter_symbol($fee, $this->checkout_currency());

Tutar Aralığı

Geçidin sunulup sunulmayacağına üç ortak ayar karar verir; taban bunları config['settings'] içine yazar.

min_amount Ondalık. En küçük ödenecek tutar; 0 alt sınırı kapatır.
max_amount Ondalık. En büyük ödenecek tutar; 0 üst sınırı kapatır. Minimumun altında bir maksimum kayıtta geri çevrilir.
amount_limit_cid Sınırların yazıldığı para birimi kimliği. Boşsa sistem para birimine düşer.

Checkout::payment_methods($ucid, ['amount' => $total]) iki sınırı da ödemenin para birimine çevirir; aralığı tutarı kapsamayan geçit listeden düşer. Ödeme sayfası, fatura, toplu ödeme ve bakiye yükleme aynı çağrıdan geçer.

Karşılaştırılan tutar, geçidin komisyonu ve taksit farkı eklenmeden önceki toplamdır. Aralığı capture() içinde yeniden denetlemeyin: oradaki tutar ücreti çoktan taşır.

Alanı ayar formundan çıkaran modül ('unusedFields' => ['amount_limits']) iki anahtarı da göndermez, saklanan aralık kalır. Tutarı bilmeyen sayfa sınırları kayıt satırından okur.

Örnek

Eksiksiz bir yönlendirme geçidi: panel sağlayıcının çağıracağı adresi kurar, geri dönüş o checkout'u bulur.

Acme.php, panel
class Acme extends PaymentGatewayModule
{
    public function area($params = [])
    {
        $cid = (int) ($params['currency_id'] ?? 0);

        // Sağlayıcının çağıracağı dönüş adresi. Dağıtıcının onu kabul etmesini
        // $this->links['callback'] içindeki kimlik jetonu sağlar; custom_id ise
        // geri dönüşün bu checkout'u oturumsuz bulma yoludur.
        $return = LinkGenerator::wQS($this->links['callback'], [
            'custom_id' => (int) ($params['checkout_id'] ?? $this->checkout_id),
        ]);

        $session = $this->open_provider_session([
            'merchant'    => $this->config['settings']['merchant_id'] ?? '',
            'amount'      => $params['amount'] ?? 0,
            'currency'    => $params['currency'] ?? '',   // ISO kodu, çevrilmiş hâlde
            'reference'   => 'chk_' . (int) $this->checkout_id,
            'return_url'  => $return,
        ]);

        if (($session['url'] ?? '') === '')
            return '';   // boş bir dönüş, payment_screen()'in "error" modu bildirmesine yol açar

        $label  = htmlspecialchars((string) ($this->lang['pay-button'] ?? 'Continue'), ENT_QUOTES);
        $target = htmlspecialchars($session['url'], ENT_QUOTES);
        $amount = htmlspecialchars(Money::formatter_symbol((float) ($params['amount'] ?? 0), $cid), ENT_QUOTES);

        return '<div class="d-grid">'
            . '<span class="price-chip num-tabular mb-2">' . $amount . '</span>'
            . '<a class="btn btn-primary btn-lg" href="' . $target . '">' . $label . '</a>'
            . '</div>';
    }
}
Acme.php, çekirdeğin mahsup edeceği dönüş
public function callback()
{
    $checkout_id = (int) Filter::init("REQUEST/custom_id", "numbers");
    $checkout    = $checkout_id ? $this->get_checkout($checkout_id) : false;

    if (!$checkout)
        return ['status' => "error", 'message' => "checkout-not-found"];

    // set_checkout(), $this->checkout, $this->checkout_id ve müşteri bilgisini
    // doldurur; böylece checkout_total() ve checkout_currency() aşağıda yanıt verir.
    $this->set_checkout($checkout);

    $reference = (string) Filter::init("REQUEST/reference", "letters_numbers");
    $signature = (string) Filter::init("REQUEST/signature", "letters_numbers");

    // Hiçbir şeye güvenmeden ÖNCE doğrulayın: geri dönüş adresi herkese açıktır.
    if (!$this->signature_matches($reference, $signature))
        return ['status' => "error", 'checkout_id' => $checkout_id, 'message' => $this->pay_lang("error-verification")];

    $remote = $this->fetch_provider_charge($reference);

    if (($remote['state'] ?? '') !== 'captured')
        return [
            'status'      => "error",
            'checkout_id' => $checkout_id,
            'message'     => (string) ($remote['reason'] ?? ($this->lang['error-declined'] ?? 'Declined.')),
        ];

    return [
        'status'      => "successful",
        'checkout_id' => $checkout_id,
        'message'     => ['Transaction ID' => $reference],

        // Anlık görüntüden farklıysa GERÇEKTEN çekilen tutarı bildirin, örneğin
        // sağlayıcının sayfasında bir taksit planı seçildikten sonra.
        'paid'        => [
            'amount'   => (float) ($remote['amount'] ?? $this->checkout_total()),
            'currency' => $this->checkout_currency(),
        ],
    ];
}
çekirdek dönüşünüzle ne yapar
// coremio/classes/PaymentGatewayModule.php, settle_checkout()
$result = $module->callback();

// 1. zaten ödendi mi?          -> kısa devre, ikinci kayıt yok
// 2. durum successful değil    -> logla, müşteriyi links['failed']'a gönder
// 3. ertelenmiş sipariş mi?    -> siparişi ve faturayı şablondan kur
// 4. anlık görüntüden fazlası mı çekildi? -> önce taksit vade farkını kaydet
// 5. her faturayı kaydet:
foreach (\Checkout::invoice_ids($fresh) as $invoiceId) {
    $due = round((float) Invoices::balance($invoiceId), 2);
    if ($due <= 0.005) continue;

    Invoices::add_payment($invoiceId, [
        'amount'         => $due,
        'currency'       => (int) (Invoices::get($invoiceId, ['select' => "currency"])["currency"] ?? 0),
        'pmethod'        => $module->name,
        'transaction_id' => $tx,                 // message['Transaction ID']'den gelir
        'description'    => $module->lang["name"] ?? $module->name,
    ]);
}

Tam ödeme faturayı ödendiye çevirir. Bağlı sipariş aktifleşir, hizmet sağlanır, gelir satırı yazılır, bildirim gider.

Tuzaklar

params['currency'] bir ISO kodudur, sayı değil

(int) $params['currency'], sağlayıcının USD beklediği yere 4 gibi anlamsız bir değer koyar. Id için currency_id'yi okuyun.

Kalem satırları toplama tamamlanmaz

Kalem toplamları nettir; data.tax anahtarı ve amount_including_discount yoktur, ikisi de V3'tü. Kalem dökümü için kalanı bir "vergi ve ücretler" satırıyla checkout_total()'a mutabık kılın.

Boş bir card_storage dizisi "saklı kart var" diye okunur

Modüller o dalı is_array($params['card_storage'] ?? null) ile kapılar; yeni kartta anahtar yoktur. Varsayılanı [] yapmayın.

Sunucudan sunucuya bildirim yönlendirme değil onay ister

callback_message döndürün, dağıtıcı onu yazar. Makine çağırana yönlendirme verirseniz sağlayıcı bildirimi başarısız sayar ve yeniden dener.

CVC hiçbir zaman saklanmaz

capture()'a geçer ve unutulur. Onu checkout verisine ya da kasaya yazmak uyumluluk ihlalidir; saklı kart yalnız sağlayıcı token'ını ve görüntüleme üst verisini tutar.

Hatayı dönüş dizisiyle bildirin

Sözleşme ['status' => 'error', 'message' => ...]'dır; mahsup bunu loglar ve gösterir. $this->error atayıp false döndürmek eski bir yedek yoldur, ancak dizi boşsa okunur.

Faydalı oldu mu?

Geri bildiriminiz için teşekkürler!

Hâlâ Yardıma mı İhtiyacınız Var?

Yukarıda bulamadığınız her şey için destek ekibimiz her zaman yanınızda.