Skip to content

Modernize SDK: full API, 54-FZ receipts, webhook IPs, and QA - #25

Merged
listopad merged 30 commits into
masterfrom
feature/update-sdk
Jul 24, 2026
Merged

Modernize SDK: full API, 54-FZ receipts, webhook IPs, and QA#25
listopad merged 30 commits into
masterfrom
feature/update-sdk

Conversation

@listopad

@listopadlistopad commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Brings the single-file SDK up to the current Unitpay backend: 54-FZ receipts,
the full REST API surface (incl. mass payouts), a dynamic webhook IP allowlist,
passive version telemetry, native type declarations, and a first PHPUnit suite +
QA tooling. No runtime dependencies added; still php >=7.4, ext-json, cURL
optional.

47 files changed (+4095 / −578).

Features

  • 54-FZ receiptsCashItem dictionaries synced with the backend (new VAT
    rates incl. calculated ones; new payment objects; deprecated the ones the
    public API rejects) and optional fields (sum, currency, measure,
    nomenclatureCode, markCode, markQuantity, pre_text, post_text),
    serialized only when set.
  • Full REST API coverage in api() — payments, refunds, confirm/cancel,
    subscriptions, account/reference calls, and mass payouts, each validating its
    required params; an explicit secretKey overrides the instance key for
    account-level methods.
  • PAYMENT_TYPE_* constants (card, cardInvoice, sbp, sberpay, tinkoffpay,
    paypal, webmoney) — convenience/typo-guard only, still passed unvalidated.
  • Dynamic webhook IP allowlistsetAllowedIps() / addAllowedIps() /
    getAllowedIps() and refreshAllowedIps() (pulls the published
    /ips/ips_webhooks.json feed, TLS-verified, fail-safe); CIDR matching for
    IPv4/IPv6 via UnitpayIpAllowlist.
  • Passive telemetryUser-Agent + X-Unitpay-Client fingerprint on
    api() and an sdk token in the form() URL; no extra requests, no PII.
    Adds UnitPay::VERSION / UnitPay::API_VERSION.

Security & hardening

  • getSignature(): PHP_INT_MAX guard retained; rejects a null/empty secret up
    front (a public call must not silently hash without the secret).
  • checkHandlerRequest(): constant-time hash_equals() + is_string() guard;
    rejects an empty secret; accepts the preauth webhook.
  • api(): fluent-setter params are cleared once the request has been attempted
    (success and transport failure), symmetric with form(), so a stale
    receipt/customer can't leak into an unrelated later call.
  • IP allowlist checked against REMOTE_ADDR (not the spoofable
    X-Forwarded-For); setAllowedIps([]) documented as fail-closed.

Reliability & DX

  • Typed exceptionsUnitpaySignatureException, UnitpayIpException,
    UnitpayTransportException, UnitpayUnsupportedMethodException,
    UnitpayValidationException, all implementing UnitpayExceptionInterface and
    still extending their former SPL classes (existing catch blocks keep working).
  • cURL transport with connect/read timeouts and no allow_url_fopen
    dependency; falls back to file_get_contents(). Locale-independent float
    formatting so a comma-locale can't corrupt the signature/amount.
  • Native type declarations across CashItem / UnitpayIpAllowlist /
    UnitPay within the PHP 7.4 subset; money/quantity params stay soft-typed by
    design (documented in PHPDoc). PHPStan raised to level 6.
  • Injectable seams ($transport / $request / $clientIp) for testing
    without the network or superglobals.

Tests & QA

  • First PHPUnit suite (121 tests, 243 assertions) — signatures (incl. the
    PHP_INT_MAX regression), handler verification, api() via injected
    transport, forms, CashItems, IP allowlist (CIDR/IPv6/fail-safe), floats.
  • QA tooling wired via Composer: parallel-lint, php-cs-fixer (PSR-12),
    phpstan, phpmd; composer check runs them all.
  • CI matrix PHP 7.4–8.4 with concurrency, least-privilege permissions, dependency
    cache and a non-blocking composer audit.

Docs

  • README trimmed to a ~90-line landing page; detailed topics moved to docs/
    (getting-started, receipts, api-methods, webhooks, telemetry).
  • Full example set (paymentForm, initPaymentApi, receipt, webhook,
    paymentInfo, refund, twoStagePayment, subscriptions, payout,
    accountInfo, offsetAdvance) reading secrets from the environment;
    config.php/order.php split. All code comments in English.

Compatibility (SemVer)

Effectively a minor at the API level (a clean bc-check against v2.0.6):

  • Payment-type constants and CashItem values removed earlier were restored as
    @deprecated (removal in 3.0).
  • Runtime validation was tightened (initPayment required params aligned with
    the backend; CashItem rejects non-numeric/non-positive count and negative
    price) — practically BC: it only rejects input the backend already refused.
  • Minimum PHP raised 5.6 → 7.4 (Composer gates installation, so widely treated
    as minor).
  • One behavior change: api() no longer retains fluent-setter params after a
    transport failure — a retry must re-apply the setters.

Verification

composer check green: parallel-lint, php-cs-fixer (0 findings), PHPStan level 6
(no errors), PHPMD, PHPUnit (121 tests).

@listopadlistopad self-assigned this Jul 23, 2026
…er split, renames; review guards (initPaymentApi, webhook)
@listopad
listopadforce-pushed the feature/update-sdk branch from 82a5546 to 9b1d870CompareJuly 23, 2026 13:54
listopad added 16 commits July 23, 2026 19:09
…witch, handler, API_UNREACHABLE skip, no-propagate)
Add native param/return/property type declarations to CashItem,
UnitpayIpAllowlist and UnitPay within the PHP 7.4 subset (no union types,
no mixed, no strict_types). Money/quantity params (form() $sum,
CashItem $count/$price, getCount) and httpGet()'s string|false stay
untyped by design — their types remain in PHPDoc. Redundant type-only
@param/@return tags dropped; prose, unions and element types kept.
Raise PHPStan from level 5 to 6: UnitPay.php is clean; test methods
gained : void and data-provider value types. No API or behavior change.
Translate every code comment across the SDK from Russian to English —
docblocks and inline comments in UnitPay.php, examples/ and tests/.
Comments only: code, string literals (incl. example data 'Доставка'),
README/CHANGELOG and docs/ are unchanged. Unit-of-measure glosses use
American spelling for consistency.
php -l, phpunit (126 tests), php-cs-fixer and phpstan all pass.
…erprint only
Handler pre-flight errors are already observable to Unitpay via the webhook
error response it receives; api() pre-flight errors are dev-time integration
mistakes. So the separate /sdk/telemetry endpoint, opt-in flag, reporter,
env kill-switch, error codes and wire-in carried little value — removed.
Keeps Layer A (User-Agent + X-Unitpay-Client on api(), sdk token in form()),
matching the Stripe/AWS/MongoDB first-party piggyback pattern. Reverts the
phpmd threshold bumps; folds the UA/client helpers into fingerprintHeaders.
User-Agent now carries the targeted Unitpay API surface (api/<v>) instead of the
PHP version; X-Unitpay-Client reports sdk_version, api_version, lang, lang_version,
platform (OS family only) and publisher. Adds UnitPay::API_VERSION; CHANGELOG and
the telemetry test updated to match.
- Read the verified getHandlerMethod()/getHandlerParams() instead of $_GET after
checkHandlerRequest()
- Handle the preauth webhook (now accepted by the handler) and add a default
branch, so an unknown method never returns an empty response — which Unitpay
treats as a failed callback
- Translate a stray Russian comment in the initPayment sample to English
- Sync the Telemetry section with the new fingerprint header format
- Extract immutable api() method-param and webhook-method dictionaries
into private const (REQUIRED_UNITPAY_METHODS_PARAMS,
SUPPORTED_PARTNER_METHODS); they were never reassigned.
- Type the $unitPay test properties natively (private UnitPay $unitPay).
- Rename caught exception variable $e -> $exception in examples to
match the project convention.
…ilure
- api(): clear the accumulated fluent-setter params in a finally (on both
success and transport failure), symmetric with form(), so a stale
receipt/customer no longer leaks into an unrelated later call on a reused
instance; a retry after a failure must re-apply the setters
- getSignature(): reject a null/empty secret up front — a public call must not
silently hash with an empty secret (the appended null coerces to '' and drops
out, yielding a plausible but secret-less signature)
- setAllowedIps([]): document as fail-closed (an empty allowlist rejects every
webhook, not a no-op)
- transport seam: document the $headers argument the transport actually receives
- add regression tests (empty-secret signing, fail-closed allowlist, cleared
params after a failed call); record the api() behavior change in CHANGELOG
Trim README to a ~90-line landing page and move the detailed sections into docs/: getting-started, receipts, api-methods, webhooks, telemetry. Each page has prev/next navigation and a See Also footer; no content was lost. README keeps badges, quick start, key features and a documentation table.
Add a concurrency group with cancel-in-progress, least-privilege permissions (contents: read) and a workflow_dispatch trigger. Install and cache dependencies via ramsey/composer-install and add a non-blocking composer audit step. The PHP 7.4-8.4 matrix and job names are unchanged.
Pin the unordered-list style to asterisk (MD004) and disable MD060 and MD013 to match the repository's existing markdown style (padded tables, long lines). Export-ignore the config from the Composer dist.
@listopad
listopadforce-pushed the feature/update-sdk branch from fef47b7 to 086fbd2CompareJuly 24, 2026 12:58
@listopadlistopad changed the title 54-ФЗ, полный REST API, безопасность вебхуков, тесты и CIModernize PHP SDK: full API, 54-FZ receipts, webhook IPs, and QAJul 24, 2026
@listopadlistopad changed the title Modernize PHP SDK: full API, 54-FZ receipts, webhook IPs, and QAModernize SDK: full API, 54-FZ receipts, webhook IPs, and QAJul 24, 2026
@listopad
listopad merged commit b9919db into masterJul 24, 2026
7 checks passed
@listopad
listopad deleted the feature/update-sdk branch July 24, 2026 15:45
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@listopad