Skip to content

Releases: shaxzodbek-uzb/pay-uz

4.0.0 — Uzbekistan fintech toolkit

22 Jun 06:01

Choose a tag to compare

4.0.0 - 2026-06-21

Expands the package from "Payme + Click webhooks" into a multi-rail Uzbekistan
fintech toolkit: new OFD fiscalization, card tokenization / recurring
(Payme Subscribe, ATMOS), card-acquiring aggregators (Octo, Multicard/Rahmat),
BNPL (Uzum Nasiya), and e-invoicing / ЭСФ (Didox) layers, each behind its
own facade on a shared Support\Http transport, with null-driver defaults so a
fresh install works offline. Major version because the runtime code "editor"
and its require-based payment hooks are removed in favour of a resolver + events
(see Breaking).

Added

  • E-invoicing layer (the Einvoice facade) with Didox as the first driver
    — issue ЭСФ and other e-documents (create → sign → submit/accept/reject/cancel →
    status), its own layer (not a payment flow):

    • EinvoiceDriver contract resolved by EinvoiceManager behind the Einvoice
      facade (config under payuz.einvoice). Value objects Document, InvoiceItem,
      Counterparty, DocumentStatus, EinvoiceResult; a Som helper for the
      single tiyin↔decimal-som-string boundary; events DocumentCreated /
      DocumentSigned / DocumentRejected / DocumentCancelled; a null default.
    • The package ships zero cryptography. E-IMZO PKCS#7 signing is a Signer
      seam (NullSigner default throws; CallableSigner wraps a closure); the
      manager's signAndSubmit() wires toSign → sign → submit, or you pass a
      pre-signed blob to the driver directly.
    • DidoxDriver: Partner-Authorization + user-key auth, decimal-som amounts,
      poll-only status (no webhook). Wire details not byte-confirmed in public docs
      are flagged UNCERTAIN inline. (Live use needs a Didox partner token.)
  • Note: Alif Nasiya was evaluated as a second BNPL driver but deferred — its
    Nasiya API is partner-gated with no public spec, so no driver was fabricated.

  • BNPL / installments layer (the Bnpl facade) with Uzum Nasiya as the
    first driver — a credit-contract lifecycle (its own layer, not Checkout/Subscribe):

    • BnplDriver contract resolved by BnplManager behind the Bnpl facade
      (config under payuz.bnpl): checkEligibilitycalculate
      createContractconfirm/cancelstatus, with extend() for new BNPL
      gateways. Value objects Eligibility, InstallmentPlan, Contract,
      ContractStatus, ContractResult.
    • UzumNasiyaDriver (coded against Uzum's official Nasiya Partner API OpenAPI):
      partner Bearer JWT auth, tiyin↔som conversion at the boundary, the two-id
      handling (confirm/status use contract_id, cancel uses the order id), and
      response_code→retryability for confirm/cancel. Two onboarding-time unknowns
      are flagged inline (som-vs-tiyin wire unit; WebView vs /v3 OTP activation).
    • Events ContractCreated, ContractConfirmed, ContractCancelled, and a
      null driver default. (Live/E2E needs an Uzum partner token; the unit-tested
      driver ships now.)
  • Multicard driver for the Checkout layer. A second card-acquiring
    aggregator (hosted checkout + saved-card charge + hold-capture + full/partial
    refund + webhook) over Multicard's REST API: a cached bearer token (POST /auth,
    refreshed + retried once on 401), the real HTTP verbs (invoice POST, capture
    PUT, refund DELETE, status GET), the {success} envelope mapped to typed
    exceptions, tiyin pass-through (no conversion), and both webhook signature
    schemes selected by callback_scheme.

  • Support\Http\HttpClient::request($method, …) — the shared transport now
    supports arbitrary HTTP verbs (GET/PUT/DELETE), not just POST.

  • rahmat Checkout alias. "Rahmat Pay" is not a separate processor — it is the
    Multicard acquiring rail (its checkout renders on app.rhmt.uz), so
    Checkout::driver('rahmat') resolves to the Multicard driver with the
    multicard config. No separate driver is shipped (none is warranted).

  • ATMOS driver for the Subscribe layer. ATMOS's verified API is a server-side
    card-vault + OTP flow (OAuth2 → card bind/confirm → create/pre-apply/apply), which
    matches the SubscribeDriver contract — so ATMOS is a Subscribe driver, not a
    Checkout one. The driver handles the OAuth client_credentials token (cached +
    auto-refreshed), passes amounts through as tiyin (no conversion), maps the
    result.code == 'OK' envelope to typed exceptions, transposes the MMYYYYmm
    expiry, and swaps the bind's binding_id for the confirmed card_token.
    confirmHold() and partial refunds are unsupported (no verified ATMOS endpoint)
    and throw / fall back to whole-transaction cancelReceipt. ATMOS-specific
    verifyCallback() / parseCallback() handle the merchant-cabinet webhook.

  • Support\Http\HttpClient::postForm() — form-encoded POST on the shared
    transport (for OAuth client_credentials token requests and similar).

  • Card-acquiring aggregator layer (the Checkout facade) with Octo as the
    first driver — one REST integration for Uzcard/Humo/Visa/Mastercard with hosted
    checkout, saved-card charges, two-stage capture, refunds and webhooks:

    • CheckoutDriver contract resolved by CheckoutManager behind the Checkout
      facade (config under payuz.checkout), with extend() and the shared
      Support\Http transport. Payment (request) + PaymentResult (normalized,
      vendor-agnostic status) value objects.
    • OctoDriver: converts the package's tiyin to Octo's som, branches on the
      error envelope (HTTP is always 200), maps Octo's status set to the
      normalized statuses, and verifies webhook signatures (hash_equals).
    • webhook() verifies the signature before emitting PaymentSucceeded /
      PaymentFailed / PaymentRefunded; an unverified payload raises
      WebhookException and is never acted on.
  • Card tokenization + recurring charges (the Subscribe facade). A
    gateway-agnostic layer for save-card / one-click / subscription billing, with
    the Payme Subscribe API as the first driver:

    • SubscribeDriver contract resolved by SubscribeManager behind the
      Subscribe facade (config under payuz.subscribe), with extend() for new
      gateways and the shared Support\Http transport.
    • Card, VerifyCode and Charge value objects (tiyin amounts, receipt state
      codes); the lifecycle is createCard → sendVerifyCode → verify → charge, plus
      two-stage authorize/capture/release (holds).
    • PaymeDriver enforcing the Subscribe X-Auth rule (cashbox id alone for the
      browser-safe cards.create/get_verify_code/verify; id:key for
      cards.check/remove and all receipts.*), JSON-RPC error → typed
      exceptions, and a null driver default.
    • Events CardVerified, ChargePaid, HoldConfirmed, ChargeCancelled.
    • Never persists the PAN — only the returned token.
  • Shared HTTP layer Support\Http (HttpClient seam + CurlHttpClient +
    JsonRpcClient, TransportException/JsonRpcException), now used by the
    fiscalization and subscribe layers (and future aggregator/e-invoice drivers).

  • OFD fiscalization layer (онлайн-ККМ). A gateway-agnostic way to register
    fiscal receipts with a Fiscal Data Operator (PKM No. 943) and obtain the fiscal
    sign / QR:

    • Value objects Fiscalization\Receipt / ReceiptItem / FiscalResult
      tiyin-integer amounts, VAT-inclusive prices, and Receipt::sale()/refund()
      factories with a cash/card payment split.
    • Mxik (17-digit IKPU/MXIK validation) and Vat (rate set {0, 12},
      pure-integer round-half-up VAT extraction) helpers.
    • A FiscalDriver contract resolved by FiscalizationManager behind the new
      Fiscalizer facade, with extend() for custom OFD providers and an
      HttpClient transport seam (default cURL, no new dependencies).
    • Drivers: null (validates + returns a synthetic sign; the safe default) and
      ofd (generic soliq fiscal-receipt HTTP gateway with tolerant response
      parsing and QR-from-parts derivation).
    • ReceiptFiscalized / FiscalizationFailed events, and a
      Transaction::attachFiscalReceipt() helper that stores the result under the
      transaction's detail JSON. Configure under payuz.fiscalization.
  • Event/resolver-based payment hooks. Application behaviour now lives in
    versioned code instead of runtime-writable PHP files:

    • A Payments\Contracts\PaymentResolver (default
      Payments\DefaultPaymentResolver, configured under payuz.payments.resolver)
      for the value-returning operations: convertModelToKey, convertKeyToModel,
      isProperModelAndAmount, beforeResponse.
    • Lifecycle events Payments\Events\PaymentBeforePay, PaymentProcessing,
      PaymentPaid and PaymentCancelled — subscribe from your EventServiceProvider
      instead of editing paying.php / after_pay.php / etc.

Removed

  • The runtime code "editor" and its require-based hooks. Deleted
    ApiController::file_put, the POST /payment/api/editable/update route, the
    editors page/controller action and editors.blade.php, and the published
    app/Http/Controllers/Payments/*.php hook files (no longer required). The
    PaymentService methods that used to require those files now call the
    configured resolver / dispatch events (see Added).

Security

  • Removed the residual authenticated-CSRF → RCE surface left after 3.0.0.
    3.0.0 fixed the unauthenticated RCE (CVE-2026-31843) but the
    write-PHP-then-require architecture remained, gated only behind auth.
    Because the writer route used Route::any (so it accepted GET, which
    Laravel's CSRF middleware does not cover), a logged-in user could be lured into
    writing a hook file via a crafted link. Deleting the editor and the
    require-based hooks removes the write-and-execute primitive entirely.

Breaking

  • Payment hooks moved from editable files to a resolver + events.
    `Pa...
Read more

3.0.0 — Security release (CVE-2026-31843)

20 Jun 12:41

Choose a tag to compare

🔐 Security release — please upgrade

This release fixes CVE-2026-31843 (GHSA-m5wg-cjgh-223j) — a critical, unauthenticated remote code execution via POST /payment/api/editable/update, which overwrote executable PHP payment-hook files. Affected: all versions ≤ 2.2.24. Fixed in 3.0.0.

composer require goodoneuz/pay-uz:^3.0

⚠️ Breaking change

The control-panel routes (dashboard, settings, transactions, payment-systems CRUD and the code editors) now require the auth middleware by default. If you published config/payuz.php, set control_panel.middleware to your own admin/authorization guard — the editor writes executable PHP and must be protected.

Highlights

  • Fixed the RCE — strict filename allow-list + realpath containment in the editor writer, and auth-by-default on the control panel.
  • Constant-time signature/credential checks (hash_equals) for Click/Payme/Paynet/Oson.
  • Paynet now validates the callback amount against the order.
  • Payme — removed the APP_ENV=testing auth bypass; atomic compare-and-set prevents double-perform.
  • Stripe — fixed reflected XSS / header injection / open redirect.
  • Octane / RoadRunner request body fix (#71).
  • DataFormat::timestamp2datetime no longer returns 1970-01-01 for numeric timestamps.
  • New: Uzum Bank gateway (Merchant API — check/create/confirm/reverse/status).

See the CHANGELOG for the full list.

Bug fixes

30 Jan 11:38
2253cee

Choose a tag to compare

Merge pull request #69 from Sodiqmirzo/fix_get_statement

fix: get statement method payme

Multi transaction condition

04 Jun 13:33
0a65b36

Choose a tag to compare

2.2.23

multi transaction condition

2.2.22

04 Jun 06:29
616f6e0

Choose a tag to compare

Expiring transactions

Minor changes

07 May 12:47
8270d8d

Choose a tag to compare

Minor changes

Payme redirection form key

16 Apr 15:31
bb52143

Choose a tag to compare

2.2.20

change payme form account key

Minor changes

30 Mar 06:27

Choose a tag to compare

2.2.19

changes

Payme: fix system date formating

17 Jan 12:24
60d9f32

Choose a tag to compare

Merge pull request #63 from asadbek-fayzulloev/master

fix: remove date formatter

Change Payme@CheckPerformTransaction default value true

25 Aug 15:35
14d0e9c

Choose a tag to compare

2.2.17

set allow default true in payme CheckPerformTransaction response