Outcome predicates, per-type/last-approved views, order_id validation, paymentMethods list - #27
Merged
Merged
Conversation
…, paymentMethods list Follow-ups from #25 — Quickpay knowledge every integration re-derives: - Operation::hasOutcome() (!pending) and isDeclined() (completed, not approved). A declined synchronized operation is a 2xx with the decline on the operation; this is how to read it. - Payment::latestOperationOfType($type), latestApprovedOperation() (the newest APPROVED op — a trailing rejected/pending attempt must not mask where the money is), hasApprovedOperation(?$type), and hasPendingOperation(?$type) widened. "Latest" is always the highest id (one definition, here). - CreatePaymentRequest validates order_id at construction: ORDER_ID_PATTERN = /^[A-Za-z0-9 ._-]{4,20}$/. Verified live: 4–20 chars, and every other character tried (/ # : @ % + , ( & = ~ ! * ' tab, non-ASCII) is rejected under the same misleading "must have length between 4 and 20" message — so the local check names the real rule. Throws the new Setono\Quickpay\Exception\InvalidArgumentException (SPL subclass + QuickpayException); CollectionRequestOptions throws it too. - CreateLinkRequest::$paymentMethods accepts string|list<string>|null and joins a list with commas (a (string) cast would send the literal "Array"). - README: Concepts (state pending during any async op, declined ops are not retried, synchronized declines are 2xx), the new helpers, order_id rule, paymentMethods list. Closes #25.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## 1.x #27 +/- ##
============================================
+ Coverage 99.08% 99.13% +0.05%
- Complexity 203 222 +19
============================================
Files 27 27
Lines 544 576 +32
============================================
+ Hits 539 571 +32
Misses 5 5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
loevgaard
commented
Aug 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #25 — the four items, in the issue's order. All additive.
1. Per-type / last-approved views on
PaymentlatestOperationOfType(OperationType|string $type): ?Operation— newest by id, any outcomelatestApprovedOperation(): ?Operation— newest approved, any type: where the money actually is; a trailing rejected or pending attempt doesn't mask ithasApprovedOperation(OperationType|string|null $type = null): boolhasPendingOperation(OperationType|string|null $type = null): bool(widened; still parameter-less compatible)"Latest" is defined once (
latestOf(): highest id) andlatestOperation()uses it too — settling the id-vs-list-position inconsistency the issue mentions.2. Outcome predicates on
OperationhasOutcome()—!pendingisDeclined()—hasOutcome() && !isApproved(); docblock spells out 4xxxx/5xxxx/3xxxx and that a synchronized decline is a2xxwith the decline on the operation.isApproved()/isDeclined()are mutually exclusive once there is an outcome (pinned by a test).The
throwOnDeclineidea is left out, as the issue suggests — separate discussion.3.
order_idvalidation inCreatePaymentRequestVerified live before writing it (2026-08-17): the API accepts 4–20 characters of letters, digits, space,
.,_,-— and rejects every other character I tried (/ # : @ % + , ( & = ~ ! * ', tab, and any non-ASCII such asø/é) under the same "must have length between 4 and 20" message. So the local check enforces the real rule (ORDER_ID_PATTERN = /^[A-Za-z0-9 ._-]{4,20}$/, public;assertValidOrderId()public for pre-validation) with a message that names it and the character count. It throws a newSetono\Quickpay\Exception\InvalidArgumentException— an SPL\InvalidArgumentExceptionthat also implementsQuickpayException(so bothcatchstyles work);CollectionRequestOptionsnow throws it too (BC-safe subclass). The property stays a plain public string; only the constructor validates.4.
CreateLinkRequest::$paymentMethodsaccepts a listConstructor takes
string|list<string>|nulland joins a list with commas (joinPaymentMethods()is public for reassignments); the property stays?string; an empty list means "not set".Docs
README: Concepts now cover
statereadingpendingduring any async operation (with the pre-operationbalance), that Quickpay doesn't retry a declined operation (a declined auto-capture leaves the paymentnew/authorized), and that a synchronized decline is a2xx; the new helpers with a decline-check snippet; theorderIdrule; thepaymentMethodslist. CHANGELOG entry added to the current release list (rides in 1.1.0 if merged before #24; say so if you'd rather ship in 1.2.0).Tests: 201 total (was 175 on
1.x); predicates on every code family, views with unordered/rejected/pending operations, live-verified valid/invalidorder_idtables, list/string/emptypaymentMethods, exception hierarchy.