From 642e4734987f54f8cff5fab4d30d12caf7456f94 Mon Sep 17 00:00:00 2001 From: done6666 Date: Tue, 25 Aug 2026 17:35:47 +0300 Subject: [PATCH 1/2] Add badges, bilingual README, examples, community files, PHP 8.4 + PHPStan CI --- .github/ISSUE_TEMPLATE/bug_report.md | 35 ++ .github/ISSUE_TEMPLATE/feature_request.md | 31 ++ .github/PULL_REQUEST_TEMPLATE.md | 22 + .github/workflows/ci.yml | 22 +- .github/workflows/static-analysis.yml | 31 ++ CHANGELOG.md | 5 +- CONTRIBUTING.md | 49 ++ README.en.md | 618 ++++++++++++++++++++++ README.md | 155 +++++- SECURITY.md | 31 ++ composer.json | 14 +- examples/README.md | 44 ++ examples/callback.php | 61 +++ examples/direct-payment.php | 85 +++ examples/iframe-payment.php | 86 +++ examples/recurring-payment.php | 86 +++ 16 files changed, 1354 insertions(+), 21 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/static-analysis.yml create mode 100644 CONTRIBUTING.md create mode 100644 README.en.md create mode 100644 SECURITY.md create mode 100644 examples/README.md create mode 100644 examples/callback.php create mode 100644 examples/direct-payment.php create mode 100644 examples/iframe-payment.php create mode 100644 examples/recurring-payment.php diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..926ad40 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,35 @@ +--- +name: Hata bildirimi +about: SDK'da bir hata mı buldunuz? Bize bildirin. +title: "[Bug] " +labels: bug +assignees: '' +--- + +**Özet** +Sorunu 1-2 cümleyle açıklayın. + +**Ortam** +- SDK sürümü: (örn. 1.1.0) +- PHP sürümü: (örn. 8.2.12) +- Kullanılan HTTP adapter: (örn. GuzzleHttpClient) + +**Yeniden üretim adımları** + +> ⚠️ Gerçek `merchant_key` / `merchant_salt` / kart verisi paylaşmayın. + +```php +// Minimum yeniden üretim kodu +``` + +**Beklenen davranış** + +Ne olmasını bekliyordunuz? + +**Gerçekleşen davranış** + +Ne oldu? (hata mesajı, stack trace vb.) + +**Ek bağlam** + +Varsa ekran görüntüsü veya log ekleyin. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..ad04867 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,31 @@ +--- +name: Özellik önerisi +about: Bu SDK için yeni bir fikir mi var? +title: "[Feature] " +labels: enhancement +assignees: '' +--- + +**Problem / ihtiyaç** + +Hangi sorunu çözüyorsunuz? Hangi senaryoda ihtiyaç duyuyorsunuz? + +**Önerilen çözüm** + +API'nin nasıl görünmesini istersiniz? Örnek kod: + +```php +// Hayal ettiğiniz kullanım +``` + +**Alternatifler** + +Düşündüğünüz başka yaklaşımlar var mı? + +**PayTR dokümantasyonu** + +İlgili PayTR API dokümanı linki (varsa): https://dev.paytr.com/... + +**Ek bağlam** + +Başka eklemek istediğiniz şeyler. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..13a14d1 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,22 @@ +## Açıklama + +Bu PR ne yapıyor? Neden gerekiyor? + +## Değişiklik türü + +- [ ] Bug düzeltme (geriye dönük uyumlu) +- [ ] Yeni özellik (geriye dönük uyumlu) +- [ ] Kırıcı değişiklik (breaking change) +- [ ] Doküman / örnek +- [ ] Refactor + +## Kontrol listesi + +- [ ] Testlerim eklendi/güncellendi ve `composer test` geçiyor +- [ ] README / CHANGELOG güncellendi (gerekiyorsa) +- [ ] Gerçek mağaza anahtarı, salt veya kart verisi içermiyor +- [ ] PSR-12 kod stiline uygun + +## İlgili issue + +Closes # diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 546c6b7..c8ff2d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,9 +9,16 @@ on: jobs: build: runs-on: ubuntu-latest + name: PHP ${{ matrix.php }} + continue-on-error: ${{ matrix.experimental }} strategy: + fail-fast: false matrix: php: ['8.0', '8.1', '8.2', '8.3'] + experimental: [false] + include: + - php: '8.4' + experimental: true steps: - uses: actions/checkout@v4 @@ -20,7 +27,7 @@ jobs: with: php-version: ${{ matrix.php }} extensions: json, mbstring - coverage: none + coverage: ${{ matrix.php == '8.3' && 'pcov' || 'none' }} - name: Get Composer cache directory id: composer-cache @@ -40,4 +47,17 @@ jobs: run: composer validate --strict - name: Run PHPUnit + if: matrix.php != '8.3' run: composer test + + - name: Run PHPUnit (with coverage) + if: matrix.php == '8.3' + run: vendor/bin/phpunit --coverage-clover=coverage.xml + + - name: Upload coverage to Codecov + if: matrix.php == '8.3' && github.event_name == 'push' + uses: codecov/codecov-action@v5 + with: + files: coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml new file mode 100644 index 0000000..2186334 --- /dev/null +++ b/.github/workflows/static-analysis.yml @@ -0,0 +1,31 @@ +name: Static Analysis + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + phpstan: + runs-on: ubuntu-latest + name: PHPStan + continue-on-error: true + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + coverage: none + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + + - name: Download PHPStan + run: | + curl -fsSL -o phpstan.phar https://github.com/phpstan/phpstan/releases/latest/download/phpstan.phar + + - name: Run PHPStan + run: php phpstan.phar analyse --level=max --no-progress --memory-limit=1G src diff --git a/CHANGELOG.md b/CHANGELOG.md index a22933c..59bd6ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ Tüm önemli değişiklikler bu dosyada listelenir. -## [Unreleased] +## [1.1.0] - 2026-08-25 ### Kart Saklama API (CAPI) tamamlama @@ -27,4 +27,5 @@ Tüm önemli değişiklikler bu dosyada listelenir. - Türkçe README, örnek kullanım ve idempotency önerisi. - CI: `composer validate` + `phpunit` (`.github/workflows/ci.yml`). -[1.0.0]: https://github.com/done6666/paytr-sdk/releases/tag/1.0.0 +[1.0.0]: https://github.com/done6666/paytr-sdk/releases/tag/v1.0.0 +[1.1.0]: https://github.com/done6666/paytr-sdk/releases/tag/v1.1.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9a0d196 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,49 @@ +# Katkı Rehberi + +PayTR PHP SDK'ya katkılarınız hoş geldiniz! 🎉 + +## Geliştirme Ortamı + +Gereksinimler: **PHP 8.0+** ve **Composer** + +```bash +git clone git@github.com:done6666/paytr-sdk.git +cd paytr-sdk +composer install +composer test +``` + +Gerçek PayTR çağrısı yapılmadan testler çalışır (`FakeHttpClient` kullanılır). + +## Kod Standartları + +- **PSR-4** otoload, **PSR-12** kod stili +- Yeni özellikler için mutlaka test yazın +- Public API değişikliklerinde README ve CHANGELOG'u güncelleyin +- Geriye dönük uyumluluğu kırıcı değişikliklerden kaçının (gerekliyse major sürüm + CHANGELOG notu) + +## Süreç + +1. Bu depoyu fork'layın +2. Feature dalı açın: `git checkout -b feature/harika-ozellik` +3. Değişikliklerinizi yapın ve testleri çalıştırın: `composer test` +4. Anlamlı commit mesajlarıyla commit'leyin +5. Fork'unuzdan bu depoya Pull Request açın + +## Pull Request Kuralları + +- PR şablonunu doldurun +- Tüm CI kontrollerinin (test + statik analiz) geçtiğinden emin olun +- Tek PR = tek amaç; büyük refactorleri ayrı PR'lara bölün + +## Hata Bildirimi + +[Hata bildirim şablonunu](.github/ISSUE_TEMPLATE/bug_report.md) kullanarak: + +- Kullandığınız SDK ve PHP sürümünü, +- Minimum yeniden üretim kodunu (**asla gerçek mağaza anahtarı/salt paylaşmayın**), +- Beklenen ve gerçekleşen davranışı ekleyin. + +## Lisans + +Katkılarınız [MIT lisansı](LICENSE) altında yayınlanır. diff --git a/README.en.md b/README.en.md new file mode 100644 index 0000000..9766800 --- /dev/null +++ b/README.en.md @@ -0,0 +1,618 @@ +# done6666/paytr-sdk + +[![CI](https://github.com/done6666/paytr-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/done6666/paytr-sdk/actions/workflows/ci.yml) +[![Static Analysis](https://github.com/done6666/paytr-sdk/actions/workflows/static-analysis.yml/badge.svg)](https://github.com/done6666/paytr-sdk/actions/workflows/static-analysis.yml) +[![Latest Stable Version](https://img.shields.io/packagist/v/done6666/paytr-sdk)](https://packagist.org/packages/done6666/paytr-sdk) +[![Total Downloads](https://img.shields.io/packagist/dt/done6666/paytr-sdk)](https://packagist.org/packages/done6666/paytr-sdk) +[![PHP from Packagist](https://img.shields.io/packagist/php-v/done6666/paytr-sdk)](https://packagist.org/packages/done6666/paytr-sdk) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![codecov](https://codecov.io/gh/done6666/paytr-sdk/graph/badge.svg)](https://codecov.io/gh/done6666/paytr-sdk) + +[**Türkçe**](README.md) | **English** + +A framework-agnostic PHP SDK for the PayTR payment platform (Turkey). **PSR-4**, **PSR-12**, **PHP 8.0+**. + +* iFrame (Token + iframe URL) +* Direct API (Payment, BIN lookup, installment rates) +* Card Storage API (CAPI) (Add card, pay with registered card, list cards, delete card, recurring payments) +* Callback verification (shared by iFrame + Direct API + CAPI notifications) +* Status inquiry +* Refunds + +## Why this SDK? + +- **Framework agnostic** — Laravel, Symfony or plain PHP; no framework dependency required. +- **Swappable HTTP layer** — Guzzle adapter included; plug in your own client via the `Contracts\HttpClient` interface. +- **Type-safe and fluent** — Request/Model classes are IDE-autocomplete friendly with built-in validation. +- **No hash/signature headaches** — all PayTR signature formulas live inside the SDK; callback verification in one line. +- **Testable** — run your tests without hitting the real API using `FakeHttpClient`; developed under full CI. +- **Built-in kuruş conversion** — `setAmountFromTL('34.56')` → `3456` kuruş; prevents the most classic integration bug. + +## Table of Contents + +- [Installation](#installation) +- [Quick Start](#quick-start) +- [Framework Integration](#framework-integration) +- [Important Notes](#important-notes) +- [Supported Integrations](#supported-integrations) +- [1) Starting an iFrame payment (token)](#1-starting-an-iframe-payment-token) +- [1b) Direct API](#1b-direct-api) +- [1c) Card Storage API (CAPI)](#1c-card-storage-api-capi) +- [2) Notification URL (callback) verification](#2-notification-url-callback-verification) +- [3) Status inquiry](#3-status-inquiry) +- [4) Refund](#4-refund) +- [Error Handling](#error-handling) +- [Backward Compatibility](#backward-compatibility) +- [Examples](#examples) +- [Testing](#testing) +- [Contributing](#contributing) +- [Security](#security) +- [License](#license) + +--- + +## Installation + +```bash +composer require done6666/paytr-sdk +``` + +Optional Guzzle adapter for HTTP requests: + +```bash +composer require guzzlehttp/guzzle +``` + +--- + +## Quick Start + +The recommended setup uses `Options` (`Options` is a fluent builder; `Config` is also available for advanced use). + +```php +use Done\PayTR\Options; +use Done\PayTR\Client; +use Done\PayTR\Adapters\GuzzleHttpClient; + +$options = (new Options()) + ->setMerchantId('MERCHANT_ID') + ->setMerchantKey('MERCHANT_KEY') + ->setMerchantSalt('MERCHANT_SALT') + ->setTestMode(true); + +$client = new Client($options, GuzzleHttpClient::default()); +``` + +To use your own HTTP client, implement the `Done\PayTR\Contracts\HttpClient` interface. + +--- + +## Framework Integration + +### Laravel + +`config/services.php`: + +```php +'paytr' => [ + 'merchant_id' => env('PAYTR_MERCHANT_ID'), + 'merchant_key' => env('PAYTR_MERCHANT_KEY'), + 'merchant_salt'=> env('PAYTR_MERCHANT_SALT'), +], +``` + +Register as a singleton in `AppServiceProvider@register`: + +```php +use Done\PayTR\Client; +use Done\PayTR\Adapters\GuzzleHttpClient; +use Done\PayTR\Options; + +$this->app->singleton(Client::class, function () { + $options = (new Options()) + ->setMerchantId(config('services.paytr.merchant_id')) + ->setMerchantKey(config('services.paytr.merchant_key')) + ->setMerchantSalt(config('services.paytr.merchant_salt')); + + return new Client($options, GuzzleHttpClient::default()); +}); +``` + +Use it in controllers via method injection: + +```php +public function pay(Request $request, Client $paytr) +{ + // $paytr->iframe()->createToken(...) +} +``` + +### Symfony + +`config/services.yaml`: + +```yaml +Done\PayTR\Options: + class: Done\PayTR\Options + calls: + - setMerchantId: ['%env(PAYTR_MERCHANT_ID)%'] + - setMerchantKey: ['%env(PAYTR_MERCHANT_KEY)%'] + - setMerchantSalt: ['%env(PAYTR_MERCHANT_SALT)%'] + +Done\PayTR\Adapters\GuzzleHttpClient: + factory: ['Done\PayTR\Adapters\GuzzleHttpClient', 'default'] + +Done\PayTR\Client: + arguments: + $configOrOptions: '@Done\PayTR\Options' + $httpClient: '@Done\PayTR\Adapters\GuzzleHttpClient' +``` + +--- + +## Important Notes + +### 1) user_ip (critical) + +In Direct API / Card Storage API calls, `user_ip` must be the **real customer IP address**. +If you are behind a reverse proxy, make sure to read it from the correct header. + +### 2) Idempotency (critical) + +PayTR notifications (callbacks) may arrive multiple times for the same `merchant_oid`. Process your orders **idempotently**: + +* Do not process an order again if it has already been marked "paid/failed". +* Check the order status in your database first. + +### 3) sync_mode difference + +* **sync_mode=0 (default):** The SDK builds the payload; you POST it to PayTR via your own HTML form. +* **sync_mode=1:** The SDK makes the HTTP call itself (you get a JSON response). Your merchant account needs the relevant permissions. + +### 4) Card data security + +Never: + +* log raw card numbers / CVV, +* store them unnecessarily, + +and always use HTTPS. Review your PCI-DSS obligations. + +--- + +## Supported Integrations + +* **iFrame API** + + * Token creation (start payment form) +* **Direct API** + + * Payment request + * BIN lookup + * Installment rates +* **Card Storage API (CAPI)** + + * Add new card (during payment, `store_card=1`) + * Pay with a registered card + * List registered cards (CAPI LIST) + * Delete registered card (CAPI DELETE) + * Recurring payment with a registered card (`recurring_payment=1`) +* **Callback verification** + + * iFrame + Direct API + CAPI all work with the same verification +* **Status inquiry** +* **Refunds** + +--- + +## 1) Starting an iFrame payment (token) + +Build a token with `Buyer`, `Basket`, `CreateTokenRequest`, then produce the iframe URL. + +> PayTR expects `payment_amount` in **kuruş** (1 TL = 100 kuruş). +> In the SDK: +> +> * `setAmountFromTL('34.56')` → SDK sends `3456` +> * or set kuruş directly with `setAmountKurus(3456)` + +```php +use Done\PayTR\Model\Buyer; +use Done\PayTR\Model\Basket; +use Done\PayTR\Model\BasketItem; +use Done\PayTR\Request\Iframe\CreateTokenRequest; + +$buyer = new Buyer( + email: 'customer@example.com', + name: 'John Doe', + phone: '5551234567', + ip: $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0', + addressLine: 'Delivery address' +); + +$basket = (new Basket()) + ->addItem(new BasketItem('Product 1', '18.00', 1)) + ->addItem(new BasketItem('Product 2', '33.25', 2)); + +$request = (new CreateTokenRequest()) + ->setMerchantOid('ORDER-' . uniqid()) + ->setAmountFromTL('34.56') + ->setCurrency('TL') + ->setBuyer($buyer) + ->setBasket($basket) + ->setOkUrl('https://yoursite.com/payment-success') + ->setFailUrl('https://yoursite.com/payment-failed') + ->setInstallmentPolicy(noInstallment: false, maxInstallment: 12); + +$response = $client->iframe()->createToken($request); +$iframeSrc = $client->iframe()->iframeUrl($response->token); +``` + +HTML iframe: + +```html + + + +``` + +> Order approval/cancellation must be handled via the **notification URL** (callback). +> `merchant_ok_url` / `merchant_fail_url` are only customer redirects. + +--- + +## 1b) Direct API + +With the Direct API the payment form lives on your server; card data is sent to PayTR. + +### Flows + +* **sync_mode=0 (default)** + + * Get the payload with `$request->toPayload($options)`. + * POST it from your own HTML form to `https://www.paytr.com/odeme`. + * The result appears on the `merchant_ok_url` / `merchant_fail_url` pages PayTR redirects to. +* **sync_mode=1** + + * The SDK POSTs via `createPayment()`. + * Returns JSON: `success | wait_callback | failed` + * Non3D and sync permissions may be required on your merchant account. + +### Example — Direct API payment (sync_mode=1) + +```php +use Done\PayTR\Model\Buyer; +use Done\PayTR\Model\Basket; +use Done\PayTR\Model\BasketItem; +use Done\PayTR\Model\Card; +use Done\PayTR\Request\Direct\CreatePaymentRequest; + +$buyer = new Buyer( + email: 'customer@example.com', + name: 'John Doe', + phone: '5551234567', + ip: $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0', + addressLine: 'Delivery address' +); + +$basket = (new Basket()) + ->addItem(new BasketItem('Product 1', '50.00', 1)) + ->addItem(new BasketItem('Product 2', '25.50', 2)); + +$card = new Card('CARDHOLDER NAME', '4111111111111111', '12', '30', '000'); + +$request = (new CreatePaymentRequest()) + ->setMerchantOid('DIR-' . uniqid()) + ->setPaymentAmount('101.00') + ->setBuyer($buyer) + ->setBasket($basket) + ->setCard($card) + ->setOkUrl('https://yoursite.com/success') + ->setFailUrl('https://yoursite.com/fail') + ->setInstallmentCount(0) + ->setNon3d(true) + ->setSyncMode(true); + +$response = $client->directApi()->createPayment($request); + +if ($response->isSuccess()) { + // Payment successful +} elseif ($response->isWaitCallback()) { + // Result will arrive via callback +} +``` + +### BIN lookup and installment rates + +```php +use Done\PayTR\Request\Direct\BinLookupRequest; +use Done\PayTR\Request\Direct\InstallmentRatesRequest; + +$binRequest = (new BinLookupRequest())->setBinNumber('411111'); +$binResult = $client->directApi()->binLookup($binRequest); +if ($binResult->isSuccess()) { + $brand = $binResult->brand; // bonus, axess, etc. +} + +$taksitRequest = (new InstallmentRatesRequest())->setRequestId('installment-' . uniqid()); +$taksitResult = $client->directApi()->installmentRates($taksitRequest); +if ($taksitResult->isSuccess()) { + $rates = $taksitResult->oranlar; + $maxInstallment = $taksitResult->maxInstNonBus; +} +``` + +> For sync_mode=0: get `$request->toPayload($options)` and POST it from your own form to +> `$client->directApi()->getPaymentFormUrl()` (default `https://www.paytr.com/odeme`). + +--- + +## 1c) Card Storage API (CAPI) + +Card Storage lets you keep users' cards at PayTR; charge stored cards, list them, delete them and take recurring payments. + +Access: + +```php +$client->cardStorage(); +``` + +### Endpoint / token summary + +* **CAPI LIST:** `POST /odeme/capi/list` + Token: `utoken + merchant_salt` +* **CAPI DELETE:** `POST /odeme/capi/delete` + Token: `ctoken + utoken + merchant_salt` +* **New card / pay with stored card / recurring payment:** `POST /odeme` + Same token formula as the Direct API: + `merchant_id + user_ip + merchant_oid + email + payment_amount + payment_type + installment_count + currency + test_mode + non_3d + merchant_salt` + +> In recurring payments the `recurring_payment` field is not included in the token string. + +--- + +### a) Add a new card (during payment) — `store_card=1` + +Do not send `utoken` for the first card; PayTR returns `utoken/ctoken` via **callback**. +When adding another card for the same user, send the existing `utoken`. + +```php +use Done\PayTR\Request\CardStorage\AddCardRequest; + +$request = (new AddCardRequest()) + ->setMerchantOid('ORD-' . uniqid()) + ->setPaymentAmount('50.00') + ->setBuyer($buyer) + ->setBasket($basket) + ->setCard($card) + ->setOkUrl('https://yoursite.com/ok') + ->setFailUrl('https://yoursite.com/fail') + ->setSyncMode(true); +// ->setUtoken($existingUtoken) // extra card for the same user + +$response = $client->cardStorage()->addCard($request); +``` + +> For sync_mode=0 the SDK does not make the HTTP call; +> get `toPayload($options)` and form-POST it to `$client->cardStorage()->getPaymentFormUrl()`. + +--- + +### b) List registered cards (CAPI LIST) + +```php +use Done\PayTR\Request\CardStorage\ListCardsRequest; + +$listReq = (new ListCardsRequest())->setUtoken($utoken); +$listResp = $client->cardStorage()->listCards($listReq); + +foreach ($listResp->cards as $card) { + // $card->ctoken + // $card->last4 + // $card->requireCvv (if 1, ask for CVV during payment) +} +``` + +--- + +### c) Pay with a registered card + +If CAPI LIST returned `require_cvv=1`, collect CVV from the user and send it. + +```php +use Done\PayTR\Request\CardStorage\PayWithRegisteredCardRequest; + +$req = (new PayWithRegisteredCardRequest()) + ->setMerchantOid('ORD-' . uniqid()) + ->setPaymentAmount('75.00') + ->setBuyer($buyer) + ->setBasket($basket) + ->setOkUrl('https://yoursite.com/ok') + ->setFailUrl('https://yoursite.com/fail') + ->setUtoken($utoken) + ->setCtoken($ctoken) + ->setRequireCvv(true) + ->setCvv('000') + ->setSyncMode(true); + +$response = $client->cardStorage()->payWithRegisteredCard($req); +``` + +--- + +### d) Delete card (CAPI DELETE) + +```php +use Done\PayTR\Request\CardStorage\DeleteCardRequest; + +$delReq = (new DeleteCardRequest()) + ->setUtoken($utoken) + ->setCtoken($ctoken); + +$client->cardStorage()->deleteCard($delReq); +``` + +--- + +### e) Recurring payment (registered card) + +Charging without user interaction. `non_3d=1`, `recurring_payment=1`. + +```php +use Done\PayTR\Request\CardStorage\RecurringPaymentRequest; + +$recReq = (new RecurringPaymentRequest()) + ->setMerchantOid('SUB-' . uniqid()) + ->setPaymentAmount('99.00') + ->setBuyer($buyer) + ->setBasket($basket) + ->setOkUrl('https://yoursite.com/ok') + ->setFailUrl('https://yoursite.com/fail') + ->setUtoken($utoken) + ->setCtoken($ctoken); + +$recResp = $client->cardStorage()->recurringPayment($recReq); + +if ($recResp->isSuccess()) { + // Charge successful +} elseif ($recResp->isWaitCallback()) { + // Result will be finalized via callback +} else { + // failed/error + // If $recResp->tryAgain === true, a transaction is in progress; retry later. +} +``` + +--- + +## 2) Notification URL (callback) verification + +iFrame + Direct API + Card Storage notifications all work with the **same verification**. + +Required fields: + +* `merchant_oid` +* `status` +* `total_amount` +* `hash` + +> Respond with **plain text only**: `OK`. +> There must be no HTML or extra output before/after. + +```php +use Done\PayTR\Exceptions\SignatureException; + +try { + $notification = $client->callback()->verify($_POST); +} catch (SignatureException $e) { + http_response_code(400); + exit('Invalid notification.'); +} + +if ($notification->isSuccess()) { + // approveOrder($notification->merchantOid, $notification->totalAmount); +} else { + // cancelOrder($notification->merchantOid, $notification->failedReasonCode, $notification->failedReasonMsg); +} + +echo $client->callback()->ok(); +exit; +``` + +--- + +## 3) Status inquiry + +```php +use Done\PayTR\Request\Query\StatusRequest; + +$request = (new StatusRequest())->setMerchantOid('ORDER-123'); +$result = $client->query()->status($request); + +// $result->paymentAmount, $result->paymentTotal, $result->currency, $result->paymentDate, $result->returns +``` + +--- + +## 4) Refund + +```php +use Done\PayTR\Request\Refund\RefundRequest; + +$request = (new RefundRequest()) + ->setMerchantOid('ORDER-123') + ->setReturnAmount('11.97') + ->setReferenceNo('REFUND-REF-001'); + +$result = $client->refundCancel()->refund($request); +``` + +--- + +## Error Handling + +| Exception | Description | +| -------------------- | ------------------------------------------------------------------------ | +| `PayTRException` | Base class of all SDK errors | +| `ValidationException`| Missing/invalid parameter (e.g. amount format, missing callback field) | +| `HttpException` | Connection / timeout | +| `SignatureException` | Callback hash mismatch | +| `ApiException` | PayTR `status: failed/error`; `getReason()`, `getPayload()` | + +--- + +## Backward Compatibility + +The legacy DTO-based API keeps working; Request/Model style is recommended for new code. + +* `iframePayment()->getToken(IframeTokenRequest)` → `iframe()->createToken(CreateTokenRequest)` +* `query()->query(QueryRequest)` → `query()->status(StatusRequest)` +* `refundCancel()->refund(RefundRequestDto)` / `refundWith(RefundRequest)` → `refundCancel()->refund(RefundRequest)` +* `UserBasket::encode()` → `Model\Basket` + `BasketItem` + +--- + +## Examples + +Copy-paste runnable scenarios live in [`examples/`](examples/): + +* [`iframe-payment.php`](examples/iframe-payment.php) — iFrame token creation +* [`direct-payment.php`](examples/direct-payment.php) — Direct API (Non3D, sync) +* [`callback.php`](examples/callback.php) — Notification URL endpoint +* [`recurring-payment.php`](examples/recurring-payment.php) — Recurring charge on a stored card + +--- + +## Testing + +```bash +composer install +composer test +``` + +No real PayTR calls are made; tests run against `FakeHttpClient`. + +--- + +## Contributing + +Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) first. + +## Security + +Please do not open public issues for security vulnerabilities; follow the process in [SECURITY.md](SECURITY.md). + +## License + +MIT. See `LICENSE`. + +--- + +## Reference + +* PayTR Developer Center: [https://dev.paytr.com/](https://dev.paytr.com/) +* Direct API: [https://dev.paytr.com/direkt-api](https://dev.paytr.com/direkt-api) +* Card Storage API: [https://dev.paytr.com/direkt-api/kart-saklama-api](https://dev.paytr.com/direkt-api/kart-saklama-api) +* iFrame API Step 1: [https://dev.paytr.com/iframe-api/iframe-api-1-adim](https://dev.paytr.com/iframe-api/iframe-api-1-adim) +* iFrame API Step 2 (Notification URL): [https://dev.paytr.com/iframe-api/iframe-api-2-adim](https://dev.paytr.com/iframe-api/iframe-api-2-adim) +* Status Inquiry API: [https://dev.paytr.com/durum-sorgu](https://dev.paytr.com/durum-sorgu) +* Refund API: [https://dev.paytr.com/iade-api](https://dev.paytr.com/iade-api) diff --git a/README.md b/README.md index e14d30f..9ac1c22 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,15 @@ # done6666/paytr-sdk +[![CI](https://github.com/done6666/paytr-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/done6666/paytr-sdk/actions/workflows/ci.yml) +[![Static Analysis](https://github.com/done6666/paytr-sdk/actions/workflows/static-analysis.yml/badge.svg)](https://github.com/done6666/paytr-sdk/actions/workflows/static-analysis.yml) +[![Latest Stable Version](https://img.shields.io/packagist/v/done6666/paytr-sdk)](https://packagist.org/packages/done6666/paytr-sdk) +[![Total Downloads](https://img.shields.io/packagist/dt/done6666/paytr-sdk)](https://packagist.org/packages/done6666/paytr-sdk) +[![PHP from Packagist](https://img.shields.io/packagist/php-v/done6666/paytr-sdk)](https://packagist.org/packages/done6666/paytr-sdk) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +[![codecov](https://codecov.io/gh/done6666/paytr-sdk/graph/badge.svg)](https://codecov.io/gh/done6666/paytr-sdk) + +**Türkçe** | [English](README.en.md) + PayTR ödeme altyapısı için framework bağımsız PHP SDK. **PSR-4**, **PSR-12**, **PHP 8.0+**. * Iframe (Token + iframe URL) @@ -9,6 +19,36 @@ PayTR ödeme altyapısı için framework bağımsız PHP SDK. **PSR-4**, **PSR-1 * Durum sorgulama * İade +## Neden bu SDK? + +- **Framework bağımsız** — Laravel, Symfony veya düz PHP; hiçbirine bağımlılık zorunluluğu yok. +- **Değiştirilebilir HTTP katmanı** — Guzzle adapter hazır; `Contracts\HttpClient` arayüzüyle kendi client'ınızı bağlayın. +- **Tip güvenli ve fluent** — Request/Model sınıflarıyla IDE autocomplete dostu, doğrulanabilir istekler. +- **Hash/signature karmaşası yok** — PayTR'nin tüm imza formülleri SDK içinde; callback doğrulaması tek satır. +- **Test edilebilir** — `FakeHttpClient` ile gerçek API çağrısı yapmadan test; %100 CI kapsamında geliştirilir. +- **Kuruş/TL dönüşümü dahili** — `setAmountFromTL('34.56')` → `3456` kuruş; en klasik entegrasyon hatasını engeller. + +## İçindekiler + +- [Kurulum](#kurulum) +- [Hızlı Başlangıç](#hızlı-başlangıç) +- [Framework Entegrasyonu](#framework-entegrasyonu) +- [Önemli Notlar](#önemli-notlar) +- [Desteklenen Entegrasyonlar](#desteklenen-entegrasyonlar) +- [1) Iframe ile ödeme başlatma (token)](#1-iframe-ile-ödeme-başlatma-token) +- [1b) Direkt API](#1b-direkt-api) +- [1c) Kart Saklama API (CAPI)](#1c-kart-saklama-api-capi) +- [2) Bildirim URL (Callback) doğrulama](#2-bildirim-url-callback-doğrulama) +- [3) Durum sorgulama](#3-durum-sorgulama) +- [4) İade](#4-iade) +- [Hata Yönetimi](#hata-yönetimi) +- [Geriye Dönük Uyumluluk](#geriye-dönük-uyumluluk) +- [Örnekler](#örnekler) +- [Test](#test) +- [Katkı](#katkı) +- [Güvenlik](#güvenlik) +- [Lisans](#lisans) + --- ## Kurulum @@ -27,7 +67,7 @@ composer require guzzlehttp/guzzle ## Hızlı Başlangıç -Önerilen kurulum `Options` ile yapılır. (`Options` fluent builder’dır; ileri kullanımda `Config` de kullanılabilir.) +Önerilen kurulum `Options` ile yapılır. (`Options` fluent builder'dır; ileri kullanımda `Config` de kullanılabilir.) ```php use Done\PayTR\Options; @@ -43,7 +83,70 @@ $options = (new Options()) $client = new Client($options, GuzzleHttpClient::default()); ``` -Kendi HTTP client’ınız için `Done\PayTR\Contracts\HttpClient` arayüzünü implement edebilirsiniz. +Kendi HTTP client'ınız için `Done\PayTR\Contracts\HttpClient` arayüzünü implement edebilirsiniz. + +--- + +## Framework Entegrasyonu + +### Laravel + +`config/services.php`: + +```php +'paytr' => [ + 'merchant_id' => env('PAYTR_MERCHANT_ID'), + 'merchant_key' => env('PAYTR_MERCHANT_KEY'), + 'merchant_salt'=> env('PAYTR_MERCHANT_SALT'), +], +``` + +`AppServiceProvider@register` içinde singleton olarak kaydedin: + +```php +use Done\PayTR\Client; +use Done\PayTR\Adapters\GuzzleHttpClient; +use Done\PayTR\Options; + +$this->app->singleton(Client::class, function () { + $options = (new Options()) + ->setMerchantId(config('services.paytr.merchant_id')) + ->setMerchantKey(config('services.paytr.merchant_key')) + ->setMerchantSalt(config('services.paytr.merchant_salt')); + + return new Client($options, GuzzleHttpClient::default()); +}); +``` + +Controller'da method injection ile kullanın: + +```php +public function pay(Request $request, Client $paytr) +{ + // $paytr->iframe()->createToken(...) +} +``` + +### Symfony + +`config/services.yaml`: + +```yaml +Done\PayTR\Options: + class: Done\PayTR\Options + calls: + - setMerchantId: ['%env(PAYTR_MERCHANT_ID)%'] + - setMerchantKey: ['%env(PAYTR_MERCHANT_KEY)%'] + - setMerchantSalt: ['%env(PAYTR_MERCHANT_SALT)%'] + +Done\PayTR\Adapters\GuzzleHttpClient: + factory: ['Done\PayTR\Adapters\GuzzleHttpClient', 'default'] + +Done\PayTR\Client: + arguments: + $configOrOptions: '@Done\PayTR\Options' + $httpClient: '@Done\PayTR\Adapters\GuzzleHttpClient' +``` --- @@ -51,19 +154,19 @@ Kendi HTTP client’ınız için `Done\PayTR\Contracts\HttpClient` arayüzünü ### 1) user_ip (kritik) -Direkt API / Kart Saklama API çağrılarında `user_ip` alanı **mutlaka gerçek müşteri IP’si** olmalıdır. -Örneğin reverse proxy arkasındaysanız doğru header’dan almayı unutmayın. +Direkt API / Kart Saklama API çağrılarında `user_ip` alanı **mutlaka gerçek müşteri IP'si** olmalıdır. +Örneğin reverse proxy arkasındaysanız doğru header'dan almayı unutmayın. ### 2) Idempotency (kritik) PayTR bildirimleri (callback) aynı `merchant_oid` için birden fazla kez gelebilir. Siparişinizi **idempotent** işleyin: -* Sipariş daha önce “paid/failed” durumuna alınmışsa tekrar işlemeyin. -* Önce DB’de sipariş durumunu kontrol edin. +* Sipariş daha önce "paid/failed" durumuna alınmışsa tekrar işlemeyin. +* Önce DB'de sipariş durumunu kontrol edin. ### 3) sync_mode farkı -* **sync_mode=0 (varsayılan):** SDK payload üretir, siz HTML form ile PayTR’ye POST edersiniz. +* **sync_mode=0 (varsayılan):** SDK payload üretir, siz HTML form ile PayTR'ye POST edersiniz. * **sync_mode=1:** SDK HTTP çağrısını kendi yapar (JSON yanıt alırsınız). Bunun için mağazada ilgili yetkilerin açık olması gerekir. ### 4) Kart verisi güvenliği @@ -104,10 +207,10 @@ Ham kart numarası / CVV / tam kart bilgisini: ## 1) Iframe ile ödeme başlatma (token) -`Buyer`, `Basket`, `CreateTokenRequest` ile token alıp iframe URL’i üretin. +`Buyer`, `Basket`, `CreateTokenRequest` ile token alıp iframe URL'i üretin. > PayTR `payment_amount` alanını **kuruş** bazlı kullanır. -> SDK’da: +> SDK'da: > > * `setAmountFromTL('34.56')` → SDK 3456 olarak gönderir > * direkt kuruş için `setAmountKurus(3456)` @@ -159,7 +262,7 @@ HTML iframe: ## 1b) Direkt API -Direkt API’de ödeme formu sizin sunucunuzdadır; kart bilgileri PayTR’ye gönderilir. +Direkt API'de ödeme formu sizin sunucunuzdadır; kart bilgileri PayTR'ye gönderilir. ### Akışlar @@ -167,7 +270,7 @@ Direkt API’de ödeme formu sizin sunucunuzdadır; kart bilgileri PayTR’ye g * `$request->toPayload($options)` ile payload alırsınız. * Kendi HTML formunuzdan `https://www.paytr.com/odeme` adresine POST edersiniz. - * Sonuç, PayTR’nin yönlendirdiği `merchant_ok_url` / `merchant_fail_url` sayfalarında görülür. + * Sonuç, PayTR'nin yönlendirdiği `merchant_ok_url` / `merchant_fail_url` sayfalarında görülür. * **sync_mode=1** * SDK `createPayment()` ile POST atar. @@ -245,7 +348,7 @@ if ($taksitResult->isSuccess()) { ## 1c) Kart Saklama API (CAPI) -Kart Saklama ile kullanıcıların kartlarını PayTR’de saklayabilir; kayıtlı karttan ödeme alabilir, listeleyebilir, silebilir ve tekrarlayan ödeme yapabilirsiniz. +Kart Saklama ile kullanıcıların kartlarını PayTR'de saklayabilir; kayıtlı karttan ödeme alabilir, listeleyebilir, silebilir ve tekrarlayan ödeme yapabilirsiniz. Erişim: @@ -263,14 +366,14 @@ $client->cardStorage(); Token formülü Direkt API ile aynıdır: `merchant_id + user_ip + merchant_oid + email + payment_amount + payment_type + installment_count + currency + test_mode + non_3d + merchant_salt` -> Tekrarlayan ödemede `recurring_payment` alanı token string’ine dahil edilmez. +> Tekrarlayan ödemede `recurring_payment` alanı token string'ine dahil edilmez. --- ### a) Yeni kart ekleme (ödeme sırasında) — `store_card=1` İlk kartta `utoken` göndermeyin; PayTR **callback** üzerinden `utoken/ctoken` döner. -Aynı kullanıcıya yeni kart eklerken mevcut `utoken`’ı gönderin. +Aynı kullanıcıya yeni kart eklerken mevcut `utoken`'ı gönderin. ```php use Done\PayTR\Request\CardStorage\AddCardRequest; @@ -313,7 +416,7 @@ foreach ($listResp->cards as $kart) { ### c) Kayıtlı karttan ödeme -CAPI LIST’ten gelen `require_cvv=1` ise kullanıcıdan CVV alıp gönderin. +CAPI LIST'ten gelen `require_cvv=1` ise kullanıcıdan CVV alıp gönderin. ```php use Done\PayTR\Request\CardStorage\PayWithRegisteredCardRequest; @@ -393,7 +496,7 @@ Zorunlu alanlar: * `hash` > Yanıt olarak **yalnızca** düz metin `OK` yazdırın. -> Öncesinde/sonrasında HTML veya ekstra çıktı olmamalı. +> Öncesinde/sonrasında HTML veya ekstra çıktı olmamalıdır. ```php use Done\PayTR\Exceptions\SignatureException; @@ -468,6 +571,17 @@ Eski DTO tabanlı API çalışmaya devam eder; yeni kod için Request/Model stil --- +## Örnekler + +Kopyala-yapıştır çalıştırılabilir senaryolar [`examples/`](examples/) klasöründe: + +* [`iframe-payment.php`](examples/iframe-payment.php) — Iframe token üretimi +* [`direct-payment.php`](examples/direct-payment.php) — Direkt API (Non3D, sync) +* [`callback.php`](examples/callback.php) — Bildirim URL endpoint'i +* [`recurring-payment.php`](examples/recurring-payment.php) — Kayıtlı kartla tekrarlayan ödeme + +--- + ## Test ```bash @@ -479,6 +593,14 @@ Gerçek PayTR çağrısı yapılmaz; testler `FakeHttpClient` ile çalışır. --- +## Katkı + +Katkılarınızı bekliyoruz! Lütfen önce [CONTRIBUTING.md](CONTRIBUTING.md) dosyasını okuyun. + +## Güvenlik + +Güvenlik açığı bildirimleri için lütfen public issue açmayın; [SECURITY.md](SECURITY.md) dosyasındaki süreci izleyin. + ## Lisans MIT. Bkz. `LICENSE`. @@ -494,4 +616,3 @@ MIT. Bkz. `LICENSE`. * iFrame API 2. Adım (Bildirim URL): [https://dev.paytr.com/iframe-api/iframe-api-2-adim](https://dev.paytr.com/iframe-api/iframe-api-2-adim) * Durum Sorgu API: [https://dev.paytr.com/durum-sorgu](https://dev.paytr.com/durum-sorgu) * İade API: [https://dev.paytr.com/iade-api](https://dev.paytr.com/iade-api) - diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..f36b159 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,31 @@ +# Güvenlik Politikası + +Bu SDK ödeme işlemleriyle çalışır; güvenlik raporlarınızı ciddiye alıyoruz. + +## Desteklenen Sürümler + +| Sürüm | Destek | +| ------ | ---------- | +| 1.1.x | ✅ Aktif | +| < 1.1 | ❌ Sonu | + +## Güvenlik Açığı Bildirimi + +**Lütfen güvenlik açıklarını public issue olarak bildirmeyin.** + +GitHub üzerinden gizli güvenlik açığı bildirimi kullanın: + +1. Depo sayfasında **Security** sekmesine gidin +2. **Report a vulnerability** bağlantısına tıklayın +3. Açığı mümkün olduğunca detaylı açıklayın (etkilenen sürüm, yeniden üretim adımları, olası etki) + +Alternatif olarak [GitHub Private Vulnerability Reporting](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability) dokümanındaki adımları izleyebilirsiniz. + +Bildirimlerinize en kısa sürede (genellikle 72 saat içinde) yanıt vermeye çalışırız. + +## Güvenlik Önerileri (kullanıcılar için) + +- `merchant_key` ve `merchant_salt` değerlerini asla repoya commitlemeyin; ortam değişkenlerinde saklayın. +- Callback doğrulamasını atlamayın; her bildirimde hash kontrolü yapın (`SignatureException`). +- Ham kart verisi (PAN/CVV) loglamayın — PCI-DSS yükümlülüklerinizi gözden geçirin. +- HTTPS zorunludur; callback endpoint'inizi imzasız isteklere karşı koruyun. diff --git a/composer.json b/composer.json index ee7ade9..615ae28 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,19 @@ "description": "Framework bağımsız PayTR ödeme altyapısı PHP SDK", "type": "library", "license": "MIT", - "keywords": ["paytr", "payment", "odeme", "turkey", "iframe", "api"], + "homepage": "https://github.com/done6666/paytr-sdk", + "keywords": [ + "paytr", + "payment", + "payment-gateway", + "odeme", + "sanal-pos", + "kredi-karti", + "turkey", + "iframe", + "api", + "callback" + ], "authors": [ { "name": "done6666", diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..aa007fa --- /dev/null +++ b/examples/README.md @@ -0,0 +1,44 @@ +# Örnekler + +Kopyala-yapıştır çalıştırılabilir senaryolar. Hiçbiri gerçek mağaza anahtarı içermez; kimlik bilgilerini **ortam değişkenleriyle** verin. + +| Dosya | Senaryo | +| --- | --- | +| [`iframe-payment.php`](iframe-payment.php) | Iframe token üretimi + iframe HTML | +| [`direct-payment.php`](direct-payment.php) | Direkt API ödeme (Non3D, `sync_mode=1`) | +| [`callback.php`](callback.php) | Bildirim URL endpoint'i (hash doğrulama + OK yanıtı) | +| [`recurring-payment.php`](recurring-payment.php) | Kayıtlı kartla tekrarlayan ödeme | + +## Kurulum + +```bash +composer install +``` + +## Çalıştırma + +```bash +export PAYTR_MERCHANT_ID="..." +export PAYTR_MERCHANT_KEY="..." +export PAYTR_MERCHANT_SALT="..." + +# Iframe token üret (HTML çıktısı verir) +php examples/iframe-payment.php + +# Direkt API ödeme (Non3D yetkisi gerektirir) +php examples/direct-payment.php + +# Tekrarlayan ödeme (utoken/ctoken gerekir) +export PAYTR_UTOKEN="..." +export PAYTR_CTOKEN="..." +php examples/recurring-payment.php + +# Callback endpoint'i yerelde test etmek için +php -S localhost:8080 examples/callback.php +``` + +## Güvenlik notları + +- Anahtarlarınızı asla dosyaya gömün veya repoya commitlemeyin. +- `callback.php` üretimde HTTPS arkasında ve imza doğrulamasıyla çalışır; doğrulamayı asla atmayın. +- Test modunda (`setTestMode(true)`) PayTR test kartlarını kullanın. diff --git a/examples/callback.php b/examples/callback.php new file mode 100644 index 0000000..8143704 --- /dev/null +++ b/examples/callback.php @@ -0,0 +1,61 @@ +setMerchantId($merchantId) + ->setMerchantKey($merchantKey) + ->setMerchantSalt($merchantSalt) + ->setTestMode(false); // callback doğrulamasında test_mode önemi yoktur; ortamınıza göre ayarlayın + +$client = new Client($options, GuzzleHttpClient::default()); + +try { + $notification = $client->callback()->verify($_POST); +} catch (SignatureException $e) { + http_response_code(400); + exit('Geçersiz bildirim.'); +} + +// TODO: Kendi sipariş iş mantığınızı burada çalıştırın. +// Idempotency için önce siparişin mevcut durumunu DB'den kontrol edin: +// +// $order = $db->findOrderByOid($notification->merchantOid); +// if ($order !== null && $order->isFinalized()) { echo $client->callback()->ok(); exit; } + +if ($notification->isSuccess()) { + // siparisOnayla($notification->merchantOid, $notification->totalAmount); +} else { + // siparisIptalEt($notification->merchantOid, $notification->failedReasonCode, $notification->failedReasonMsg); +} + +echo $client->callback()->ok(); +exit; diff --git a/examples/direct-payment.php b/examples/direct-payment.php new file mode 100644 index 0000000..f40a007 --- /dev/null +++ b/examples/direct-payment.php @@ -0,0 +1,85 @@ +setMerchantId($merchantId) + ->setMerchantKey($merchantKey) + ->setMerchantSalt($merchantSalt) + ->setTestMode(true); + +$client = new Client($options, GuzzleHttpClient::default()); + +$buyer = new Buyer( + email: 'musteri@ornek.com', + name: 'Ad Soyad', + phone: '5551234567', + ip: $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1', + addressLine: 'Teslimat adresi' +); + +$basket = (new Basket()) + ->addItem(new BasketItem('Ürün 1', '50.00', 1)) + ->addItem(new BasketItem('Ürün 2', '25.50', 2)); + +// PayTR test kartlarından biri; üretimde asla sabit kart göndermeyin +$card = new Card('TEST KARTI', '4111111111111111', '12', '30', '000'); + +$request = (new CreatePaymentRequest()) + ->setMerchantOid('DIR-' . uniqid()) + ->setPaymentAmount('101.00') + ->setBuyer($buyer) + ->setBasket($basket) + ->setCard($card) + ->setOkUrl('https://siteniz.com/basarili') + ->setFailUrl('https://siteniz.com/hata') + ->setInstallmentCount(0) + ->setNon3d(true) + ->setSyncMode(true); + +try { + $response = $client->directApi()->createPayment($request); +} catch (PayTRException $e) { + fwrite(STDERR, 'Hata: ' . $e->getMessage() . "\n"); + exit(1); +} + +if ($response->isSuccess()) { + echo "Ödeme başarılı.\n"; +} elseif ($response->isWaitCallback()) { + // Nihai sonucu callback endpoint'inizden doğrulayın (bkz. examples/callback.php) + echo "Sonuç callback üzerinden gelecek.\n"; +} else { + echo "Ödeme başarısız.\n"; +} diff --git a/examples/iframe-payment.php b/examples/iframe-payment.php new file mode 100644 index 0000000..497b13e --- /dev/null +++ b/examples/iframe-payment.php @@ -0,0 +1,86 @@ +setMerchantId($merchantId) + ->setMerchantKey($merchantKey) + ->setMerchantSalt($merchantSalt) + ->setTestMode(true); // canlıya geçerken false yapın + +$client = new Client($options, GuzzleHttpClient::default()); + +$buyer = new Buyer( + email: 'musteri@ornek.com', + name: 'Ad Soyad', + phone: '5551234567', + ip: $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1', + addressLine: 'Teslimat adresi' +); + +$basket = (new Basket()) + ->addItem(new BasketItem('Ürün adı 1', '18.00', 1)) + ->addItem(new BasketItem('Ürün adı 2', '33.25', 2)); + +$request = (new CreateTokenRequest()) + ->setMerchantOid('SIPARIS-' . uniqid()) + ->setAmountFromTL('34.56') // 34.56 TL → 3456 kuruş olarak gönderilir + ->setCurrency('TL') + ->setBuyer($buyer) + ->setBasket($basket) + ->setOkUrl('https://siteniz.com/odeme-basarili') + ->setFailUrl('https://siteniz.com/odeme-hata') + ->setInstallmentPolicy(noInstallment: false, maxInstallment: 12); + +try { + $response = $client->iframe()->createToken($request); +} catch (\Done\PayTR\Exceptions\PayTRException $e) { + fwrite(STDERR, 'Hata: ' . $e->getMessage() . "\n"); + exit(1); +} + +$iframeSrc = $client->iframe()->iframeUrl($response->token); +?> + + + + + PayTR Iframe Ödeme + + + + + + + + + diff --git a/examples/recurring-payment.php b/examples/recurring-payment.php new file mode 100644 index 0000000..895fcdf --- /dev/null +++ b/examples/recurring-payment.php @@ -0,0 +1,86 @@ +setMerchantId($merchantId) + ->setMerchantKey($merchantKey) + ->setMerchantSalt($merchantSalt) + ->setTestMode(false); + +$client = new Client($options, GuzzleHttpClient::default()); + +$buyer = new Buyer( + email: 'musteri@ornek.com', + name: 'Ad Soyad', + phone: '5551234567', + ip: $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1', + addressLine: 'Teslimat adresi' +); + +$basket = (new Basket()) + ->addItem(new BasketItem('Aylık abonelik', '99.00', 1)); + +$request = (new RecurringPaymentRequest()) + ->setMerchantOid('ABO-' . uniqid()) + ->setPaymentAmount('99.00') + ->setBuyer($buyer) + ->setBasket($basket) + ->setOkUrl('https://siteniz.com/ok') + ->setFailUrl('https://siteniz.com/fail') + ->setUtoken($utoken) + ->setCtoken($ctoken); + +try { + $response = $client->cardStorage()->recurringPayment($request); +} catch (PayTRException $e) { + fwrite(STDERR, 'Hata: ' . $e->getMessage() . "\n"); + exit(1); +} + +if ($response->isSuccess()) { + echo "Tahsilat başarılı.\n"; +} elseif ($response->isWaitCallback()) { + echo "Sonuç callback ile netleşecek.\n"; +} else { + echo "Tahsilat başarısız.\n"; + // Devam eden bir işlem varsa daha sonra tekrar deneyin: + if ($response->tryAgain === true) { + echo "try_again=true: kısa süre sonra tekrar deneyin.\n"; + } +} From c61ce502d74921d130bc0b19f7dc89a08016c1da Mon Sep 17 00:00:00 2001 From: done6666 Date: Tue, 25 Aug 2026 17:41:17 +0300 Subject: [PATCH 2/2] Fix CI: resolve fresh deps for PHP 8.0 (lock built on PHP 8.1+) --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8ff2d3..e00d766 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,8 +41,14 @@ jobs: restore-keys: ${{ runner.os }}-composer- - name: Install dependencies + if: matrix.php != '8.0' run: composer install --no-interaction --prefer-dist + # composer.lock PHP >=8.1 ile üretilmiştir; PHP 8.0 için uyumlu set taze çözülür + - name: Install dependencies (resolve fresh for PHP 8.0) + if: matrix.php == '8.0' + run: composer update --no-interaction --prefer-dist --no-progress + - name: Validate Composer run: composer validate --strict