# Ödeme Geçidi Yazma

https://dev.wisecp.com/tr/odeme-geciti-yazma

Ö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.

```text
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](https://dev.wisecp.com/tr/modul-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ı

```text
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.

| Arketip | Bildirdiğiniz | Müşteri deneyimi | Kopyalanacak örnek |
| --- | --- | --- | --- |
| Kendi kart formu | `capture()` ve `standard_card = true` | Ödeme sayfasında kart alanları, çekimi siz yaparsınız | `SampleMerchant` |
| Yönlendirme / barındırılan sayfa | `area()` ve `callback()` | Yönlendirme paneli, ardından sağlayıcının kendi sayfası | `SampleThirdParty` |
| Kart kasası | `capture()`, `card_setup_result()`, meta `card-storage-supported` | Kayıtlı kartlar ve oturum dışı otomatik ödeme | `SampleTokenized` |
| Yinelenen abonelik | `payment_screen()` ezmesi, `callback()`, `cancel_subscription()` | Tek seferlik öde ya da abone ol, yenilemeleri sağlayıcı çeker | `SampleSubscription` |

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.

```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.

```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.

```php
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

```php
// İş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.

```php
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

| Metot | Anahtar | Anlamı |
| --- | --- | --- |
| `capture()` | `status` | Mahsup edilenler: `successful`, `success`, `paid`, `pending`, `papproval`. Tarayıcıya geri verilenler: `redirect`, `3d`, `output`. Gerisi `error` sayılır |
| `redirect` | 3-D ya da ek doğrulama için tarayıcının gideceği adres. Yalnız `redirect` ve `3d` durumlarında okunur |  |
| `message` | Başarıda `etiket => değer` haritası, hatada bir cümle |  |
| `card` | Token'lanmış kart paketi; müşteri saklamak istediyse kasaya verilir |  |
| `output` | Tam sayfa basılacak ham işaretleme; kendini gönderen banka formları için. Yalnız `output` ve `3d` durumlarında okunur |  |
| `callback()` | `status` | `successful`, `pending` ya da `error` |
| `message` | Sağ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_message` | Yönlendirme yerine aynen yazılır; onay dizesi bekleyen sağlayıcılar için |  |
| `payment_screen()` | `mode` | `card`, `html`, `redirect`, `choices`, `legacy`, `none` ya da `error` |
| `html` | Sunucuda üretilen işaretleme; kaçışsız gösterilir |  |
| `redirect` | Tek 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.

```php
$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.

```php
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>';
    }
}
```

```php
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(),
        ],
    ];
}
```

```php
// 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.

## İlgili Makaleler

- [Modül Anatomisi](https://dev.wisecp.com/tr/modul-anatomisi)
- [Modül Yapılandırması](https://dev.wisecp.com/tr/modul-yapilandirmasi)
- [Kur Modülü Yazma](https://dev.wisecp.com/tr/kur-modulu-yazma)
- [Dolandırıcılık Modülü Yazma](https://dev.wisecp.com/tr/dolandiricilik-modulu-yazma)
- [Sepet ve Ödeme](https://dev.wisecp.com/tr/sepet-ve-odeme)
- [Alan Yardımcıları](https://dev.wisecp.com/tr/alan-yardimcilari)
