Skip to content

Developer guide

app/addons/kv_paytr_payments/
├── addon.xml scheme 3.0, settings, 5 tables, install/uninstall functions
├── init.php hooks: prepare_checkout_payment_methods, change_order_status_post, delete_order,
│ update_company, delete_company; license client boot
├── func.php install/uninstall, payment start, payment_notification handler, hook handlers,
│ settings info handlers, template helpers
├── payments/
│ ├── kv_paytr_iframe.php processor script "PayTR iFrame"
│ └── kv_paytr_direct.php processor script "PayTR Direct API (card form)"
├── controllers/frontend/kv_paytr_payments.php pay (iFrame page), card (Direct API form), installments (AJAX, BIN)
├── controllers/backend/kv_paytr_payments.php refund, links, status check, transfers, rates refresh, manage (log), process_transfers (cron)
├── controllers/backend/kv_paytr_payments_license.php "Check now" / cron license check
├── schemas/block_manager/block_cache_properties.post.php license state in the block cache key
├── schemas/menu/menu.post.php Orders → PayTR transactions
├── schemas/permissions/admin.post.php
└── Tygh/Addons/KvPaytrPayments/ PSR-0 classes
├── Signer.php every PayTR signature (pure PHP)
├── Payload.php amounts, currencies, field limits, basket, merchant_oid, IBAN (pure PHP)
├── InstallmentTable.php rates normalization and plan calculation (pure PHP)
├── ProcessorConfig.php validated processor_params of a payment method
├── ApiClient.php PayTR HTTP calls (Tygh\Http), endpoint override for development
├── Repository.php db_* access to the add-on tables, atomic "claim" updates
├── Gateway.php payment attempts, notifications, customer return, status mapping
├── RefundService.php refunds
├── LinkService.php pay-by-link (Link API) and its notification
├── InstallmentService.php rates cache (storage_data, 24 h)
├── TransferService.php Multi-Vendor platform transfers
├── License/Client.php license check client (generated from the shared Kavio Labs template)
└── Upgrade/Synchronizer.php self-healing in-place upgrades
design/backend/templates/addons/kv_paytr_payments/
├── views/payments/components/cc_processors/kv_paytr_{iframe,direct}.tpl → components/processor_settings.tpl
├── hooks/orders/payment_info.post.tpl PayTR panel (inside the order form: summary and buttons)
├── hooks/orders/tabs_extra.post.tpl refund / link / transfer dialogs (outside the order form)
├── hooks/companies/detailed_content.post.tpl vendor bank account (Add-ons tab)
├── settings/{setup_info,marketplace_info}.tpl
├── license/info.tpl License tab
└── views/kv_paytr_payments/manage.tpl transactions and transfers log
var/themes_repository/responsive/templates/addons/kv_paytr_payments/
├── views/kv_paytr_payments/{pay,card}.tpl
├── views/orders/components/payments/kv_paytr.tpl checkout hint (processor template)
├── components/installment_table.tpl
└── hooks/products/product_detail_bottom.post.tpl, hooks/index/{styles,scripts}.post.tpl
js/addons/kv_paytr_payments/func.js Direct API form: BIN lookup, plan selection, validation

No core files are modified.

Registered on install (fn_kv_paytr_payments_install(), idempotent) in cscart_payment_processors with addon = 'kv_paytr_payments', callback = 'N', type = 'P', the same pattern as the bundled pay360 add-on. Uninstall deletes them and disables the payment methods that used them.

  1. fn_start_payment() includes the processor script, which calls fn_kv_paytr_payments_start_payment().
  2. Gateway::startIframe() builds the request (amount in the payment currency × 100, base64 JSON basket, customer data), stores a pending row in kv_paytr_transactions and calls POST /odeme/api/get-token. The customer is redirected to kv_paytr_payments.pay, which embeds https://www.paytr.com/odeme/guvenli/<token>. Gateway::startDirect() signs the hidden fields of the card form instead (single payment); Gateway::signDirectPlan() re-signs them for the plan picked by the customer (installment count and card program are part of the token). Payment page access (kv_paytr_payments.pay|card and the installments AJAX call): the session that placed the order (the attempt is recorded in the session), the logged-in owner of the order, or the key parameter of the checkout redirect. The key is Signer::pageKey(), an HMAC of the order, the attempt and the session ID with the method’s credentials, so it still works when a concurrent request of the same visitor overwrote the session data written at checkout (guests had no other way back to the page), but is useless in any other session and for any other order. A valid key restores the session record and the guest’s order access (auth.order_ids). The card form fields are cached in the session and rebuilt from the order and the stored attempt (Gateway::rebuildDirectFields(), same amount and currency) when missing.
  3. PayTR posts the result to payment_notification.notify?payment=kv_paytr_iframe (payment_notification is a trusted controller: no CSRF check, no regional redirect). Gateway::handleNotification():
    • looks up the attempt by merchant_oid and loads the credentials of its payment method (vendor-owned methods work too);
    • verifies hash with hash_equals;
    • claims the attempt with UPDATE … SET finalized_at = NOW WHERE merchant_oid = ? AND finalized_at = 0. Only the first notification gets a row back; repeats are answered OK without side effects;
    • success: payment info + status change only from Incomplete, Awaiting confirmation or Failed; failure: status change only if no attempt was paid and no newer attempt is pending;
    • deletes the core “payment started” marker (order_data type S);
    • output buffering is active while the order is processed, so notices or e-mail rendering output can never corrupt the OK answer.
  4. The customer returns to payment_notification.return|fail. If the page loads inside the PayTR iframe (Sec-Fetch-Dest: iframe), a small page moves it to the top window first. Gateway::handleReturn() polls the attempt for a few seconds, then calls POST /odeme/durum-sorgu; a successful inquiry is applied with the same claim. Otherwise the order gets the pending status. Finally fn_order_placement_routines('route').

All are base64(HMAC-SHA256(fields . merchant_salt, merchant_key)); see Signer.php.

Call Concatenated fields
iFrame get-token merchant_id, user_ip, merchant_oid, email, payment_amount, user_basket, no_installment, max_installment, currency, test_mode
Direct API merchant_id, user_ip, merchant_oid, email, payment_amount, payment_type, installment_count, currency, test_mode, non_3d
Notification (iFrame/Direct) merchant_oid, salt, status, total_amount
Link notification callback_id, merchant_oid, salt, status, total_amount
Refund merchant_id, merchant_oid, return_amount
Status inquiry merchant_id, merchant_oid
Installment rates merchant_id, request_id
BIN lookup bin_number, merchant_id
Link create name, price, currency, max_installment, link_type, lang, then min_count (product) or email (collection)
Link delete / send id, merchant_id[, cell_phone or email]
Platform transfer merchant_id, merchant_oid, trans_id, submerchant_amount, total_amount, transfer_name, transfer_iban
Transfer result notification trans_ids (JSON, backslashes removed), salt
Type Hook Purpose
PHP prepare_checkout_payment_methods Hides the PayTR payment methods at checkout without a valid license
PHP before_dispatch (admin area and console only; static handler License\Client::onBeforeDispatch) Runs the due license check and shows license notices
PHP change_order_status_post Queue a marketplace transfer at the configured vendor order status (MV)
Schema block_manager/block_cache_properties (callable_handlers) License state in every block cache key (installment table)
PHP delete_order Delete the add-on rows of the order
PHP update_company, delete_company Save / delete the vendor bank account (MV)
Template (admin) orders:payment_info (post), orders:tabs_extra (post) PayTR panel and dialogs
Template (admin) companies:detailed_content (post) Vendor bank account fields
Template (storefront) products:product_detail_bottom (post) Installment table
Template (storefront) index:styles, index:scripts (post) LESS and JS

All hooks exist in CS-Cart 4.17.2 (Ultimate and Multi-Vendor), Multi-Vendor 4.18.3 and Multi-Vendor 4.21.2.

Callable as Smarty modifiers: fn_kv_paytr_payments_get_installment_plans($price), fn_kv_paytr_payments_get_max_installment($payment_id), fn_kv_paytr_payments_get_order_panel($order_id), fn_kv_paytr_payments_format_amount($amount, $currency).

To show the installment table elsewhere (for example in a custom product template):

{include file="addons/kv_paytr_payments/components/installment_table.tpl" kv_price=$product.price}

Tygh\Addons\KvPaytrPayments\License\Client verifies the license against the Kavio Labs license server.

  • fn_kv_paytr_payments_is_licensed() is the gate. When it returns false, fn_kv_paytr_payments_prepare_checkout_payment_methods() removes the PayTR iFrame and Direct API methods from the checkout list, so fn_kv_paytr_payments_get_installment_plans() returns no plans and the product page hook prints nothing. It reads the stored state only (one storage read per request) and never contacts the server.
  • Nothing that completes an already started payment depends on the license: the processor scripts, kv_paytr_payments.pay|card|installments, payment_notification.notify|return|fail, refunds, status inquiries, pay-by-link, platform transfers and the transfer cron work for existing orders whatever the license state.
  • The installment table is printed inside the product page’s main block, which CS-Cart may cache. The license state is part of every block cache key (block_cache_properties.post.php, callable_handlers), so the table appears or disappears at once.
  • Client::boot() (init.php) registers a before_dispatch handler for the admin area and the console only. On admin page views and console requests it runs the check when it is due (key changed, or 24 hours after the last check; 1 hour after a failure) and shows the admin notice once per session.
  • Admin license UI (generated): the License card at the top of the add-on page (license/card.tpl through the addons:tabs_content pre hook), the badge in Add-ons > Manage (license/badge.tpl, addons:manage post hook), the status bar (license/status_bar.tpl, first line inside the mainbox capture of views/kv_paytr_payments/manage.tpl; nothing for vendors) and css/addons/kv_paytr_payments/license.less (index:styles post hook). The off-effect sentence is the language variable kv_paytr_payments.license_off_effect.
  • Request: POST https://cs-cart.kaviolabs.com/index.php?dispatch=kv_licenses.check with JSON {"v":1, "license", "addon", "version", "hosts":[…], "platform":{"version", "edition"}, "nonce"}, sent through Tygh\Http (3 s connect, 5 s total; kept out of the CS-Cart request log). The answer is accepted only when its Ed25519 signature matches a public key embedded in the client and it echoes the nonce.
  • State: storage key kv_license_kv_paytr_payments (?:storage_data), or var/kv_storage/kv_paytr_payments/license.json while the full page cache is active, protected by an HMAC with the store’s crypt key.
  • Cron: php admin.php --dispatch=kv_paytr_payments_license.check.
  • Development stores only (http_host and https_host are development hosts): the constants KV_LICENSE_SERVER_URL, KV_LICENSE_TEST_HOST (act as if the development storefronts ran on that host) and KV_LICENSE_TEST_TIME_OFFSET (seconds). They are ignored on production hosts.
php /path/to/store/admin.php --dispatch=kv_paytr_payments.process_transfers

Sends due vendor transfers in automatic mode and prints PayTR transfers sent: N, failed: M.

Nothing in tests/ is packaged.

  • tests/paytr_signer_test.php: standalone checks of every signature and payload rule. Vectors marked “PayTR tool” were produced by PayTR’s public hash calculator (dev.paytr.com > Servis Test Araçları > Hash Hesaplama) from the samples on that page.

    php81 tests/paytr_signer_test.php
  • tests/mock-paytr/server.js: a local mock of the PayTR endpoints (get-token, hosted payment page, Direct API with a 3D Secure step, refund, status inquiry, installment rates, BIN lookup, Link API, platform transfer) that validates every token and sends signed notifications to the store. Node 18+, no dependencies.

    node tests/mock-paytr/server.js --port 8790 --merchant-id 123456 --key TESTKEYTESTKEY --salt TESTSALTTESTSALT

    Point a development store at it in config.local.php (or local_conf.php):

    define('KV_PAYTR_API_BASE_URL', 'http://localhost:8790');

    All PayTR URLs (API calls, iFrame page, resizer script, Direct API form action) then use the mock, and the admin panel shows a warning. Never define this constant on a live store.

  • tests/e2e/checkout-e2e.mjs (CS-Cart and Multi-Vendor) and tests/e2e/marketplace-e2e.mjs (Multi-Vendor) drive a local store over HTTP against the mock: checkout success, failure, late notification, status inquiry, forged and repeated notifications, refunds, Direct API with installments, pay-by-link, installment table, vendor IBAN, transfers, cron and vendor panel.

    node tests/e2e/checkout-e2e.mjs --store http://localhost:8171 --db cscart_cs417 --product 12
    node tests/e2e/checkout-e2e.mjs --store http://localhost:8172 --db cscart_mv417 --product 145
    node tests/e2e/marketplace-e2e.mjs --store http://localhost:8172 --db cscart_mv417

Tygh/Addons/KvPaytrPayments/License/Ed25519.php (checks the signature of the license server’s answers on servers whose PHP has no sodium extension) is derived from paragonie/sodium_compat, ISC License, Copyright (c) 2016-2023 Paragon Initiative Enterprises and Copyright (c) 2013-2019 Frank Denis. The full license notice is in the file header.