Skip to content

Releases: Setono/quickpay-php-sdk

v1.2.0

Choose a tag to compare

@loevgaard loevgaard released this 17 Aug 12:50
8188a81

Two follow-ups from putting 1.1 to work in setono/payum-quickpay — Quickpay knowledge every integration re-derives, now in the SDK (#22, #25). Additive; both verified against the live API.

composer require setono/quickpay-php-sdk:^1.2

Highlights

  • Your callbacks for captures, refunds and cancels now actually arrive. Quickpay POSTs the callback of an API-issued operation to the account-wide callback URL (empty by default) — not to the callback_url on the payment link — so a shop that only set the link's URL never heard about them. Every operation now takes callbackUrl:, sent as the QuickPay-Callback-Url header (Client::CALLBACK_URL_HEADER) so that operation notifies the URL you name:
    $client->payments()->capture($id, new CaptureRequest(1000), callbackUrl: $notifyUrl);
    The low-level get()/post()/put()/patch()/delete() accept extra request headers too (renew, session, … can send it), and Link::$autoCapture / $autoCaptureAt are typed. (#26)
  • Read a decline. A declined synchronized capture/refund/cancel is a 2xx with the decline on the operation. Operation::hasOutcome() / isDeclined() say it plainly, and Payment gained the views that decide an order's status: latestOperationOfType(), latestApprovedOperation() (where the money actually is — a trailing rejected or pending attempt no longer masks it), hasApprovedOperation(?type), and hasPendingOperation(?type). "Latest" is always the highest operation id, defined once. (#27)
  • order_id fails fast, with the real rule. Live probing showed the API accepts 4–20 characters of letters, digits, space, ., _, - and rejects everything else (/ # : @ % + …, any non-ASCII) under the misleading "must have length between 4 and 20". CreatePaymentRequest now validates that at construction (ORDER_ID_PATTERN) and throws the new Setono\Quickpay\Exception\InvalidArgumentException — an SPL InvalidArgumentException that is also a QuickpayException. (#27)
  • paymentMethods as a list. new CreateLinkRequest(..., paymentMethods: ['creditcard', '!amex']) is joined the way Quickpay expects (a (string) cast of a list used to send the literal Array and reject every payment). (#27)
  • README — where operation callbacks go; state reads pending during any asynchronous operation (with the pre-operation balance); Quickpay does not retry a declined operation (a declined auto-capture leaves the payment new/authorized); synchronized declines are 2xx; the orderId rule; the new helpers with a decline-check snippet. The e2e operate.php now routes operation callbacks to the listener.

Changed

  • CreatePaymentRequest validates orderId at construction (see above). Code that relied on the API's ValidationException for a bad order_id now fails earlier — locally, and with a message naming the actual rule.
  • CollectionRequestOptions throws Setono\Quickpay\Exception\InvalidArgumentException — a subclass of the SPL exception it threw before, so existing catches keep working.

BC notes

  • ClientInterface::get()/post()/put()/patch()/delete() and the protected ResourceEndpoint::postOperation() gained an optional $headers parameter. Callers are unaffected; only code that implements the mock-only interface (Client is its sole implementation) or subclasses the SDK's endpoint base (unsupported) needs to add the parameter. The Roave BC check reported these on #26 and it was knowingly merged red.

Quality

210 tests (1.1.0: 166), PHPStan level max, CI across PHP 8.1–8.5 × lowest/highest, plus the Roave backwards-compatibility check on every PR.

Full changelog: v1.1.0...v1.2.0 · CHANGELOG.md

v1.1.0

Choose a tag to compare

@loevgaard loevgaard released this 17 Aug 11:40
3762a69

The developer-experience release. 🧰

v1.0.0 got a top-to-bottom review through a first-time consumer's eyes (#10); this release ships the follow-ups. Everything is additive — no changes to existing signatures for callers — and every API-facing claim was verified against the live Quickpay API.

composer require setono/quickpay-php-sdk:^1.1

Highlights

  • Find the payment for an order. $client->payments()->findByOrderId($orderId) — exact match, null when absent. Together with create() it makes checkout find-or-create a one-liner (Quickpay enforces order_id uniqueness per account, so a retried checkout used to explode with "already exists on another payment"). Listing gained a typed PaymentsQuery (state, accepted, minTime/maxTime, acquirer, sortBy/sortDir, …) for getPage()/paginate(). (#12)
  • Read what happened without hand-rolling it. Operation::isApproved(), and on Payment: authorizedAmount(), capturedAmount(), refundedAmount(), isCancelled(), hasPendingOperation(), latestOperation(), operation($id), operationsOfType(). Only approved operations count — the exact place integrations used to get subtly wrong. (#13)
  • The whole API within reach. Client::delete(), plain-array bodies on post()/put()/patch() (sent as given, so unmodeled endpoints — renew, subscriptions, … — are a one-liner), PaymentsEndpoint::deleteLink(), and a README section on calling endpoints the SDK doesn't model. Bodies are Payload|array $body = [], never null; an empty body goes out as {}. (#14)
  • catch (QuickpayException $e) now really nets everything. Transport failures (DNS, refused connection, timeout) are wrapped in TransportException, which is also a PSR-18 ClientExceptionInterface, so existing catches keep working; the original is getPrevious(), isNetworkError() tells network from request errors. (#18)
  • Callbacks from any stack. handleGlobals() for plain PHP (php://input + $_SERVER), handleRaw() now takes the optional accountId/apiVersion headers, and the README has Symfony / Laravel / plain-PHP snippets. (#17)
  • Production cache in one argument. new Client('…', cache: new FileSystemCache($dir)) (and the same on CallbackHandler) — no more hand-wiring Valinor builders and forgetting the SDK's configuration. (#16)
  • Small things that add up. Payment::variables() (your keys, verbatim), Payment::$deadlineAt / $acquirer, CreatePaymentRequest::$shopsystem so a plugin can identify itself, a memoized Callback::payment(), and the SDK version in the User-Agent. (#19)
  • Docs that teach the model, not just the SDK. Table of contents; a "Concepts" section (the two keys, minor units, a payment as a ledger of operations, async operations, the redirect is not proof of payment); "Handling callbacks robustly" (retries → idempotency on operation ids, per-payment ordering, respond fast, why 403/400); an end-to-end checkout recipe and Symfony wiring; accurate install text; a CHANGELOG.md; docs shipped in the dist again. (#20, #21)
  • Safety and hygiene. Client::request() now runs the same host-pinning guard as every other method (a consumer-built request could previously carry the API key to another host — #11). PRs are now gated by a Roave BC check (#23).

BC notes

Nothing changes for code that calls the SDK. Two things could touch unusual code:

  • ClientInterface gained delete(), and post()/put()/patch() take Payload|array $body = [] instead of ?Payload $body = null — relevant only if you implement the interface (a mock-only interface) or passed an explicit null body (pass nothing, or []). (#14)
  • Code that caught Psr\Http\Client\NetworkExceptionInterface / RequestExceptionInterface specifically (rather than ClientExceptionInterface) should catch TransportException and inspect getPrevious() / isNetworkError(). (#18)
  • CollectionRequestOptions is no longer final, and its page/pageSize are plain public properties (a readonly property cannot be reinitialized during clone before PHP 8.3) — the withers still validate. (#12)

Won't do

  • A shipped test double (#15): the seam is the PSR-18 client, and every stack already has a mock for it — the README now shows how to plug it in.

Quality

166 tests (was 109), PHPStan level max, MSI gate unchanged, CI across PHP 8.1–8.5 × lowest/highest, plus the new backwards-compatibility check on every PR — and a live smoke run (create → link → get) against the real API on the merged branch.

Full changelog: v1.0.0...v1.1.0 · CHANGELOG.md

v1.0.0

Choose a tag to compare

@loevgaard loevgaard released this 10 Aug 13:14
cc4b95c

First stable release. 🎉

A small, strongly-typed PHP SDK for the Quickpay API (v10), deliberately focused on the payments resource, the /ping health check, the payment-window link flow, and callback (webhook) verification.

composer require setono/quickpay-php-sdk

Requires PHP >= 8.1 and any PSR-18 HTTP client / PSR-17 factories (auto-discovered via php-http/discovery).

Highlights

  • Typed end to end — request DTOs with live-API-verified required fields, response DTOs mapped with Valinor, and a full typed exception hierarchy under a single QuickpayException marker interface.
  • Safe by default — host pinning so credentials only ever go to api.quickpay.net, sanitized URLs in exception messages, timing-safe HMAC verification of callbacks over the raw request body, and strict validation of the QuickPay-Resource-Type header.
  • Resilient to API evolution — non-exhaustive PaymentState / OperationType enums that never throw on server-added values, and a $raw escape hatch on every response for fields the SDK doesn't model.
  • Async operations, modeled honestly — capture/refund/cancel/authorize support Quickpay's ?synchronized flag per call or as a client-wide default, and the docs explain exactly what a 202 Accepted response does (and does not) tell you.
  • Well-tested — 109 unit tests, PHPStan at level max, mutation score (MSI) 85%, CI across PHP 8.1–8.5 with lowest & highest dependencies, plus a committed end-to-end harness (examples/e2e/) for verifying the whole flow against the real API.

See the README for full usage — from creating a payment and redirecting to the payment window, to verifying the signed callback.

Changes since v1.0.0-beta.2

  • Dist installs are now lean: CLAUDE.md, examples/ and .env.local.example are export-ignored, so a composer require ships only LICENSE, composer.json and src/ (#9)
  • Fixed the live test's generated order_id exceeding Quickpay's 20-character limit (#9)
  • Fixed a docblock referencing the nonexistent CallbackHandler::handleRequest() (now handleRaw()) (#9)
  • Removed an undefined env.PHP_EXTENSIONS reference from the CI workflow (#9)

Full changelog: v1.0.0-beta.2...v1.0.0

v1.0.0-beta.2

v1.0.0-beta.2 Pre-release
Pre-release

Choose a tag to compare

@loevgaard loevgaard released this 10 Aug 11:31
18b4362

Dependency slim-down, plus one small deliberate BC cleanup (#8) — the last of its kind planned before v1.0.0.

Changed

  • webmozart/assert is no longer a dependency. CollectionRequestOptions was its only user in the SDK (two >= 1 checks); they are now plain inline guards throwing \InvalidArgumentException. Since Webmozart's exception already extended \InvalidArgumentException, catch sites see no behavioral difference — but every consumer's dependency tree gets one package lighter.

Removed (BC break)

  • CollectionRequestOptions::new(). It existed only to start fluent wither chains (::new()->withPageSize(50)); constructor named arguments express that better: new CollectionRequestOptions(pageSize: 50). The withers (withPage()/withPageSize()) remain. If you call ::new(), replace it with new CollectionRequestOptions().

    beta.1 stated no further BC breaks were planned — this cosmetic removal was judged worth the exception as a last-cheap-moment cleanup while still pre-1.0. Apologies if it bites; the fix is mechanical.

Full changelog: v1.0.0-beta.1...v1.0.0-beta.2

v1.0.0-beta.1

v1.0.0-beta.1 Pre-release
Pre-release

Choose a tag to compare

@loevgaard loevgaard released this 06 Aug 12:59
419fb53

First beta — the API surface is now considered stable for 1.0. The alpha series deliberately made its breaking changes early; no further BC breaks are planned before v1.0.0, and remaining work is feedback, polish and documentation.

Where the SDK stands

  • Scope: the payments resource (create/read/list/update + authorize/capture/refund/cancel), the /ping health check, the payment-window link flow, and callback (webhook) verification — deliberately narrow and typed.
  • Contracts verified against the live API, not the docs: required request fields are required constructor arguments (orderId/currency on create, amount on links and operations, all five BasketItem fields); Address/Shipping verified lenient; response quirks (e.g. is3d_secure returning a boolean) modeled from real responses.
  • Async operations documented honestly: without ?synchronized the API answers 202 with a queued-operation snapshot — the docblocks explain what you can and cannot read from it, and the client-wide synchronized constructor flag sets the default once.
  • Callback verification is deliberately un-mockable: CallbackHandler/CallbackValidator are final with no interfaces; the README's new "Testing your callback endpoint" section (#7, the one change since alpha.4) documents the intended pattern — a real handler with a test key and validator()->sign()-forged signatures.
  • Quality gates: 109 tests across PHP 8.1–8.5 × lowest/highest, PHPStan level max, mutation score MSI 85% / covered 85% (gate 70), plus a committed real-API e2e harness (examples/e2e/).

Changed since v1.0.0-alpha.4

  • README: "Testing your callback endpoint" section (#7). Docs only — no code changes.

Full changelog: v1.0.0-alpha.4...v1.0.0-beta.1

v1.0.0-alpha.4

v1.0.0-alpha.4 Pre-release
Pre-release

Choose a tag to compare

@loevgaard loevgaard released this 06 Aug 09:53
425bd28

Internal-quality release — no API changes and no BC impact.

Improved

  • ResponseAwareException is now fully covered (#5): the lazy stream-read parse path, getResponse(), the integer error_code cast and the non-map errors branch all gained tests, plus two behavior pins — the pre-read body winning over an already-drained (non-seekable) response stream, and the default message's query/fragment stripping asserted character-exact so the secret-leak guard cannot silently regress. Suite mutation score: MSI 80% → 85%, covered MSI 82% → 85%.
  • Consistent ResourceEndpoint helper naming (#6): update()updateOne(), operation()postOperation(), putSub()putSubResource() — every helper now follows either semanticVerb + One (typed single-resource CRUD) or httpVerb + target (transport). Internal-only: the helpers are protected and endpoint subclassing is outside the BC promise. Also fixes the class docblock's update() bullet, which said PUT while the helper sends PATCH.

Full changelog: v1.0.0-alpha.3...v1.0.0-alpha.4

v1.0.0-alpha.3

v1.0.0-alpha.3 Pre-release
Pre-release

Choose a tag to compare

@loevgaard loevgaard released this 06 Aug 09:21
52dd84a

Breaking changes

Unconditionally-required request fields are now required constructor parameters (#2, #3). Every field was verified against the live Quickpay API (not just the docs) before being tightened; all of them were already the first constructor parameters, so positional callers are unaffected:

  • CreatePaymentRequest::$orderId, ::$currency
  • $amount on CreateLinkRequest, CaptureRequest, RefundRequest, AuthorizePaymentRequest — note the API does not fall back to capturing/refunding the remaining balance when amount is omitted; it rejects the operation
  • all five BasketItem fields (qty, itemNo, itemName, itemPrice, vatRate) — the API treats a basket item as all-or-nothing, and a fully-empty item even triggered an HTTP 500 on Quickpay's side
  • PaymentsEndpoint::authorize() now requires its $request (a bodyless authorize can never succeed)

A missing required field now fails at the call site (and in static analysis) instead of surfacing as a ValidationException after a network round-trip. Address and Shipping stay all-optional — verified lenient against the live API. Beware Shipping::$method: when present it must be one of Quickpay's accepted values.

Fixed

  • A Payload with no set fields was serialized as [] (a JSON array), which the API always rejects (body: "is invalid") — e.g. updatePayment($id, new UpdatePaymentRequest()) could never succeed. It is now sent as {}. (#2)
  • updatePayment()'s docblock said PUT; the SDK sends PATCH. (#4)

Improved

  • Docblocks on authorize/capture/refund/cancel now document what the async (202) response actually contains — a snapshot taken when the operation was queued (pending: true, pre-operation state/balance) — and how to confirm the outcome (callback, or poll getById()).
  • Test suite audit: 92 → 103 tests; mutation score MSI 72% → 80%, covered MSI 76% → 82%. New regression guards include the strict-mapping failure path (MappingException), both supported date formats, 202 handling, and the host-pinning guard's edge cases. (#4)
  • README: documents the required-constructor-argument rule and fixes the ValidationException example's order id (3 chars would nowadays fail the API's 4–20 length rule). (#4)

Full changelog: v1.0.0-alpha.2...v1.0.0-alpha.3

v1.0.0-alpha.2

v1.0.0-alpha.2 Pre-release
Pre-release

Choose a tag to compare

@loevgaard loevgaard released this 06 Aug 07:57
12a4a7e

What's new

Client-wide default for the synchronized operation flag (#1)

The payment operation methods (authorize / capture / refund / cancel) now take ?bool $synchronized = null. When null (the default) they fall back to a new synchronized flag on the Client constructor, so integrations that always want to wait for the completed transaction set it once instead of repeating it on every call — a non-null per-call argument still overrides the default:

$client = new Client('YOUR_API_KEY', synchronized: true);

$client->payments()->capture($id, new CaptureRequest(1000)); // waits (client default)
$client->payments()->refund($id, new RefundRequest(250), synchronized: false); // per-call override

The default is exposed as ClientInterface::isSynchronized(). Behavior is unchanged for existing code.

BC note

ClientInterface gained the isSynchronized() method — custom implementations or decorators of that interface must add it. Code using Client directly is unaffected.

Full changelog: v1.0.0-alpha.1...v1.0.0-alpha.2

v1.0.0-alpha.1

v1.0.0-alpha.1 Pre-release
Pre-release

Choose a tag to compare

@loevgaard loevgaard released this 30 Jun 12:03

First alpha of the Quickpay PHP SDK — a small, strongly-typed client for the Quickpay API.

⚠️ Alpha — the public API may still change before 1.0.0.

Features

  • Payments: create, get, list/paginate, update (PATCH), authorize, capture, refund, cancel — with an optional synchronized flag to wait for the completed transaction.
  • Payment-window link flow: createLink() returns the hosted payment-window URL to redirect the customer to.
  • Callbacks (webhooks): timing-safe checksum verification (hash_equals over the raw body, account private key) + typed deserialization, with strict QuickPay-Resource-Type handling (Payment / Subscription).
  • /ping health check.
  • Typed request/response DTOs with a $raw fallback for unmodeled fields, and a typed exception hierarchy (everything implements QuickpayException).
  • Credential-leak protection: the client refuses to send to any host other than api.quickpay.net.

Requirements

  • PHP 8.1 – 8.5
  • A PSR-18 HTTP client + PSR-17 factories (auto-discovered via php-http/discovery)

Install

composer require setono/quickpay-php-sdk:"^1.0@alpha"
# plus an HTTP client + PSR-17 factory if you don't have one, e.g.:
composer require kriswallsmith/buzz nyholm/psr7

See the README for usage, the callback flow, and the end-to-end test harness.

Quality

Tested against PHP 8.1–8.5 (lowest + highest deps), PHPStan level max, ECS, Rector, and mutation testing (Stryker), plus a real-API end-to-end verification of the payment + callback flow.