# Dolandırıcılık Modülü Yazma

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

Dolandırıcılık modülü, hiçbir şey kaydedilmeden önce sipariş denemesini inceler ve tek bir soruyu yanıtlar: ilerleyebilir mi.

## Genel Bakış

Bir dolandırıcılık modülü `FraudModule`'ü genişletir ve `coremio/modules/Fraud/{Ad}/{Ad}.php` altında yaşar. Her şekilden bir tane gelir: `WFraud` yerel veriden puanlar, `MaxMind` denemeyi dış bir sağlayıcıya iletir.

Kapı bir kez, ödeme akışından, sipariş, fatura ya da hizmet yokken çalışır.

```php
// Bu noktada hiçbir şey kalıcılaştırılmamıştır: engellenen bir deneme geriye
// ne sipariş, ne fatura, ne de hizmet bırakır.
if ($fraudError = \FraudModule::run_checks($this->fraud_payload($pricing, $pmethod, (float) $taxCalc['total'], $member)))
    throw new \Exception($fraudError);
```

Bu kapının iki özelliği modülü nasıl yazacağınızı belirler:

- **Fırlatan modül geçmiş sayılır**, hata uyarı olarak loglanır.
- **Engelleme, false dönüşü artı bir mesaj.** Puan, inceleme durumu ya da kuyruk yoktur.

## Ön Koşullar

- Yükün ne taşıdığını bilin; orada olmayanı kendiniz getirirsiniz.
- Uzaktan puanlıyorsanız bir sağlayıcı hesabı; yerel puanlıyorsanız ek bir şey gerekmez.
- Modül iskeleti: bkz. [Modül Anatomisi](https://dev.wisecp.com/tr/modul-anatomisi).
- Tekrar tekrar verebileceğiniz bir test siparişi. Fazla engelleyen kural yalnız kayıtlar tablosunda görünür.

## Yapı

```text
Acme.php        namespace WISECP\Modules\Fraud; class Acme extends FraudModule
config.php      ['meta' => [...], 'status' => false, 'settings' => []]
lang/en.php
lang/tr.php
logo.png
```

> **status üst seviye bir anahtar, meta'nın parçası değil**
> 
> `run_checks()` `$row['config']['status']` true değilse modülü atlar; ayar ekranı da oraya yazar. `meta` altında sorunsuz kaydedilir ve hiç çağrılmaz.

Ayar ekranını siz kurmazsınız; onu taban birleştirir. Durum anahtarı kapalıyken bütün alanlar devre dışıdır.

## Adım Adım

### Sınıfı Bildirin

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

// Dosya bir ad alanı içindedir, yani her global sınıf import edilmeli ya da
// başına ters bölü konmalıdır. Nitelenmemiş bir ad bu ad alanının içine çözülür
// ve çalışma anında patlar; sözdizimi denetimi bunu yakalamaz.
use FraudModule;
use Language;
use User;
use UserManager;

class Acme extends FraudModule
{
    public function check($params = []): bool
    {
        return true;
    }
}
```

### Ayar Alanlarını Bildirin

`fields()` alan tanımları döner. Anahtarlar: `type`, `name`, `description`, `value`, anahtar için `checked`, açılır liste için `options`. Güncel değerler `$this->config['settings']`'ten gelir.

### Bozuk Kimlik Bilgisini Kaydetmeden Reddedin

`save_fields()` bildirirseniz kaydetme onu kalıcılaştırmadan önce çalıştırır. Açılmanın gerçek bir iş yapması gerekiyorsa `activate()` ve `deactivate()` bildirin.

### Kuralı Yazın

Yükü okuyun ve karar verin. Engellerken sırayla: `$this->error`'ı çevrilmiş bir cümleye ayarlayın, `insert_record()` çağırın, false dönün.

### İki Yönü de Doğrulayın

1. Geçmesi gereken bir sipariş verin. Tamamlanır ve kayıtlar tablosu boş kalır.
2. Engellenmesi gereken birini verin. Ödeme ekranı mesajınızı gösterir, hiçbir şey oluşmaz.
3. Sağlayıcıyı yanlış bir anahtarla bozun. Sipariş yine tamamlanır ve log modülünüzün adını taşıyan bir uyarı içerir.

## Referans

### Taban Sınıfın Verdikleri

```php
// Kapı. Ödeme akışı çağırır, bir modül asla çağırmaz.
public static function run_checks(array $params = []): string;   // '' = devam, aksi hâlde mesaj

// Kayıtlar
public function insert_record($user_id = 0, $message = '', $ip = '');
public function records($rCount = false, $filters = [], $orders = [], $start = 0, $end = -1);
public function record_table(): WISECP\Components\Table|string;

// Ayar ekranı, sizin için birleştirilir
public function page_settings(): string;
public function save_config($data = []): int|bool;

// Yönetici controller'ları, operation=module_controller&controller={name} ile erişilir
public function controller_records(): string;
public function controller_save_settings(): array;
public function controller_clear_records(): array;

// Public durum
public ?string $error;     // false döndüğünüzde müşterinin gördüğü mesaj
public ?array  $config;    // ['status' => bool, 'settings' => [...], 'meta' => [...]]
public ?array  $lang;
public ?array  $user;      // giriş yapmış üye, yapıcıda çözülür
public ?array  $admin;
public ?string $dir;
public string  $url;       // dikkat: komşularının aksine nullable değil
public ?string $area_link;
```

- **run_checks()**: `config['status']`'ü true olan her modülü kayıt sırasıyla gezer, `check` metodu olmayanı atlar, ilk false'ta döner. Boş dize "ilerleyebilir" demektir.
- **insert_record()**: `fraud_detected_records`'a modül adınız, kullanıcı id'si, gerekçeniz ve IP ile tek satır yazar. IP boşsa istek IP'si kullanılır.
- **records()**: Modülünüze göre süzülmüş, müşteriye bağlanmış hâlde geri okur.
- **page_settings()**: Ayar ekranını kurar: durum anahtarı, alanlarınız, kayıtlar sekmesi ve `setConfigureTab()`'in eklediği sekmeler. Ezmeyin, geri çağrımı kullanın.
- **$this->error**: Boş hatayla engellemek de engeller: kapı, modül adınızla `website/checkout/error-fraud`'a düşer.

### Sizin Bildirdikleriniz

```php
// ZORUNLU. true = geçir, false = engelle ($this->error ayarlanmış olarak).
public function check($params = []): bool;

// İSTEĞE BAĞLI
public function fields(): array;                              // ayar formu tanımlayıcıları
public function save_fields($fields = []): array;             // kalıcılaştırmadan önce doğrula
public function activate(): bool;                             // modülü açma
public function deactivate(): bool;                           // modülü kapatma
public function setConfigureTab(\WISECP\Components\Tab $obj): void;   // ek ayar sekmeleri
```

> **save_fields ya alanları ya bir hata zarfı döner**
> 
> Başarıda (gerekirse temizlenmiş) alan dizisini dönün; o `config['settings']` olur. Başarısızlıkta `['status' => 'error', 'message' => '...']` dönün, kaydetme fırlatır.

### $params İçinde Ne Var

Ödeme akışı bunu kapıdan hemen önce kurar; girdinin tamamı budur.

- **user_data**: `id`, `email`, `name`, `surname`, `full_name`, `phone`, `country`, `blacklist`, `company_name`, `identity`, `gsm_cc`, `gsm_number`, `ip`, `user_agent` ve ayrıca `address`. Cep telefonu `gsm_number`'dır, `gsm` değil.
- **user_data.address**: `address`, `city`, `country_code`, `zipcode`. Form yalnız ülke id'si gönderdiyse ülke kodu ondan çözülür.
- **misafir denemesi**: `user_data.id` `0`'dır ve kimlik gönderilen formdan gelir; yalnız `email`, `name`, `surname` ve `full_name` dolar. Profil alanları boş değil, hiç yoktur.
- **currency**: Para birimi *id*'si. Sağlayıcı ISO kodu istiyorsa `Money::Currency()` ile çözün.
- **total**: Float; o para biriminde, vergi ve ücretler dahil.
- **pmethod**: Ödeme modülünün adı, örneğin `PayTR`. MaxMind bunu `payment-gateways` tablosuyla kendi sözcüklerine eşler.
- **items**: Fiyatlanmış hâliyle sepet satırları.
- **discounts**: `['items' => ['coupon' => [['name' => 'KOD'], ...]]]`. Üç seviye iç içedir, kupon kullanılmadıysa hiç yoktur.

### İki Şekil

| Yön | Yerel puanlama | Uzak puanlama |
| --- | --- | --- |
| Referans | `WFraud` | `MaxMind` |
| Kurallar | Kara liste, IP ülkesine karşı fatura ülkesi, proxy ya da VPN | Tek sağlayıcı puanının eşikle karşılaştırılması |
| Ayarlar | Kural başına bir anahtar | Kimlik bilgileri, sağlayıcı katmanı, risk puanı, sağlayıcı anahtarları |
| Kimlik bilgisi | Yok | Zorunlu; `save_fields()`'te doğrulanır |
| Sipariş başına maliyet | Sıfır | Faturalanabilir bir API çağrısı |

- **action:module.fraud_settings_saved**: Bir dolandırıcılık modülünün ayarları yazıldıktan sonra modül adı ve yapılandırmayla çalışır.

## Örnek

Anahtarı, eşiği ve kaydı olan bir kural. Sözleşme alan anahtarı: `fields()` bildirir, `save_fields()` doğrular, `check()` okur.

```php
public function fields(): array
{
    $settings = $this->config['settings'] ?? [];

    return [
        'api-key' => [
            'wrap_width' => 100,
            'type'       => 'text',
            'name'       => $this->lang['api-key'] ?? 'API Key',
            'value'      => $settings['api-key'] ?? '',
        ],
        'risk-score' => [
            'wrap_width'  => 100,
            'width'       => 15,
            'type'        => 'number',
            'name'        => $this->lang['risk-score'] ?? 'Risk Score',
            'description' => $this->lang['risk-score-desc'] ?? '',
            'value'       => $settings['risk-score'] ?? '20',
        ],
        'block-country-mismatch' => [
            'wrap_width' => 100,
            'type'       => 'approval',      // bir anahtar: 'value', kutu işaretliyken gönderilen değerdir
            'name'       => $this->lang['country-mismatch'] ?? 'Block country mismatch',
            'value'      => 1,
            'checked'    => $settings['block-country-mismatch'] ?? false,
        ],
    ];
}

public function save_fields($fields = []): array
{
    // Burada reddetmek, modülün yarım yapılandırılmışken açılmayı geri çevirme yoludur.
    if (\Validation::isEmpty($fields['api-key'] ?? ''))
        return ['status' => "error", 'message' => $this->lang['error-is-empty'] ?? 'API key is required.'];

    return $fields;
}
```

```php
public function check($params = []): bool
{
    $settings = $this->config['settings'] ?? [];
    $user     = (array) ($params['user_data'] ?? $this->user ?? []);
    $ip       = (string) ($user['ip'] ?? '');
    $uid      = (int) ($user['id'] ?? 0);          // misafir denemesinde 0

    // Her kural bağımsız olarak açılıp kapanır, yani değeri değil bayrağı okuyun.
    if ((int) ($settings['block-country-mismatch'] ?? 0) === 1) {
        $billing = strtoupper((string) ($user['address']['country_code'] ?? ''));
        $origin  = strtoupper((string) (UserManager::ip_info($ip)['countryCode'] ?? ''));

        // Yalnız KESİN bir uyuşmazlık engeller. Aksi hâlde çözülemeyen bir IP ya da
        // eksik bir fatura ülkesi, her yarım profili bir yanlış pozitife çevirirdi;
        // burada bir yanlış pozitif ise kaybedilmiş bir satıştır.
        if ($billing !== '' && $origin !== '' && $billing !== $origin) {
            // Bunun için çekirdekte bir dize var; WFraud da aynı anahtarı kullanır.
            $this->error = Language::gc("website/checkout/error-fraud-country");
            $this->insert_record($uid, "IP country ({$origin}) does not match billing country ({$billing})", $ip);

            return false;
        }
    }

    // Uzak bir puan. Buradaki hiçbir hata engellememeli: kapı fırlatılan bir
    // istisnayı geçiş sayar, bu erken dönüş de bozuk bir yanıt için aynısını yapar.
    $score = $this->remote_score($params);
    if ($score === null) return true;

    if ($score >= (int) ($settings['risk-score'] ?? 20)) {
        // Kendi ifadeniz SİZİN dil dosyanıza aittir. Language::gc(), çekirdekte
        // olmayan bir anahtar için false döner; bu da $error'ı boş bırakır ve
        // genel "güvenlik denetimlerimizden geçemedi" cümlesine düşülür.
        $this->error = $this->lang['error-risk-score'] ?? 'Your order could not be approved automatically.';
        $this->insert_record($uid, "Risk score {$score} reached the configured threshold", $ip);

        return false;
    }

    return true;
}
```

```php
// coremio/classes/FraudModule.php, run_checks()
foreach ($modules as $name => $row) {
    if (!(bool) ($row['config']['status'] ?? false)) continue;      // etkin değil: atlanır

    try {
        $module = Modules::getInstance("Fraud", (string) $name);
        if (!$module || !method_exists($module, 'check')) continue;
        if ($module->check($params) !== false) continue;            // geçti: sonraki modül

        $message = trim((string) ($module->error ?? ''));

        return $message !== ''
            ? $message
            : Language::gc("website/checkout/error-fraud", ['{module}' => (string) $name]);
    }
    catch (\Throwable $e) {
        // Bir sağlayıcının çökmesi ödeme akışını da beraberinde çökertmemelidir.
        Logger::warning("Fraud module '{$name}' check failed: " . $e->getMessage());
    }
}

return '';   // etkin her modül geçti
```

Dönen dize, ödeme akışının fırlattığı istisnaya dönüşür; müşteri sizin `$this->error`'unuzu okur. Ayrıntıyı kayda bırakın.

## Tuzaklar

> **Yanlış pozitif, reddedilmiş bir müşteridir**
> 
> Olumlu kanıt arayın: doğrulanmış uyuşmazlıkta engelleyin, eksik değerde asla. Buradaki hata, hiç verilmemiş siparişler olarak görünür.

> **Sağlayıcı hatasında asla kapalı düşmeyin**
> 
> Zaman aşımı dolandırıcılık kanıtı değildir. İstisna kaçsın ya da true dönün; kapı bir uyarı loglar ve sipariş geçer.

> **Global sınıflarınızı import edin**
> 
> Dolandırıcılık modülleri ad alanı içindedir; çıplak bir `UserManager::ip_info()` `WISECP\Modules\Fraud\UserManager`'a çözülür ve patlar. Global sınıfları import edin ya da başına ters bölü koyun.

> **Misafir denemesini karşılayın**
> 
> Misafir denemesinde `user_data.id`'ye dayanan kural ya çöker ya sessizce hiç çalışmaz.

> **Her engellemeyi gerekçesiyle kaydedin**
> 
> Operatörün kapıya açılan tek penceresi kayıtlar sekmesidir. Kaydı olmayan bir engelleme, izsiz kaybolmuş bir sipariştir.

## İ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)
- [Tema Formlarını Güvenli Hale Getirme](https://dev.wisecp.com/tr/tema-formlarini-guvenli-hale-getirme)
- [Sepet ve Ödeme](https://dev.wisecp.com/tr/sepet-ve-odeme)
- [Kanca Dinleyicisi Yazma](https://dev.wisecp.com/tr/kanca-dinleyicisi-yazma)
