Skip to content

Outcome predicates, per-type/last-approved views, order_id validation, paymentMethods list - #27

Merged
loevgaard merged 2 commits into
1.xfrom
feat/outcome-predicates-and-validation
Aug 17, 2026
Merged

Outcome predicates, per-type/last-approved views, order_id validation, paymentMethods list#27
loevgaard merged 2 commits into
1.xfrom
feat/outcome-predicates-and-validation

Conversation

@loevgaard

Copy link
Copy Markdown
Member

Closes #25 — the four items, in the issue's order. All additive.

1. Per-type / last-approved views on Payment

  • latestOperationOfType(OperationType|string $type): ?Operation — newest by id, any outcome
  • latestApprovedOperation(): ?Operation — newest approved, any type: where the money actually is; a trailing rejected or pending attempt doesn't mask it
  • hasApprovedOperation(OperationType|string|null $type = null): bool
  • hasPendingOperation(OperationType|string|null $type = null): bool (widened; still parameter-less compatible)

"Latest" is defined once (latestOf(): highest id) and latestOperation() uses it too — settling the id-vs-list-position inconsistency the issue mentions.

2. Outcome predicates on Operation

  • hasOutcome()!pending
  • isDeclined()hasOutcome() && !isApproved(); docblock spells out 4xxxx/5xxxx/3xxxx and that a synchronized decline is a 2xx with the decline on the operation. isApproved()/isDeclined() are mutually exclusive once there is an outcome (pinned by a test).

The throwOnDecline idea is left out, as the issue suggests — separate discussion.

3. order_id validation in CreatePaymentRequest

Verified 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 new Setono\Quickpay\Exception\InvalidArgumentException — an SPL \InvalidArgumentException that also implements QuickpayException (so both catch styles work); CollectionRequestOptions now throws it too (BC-safe subclass). The property stays a plain public string; only the constructor validates.

4. CreateLinkRequest::$paymentMethods accepts a list

Constructor takes string|list<string>|null and 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 state reading pending during any async operation (with the pre-operation balance), that Quickpay doesn't retry a declined operation (a declined auto-capture leaves the payment new/authorized), and that a synchronized decline is a 2xx; the new helpers with a decline-check snippet; the orderId rule; the paymentMethods list. 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/invalid order_id tables, list/string/empty paymentMethods, exception hierarchy.

…, 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

codecov Bot commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.13%. Comparing base (3762a69) to head (1cef519).

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.
📢 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.

Comment thread src/Request/Payment/CreateLinkRequest.php Outdated
@loevgaard
loevgaard merged commit a32317c into 1.x Aug 17, 2026
36 checks passed
@loevgaard
loevgaard deleted the feat/outcome-predicates-and-validation branch August 17, 2026 12:41
loevgaard added a commit that referenced this pull request Aug 17, 2026
Move the #26 / #27 entries out of 1.1.0 into a 1.2.0 section (Added / Changed /
BC notes) and add the compare links. 1.1.0 stays the DX-review release
(tagged before #27/#26 landed); 1.2.0 is the two payum-quickpay follow-ups.
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.

Result helpers, outcome predicates and request validation that setono/payum-quickpay still hand-rolls

1 participant