Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsCommunityBlog
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

Back to skills

Perfex Payment Gateway

ASecurity

Use whenever the user is creating, modifying, or debugging a Perfex CRM payment gateway module — a class extending `App_gateway` in `modules/<module>/libraries/<Id>_gateway.php`, calling `setId`, `setName`, `setSettings`, implementing `process_payment($data)`, or registering via `register_payment_gateway`. Also trigger when the user says "create a payment gateway for Perfex", "my gateway webhook gets CSRF blocked", "process_payment not firing", "Stripe/PayPal/Mollie integration for Perfex", "...

3 stars
0 votes
0 copies
0 views
Added 9/19/2026
ai-agentsphpdebuggingapidatabasesecurity

Works with

cliapi

Security Analysis

A100/100

Scanned 9/19/2026

Install to Claude Code

$npx -y skills add yasserstudio/perfex-crm-skills --skill perfex-payment-gateway --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Perfex Payment Gateway?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Perfex Payment Gateway
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/yasserstudio-perfex-payment-gateway/badge)](https://www.skillsdirectory.com/skills/yasserstudio-perfex-payment-gateway)

More formats (shields.io, HTML) on the badges page.

Download Zip
Files
SKILL.md
---
name: perfex-payment-gateway
description: Use whenever the user is creating, modifying, or debugging a Perfex CRM payment gateway module — a class extending `App_gateway` in `modules/<module>/libraries/<Id>_gateway.php`, calling `setId`, `setName`, `setSettings`, implementing `process_payment($data)`, or registering via `register_payment_gateway`. Also trigger when the user says "create a payment gateway for Perfex", "my gateway webhook gets CSRF blocked", "process_payment not firing", "Stripe/PayPal/Mollie integration for Perfex", "payment gateway settings not saving", "encrypted setting", or "how do I handle the payment callback in Perfex". Covers the App_gateway lifecycle, settings encryption, webhook CSRF exclusion, and the Stripe API (Basil) changes in Perfex 3.3.0.
license: MIT
metadata:
  author: yasserstudio
  version: "1.5.0"
---

# Perfex Payment Gateway Development

You are a Perfex CRM payment-gateway engineer. Your job is to build gateway modules that extend `App_gateway` correctly — with encrypted secrets, proper webhook CSRF exclusion, and defensive callback handling — so payments process reliably across Stripe API updates and concurrent invoice payments.

Perfex supports custom payment gateways as modules since **v2.3.4**. A gateway is a class in `modules/<module>/libraries/<Id>_gateway.php` that extends `App_gateway` and implements `process_payment($data)`.

## File structure

```
modules/my_gateway/
├── my_gateway.php                    # module entry (hooks, register_payment_gateway)
├── install.php                       # optional: module-owned tables (transaction log, webhook log)
├── libraries/
│   └── My_gateway_gateway.php        # class My_gateway_gateway extends App_gateway
├── controllers/
│   └── My_gateway_webhook.php        # webhook receiver (CSRF-excluded)
├── views/
│   └── payment_form.php              # optional inline payment form
└── language/
    └── english/
        └── my_gateway_lang.php
```

The library filename **must** end with `_gateway.php`. Class name must match filename (capitalized first letter).

## Gateway class skeleton

```php
<?php
defined('BASEPATH') or exit('No direct script access allowed');

class My_gateway_gateway extends App_gateway
{
    public function __construct()
    {
        parent::__construct();

        $this->setId('my_gateway');

        $this->setName('My Gateway');

        $this->setSettings([
            [
                'name'      => 'api_key',
                'encrypted' => true,
                'label'     => 'API Key',
                'type'      => 'input',
            ],
            [
                'name'      => 'api_secret',
                'encrypted' => true,
                'label'     => 'API Secret',
                'type'      => 'input',
            ],
            [
                'name'      => 'test_mode',
                'label'     => 'Test Mode',
                'type'      => 'yes_no',
                'default_value' => '1',
            ],
            [
                'name'          => 'currencies',
                'label'         => 'settings_paymentmethod_currencies',
                'default_value' => 'USD,EUR',
            ],
        ]);
    }

    public function process_payment($data)
    {
        // $data is the POST from the invoice "Pay" form plus what Payments_model adds
        // (see "The $data array" below). No $data['currency'] key exists.
        $invoice  = $data['invoice'];                 // tblinvoices row (object)
        $amount   = $data['amount'];                  // float, already includes gateway_fee
        $currency = $invoice->currency_name;          // ISO code, e.g. 'USD'

        // Build the charge via your gateway's API
        $api_key = $this->decryptSetting('api_key');

        // ... gateway-specific logic ...

        // Redirect to gateway checkout page
        redirect($checkout_url);
    }
}
```

## Registration (module entry file)

```php
// my_gateway.php
register_payment_gateway('my_gateway_gateway', 'my_gateway');
```

First param: class name (lowercase). Second param: module system name. After activation, the gateway appears in **Setup → Settings → Payment Gateways**.

## Settings system

| Type | Renders as | Stored as |
|---|---|---|
| `input` | Text input | Plain or encrypted string |
| `textarea` | Multi-line input | Plain or encrypted string |
| `yes_no` | Toggle switch | `'1'` or `'0'` |
| *(no type)* | Text input | Plain string |

### Encrypted settings

Set `'encrypted' => true` on any setting holding secrets (API keys, webhook signing keys). Perfex encrypts at rest using the application encryption key. Access via `$this->decryptSetting('name')` — never read directly from DB.

```php
// ✅ Correct — decrypts automatically
$secret = $this->decryptSetting('api_secret');

// ❌ Wrong — returns encrypted gibberish
$secret = $this->getSetting('api_secret');
```

### Reading non-encrypted settings

```php
$mode = $this->getSetting('test_mode');  // '1' or '0'
$currencies = explode(',', $this->getSetting('currencies'));
```

## Webhook handling

External gateways POST payment confirmations to your callback URL. Two requirements:

### 1. CSRF exclusion

Perfex's global CSRF protection blocks external POSTs. Ship the exclusion **inside the module** — `application/hooks/InitModules.php` picks it up automatically:

```php
// modules/my_gateway/config/csrf_exclude_uris.php
defined('BASEPATH') or exit('No direct script access allowed');

return [
    'my_gateway_webhook/handle',
];
```

Entries are regexes matched `^…$` against the URI (no leading slash) — use `[0-9a-z]+`, not `(:any)`. The `csrf_exclude_uris` filter (`perfex-security`) is the same mechanism by hand; never edit `application/config/config.php` — it is overwritten on update.

Also know that core switches CSRF **off entirely** for any request whose `REQUEST_URI` contains the substring `gateways/` (`config.php`). That's how the built-in gateway callbacks work; a controller you route under `gateways/…` needs no exclusion — and any other endpoint that happens to match the substring is unprotected too.

### 2. Webhook controller

Extend `App_Controller` — that's what core's own `controllers/gateways/*.php` do. `AdminController` redirects unauthenticated requests to the login page, and neither admin nor client base classes add anything a server-to-server POST needs.

```php
<?php
defined('BASEPATH') or exit('No direct script access allowed');

class My_gateway_webhook extends App_Controller
{
    public function handle()
    {
        $payload = file_get_contents('php://input');
        $sig     = $this->input->get_request_header('X-Signature');

        // The library is loaded by register_payment_gateway() on every request
        // while the module is active; load defensively anyway.
        if (!$this->load->is_loaded('my_gateway_gateway')) {
            $this->load->library('my_gateway/my_gateway_gateway');
        }
        $gateway = $this->my_gateway_gateway;

        $secret = $gateway->decryptSetting('webhook_secret');
        if (!$sig || !hash_equals(hash_hmac('sha256', $payload, $secret), $sig)) {
            log_activity('my_gateway: webhook signature mismatch');
            $this->output->set_status_header(401);
            return;
        }

        $event = json_decode($payload, true);
        if (($event['type'] ?? '') !== 'payment.completed') {
            $this->output->set_status_header(200);   // ack events you don't handle
            return;
        }

        $invoice_id = (int) ($event['metadata']['invoice_id'] ?? 0);
        $trans_id   = $event['transaction_id'] ?? '';

        $this->load->model(['invoices_model', 'payments_model']);
        $invoice = $this->invoices_model->get($invoice_id);

        if (!$invoice) {
            log_activity('my_gateway: webhook for unknown invoice ' . $invoice_id);
        } elseif ($this->payments_model->transaction_exists($trans_id, $invoice_id)) {
            log_activity('my_gateway: duplicate webhook for transaction ' . $trans_id);   // PSP retry — already recorded
        } else {
            $gateway->addPayment([
                'invoiceid'     => $invoice_id,
                'amount'        => $event['amount'] / 100,        // minor units → major; check your PSP
                'transactionid' => $trans_id,
                'paymentmethod' => $event['method'] ?? '',        // optional, free text
            ]);
        }

        $this->output->set_status_header(200);
    }
}
```

Use **`$gateway->addPayment()`**, not `payments_model->add()` directly: it sets `paymentmode` from `getId()`, reconciles the processing-fee attempt (see below), and is what every core gateway calls. Log with `log_activity()` (DB-backed, visible in Utilities → Activity Log) — `log_message()` is a no-op in production (see `perfex-core-apis`).

## The `$data` array in `process_payment`

`Payments_model::process_payment()` takes the raw POST from the invoice "Pay" form and adds keys before calling your gateway:

| Key | Type | Description |
|---|---|---|
| `invoice` | object | Full invoice row from `tblinvoices` (`->id`, `->hash`, `->currency_name`, `->total`, `->clientid`…) |
| `invoiceid` | int | Same as `invoice->id` |
| `amount` | float | Amount to charge — partial-payment aware, **and already includes `gateway_fee`** |
| `paymentmode` | string | Your gateway ID |
| `gateway_fee` | float | Result of `getFee($amount)` — `0.0` unless you enabled processing fees |
| `payment_attempt` | object\|null | `tblpayment_attempts` row for this click (`->id`, `->reference`, `->amount`, `->fee`) — 3.x |
| *(everything else)* | mixed | Whatever the form POSTed |

There is **no `currency` key** — read `$data['invoice']->currency_name` (ISO code) or `get_currency($data['invoice']->currency)` for the full row. Redirect URLs aren't passed either: build them with `site_url('invoice/' . $invoice->id . '/' . $invoice->hash)` for the customer-facing invoice.

**Core does not re-validate the gateway on POST.** `is_payment_mode_allowed_for_invoice()` (per-invoice allowed modes) and your `currencies` setting are only consulted when the pay form is *rendered*. The form lives on the public invoice-hash URL, so a crafted or stale POST can reach `process_payment()` with a gateway/currency combination the admin excluded. Re-check both at the top of `process_payment()`:

```php
if (!is_payment_mode_allowed_for_invoice($this->getId(), $data['invoice']->id)) {   // checks allowed modes AND the currencies setting
    set_alert('danger', _l('invoice_html_payment_modes_not_selected'));
    redirect(site_url('invoice/' . $data['invoice']->id . '/' . $data['invoice']->hash));
}
```

Same for the amount: re-derive it server-side if your PSP lets the customer edit it.

### Processing fees (3.x)

`App_gateway` supports a per-gateway fee, off by default. Declare `public bool $processingFees = true;` on your class — that adds the `fee_fixed` / `fee_percent` settings and makes `getFee()` return non-zero (overriding `getFixedFee()` / `getPercentageFee()` alone does nothing). Core then records a `tblpayment_attempts` row and bumps `$data['amount']`. To reconcile, carry the attempt's reference through your PSP round-trip and hand it back:

```php
// process_payment(): stash it in the checkout session / metadata
$reference = $data['payment_attempt']->reference ?? null;

// webhook / return handler
$gateway->addPayment([
    'invoiceid'                 => $invoice_id,
    'amount'                    => $charged,               // gross, including the fee
    'transactionid'             => $trans_id,
    'payment_attempt_reference' => $reference,             // addPayment() subtracts the fee and deletes the attempt
]);
```

Note `addPayment()` reads `$data['payment_attempt_reference']` without `isset` — always pass the key (null is fine) or PHP 8 warns. If you record payments any other way, fees are double-counted or lost.

## Stripe API (Basil) changes — Perfex 3.3.0+

Perfex 3.3.0 updated to Stripe API version "Basil". If your module wraps Stripe:

- **Webhooks must be recreated** after upgrading to 3.3.0 — event payload format changed
- Stripe now respects allowed payment methods from the Stripe Dashboard (no longer hardcoded in Perfex)
- The `after_invoice_added` hook now fires **before** email sending (changed in 3.2.0) — if your gateway listens to this hook to auto-charge, the invoice email may not have been sent yet

## Existing gateway reference

Study Perfex's built-in gateways for patterns:
- `application/libraries/gateways/` — Stripe, PayPal, 2Checkout, Mollie
- `application/controllers/gateways/` — webhook receivers for built-in gateways

## Common pitfalls

- **Filename must end with `_gateway.php`** — `My_gateway.php` alone won't be detected.
- **`setId()` is lowercase `[a-z0-9_]`** — underscores are fine (core ships `paypal_checkout`, `stripe_ideal`); no hyphens, no spaces. The ID becomes the `paymentmode` stored on every payment record and the `paymentmethod_<id>_*` option keys, so never change it after release.
- **Don't store card data** — let the external gateway handle PCI compliance. Your module only stores transaction IDs.
- **`process_payment` is called on every "Pay Now" click** — it must be idempotent or create a new checkout session each time. Don't create duplicate charges.
- **Test with multiple currencies** — the `currencies` setting is a comma-separated string that Perfex checks before showing the gateway as available for an invoice.
- **Webhook retries** — most gateways retry failed webhooks. Your handler must be idempotent: check `tblinvoicepaymentrecords` for the `transactionid` before inserting (as above), and verify the invoice still exists — a deleted invoice plus a late webhook is a real sequence.
- **Stale `paymentmode` on the public invoice URL** — the pay form is reachable via the invoice hash link without login. If your gateway is deactivated while a customer has the page open, the POST arrives with a `paymentmode` core can no longer resolve. Stock 3.4 dereferences the instance and fatals; guard your own controller paths the same way core should (`if (!$gateway || empty($gateway->instance)) return false;`).

## Related skills

- **`perfex-security`** — CSRF exclusion mechanics, webhook signature verification, `app_generate_hash()` for nonces.
- **`perfex-module-dev`** — module lifecycle, `register_payment_gateway()` lives in the module entry file.
- **`perfex-database`** — if you add a `tbl<module>_transactions` table for logging.

## Upstream docs

- Perfex payment gateway guide: https://help.perfexcrm.com/module-as-payment-gateway/
- Perfex module basics: https://help.perfexcrm.com/module-basics/
- Stripe API changelog: https://stripe.com/docs/changelog

---

*Verified against Perfex CRM 3.4.0 core source on 2026-09-15 with `scripts/verify-against-core.sh`. Version-specific notes in the text are from official changelogs.*

Attribution

yasserstudioyasserstudio
View sourceMore from yasserstudio →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

Caveman

Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra, wenyan-lite, wenyan-full, wenyan-ultra. Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens", "be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested.

1023331 votes

Hyperplan

Adversarial multi-agent planning skill. Self-orchestrates 5 hostile category members (unspecified-low, unspecified-high, deep, ultrabrain, artistry) via team-mode for ruthless cross-critique debate, distills only the defensible insights, then MANDATORILY hands the distilled insight bundle to the `plan` agent for executable plan formalization. Use when planning needs maximum rigor and surfacing of weak assumptions, blind spots, and over-engineering. Triggers: 'hyperplan', 'hpp', '/hyperplan', ...

686011 votes

Mcp Code Execution

Routes multi-tool workflows through MCP servers for large datasets and pipelines. Use when Bash tool overhead is limiting throughput on data-heavy tasks.

3331 votes

catchup

Recovers prior coding-agent session context by running `catchup <agent> --since-compact`, which extracts a clean summary of a previous Codex, Claude Code, Antigravity, OpenCode, or Pi Agent session. Use when the user says "catch up", "what did the last session do", "get me up to speed", "I switched agents", or asks to recover/summarize a previous session before continuing. Do NOT use for the current conversation, git history, or any non-agent log.

611 votes

math-skill

A comprehensive mathematical reasoning skill for AI assistants — handles arithmetic to research-level problems with rigorous step-by-step reasoning, systematic verification, and transparent uncertainty handling

381 votes
View all in ai-agents →