# Kur Modülü Yazma

https://dev.wisecp.com/tr/kur-modulu-yazma

Kur modülü bir oran kaynağıdır. Tek bir soruyu yanıtlar: benim para birimimin bir birimi şu diğerlerinde ne eder? Zamanlanmış eşitleme yanıtı para birimi tablosuna yazar.

## Genel Bakış

Kur, en küçük modül tipidir ve **taban sınıfı olmayan** tek tiptir. Sözleşme ördek tiplemesiyle kurulur: çekirdek sınıfınızı çözer, adına göre bir metot arar ve çağırır. Yedi modül gelir; ücretsiz varsayılan `OpenRates` ve platformun kendi `WAtlas`'ı dahil.

Taban sınıf olmadığı için sözleşme yalnız çağrı noktalarında görünür. Üç tanedir ve tek bir zorunlu metotta anlaşırlar.

```php
// 1. coremio/helpers/Money.php: tek oran yolu, eşitleme görevi bunu kullanır
$instance = self::currency_module();               // yapılandırılmış modül, yoksa OpenRates
if (!$instance) return (string) Language::gc("admin/financial/currency-module-unavailable");
$rates = $instance->exchange_rates($from, $to);
if (!$rates) return $instance->error;              // modülün kendi mesajı operatöre ulaşır

// 2. coremio/operations/AdminFinancialCurrencies.php: ayar kaydı, ardından test butonu
$instance = Modules::getInstance("Currency", $module);
if ($instance && method_exists($instance, "save_config")) $instance->save_config($module_data[$module]);
// ...
if (!method_exists($instance, "exchange_rates")) throw new Exception("Module does not implement exchange_rates().");
$instance->config = array_replace_recursive($instance->config, $module_data[$module]);   // kaydedilmemiş form değerleri
$rates = $instance->exchange_rates($localCode, $targets);

// 3. coremio/api/Resources/Admin/Concerns/FinancialCurrencies.php: aynı ikisi, API üzerinden
```

`exchange_rates()` zorunludur; modülünüzün tek bir ayarı olduğu anda `save_config()` da zorunlu olur. `page_settings()` o ayarı gösterir. `$config` ve `$error` çağıran taraftan doğrudan okunur, yani var ve public olmak zorundadır.

## Ön Koşullar

- Bir temel para birimi ve karşılık kümesi dönen bir oran sağlayıcısı. Ücretsiz katmanına dikkat edin: eşitleme zamanlanmış çalışır.
- Sistemin yerel para birimi tanımlı olmalıdır; her oran ona karşı ifade edilir.
- Örnek `Modules::getInstance('Currency', 'Acme')` ile alınır.
- Önce `coremio/modules/Currency/OpenRates`'i okuyun: yüz satırın altında, kimlik bilgisi istemez, sözleşmenin tamamı tek dosyada. `ExchangeRateAPI` buna API anahtarı ve ayar alanı ekler.

## Yapı

```text
Acme.php        namespace WISECP\Modules\Currency; class Acme  (extends yok)
config.php      DÜZ bir dizi, diğer modül tiplerindeki meta/settings ayrımı değil
lang/en.php
lang/tr.php
```

> **Yapılandırma dosyası burada düzdür**
> 
> Sunucu, Ödeme ve Servis Sağlayıcı modülleri `['meta' => [...], 'settings' => [...]]` döner. Kur modülü ayarların kendisini döner ve onları `$this->config['api_key']` olarak okur. İç içe şekil, hep boş kalan bir anahtar verir.

Sınıf hiçbir şeyi genişletmez, yani kendi durumunu kendi bildirir. Çağıranların dokunduğu arayüz bu dört özelliktir.

## Adım Adım

### Sınıfı ve Durumunu Bildirin

```php
<?php
namespace WISECP\Modules\Currency;

class Acme
{
    public string  $name   = "Acme";
    public ?array  $config = null;
    public ?array  $lang   = null;
    public ?string $error  = null;      // false döndüğünüzde çağıran bunu okur

    public function __construct()
    {
        // Global sınıflar bu ad alanının içinde baştaki ters bölüyü ister,
        // yoksa PHP WISECP\Modules\Currency\Modules sınıfını arar.
        $this->config = \Modules::Config("Currency", $this->name);
        $this->lang   = \Modules::Lang("Currency", $this->name);
    }
}
```

### Oranları Çekin

Sağlayıcıdan `$from`'a karşı elinde ne varsa isteyin ve büyük harfli bir kod haritası döndürün. Sonucu `$to`'ya göre süzmeyin.

### Ayarı Gösterin ve Kalıcılaştırın

`page_settings()` kur ayarları ekranı için ham işaretleme döner. Her girdiye `module_data[{ModulAdi}][{anahtar}]` adını verin: kaydetme operation'ının `save_config()`'a verdiği dizi budur. Test butonu aynı diziyi gönderir ve bellekteki `$this->config`'in üzerine bindirir. Bir anahtar kaydedilmeden önce denenebilir.

### Güvenmeden Önce Doğrulayın

1. Modülü kur ayarları ekranından seçip test butonuna tıklayın. Yerel para birimine karşı iki hedefle `exchange_rates()` çağırır.
2. Eşitlemeyi çalıştırın, sonra bir para biriminin `currencies.rate` değerini sağlayıcının kendi sitesiyle karşılaştırın.
3. Bir çevrim yapın. Ters kaydedilmiş bir oran makul görünen ama yanlış fiyatlar üretir.

## Referans

### Sözleşme

```php
// ZORUNLU
public function exchange_rates(string $from = '', array $to = []): array|false;

// Modülün herhangi bir ayarı olduğu anda ZORUNLU
public function save_config(array $data = []): bool;

// İSTEĞE BAĞLI: kur ayarları ekranı için işaretleme
public function page_settings(): string;

// Çağıranların doğrudan okuduğu public durum
public string  $name;
public ?array  $config;    // test butonu bellekte bunun üzerine yazar
public ?array  $lang;
public ?string $error;     // exchange_rates() false döndüğünde operatöre gösterilir
```

- **$from**: Sistemin yerel para biriminin büyük harfli ISO kodu; `general/currency`'den çözülür. Sağlayıcınız küçük harf istiyorsa siz küçültün.
- **$to**: Yol göstericidir ve **biçimi çağırana göre değişir**. Eşitleme `[para_birimi_id => 'KOD']` geçer; test butonu düz bir kod listesi geçer. Sonucunuzun anahtarı olarak asla kullanmayın.
- **dönüş değeri**: `['USD' => 32.15, 'EUR' => 35.02]`; büyük harfli kodlar, float değerler ya da `false`. Boş dizi, test noktasında başarısızlık sayılır.
- **oran yönü**: `$from`'un bir biriminin hedef cinsinden karşılığı. Saklanan kolon `tutar / oran_kaynak * oran_hedef` olarak kullanılır; ters çevirmek her fiyatı bozar.
- **Money::currency_module()**: Seçili modülü çözer, o eksik ya da kaldırılmışsa `OpenRates`'e düşer. Yalnız OpenRates yüklenemezse null döner.
- **Money::get_exchange_rates()**: Sağlayıcıya giden tek yol. Başarıda dizi döner. Başarısızlıkta `$error`'ünüzü aynen geri verir: çağırana bir **dize** (hiç mesaj koymadıysanız null) ulaşır, asla `false` değil. Her çağıranın `is_array()` ile bakmasının sebebi budur.

### Günlük Çağrı Tavanı

Yardımcı, modülünüze ulaşmadan önce günün çağrılarını `coremio/storage/currency-overload-limit.php` içinde sayar. 48 çağrıda sağlayıcıya dokunmadan `admin/financial/currency-api-limit-exceeded` mesajını etkin dilde döner. Bu, yanlış zamanlanmış bir görevden ücretsiz katmanı korur.

### Eşitleme Yanıtınızla Ne Yapar

```php
$rates = Money::get_exchange_rates($localCode, $targets);

if (!is_array($rates))
    return ['success' => false, 'result' => ['error' => is_string($rates) && trim($rates) !== '' ? $rates : self::NO_RATES]];

// Genişletme noktası: sağlayıcının fiyatlayamadığı bir para birimini enjekte edin ya da birini ezin.
Hook::runRefs('filter:money.exchange_rates_fetch', $rates, $localCode, $targets);

// Büyük harfli kod => para birimi id'si; böylece her şeyi döndüren bir sağlayıcı
// yine de yalnız sistemde gerçekten var olan para birimlerini günceller.
$codeToId = [];
foreach ($targets as $cid => $code) $codeToId[strtoupper($code)] = (int) $cid;

foreach ($rates as $code => $rate) {
    $codeUp = strtoupper((string) $code);
    $rate   = (float) $rate;

    if (!isset($codeToId[$codeUp])) continue;          // burada bir para birimi değil: yok sayılır
    if ($rate <= 0 || $rate > 999999999999.99999999) { $skipped++; continue; }   // tutarlılık koruması

    WDB::update('currencies', ['rate' => $rate])->where('id', '=', $codeToId[$codeUp])->save();
}

Hook::run('action:money.exchange_rates_updated', $changes, $localCode);
```

- **filter:money.exchange_rates_fetch**: Çekilen harita üzerinde, hiçbir şey yazılmadan önce çalışır. Sağlayıcının fiyatlayamadığı bir para birimi buradan enjekte edilir.
- **action:money.exchange_rates_updated**: Kalıcılaştırmadan sonra, yalnız gerçekten değişen oranlarla çalışır.
- **filter:money.exchange_rate**: Eşitleme anında değil, her çevrimin içinde çalışır. Onu marj gibi politikalara saklayın, veri çekmeye asla.

İkisinin de API ikizi vardır: `POST /financial/currency-modules/test` canlı sağlayıcı çağrısını yapar, `POST /financial/currencies/sync` görevi anında dağıtır.

## Örnek

Tek kimlik bilgisi olan eksiksiz bir modül. Üçünü bağlayan şey alanın adıdır: `page_settings()`'in gösterdiği şey `save_config()`'un aldığı şeydir, o da `exchange_rates()`'in okuduğu şeydir.

```php
public function exchange_rates(string $from = '', array $to = []): array|false
{
    $apiKey = (string) ($this->config["api_key"] ?? '');

    if ($apiKey === '') {
        $this->error = "Acme api_key is not configured.";
        return false;
    }

    $url = "https://api.example.com/v1/" . urlencode($apiKey) . "/latest/" . urlencode(strtoupper($from));

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 15);

    $response = curl_exec($ch);
    if ($response === false) $this->error = curl_error($ch) ?: "Currency provider request failed.";
    curl_close($ch);

    // Erken dönüşlerden ÖNCE kaydedin: bozuk bir anahtarı ayıklayan operatörün
    // yalnız hatayı değil, hatayı üreten isteği de görmesi gerekir.
    \Modules::save_log("Currency", $this->name, "exchange", [
        'api_url' => $url,
        'from'    => $from,
        'to'      => $to,
    ], $response, $this->error);

    if ($this->error) return false;

    $decoded = \Utility::jdecode(trim((string) $response), true);

    if (!is_array($decoded) || ($decoded["result"] ?? '') !== 'success') {
        $this->error = (string) ($decoded["error-type"] ?? 'Acme returned an error.');
        return false;
    }

    // Büyük harfli anahtarlar, float değerler. $to süzgeç olarak KULLANILMAZ: eşitleme
    // tanıdığını tutar, kalanını yok sayar.
    $result = [];
    foreach ((array) ($decoded["conversion_rates"] ?? []) as $code => $rate)
        $result[strtoupper((string) $code)] = (float) $rate;

    return $result;
}
```

```php
public function page_settings(): string
{
    $apiKey = htmlspecialchars((string) ($this->config["api_key"] ?? ''), ENT_QUOTES);

    // Girdinin ADI, kaydetme operation'ıyla yapılan sözleşmedir:
    // module_data[Acme][api_key], save_config() içine $data['api_key'] olarak gelir.
    return '<div class="row mb-0 align-items-center">'
        . '<label for="acme-apikey" class="col-sm-3 col-form-label fw-semibold">API Key</label>'
        . '<div class="col-sm-9">'
        . '<input type="text" class="form-control" id="acme-apikey"'
        . ' name="module_data[Acme][api_key]" value="' . $apiKey . '" placeholder="api_key">'
        . '</div></div>';
}

public function save_config(array $data = []): bool
{
    // Birleştirin, değiştirmeyin: kısmi bir gönderim diğer anahtarları silmemelidir.
    $merged = array_replace_recursive($this->config ?: [], $data);

    // file_write() bir .php hedefinde opcode önbelleğini geçersiz kılar; kaydedilen
    // değerin daha bir sonraki istekte okunabilmesinin sebebi budur.
    return (bool) \FileManager::file_write(
        __DIR__ . DS . "config.php",
        \Utility::array_export($merged, ['pwith' => true])
    );
}
```

```php
<?php
return [
    'api_key'   => '',
    'help-link' => 'https://example.com',
];
```

## Tuzaklar

> **Yanıtınızı $to'ya göre süzmeyin**
> 
> Anahtarları eşitlemeden gelirken para birimi id'si, test butonundan gelirken düz tam sayıdır. Ona göre süzmek iki çağıranın farklı sonuç almasına yol açar. Her şeyi döndürün, eşlemeyi kodla çekirdek yapsın.

> **Hata $error'ı doldurup false dönmelidir**
> 
> Çağıran `$instance->error`'ı aynen yazar. Mesaj yoksa operatör yalnız "kur modülü kullanılabilir bir kur döndürmedi" gibi genel bir not görür; bu not sebebi söylemez.

> **Günde kırk sekiz çağrı, sizin kodunuz koşmadan sayılır**
> 
> Sayaç başarılı çağrı başına değil, çağrı başına artar; bir test döngüsü zamanlanmış çalışmayla aynı bütçeyi yakar. Sağlayıcıdan şüphelenmeden önce tavana bakın.

> **Global sınıflarda baştaki ters bölüyü kullanın**
> 
> Dosya `namespace WISECP\Modules\Currency` bildirir; nitelenmemiş bir `Modules::Config()` o ad alanındaki bir sınıfa çözülür ve çalışma anında patlar.

> **Eksik bir modül fiyatlamayı bozmaz**
> 
> Seçili modül yüklenemediğinde çözümleyici kimlik bilgisi istemeyen `OpenRates`'e düşer. Oranlar kaybolmaz, bayatlar.

## İlgili Makaleler

- [Modül Anatomisi](https://dev.wisecp.com/tr/modul-anatomisi)
- [Modül Yapılandırması](https://dev.wisecp.com/tr/modul-yapilandirmasi)
- [Ödeme Geçidi Yazma](https://dev.wisecp.com/tr/odeme-geciti-yazma)
- [Alan Yardımcıları](https://dev.wisecp.com/tr/alan-yardimcilari)
- [Zamanlanmış Görev Ekleme](https://dev.wisecp.com/tr/zamanlanmis-gorev-ekleme)
- [Kanca Dinleyicisi Yazma](https://dev.wisecp.com/tr/kanca-dinleyicisi-yazma)
