Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0f929c0
build: project infrastructure — Composer, autoload, QA tooling and CI
listopad Jul 14, 2026
60e4491
feat: 54-FZ receipts, full REST API coverage, typed exceptions and cU…
listopad Jul 15, 2026
8078c0f
test: baseline coverage — signature, form, API, handler, CashItem, re…
listopad Jul 15, 2026
133c26f
docs(examples): scenarios for the new API methods — refund, two-stage…
listopad Jul 16, 2026
b1a061d
docs: README and CHANGELOG for the 2.1.0 feature set
listopad Jul 17, 2026
f021baa
feat: dynamic webhook IP allowlist and PAYMENT_TYPE_* constants
listopad Jul 18, 2026
b1fd520
test: IP-allowlist and payment-type coverage (+ examples, docs)
listopad Jul 19, 2026
d37da43
refactor: unify comment style, translate to Russian, cleanup; review …
listopad Jul 20, 2026
7be4e98
docs: translate CHANGELOG to Russian, yookassa-sdk-php format
listopad Jul 21, 2026
2495274
test: extended coverage — floats, IP-allowlist, edge cases + IPv6-nor…
listopad Jul 21, 2026
0426b76
refactor(examples): overhaul — correctness, 54-FZ receipt, config/ord…
listopad Jul 22, 2026
69c4b59
feat(telemetry): VERSION const + sdk fingerprint token in form()
listopad Jul 23, 2026
13683f0
feat(telemetry): Layer A fingerprint headers on api(), extend httpGet
listopad Jul 23, 2026
5820192
feat(telemetry): enableTelemetry flag, derived endpoint, reporter, ki…
listopad Jul 23, 2026
9fc2a6a
test(telemetry): plumbing + wire-in coverage (disabled/enabled/kill-s…
listopad Jul 23, 2026
a623277
docs(telemetry): README disclosure + CHANGELOG entry
listopad Jul 23, 2026
6ca201f
chore(telemetry): satisfy phpmd — drop else in httpGet, retune field/…
listopad Jul 23, 2026
2c48a9e
refactor(types): native type declarations across the SDK; PHPStan 5→6
listopad Jul 23, 2026
6a01d5f
chore(phpstan): drop config comments
listopad Jul 23, 2026
d0f95a4
docs: translate all code comments to English
listopad Jul 23, 2026
71796a8
refactor(telemetry): drop Layer B (opt-in beacon) — keep passive fing…
listopad Jul 23, 2026
0bad922
refactor(telemetry): API_VERSION + richer X-Unitpay-Client fields
listopad Jul 23, 2026
787fa1e
docs(readme): fix webhook handler sample + sync telemetry section
listopad Jul 23, 2026
0a9fe17
refactor: address code-review notes
listopad Jul 24, 2026
a0b0d95
docs(changelog): translate to English
listopad Jul 24, 2026
fafa116
docs(examples): translate README to English
listopad Jul 24, 2026
a6f5078
fix: harden signing/allowlist edge cases and clear api() params on fa…
listopad Jul 24, 2026
b6823d2
docs: split README into landing page and docs/ topic pages
listopad Jul 24, 2026
21e06ba
ci: add concurrency, permissions, dependency cache and audit
listopad Jul 24, 2026
086fbd2
chore: add markdownlint config
listopad Jul 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 11 additions & 13 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
# https://git-scm.com/book/en/v2/Customizing-Git-Git-Attributes

# A list of files and folders those will be excluded from archives and the
# Composer package (for purposes of making it smaller).
/.coveralls.yml export-ignore
/.github export-ignore
/.gitattributes export-ignore
/.travis.yml export-ignore
/.vscode export-ignore
/examples export-ignore
/phpunit.xml export-ignore
/phpunit.no_autoload.xml export-ignore
/tests export-ignore
/.github export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/.markdownlint.json export-ignore
/.vscode export-ignore
/.php-cs-fixer.dist.php export-ignore
/examples export-ignore
/phpmd.xml export-ignore
/phpstan.neon export-ignore
/phpunit.xml export-ignore
/tests export-ignore
76 changes: 76 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: CI

on:
push:
branches: [ master ]
pull_request:
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
tests:
name: Tests (PHP ${{ matrix.php }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4']
steps:
- uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: json, curl
coverage: none

- name: Install dependencies
uses: ramsey/composer-install@v3
with:
dependency-versions: highest
composer-options: "--prefer-dist"

- name: Lint
run: composer lint

- name: Tests
run: composer test

quality:
name: Static analysis & code style
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: json, curl
coverage: none

- name: Install dependencies
uses: ramsey/composer-install@v3
with:
dependency-versions: highest
composer-options: "--prefer-dist"

- name: Code style (php-cs-fixer)
run: composer cs-check

- name: Static analysis (PHPStan)
run: composer stan

- name: Mess detection (PHPMD)
run: composer md

- name: Security audit (composer)
run: composer audit
continue-on-error: true
8 changes: 2 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
.idea
.idea/
.DS_Store
/vendor/
composer.lock
.php-cs-fixer.cache
clover.xml
.php_cs
.php_cs.cache
.phpstan.neon
.phpdoc/*
phpdoc.xml
6 changes: 6 additions & 0 deletions .markdownlint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"default": true,
"MD004": { "style": "asterisk" },
"MD013": false,
"MD060": false
}
17 changes: 17 additions & 0 deletions .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

$finder = (new PhpCsFixer\Finder())
->in([__DIR__ . '/tests', __DIR__ . '/examples'])
->append([__DIR__ . '/UnitPay.php']);

return (new PhpCsFixer\Config())
->setRiskyAllowed(false)
// Dev machine runs PHP 8.5 while the project targets PHP >=7.4; allow the
// newer runtime instead of exporting the deprecated PHP_CS_FIXER_IGNORE_ENV.
->setUnsupportedPhpVersionAllowed(true)
->setRules([
'@PSR12' => true,
'array_syntax' => ['syntax' => 'short'],
'no_unused_imports' => true,
])
->setFinder($finder);
108 changes: 105 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,107 @@
# Changelog

## 2.0.4 - 2021-03-17
* Updated getSignature method (2Garin)
* Changelog started
### v2.1.0

* Telemetry: passive anonymous version fingerprint (`User-Agent` and `X-Unitpay-Client` headers in `api()`, `sdk` parameter in the `form()` URL) — SDK self-identification with no extra network requests and no PII; there is no dedicated telemetry endpoint. `User-Agent: unitpay-php-sdk/<ver> api/<v>` and a JSON `X-Unitpay-Client` header with fields `sdk_version`, `api_version` (the Unitpay API version the SDK targets), `lang`, `lang_version`, `platform` (OS family only), `publisher`. Added constants `UnitPay::VERSION` and `UnitPay::API_VERSION`
* `CashItem`: 54-FZ dictionaries synced with the backend:
* Added VAT rates: vat5, vat7, vat22 and the calculated vat105, vat107, vat110, vat120, vat122
* Added payment objects: payment_2, deposit, expense, pension_insurance_ip, pension_insurance, medical_insurance_ip, medical_insurance, social_insurance, casino_payment, issuance_bank, commodity_without_mark, commodity_mark
* Marked as deprecated (kept for backward compatibility, removal in 3.0) the values rejected by the public API: excise, gambling_bet, gambling_prize, lottery_prize, composite
* `CashItem`: added optional backend fields — sum, currency, measure (`MEASURE_*` constants), nomenclatureCode, markCode, markQuantity, pre_text, post_text; `setCashItems()` serializes them only when they are set
* `api()`: added support for the methods refundPayment, confirmPayment, cancelPayment, listSubscriptions, getSubscription, closeSubscription, getMethodsAvailable, getCommissions, getCurrencyCourses, getPartner, offsetAdvance — each with validation of its required parameters (secretKey is supplied automatically)
* `api()`: added mass-payout methods — massPayment, massPaymentStatus, massPaymentAvailableAmount, massPaymentCommissions, getSbpBankList, getBinInfo (require the account login and secretKey)
* `api()`: added `PAYMENT_TYPE_*` constants for the current Unitpay payment methods (card, cardInvoice, sbp, sberpay, tinkoffpay, paypal, webmoney) — for convenience and typo protection only; `paymentType` is still passed without validation; in the README and the `initPaymentApi` example they replaced the deprecated qiwi/yandex/mc/alfaClick
* `api()`: an explicit secretKey in the call parameters overrides the key from the constructor — account-level and payout methods can be called with the account key instead of the project key
* `api()`: the required initPayment parameters were aligned with the backend — account, sum, projectId, paymentType (secretKey is validated separately); desc is no longer required
* `api()`: parameters are now sent flat (`method=X&account=…&secretKey=…`), as Unitpay documents and has accepted since 05.2026, instead of the deprecated `params[...]` nesting (still accepted by the backend, so this is not a breaking change); the inbound webhook handler is unaffected and keeps reading `params[...]`
* `api()`: parameters from the fluent setters (setCashItems, setCustomerEmail, setCustomerPhone, setBackUrl) now reach `api()` calls too, not just `form()`; explicit `api()` parameters take precedence over the accumulated ones
* `CashItem`: the constructor now rejects non-numeric count and price (previously only 0/negative were caught) and normalizes numeric strings to int/float
* `CashItem`: the constructor rejects a non-positive count and a negative price (behavior change)
* `CashItem`: preserves a fractional count (weight/volume goods) instead of truncating to int
* handler: the IP allowlist was trimmed to the officially published addresses (31.186.100.49, 51.250.20.9); 127.0.0.1 is not trusted by default (behind a reverse proxy on the same host it would nullify the IP check) — add it via `setAllowedIps()` for local debugging; `setAllowedIps()` itself was added to override the list
* handler: `isAllowedIp()` now matches not only exact IPs but also CIDR subnets (IPv4/IPv6) — `setAllowedIps(['77.75.153.0/25'])` works
* handler: added `refreshAllowedIps()` — pulls the current webhook IP list from the public feed `/ips/ips_webhooks.json` and replaces the built-in one (a decommissioned IP drops off automatically); fail-safe: on any transport/parse/validation error it keeps the built-in list and does not throw, so it can be called before `checkHandlerRequest()`
* handler: added `addAllowedIps()` — adds the merchant's own IPs/CIDRs (e.g. your own proxy/relay) on top of the Unitpay list; unlike `setAllowedIps()`, they are preserved across `refreshAllowedIps()`/`setAllowedIps()`
* handler: added `getAllowedIps()` — returns the effective list (Unitpay + merchant IPs); cache it after `refreshAllowedIps()` and feed it back via `setAllowedIps()` to avoid hitting the network on every webhook
* `UnitpayIpAllowlist::isValidEntry()` validates every loaded IP/CIDR entry, so malformed JSON can never empty the list
* handler: `checkHandlerRequest()` now accepts the `preauth` webhook (a hold notification in two-stage payments, when funds are blocked but not yet captured) — it used to be rejected as an unsupported method, which prevented two-stage/subscription handlers from verifying it
* Added typed exceptions (UnitpaySignatureException, UnitpayIpException, UnitpayTransportException, UnitpayUnsupportedMethodException) with the UnitpayExceptionInterface; each still extends its former SPL class, so existing catch blocks keep working
* `api()`: optional cURL transport with connect/read timeouts and no dependency on allow_url_fopen (falls back to file_get_contents); ext-curl added to composer "suggest"
* `api()`: the cURL transport does not call curl_close() on PHP 8.0+ (there it is a deprecated no-op that raises E_DEPRECATED on PHP 8.5 on every API call); on PHP <8.0 the handle (resource) is closed explicitly via a PHP_VERSION_ID check
* examples: a full set of scenarios — payment form (`paymentForm`), API (`initPaymentApi`), 54-FZ receipt via `CashItem` (`receipt`), webhook (`webhook`), `getPayment` (`paymentInfo`), refund (`refund`), two-stage (`twoStagePayment`), subscriptions (`subscriptions`), SBP payouts (`payout`), account reference calls (`accountInfo`), advance-offset receipt (`offsetAdvance`); added an `examples/README.md` index
* examples: connection settings and order data separated — `config.php` (domain, project/account keys, login) and `order.php` (order data); secrets are read from the environment (`UNITPAY_SECRET_KEY`, `UNITPAY_LOGIN`, `UNITPAY_ACCOUNT_SECRET_KEY`) instead of being hardcoded
* examples: robustness — `require` via `__DIR__` (independent of the working directory), `exit` after `header('Location:')`, `api()`/`form()` calls wrapped in try/catch (`UnitpayExceptionInterface`); `webhook` returns `application/json` and no longer leaves an empty response for an unknown method (`default` in the switch)
* examples: removed the unreachable "refund" handler branch; added a "preauth" branch (hold notification — acknowledge receipt but do not deliver goods, that waits for "pay"); handling of the "response" reply type from initPayment (e.g. recurring/subscription charges without a redirect)
* Added a PHPUnit test suite and injectable seams (getIp / API transport) for testability
* Added QA tooling: phpstan, php-cs-fixer, phpmd and parallel-lint
* Native type declarations: parameters, return types and typed properties across all three classes (`CashItem`, `UnitpayIpAllowlist`, `UnitPay`) within the PHP 7.4 limits (no union types, `mixed`, or `declare(strict_types)`) — the public API and behavior are unchanged. Money and quantity parameters (`form()` `$sum`, `CashItem` `$count`/`$price`, and the `httpGet()` `string|false` return) are deliberately left untyped to preserve the previous "soft" scalar ergonomics; their types are still documented in PHPDoc. PHPStan raised from level 5 to level 6
* Minimum PHP version raised to 7.4
* Hardening from code review:
* `api()`: folds in only the fluent-setter parameters (cashItems/customerEmail/customerPhone/backUrl), not the whole set — a reused instance no longer leaks the key `form()` parameters or the stale signature into an unrelated `api()` call
* `api()`: the fluent-setter parameters are now cleared once the request has been attempted — on a transport failure too, not only on success — so a stale receipt/customer can no longer leak from a failed call into an unrelated later call on a reused instance (symmetric with `form()`); a retry after a failure must re-apply the setters
* `setCashItems()`: throws on a json_encode failure (e.g. a product name that is not UTF-8) instead of silently attaching an empty 54-FZ receipt
* `CashItem`: preserves a fractional count (weight/volume goods) instead of truncating to int
* `form()`: throws on an empty secret instead of returning an unsigned URL — like `api()`/`checkHandlerRequest()`
* `getSignature()`/`api()`/`form()`: format float parameters locale-independently, so a comma-separator locale on PHP <8.0 cannot corrupt the signature or the amount
* `api()`: an empty explicit secretKey (e.g. a getenv() that did not resolve) falls back to the instance key instead of throwing
* `httpGet()`: suppresses the file_get_contents warning so a URL containing the secret does not leak into the error log
* `checkHandlerRequest()`: exposes the verified method/params via `getHandlerMethod()`/`getHandlerParams()`, so the consumer does not re-read $_GET
* `getSignature()`: rejects a null/empty secret up front — as a public method it must not silently hash with an empty secret (the appended null would coerce to '' and drop out, yielding a plausible but secret-less signature); the normal `form()`/`checkHandlerRequest()` paths already guarded this, this is defense-in-depth for direct calls
* docs: `setAllowedIps([])` is documented as fail-closed (an empty allowlist rejects every webhook rather than being a no-op); the `$transport` seam docblock now documents the `$headers` argument the transport actually receives
* every SDK exception implements UnitpayExceptionInterface (UnitpayValidationException was added for the missing-parameter/secret/method cases)

### v2.0.6 — 2025-05-14

* Added a new supported Unitpay IP address
* Updated README.md

### v2.0.5 — 2022-02-04

* Updated the list of Unitpay IP addresses
* Updated documentation links
* Improved code quality and structure

### v2.0.4 — 2021-03-17

* Updated the `getSignature` method (2Garin)

### v2.0.3 — 2021-02-20

* Filtering of signature input parameters (removing the sign/signature fields before signing)

### v2.0.2 — 2020-08-31

* Added the nds, type and paymentMethod parameters to `CashItem`

### v2.0.1 — 2020-03-03

* Added domain selection in the examples

### v2.0.0 — 2020-03-03

* Added domain selection (configurable API domain)
* Updated the documentation URL

### v1.1.2 — 2018-06-15

* Fixed the array_merge exception ("Argument #1 is not an array") when no receipt is set

### v1.1.1 — 2018-02-08

* Added a LICENSE file
* Fixed the composer file

### v1.1.0 — 2017-08-01

* Added customerEmail, customerPhone and cashItems to the payment form

### v1.0.0 — 2017-04-10

* First public release of the Unitpay PHP SDK
* Switched to SHA-256 signatures for all methods (MD5 support removed)
* Added the getPayment API method and the orderInfo.php example
* secretKey became a required parameter for API calls
* billingCode renamed to paymentType
* statusUrl deprecated in favor of receiptUrl
* Added support for the partner handler method "error"
* Added an overridable `getIp()` method
2 changes: 1 addition & 1 deletion LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License

Copyright (c) 2013-2021 Unitpay (https://unitpay.ru)
Copyright (c) 2013-2026 Unitpay (https://unitpay.ru)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
Loading
Loading