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

@listopad listopad 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).

@listopad
listopad requested a review from rakomissarenko July 23, 2026 12:27
@listopad listopad self-assigned this Jul 23, 2026
…er split, renames; review guards (initPaymentApi, webhook)
@listopad
listopad force-pushed the feature/update-sdk branch from 82a5546 to 9b1d870 Compare July 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
listopad added 3 commits July 24, 2026 15:41
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
listopad force-pushed the feature/update-sdk branch from fef47b7 to 086fbd2 Compare July 24, 2026 12:58
@listopad listopad changed the title 54-ФЗ, полный REST API, безопасность вебхуков, тесты и CI Modernize PHP SDK: full API, 54-FZ receipts, webhook IPs, and QA Jul 24, 2026
@listopad listopad changed the title Modernize PHP SDK: full API, 54-FZ receipts, webhook IPs, and QA Modernize SDK: full API, 54-FZ receipts, webhook IPs, and QA Jul 24, 2026
@listopad
listopad merged commit b9919db into master Jul 24, 2026
7 checks passed
@listopad
listopad deleted the feature/update-sdk branch July 24, 2026 15:45
Sign up for free to 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