Skip to content

Add PaymentsEndpoint::findByOrderId() and typed list filters (PaymentsQuery) - #12

Merged
loevgaard merged 2 commits into
1.xfrom
dx/find-by-order-id
Aug 17, 2026
Merged

Add PaymentsEndpoint::findByOrderId() and typed list filters (PaymentsQuery)#12
loevgaard merged 2 commits into
1.xfrom
dx/find-by-order-id

Conversation

@loevgaard

Copy link
Copy Markdown
Member

Part of #10 (finding 3).

Why

Every integration needs "look up the payment for this order": order_id is unique per Quickpay account (a second create() fails with ValidationException — "already exists on another payment"), so creating payments idempotently means find-or-create — and the SDK had no typed way to search. GET /payments supports order_id, state, accepted, min_time/max_time, acquirer, fraud_suspected, id, sort_by/sort_dir and operations_size, but CollectionRequestOptions only carried page/page_size.

What

  • PaymentsQuery extends CollectionRequestOptions — typed, optional filters, usable with getPage() and paginate() (filters are carried across pages):
    $client->payments()->paginate(new PaymentsQuery(state: PaymentState::New, accepted: true, minTime: new \DateTimeImmutable('-7 days'), pageSize: 100));
    state accepts the enum or a plain string (non-exhaustive enum), booleans go out as true/false, minTime/maxTime are formatted as the API documents (Y-m-d H:i:s O), and an extra hash passes anything unmodeled through verbatim (page/page_size always come from the typed options).
  • PaymentsEndpoint::findByOrderId(string $orderId): ?PaymentGET /payments?order_id=…&page_size=1, exact match, with a defensive client-side equality check so it can never hand back a different order.
  • CollectionRequestOptions is no longer final (BC-safe) so resource-specific query classes can extend it; withPage()/withPageSize() are documented as overridable and PaymentsQuery overrides them.
  • README: filtering example + a "Finding the payment for an order (idempotent create)" recipe:
    $payment = $client->payments()->findByOrderId($orderId)
        ?? $client->payments()->create(new CreatePaymentRequest(orderId: $orderId, currency: 'DKK'));

Verified against the live API (read-only probes + two throwaway unpaid payments)

  • order_id filter is an exact, case-sensitive match (a prefix or a different casing returns nothing).
  • order_id is unique per account even for unpaid initial payments → findByOrderId(): ?Payment is well-defined.
  • accepted=true|false and 1|0 are both accepted; accepted=maybe → 400. sort_dir other than asc/desc → 400.
  • min_time/max_time accept the documented %Y-%m-%d %H:%M:%S %z (and ISO 8601). Default order is newest first.

Tests: 12 new (query mapping, boolean/time encoding, extra precedence, filters surviving withPage(), filtered getPage(), filtered paginate() across pages, findByOrderId() found / not found / never a different order).

…sQuery)

Every integration needs "look up the payment for this order": order_id is
unique per Quickpay account (a second create fails with "already exists on
another payment"), so creating payments idempotently means find-or-create —
and the SDK had no typed way to search. GET /payments supports order_id, state,
accepted, min_time/max_time, acquirer, fraud_suspected, id, sort_by/sort_dir and
operations_size, but CollectionRequestOptions only carried page/page_size.

- New `PaymentsQuery extends CollectionRequestOptions` with typed, optional
  filters (state accepts the PaymentState enum or a plain string; booleans are
  sent as true/false; min/max time formatted as the API documents; an `extra`
  hash for anything unmodeled) — usable with getPage() and paginate(), which
  carries the filters across pages.
- New `PaymentsEndpoint::findByOrderId(string): ?Payment` — exact match, with
  a defensive client-side equality check.
- CollectionRequestOptions is no longer final (BC-safe) so resource-specific
  query classes can extend it; withPage()/withPageSize() are overridable.
- README: filtering example + a "Finding the payment for an order (idempotent
  create)" recipe.

Filter semantics (exact/case-sensitive order_id, boolean encoding, time
formats, order_id uniqueness) were verified against the live API.

Refs #10 (finding 3).
@codecov

codecov Bot commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.69%. Comparing base (cc4b95c) to head (8832089).
⚠️ Report is 2 commits behind head on 1.x.

Additional details and impacted files
@@             Coverage Diff              @@
##                1.x      #12      +/-   ##
============================================
+ Coverage     98.50%   98.69%   +0.19%     
- Complexity      147      163      +16     
============================================
  Files            24       26       +2     
  Lines           401      461      +60     
============================================
+ Hits            395      455      +60     
  Misses            6        6              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

… overrides

withPage()/withPageSize() now clone and set, returning static — so a
subclass's filters come along for free and PaymentsQuery no longer needs its
own withers or a 14-argument copy constructor. Only page/pageSize lose
readonly (reinitializing readonly properties during clone requires PHP 8.3;
the SDK supports 8.1); the filters on PaymentsQuery stay readonly. The
withers validate like the constructor does.
@loevgaard

Copy link
Copy Markdown
Member Author

Follow-up per review: the withers are now clone-based in CollectionRequestOptions (withPage()/withPageSize() clone, set, return static), so PaymentsQuery needs no overrides and no copy constructor — it's down to a constructor + toArray().

The one trade-off, and why the first version didn't do this: reinitializing a readonly property during clone only became legal in PHP 8.3, and the SDK supports 8.1. So page/pageSize are now plain public properties (the withers still validate >= 1; a direct write bypasses that), while every filter on PaymentsQuery stays readonly. If you'd rather keep page/pageSize readonly, the alternative is the explicit copy constructor from the previous commit.

@loevgaard
loevgaard merged commit b1ac5d6 into 1.x Aug 17, 2026
35 checks passed
@loevgaard
loevgaard deleted the dx/find-by-order-id branch August 17, 2026 10:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant