Skip to content

feat(payments): explicit PayPal capture state handling (#377) - #1104

Merged
Calebux merged 1 commit into
Calebux:mainfrom
vibenedict:feat/paypal-explicit-capture-states-377
Jul 24, 2026
Merged

feat(payments): explicit PayPal capture state handling (#377)#1104
Calebux merged 1 commit into
Calebux:mainfrom
vibenedict:feat/paypal-explicit-capture-states-377

Conversation

@vibenedict

Copy link
Copy Markdown
Contributor

Summary

Relates to #377.

The real PayPal order/capture flow already exists in PaymentService (created in prior work — d718702, a1045c7), so this PR focuses on the one acceptance-criteria gap that was still weak: "Handle declined/failed states explicitly."

Previously the capture path treated every non-COMPLETED status as one generic error:

if (status === 'COMPLETED' && captureId) { /* success */ }
else { error: `Payment capture failed with status: ${status}` }

So a genuinely declined card, a transient failure, and a payment held for review (PENDING) were indistinguishable to UI/API callers.

Changes

client/lib/payment-service.ts — branch on the PayPal capture status explicitly:

Status Result
COMPLETED success (now also guards against a missing capture ID)
DECLINED / FAILED explicit permanent-failure error, including PayPal's status_details.reason when present
PENDING surfaced with requiresAction so it's persisted as pending and finalized by the webhook, instead of being reported as a hard success
unknown generic fallback error

client/lib/paypal-service.ts — add status_details.reason to PayPalCaptureResponse so the decline reason can be surfaced.

Testing

vitest run lib/__tests__/payment-service.test.ts:

  • Added 3 tests (DECLINED, FAILED, PENDING) — all pass.
  • Verified no regressions: the pre-existing failures in the payment test files (Paystack feature-flag gating + a stale DB-log-prefix assertion) fail identically with and without this change — baseline 11 passed / 5 failed14 passed / 5 failed after adding the 3 new passing tests. Those pre-existing failures are unrelated to PayPal and out of scope here.

Note for maintainers: local npm install fails on macOS/arm64 because client/package.json pins the linux-only @rolldown/binding-linux-x64-gnu as a hard dependency and the lockfile omits the darwin binding — worth a separate fix so the vitest suite is runnable off-CI.

🤖 Generated with Claude Code

The PayPal capture path collapsed every non-COMPLETED status into a
single generic error, so callers could not tell a declined card from a
transient failure or a payment held for review.

Handle each documented PayPal capture status explicitly:
- COMPLETED -> success (guards against a missing capture id)
- DECLINED / FAILED -> explicit permanent-failure error, including
  PayPal's status_details.reason when present
- PENDING -> surfaced as requiresAction so it is persisted as pending
  and finalized by the webhook rather than reported as a hard success
- unknown -> generic fallback error

Adds status_details.reason to PayPalCaptureResponse and unit tests for
the DECLINED, FAILED, and PENDING branches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vibenedict
vibenedict requested a review from Calebux as a code owner July 23, 2026 20:39
@Calebux
Calebux merged commit c556c58 into Calebux:main Jul 24, 2026
22 of 33 checks passed
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.

2 participants