Install Payme into OJS.

Install Payme into OJS.

custom payment plugin. Paycom + OJS

Steps:

  1. Paycom repository-ni o'rnatib, fayllarga kerakli o'zgartirishlarni kiritish.
  2. APIRouter va OJSPaymentManager fayllariga payme ishlashi uchun kod qo'shish.

  3. api/v1 da payme papkasini yangi ochish va index.php faylini joylash.

  4. Paycom repo fayllarini ishlatish uchun sozlash.

  5. Payme plugin yozish.

  6. Ma'lumotlar bazasiga yangi orders va transactions jadvallarini qo'shish.

  7. Orders.php fayliga qilingan qabul qilingan va(yoki) rad etilgan to'lovlarni completed_payments jadvaliga saqlash uchun kod yozish.

  8. localhostda maqola yuklab, to'lov jarayonlarigacha testlash.

  9. ngrok orqali localhostdagi api ni test.paycom.uz da tekshirish.

  10. Done

1-qadam.

Githubdan Paycom-integration-php-template repositoryni yuklab oling va uni ojs turgan papkaning plugin/paymethod/payme papkasiga yuklangan repository fayllarini joylang. Quyidagidek bo'ladi:

config.inc.php faylga ushbularni ham qo'shib qo'ying.

2-qadam.

faylning boshidagi use larga pastdagini qo'shing:

use APP\plugins\paymethod\payme\paycom\PaycomApplication;
use PKP\config\Config;

shu fayldagi route funksiyasini quyidagiga o'zgartiring:

public function route($request)
    {
        // Ensure slim library is available
        require_once('lib/pkp/lib/vendor/autoload.php');

        $sourceFile = sprintf('api/%s/%s/index.php', $this->getVersion(), $this->getEntity());

        if (!file_exists($sourceFile)) {
            http_response_code('404');
            header('Content-Type: application/json');
            echo json_encode([
                'error' => 'api.404.endpointNotFound',
                'errorMessage' => __('api.404.endpointNotFound'),
            ]);
            exit;
        }

        if (!SessionManager::isDisabled()) {
            // Initialize session
            SessionManager::getManager();
        }

        // if sourceFile is equal to api/v1/payme/index.php and the request method is POST then we are handling the request

        if ($sourceFile == 'api/v1/payme/index.php') {
            try {
                $paycomConfig = [
                    'merchant_id' => Config::getVar('paycom', 'merchant_id'),
                    'login'       => Config::getVar('paycom', 'login'),
                    'keyFile'     => Config::getVar('paycom', 'keyFile'),
                    'db'          => [
                        'host'     =>  Config::getVar('database', 'host'),
                        'database' => Config::getVar('database', 'name'),
                        'username' => Config::getVar('database', 'username'),
                        'password' => Config::getVar('database', 'password'),
                    ],
                ];

                $application = new PaycomApplication($paycomConfig);
                // error_log()
                $application->run();
            } catch (Exception $e) {
                echo json_encode([
                    'error' => [
                        'code'    => -31003,
                        'message' => 'Internal server error',
                        // return $e->getMessage() as error message
                        'errorMessage' => $e->getMessage(),
                    ],
                ]);
            }
        }
        else {
            $handler = require('./' . $sourceFile);
            $this->setHandler($handler);
            $handler->getApp()->run();
        }
    }

OJSPaymentManager.php file.

shu fayldagi createQueuedPayment ichiga switch-case dan keyin quyidagi kodlarni qo'shib qo'ying.

try {
            $order = new Order($this->request->id);
            $order->amount = $amount;
            $order->state = 1;
            $order->user_id = $userId;
            $order->product_ids = $assocId;
            $order->save();
        } catch (\Throwable $th) {
            //throw $th;
            error_log($th->getMessage());
            throw new \Exception("Error creating payment order. Please try again later.");
        }

3-qadam.

rasmdagi index.php faylini oching va shuni joylang:

<?php
use APP\plugins\paymethod\payme\paycom\PaycomApplication;
use PKP\config\Config;

$paycomConfig = [
    // Get it in merchant's cabinet in cashbox settings
    'merchant_id' => Config::getVar('paycom', 'merchant_id'),
    'login'       => Config::getVar('paycom', 'login'),
    // File with cashbox key (key can be found in cashbox settings)
    'keyFile'     => Config::getVar('paycom', 'keyFile'),
    // Your database settings
    'db'          => [
        'host'     =>  Config::getVar('database', 'host'),
        'database' => Config::getVar('database', 'name'),
        'username' => Config::getVar('database', 'username'),
        'password' => Config::getVar('database', 'password'),
    ],
];

try {
    error_log("Index php chaqirildi");
    $application = new PaycomApplication($paycomConfig);
    $application->run();
} catch (Exception $e) {
    echo json_encode([
        'error' => [
            'code'    => -31003,
            'message' => 'Internal server error',
        ],
    ]);
}

4-qadam.

paycom papka ichidagi barcha fayllarga shuni qo'shib chiqing. oldingi namespace ni o'chiring.

Yuqoridagi rasmda Database.php faylini o'zgartirish kerak bo'lgan joyi keltirilgan. Paycom papkadagi Application.php ni PaycomApplication.php ga o'zgartiring va CreateTransactiondagi responseda 'create_time' ni Format::datetime2timestamp($transaction->create_time) ga almashtiring:

5-qadam

Payme plugin fayl strukturasi:

payme_request_payment.tpl fayli:

{**
 * plugins/paymethod/mpesa/templates/mpesa_request_payment.tpl
 *
 * Copyright (c) 2024 HyperLink DSL
 * Copyright (c) 2024 Otuoma Sanya
 * Distributed under the GNU GPL v3.
 *
 * Mpesa payment plugin
 *}
{include file="frontend/components/header.tpl" pageTitle="plugins.paymethod.payme"}

<div class="page page_payment_form">
    <div class="container">
        <div class="column column-2">
            <h1 class="page_title">
                {translate key="plugins.paymethod.payme"}
            </h1>

            <form class="pkp_form" id="paymeRequestPayment" method="GET" action="{$generated_pay_link}">
                <table class="cmp_table">
                    <tr>
                        <th>{translate key="plugins.paymethod.payme.purchase.title"}</th>
                        <td>{$itemName|escape}</td>
                    </tr>
                    {if $itemAmount}
                        <tr>
                            <th>{translate key="plugins.paymethod.payme.purchase.fee"}</th>
                            <td>{$itemAmount|string_format:"%.2f"}{if $itemCurrencyCode} ({$itemCurrencyCode|escape}){/if}</td>
                        </tr>
                    {/if}

                </table>
                <p>
                    {translate key="plugins.paymethod.payme.purchase.description"}
                </p>
                <p>
                    {if $lang}
                        {if $lang === 'uz'}
                            <button type="submit"> 
                                <img src="{$paymeLogoUrl_uz}" alt="payme logo">
                            </button>
                        {else}
                            <button type="submit"> 
                                <img src="{$paymeLogoUrl_ru}" alt="payme logo">
                            </button>
                        {/if}
                    {else}
                        <button type="submit"> 
                            <img src="{$paymeLogoUrl_ru}" alt="payme logo">
                        </button>
                    {/if}                
                </p>
            </form>
        </div>
    </div>
</div>


{include file="frontend/components/footer.tpl"}

.../payme/locale/en/locale.po fayli esa:

msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-01-07T06:56:47-07:00\n"
"PO-Revision-Date: 2024-01-07T06:56:47-07:00\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"

msgid "plugins.paymethod.payme.displayName"
msgstr "Payme Fee Payment"

msgid "plugins.paymethod.payme.description"
msgstr "Payme Integration for OJS"

msgid "plugins.paymethod.payme"
msgstr "Payme Fee Payment"

msgid "plugins.paymethod.payme.settings"
msgstr "Payme Payment Instructions"

msgid "plugins.paymethod.payme.settings.paymeTestMode"
msgstr "Test Mode"

msgid "plugins.paymethod.payme.settings.accountName"
msgstr "Payme Account Name"

msgid "plugins.paymethod.payme.settings.login"
msgstr "Payme Merchant ID"

msgid "plugins.paymethod.payme.settings.key"
msgstr "Payme Secret Key"

msgid "plugins.paymethod.payme.sendNotificationOfPayment"
msgstr "Send notification of payment"

msgid "plugins.paymethod.payme.paymentNotification"
msgstr "Payment Notification"

msgid "plugins.paymethod.payme.notificationSent"
msgstr "Payment notification sent"

msgid "plugins.paymethod.payme.purchase.title"
msgstr "Title"

msgid "plugins.paymethod.payme.purchase.fee"
msgstr "Fee"

msgid "plugins.paymethod.payme.purchase.description"
msgstr "Payment for article processing charges"

msgid "plugins.paymethod.payme.purchase.phoneNumber"
msgstr "Payme Number"

msgid "plugins.paymethod.payme.paymePaymentNotify.name"
msgstr "Payme Payment Notify"

msgid "plugins.paymethod.payme.error"
msgstr "Payme Payment Error Occurred "

msgid "plugins.paymethod.payme.stkRequestedHeader"
msgstr "Payme Payment Requested"

msgid "plugins.paymethod.payme.transactionStatusHeader"
msgstr "Payme Transaction Status"

msgid "plugins.paymethod.payme.transactionSucceeded"
msgstr "Payme Transaction Succeeded"

msgid "plugins.paymethod.payme.transactionFailed"
msgstr "Payme transaction failed with the following message: "

.../payme/PaymePaymentForm.php :

<?php

/**
 * @file plugins/paymethod/payme/templates/PaymePaymentForm.php
 *
 * Copyright (c) 2024 HyperLink DSL
 * Copyright (c) 2024 Otuoma Sanya
 * Distributed under the GNU GPL v3.
 * @class PaymePaymentForm
 * @brief Payme payment plugin
 */

namespace APP\plugins\paymethod\payme;

use APP\core\Application;
use PKP\form\Form;
use PKP\payment\QueuedPayment;
use APP\template\TemplateManager;

class PaymePaymentForm extends Form {
    public $_paymePaymentPlugin;

    /** @var QueuedPayment */
    public QueuedPayment $_queuedPayment;

    public function __construct($paymePaymentPlugin, $queuedPayment) {
        $this->_paymePaymentPlugin = $paymePaymentPlugin;
        $this->_queuedPayment = $queuedPayment;
        parent::__construct(null);
    }

    public function display($request = null, $template = null){

        $journal = $request->getJournal();
        $paymentManager = Application::getPaymentManager($journal);
        $queuedPaymentId = $this->_queuedPayment->getId();
        $pluginName = $this->_paymePaymentPlugin->getName();

        $templateMgr = TemplateManager::getManager($request);

        $templateMgr->display($this->getTemplateResource('example.tpl'));

    }
}

.../payme/PaymePlugin.php:

<?php
namespace APP\plugins\paymethod\payme;

use APP\core\Application;
use APP\template\TemplateManager;
use PKP\db\DAORegistry;
use PKP\form\Form;
use PKP\plugins\Hook;
use PKP\plugins\PaymethodPlugin;
use APP\plugins\paymethod\payme\Utilities;
use PKP\config\Config;


class PaymePlugin extends PaymethodPlugin {

    public function getPaymentForm($context, $queuedPayment): Form {

        $paymentForm = new Form($this->getTemplateResource('payme_request_payment.tpl'));

        $paymentManager = Application::getPaymentManager($context);

        $paycom_url = Config::getVar('paycom', 'url');
        $merchant_id = Config::getVar('paycom', 'merchant_id');
        $payment_detail = Config::getVar('paycom', 'payment_detail');
        $amount = $queuedPayment->getAmount();
        $callBackUrl = $this->getRequest()->url(null, 'api', 'v1', 'payme');
        $lang = $context->getPrimaryLocale();

        // Generate pay link look like this: https://checkout.paycom.uz/base64(m=587f72c72cac0d162c722ae2;ac.order_id=197;a=500)
        $base64_encoded_params = base64_encode("m={$merchant_id};ac.order_id={$queuedPayment->getId()};a={$amount};l={$lang};c={$callBackUrl}");
        $generated_pay_link = "{$paycom_url}/{$base64_encoded_params}";


        $paymentForm->setData([
            'itemName' => $paymentManager->getPaymentName($queuedPayment),
            'itemAmount' => $queuedPayment->getAmount() > 0 ? $queuedPayment->getAmount() : null,
            'itemCurrencyCode' => $queuedPayment->getAmount() > 0 ? $queuedPayment->getCurrencyCode() : null,
            'queuedPaymentId' => $queuedPayment->getId(),
            'pluginName' => $this->getName(),
            'generated_pay_link' => $generated_pay_link,
            'lang' => $lang
            // get app locale
        ]);
        return $paymentForm;
    }

    public function handle($args, $request): void {
        $journal = $request->getJournal();
        $queuedPaymentDao = DAORegistry::getDAO('QueuedPaymentDAO');
        $paymentManager = Application::getPaymentManager($journal);
        $queuedPaymentId = $args[1];

        $darajaCallback = $this->getRequest()->url(null, 'payment', 'plugin', [$this->getName(), 'daraja-callback', $queuedPaymentId], []);
        $action = $args[0];

        $queuedPayment = $queuedPaymentDao->getById($queuedPaymentId);
        if (!$queuedPayment) {
            throw new \Exception("Invalid queued payment ID {$queuedPaymentId}!");
        }
    }

    public function register($category, $path, $mainContextId = NULL): bool{

        $success = parent::register($category, $path, $mainContextId);

        if ($success && $this->getEnabled()) {
            $this->addLocaleData();
            Hook::add('Form::config::before', [$this, 'addSettings']);

            $request = Application::get()->getRequest();

            $templateMgr = TemplateManager::getManager($request);
            // $paymeLogoUrl_en = $request->getBaseUrl() . '/' . $this->getPluginPath() . "/images/payme-logo-en.png";
            $paymeLogoUrl_uz = $request->getBaseUrl() . '/' . $this->getPluginPath() . "/images/payme-logo-uz.png";
            $paymeLogoUrl_ru = $request->getBaseUrl() . '/' . $this->getPluginPath() . "/images/payme-logo-ru.png";

            // $templateMgr->assign('paymeLogoUrl_en', $paymeLogoUrl_en);
            $templateMgr->assign('paymeLogoUrl_ru', $paymeLogoUrl_ru);
            $templateMgr->assign('paymeLogoUrl_uz', $paymeLogoUrl_uz);
            $styleUrl = $request->getBaseUrl() . '/' . $this->getPluginPath() . '/css/style.css';
            $templateMgr->addStyleSheet('paymeStyles', $styleUrl);
        }

        return $success;
    }

    public function saveSettings(string $hookname, array $args): void {
        $slimRequest = $args[0];
        $request = $args[1];
        $updatedSettings = $args[3];

        $allParams = $slimRequest->getParsedBody();
        $saveParams = [];
        foreach ($allParams as $param => $val) {
            switch ($param) {
                case 'paymeTestMode':
                    $saveParams[$param] = $val === 'true';
                    break;
                case 'login':
                case 'key':
                    $saveParams[$param] = (string) $val;
                    break;
            }
        }
        $contextId = $request->getContext()->getId();
        foreach ($saveParams as $param => $val) {
            $this->updateSetting($contextId, $param, $val);
            $updatedSettings->put($param, $val);
        }
    }

    public function addSettings($hookName, $form): void {
        // TODO: Implement saveSettings() method.
        import('lib.pkp.classes.components.forms.context.PKPPaymentSettingsForm'); // Load constant
        if ($form->id !== FORM_PAYMENT_SETTINGS) {
            return;
        }

        $context = Application::get()->getRequest()->getContext();
        if (!$context) {
            return;
        }

        $form->addGroup([
            'id' => 'paymepayment',
            'label' => 'Payme Fee Payment',
            'showWhen' => 'paymentsEnabled',
        ])
            ->addField(new \PKP\components\forms\FieldOptions('paymeTestMode', [
                'label' => 'Test mode',
                'options' => [
                    ['value' => true, 'label' => __('common.enable')]
                ],
                'value' => (bool) $this->getSetting($context->getId(), 'paymeTestMode'),
                'groupId' => 'paymepayment',
            ]))
            ->addField(new \PKP\components\forms\FieldText('login', [
                'label' => 'Merchant ID',
                'value' => $this->getSetting($context->getId(), 'login'),
                'groupId' => 'paymepayment',
            ]))
            ->addField(new \PKP\components\forms\FieldText('key', [
                'label' => 'Secret Key',
                'value' => $this->getSetting($context->getId(), 'key'),
                'groupId' => 'paymepayment',
            ]));

    }

    public function getDisplayName(): string{
        return 'Payme Payment';
    }
    public function getName(): string{
        return 'PaymePayment';
    }

    public function getDescription(): string {
        return 'Allows Payme integration with OJS >= 3.4';
    }
    public function isConfigured($context): bool {
        if (!$context) {
            return false;
        }
        if ($this->getSetting($context->getId(), 'login') == '') {
            return false;
        }
        return true;
    }

    public function isTestMode($context): bool {

        if (!$context) return false;

        if ($this->getSetting($context->getId(), 'paymeTestMode') == '1') {
            return true;
        }
        return false;
    }

}

.../payme/version.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE version SYSTEM "../../../lib/pkp/dtd/pluginVersion.dtd">
<version>
    <application>payme</application>
    <type>plugins.paymethod</type>
    <release>1.0.0.0</release>
    <date>2024-01-09</date>
</version>

static fayllar:

6-qadam.

DBga jadvallar qo'shish:

CREATE TABLE orders
  (
      id          INT AUTO_INCREMENT PRIMARY KEY,
      product_ids VARCHAR(255)   NOT NULL,
      amount      DECIMAL(18, 2) NOT NULL,
      state       TINYINT(1)     NOT NULL,
      user_id     INT            NOT NULL,
  ) ENGINE = InnoDB;

CREATE TABLE `transactions` (
    `id` INT(11) NOT NULL AUTO_INCREMENT,
    `paycom_transaction_id` VARCHAR(25) NOT NULL COLLATE 'utf8_unicode_ci',
    `paycom_time` VARCHAR(13) NOT NULL COLLATE 'utf8_unicode_ci',
    `paycom_time_datetime` DATETIME NOT NULL,
    `create_time` DATETIME NOT NULL,
    `perform_time` DATETIME NULL DEFAULT NULL,
    `cancel_time` DATETIME NULL DEFAULT NULL,
    `amount` INT(11) NOT NULL,
    `state` TINYINT(2) NOT NULL,
    `reason` TINYINT(2) NULL DEFAULT NULL,
    `receivers` VARCHAR(500) NULL DEFAULT NULL COMMENT 'JSON array of receivers' COLLATE 'utf8_unicode_ci',
    `order_id` INT(11) NOT NULL,

    PRIMARY KEY (`id`)
  )
    COLLATE='utf8_unicode_ci'
    ENGINE=InnoDB
    AUTO_INCREMENT=1;

7-qadam.

Order.php faylidagi changeState funksiyasini esa quyidagiga almashtiring:

public function changeState($state)
    {
        // todo: Implement changing order state (reserve order after create transaction or free order after cancel)

        // Example implementation
        $this->state = 1 * $state;
        $this->save();

        try {
            if ($state == self::STATE_PAY_ACCEPTED) {
                $this->create_completed_payment();
                error_log("Completed payment created. Order ID " . $this->id);
            } else if ($state == self::STATE_CANCELLED) {
                $this->delete_completed_payment();
                error_log("Completed payment deleted. Order ID " . $this->id);
            }

        } catch (\Throwable $th) {

            error_log("Order::changeState: ". $th->getMessage());
            error_log("Completed payment creation failed. Order ID " . $this->id);
        }


        // try to create completed payment
    }

    public function create_completed_payment(){
        $sql = "insert into completed_payments set payment_type = :pPayment_type, \
            amount = :pAmount, assoc_id = :pAssoc_id, user_id = :pUserId, \
            context_id = :pContext_id, timestamp = now()";
        $sth        = $db->prepare($sql);
        $is_success = $sth->execute([
            ':pPayment_type' => 7,
            ':pAmount'  => $this->amount,
            ':pAssoc_id'   => $this->product_ids[0] ?? null,
            ':pUserId'  => $this->user_id,
            ':pContext_id' => 1,
        ]);
    }

    public function delete_completed_payment(){
        $sql = "delete from completed_payments where assoc_id = :pAssoc_id";
        $sth        = $db->prepare($sql);
        $is_success = $sth->execute([
            ':pAssoc_id'   => $this->product_ids[0] ?? null,
        ]);
    }

8-qadam. Serverni ishga tushiring va testlashga tayyorlang.

Payme plugin callback_url => https://example.com/index.php/{journal_short_name}/api/v1/payme

9-qadam.

  1. Tugadi. Omad!