Repository navigation
feat: add PayPal gateway - #7
Merged
Merged
Conversation
4 tasks done
ishad05
marked this pull request as draft
October 11, 2026 09:04
ishad05
marked this pull request as ready for review
October 11, 2026 09:41
ishad05
marked this pull request as draft
October 12, 2026 06:46
ishad05
force-pushed
the
feat/paypal-gateway
branch
2 times, most recently
from
October 12, 2026 15:47
cf28761 to
fc1fa2d
Compare
A consumer building a gateway settings screen had to keep its own list of gateways, with each one's description, docs link and settings DocType, so every new gateway meant a change in each consumer. - api.get_gateways() returns a GatewayInfo (name, settings_doctype, description, docs_url, capabilities, logos) for every gateway Settings Single on the site, enabled or not, including another app's gateways. - The contract gains description, docs_url and capabilities class attributes. REUSABLE_CHECKOUT says the checkout link stays payable after the shopper leaves it. - Razorpay, Stripe, Telr and Tabby describe themselves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ishad05
force-pushed
the
feat/paypal-gateway
branch
from
October 12, 2026 17:10
fc1fa2d to
633479e
Compare
harshtandiya
left a comment
There was a problem hiding this comment.
left a few comments. the two that need fixing before merge are the capture inside the webhook and the token cache key, rest is smaller stuff.
also:
- this is a new doctype + api going into
v1-stable. shouldn't it go to main and get backported? - title says "per-gateway currency support" but
supported_currenciesis already on v1-stable. alsoget_gatewaysand paypal are two separate things, would be cleaner as two PRs - docs on docs.bwh.tech need updating for
get_gatewaysand paypal
5 tasks
A tuple of capability strings and a constant carried one flag that nothing in this app reads. reusable_checkout: bool says the same on the contract and on GatewayInfo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ishad05
force-pushed
the
feat/paypal-gateway
branch
from
October 14, 2026 09:17
633479e to
a666a77
Compare
get_gateways imported every `% Gateway Settings` controller, so one DocType left behind by a removed app, or by code on another branch, raised ImportError and broke every screen that lists gateways, such as a storefront checkout. It is now skipped and logged to the file log. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PayPal Orders v2 behind the gateway contract, on the base/live/stub
service-client layout:
- PayPal Gateway Settings (Sandbox/Live client credentials, webhook ID,
return URLs) and services/paypal/{base,live,stub}.py.
- Checkout creates an order and sends the shopper to PayPal's approval
link; the return page and the CHECKOUT.ORDER.APPROVED webhook both
capture, and losing that race (ORDER_ALREADY_CAPTURED) counts as paid.
- Webhooks are verified by calling PayPal back with the webhook ID.
- supported_currencies() leaves out what PayPal cannot settle (SAR, AED,
KWD, BHD, QAR, INR); HUF, JPY and TWD are charged in whole units.
- Refunds go against the completed capture and report processed or
pending; get_refund_status settles a pending one, get_transaction_ref
returns the capture, and create_session honours return_url.
- get_gateways() lists PayPal with REUSABLE_CHECKOUT: an approval link
stays payable until the order is captured or expires.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fixes for the PayPal gateway. - CHECKOUT.ORDER.APPROVED no longer captures inside the guest webhook. It queues sync_status, as Administrator, for our own Pending request with that order_ref and returns None. An order that is not ours is never captured, and a declined capture fails the job, not the delivery. - The cached access token is keyed on mode and client id, so new credentials never create orders in the old account. - Refund webhooks: PAYMENT.CAPTURE.REFUNDED, PAYMENT.CAPTURE.REVERSED, PAYMENT.REFUND.PENDING and PAYMENT.REFUND.FAILED. A dashboard refund is found through the capture id in the refund's `up` link. - Webhook verification stays a call back to PayPal, documented as the one deliberate exception to local signature checks. - Smaller: only COMPLETED is mapped for refunds, the `approve` link fallback is gone, supports_currency replaces validate_paypal_currency, and the live client reads the error detail once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up review fixes. - WebhookEvent gains `sync_status`: the request asks the gateway instead of applying `status`. CHECKOUT.ORDER.APPROVED returns one, so the capture runs in process_webhook with the gateway check, event id dedupe and Integration Request log. A failed capture is a Failed request that retry_webhook can replay, instead of a lost background job. - A request that is no longer Pending is not synced, so a closed request never captures. - A PayPal refund with no status stays pending instead of reading as Processed. - PAYMENT.CAPTURE.REVERSED is no longer recorded as a refund. PayPal can reinstate a disputed capture with no webhook, and a settled Gateway Refund never changes. - The PayPal-only sync_payment_request job is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Second review round. - PayPal completes an order once captured, even while the capture waits for an eCheck or review, or was declined. get_payment_status now reads the capture: settled is Paid, DECLINED or FAILED is Not Paid, anything else stays Pending. PAYMENT.CAPTURE.DENIED syncs the request, and PAYMENT.CAPTURE.COMPLETED still marks it paid. - sync_status asks the gateway only while the request is Pending. Asking may capture, so the Sync Status button or another app calling it can no longer take money for a closed request and drop the answer. - services.GatewayUnavailableError marks a 5xx, a 429 or a dropped connection. process_webhook logs it as Failed, commits and raises RetryBackgroundJobError, so Frappe retries a capture PayPal could not answer and only a real decline waits for retry_webhook. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ishad05
force-pushed
the
feat/paypal-gateway
branch
from
October 14, 2026 10:01
05c6458 to
23feae1
Compare
AGENTS.md and README.md both gained a gateway on each side; kept both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PayU arrived from v1-stable without the description and docs_url that get_gateways() shows for every other gateway. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
harshtandiya
marked this pull request as ready for review
October 15, 2026 08:11
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.
Summary
CHECKOUT.ORDER.APPROVEDwebhook arrives.api.get_gateways()). Until feat(api): describe every installed gateway for settings screens #27 merges, its 3 commits show here too. The PayPal changes are the last 4 commits.Changes
PayPal Gateway SettingsDocType: Sandbox/Live credentials, webhook ID, redirect URLsservices/paypal/{base,live,stub}.pyservice client, with stub-driven testsWebhookEvent.sync_status: a webhook can ask the request to sync with the gatewayreusable_checkout = True: an approval link stays payable until the order is captured or expiresWorth a look
PayPal-Request-Id, andORDER_ALREADY_CAPTUREDcounts as success.retry_webhook.verify-webhook-signatureAPI. This is a deliberate exception to local signature checks, recorded in AGENTS.md.PayPal-Request-Id, so a second partial refund isn't replayed as the first. The row lock and refund ledger prevent double refunds.PAYMENT.CAPTURE.REVERSEDis ignored, because PayPal can reinstate the money without sending a webhook.Testing
Setup
/api/method/bwh_payments.bwh_payments.webhook.handle?gateway=PayPalforCHECKOUT.ORDER.APPROVED,PAYMENT.CAPTURE.COMPLETED,PAYMENT.CAPTURE.DENIED,PAYMENT.CAPTURE.REFUNDED,PAYMENT.REFUND.PENDINGandPAYMENT.REFUND.FAILED, and save the webhook ID.PayPal.PayPalMode of Payment a default account for each company.IMG_5680.MOV
🤖 Generated with Claude Code