feat(connector): implement IncrementalAuthorization for paypal (Card + Wallet(PayPal)) - #2032
Open
shuklatushar226 wants to merge 5 commits into
Open
feat(connector): implement IncrementalAuthorization for paypal (Card + Wallet(PayPal))#2032shuklatushar226 wants to merge 5 commits into
shuklatushar226 wants to merge 5 commits into
Conversation
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
|
[blocking] This PR implements a new money-moving connector flow but does not commit tests for the request serialization, URL construction, or response-status mapping. Please add unit tests covering the amount/currency body, |
…thorization
Extend the IncrementalAuthorization flow for Paypal to Wallet(PayPal), covering
both the PaypalRedirect and PaypalSdk sub-variants.
No functional change was required. PayPal's REAUTHORIZE endpoint
(POST /v2/payments/authorizations/{id}/reauthorize) is funding-source agnostic:
the path selector is an authorization id, the request body accepts only `amount`,
and the `authorization-2` response carries no funding-instrument block. The
existing implementation was verified in code and at runtime to already work for
Wallet(PayPal), so the diff is documentation only.
- paypal.rs: comment on the IncrementalAuthorization macro block recording why it
deliberately carries no funding-source branch.
- transformers.rs: doc comment on `extract_incremental_authorization_id`
recording which Authorize leg mints the authorization id per funding source
(Card: create-order; Wallet/PaypalRedirect: the second Authorize carrying
`connector_order_id`; Wallet/PaypalSdk: the single Authorize using the SDK
token as the order id).
Two findings from the parity review against the hyperswitch reference: - `PaypalIncrementalStatus` derived `strum::Display` without a casing directive. `serde(rename_all)` governs only the wire format, so a status PayPal sent as `PARTIALLY_CAPTURED` was logged as `PartiallyCaptured`. Added `strum(serialize_all = "SCREAMING_SNAKE_CASE")` so logs quote the value PayPal actually sent. - The `connector_feature_data` parse failure on the IncrementalAuthorization request reported `field_name: "unknown"` with no suggested action. Named the field and filled in the context — this is the field carrying the authorization id the reauthorize call targets, so an opaque error here is the difference between a one-line fix and a debugging session.
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
Implement IncrementalAuthorization flow for the Paypal connector.
PayPal has no dedicated incremental-authorization API, so the flow is mapped onto PayPal's
REAUTHORIZE operation:
IncrementalAuthorizationwas removed frompaypal.rs'smacro_connector_flow_status_impls!(not_implemented: [...])list and implemented with the standard UCSmacro_connector_implementation!idiom (cybersource-shaped). The authorization id is read out ofPaypalMetainconnector_feature_data. Both HTTP 201 (normal) and HTTP 200 (PayPal'sidempotent replay of a previously seen
PayPal-Request-Id) are treated as success.This implementation was generated and validated by GRACE (automated connector integration pipeline).
Changes
IncrementalAuthorizationtocreate_all_prerequisites!and a newmacro_connector_implementation!block inpaypal.rs, withget_headers(OAuth bearer +partner-attribution headers) and
get_url(authorization id fromPaypalMeta).PaypalIncrementalAuthRequest/PaypalIncrementalAuthResponse(authorization-2schema) andtheir
TryFromimplementations inpaypal/transformers.rs, plus theAUTHORIZATIONS_PATH/REAUTHORIZE_ACTIONconstants and apaypal_err_ctxhelper that attachesdoc URLs and suggested actions to every
IntegrationErrorraised by the flow.PaypalIncrementalStatusenum: thePARTIALLYCAPTURED/PENDINGREVIEWvariants serialized without the underscore PayPal actually sends. Renamed toCamelCase so
SCREAMING_SNAKE_CASEproduces the correct wire values (PARTIALLY_CAPTURED,PENDING_REVIEW), addedExpired, and added#[serde(other)] Unknownwith atracing::warnso anundocumented status is reconciled as
Unresolvedrather than failing deserialization.Why
domain_types/src/types.rsis in scope (required core plumbing)PaymentFlowData::foreign_try_from(PaymentServiceIncrementalAuthorizationRequest, ...)hardcodedaccess_token: Noneandconnector_feature_data: None. As a result PayPal's OAuth bearer header couldnever be built and the flow was unreachable — it returned
FailedToObtainAuthTyperegardless of whatthe connector code did. The fix mirrors the existing
Captureimpl exactly: it threadsstate.access_tokenand the caller-suppliedconnector_feature_datathrough ontoPaymentFlowData.Behaviour change that affects other flows — please read
Paypal::build_error_responseis shared by Capture / Void / Refund / RSync. It previously hardcodedNO_ERROR_CODE/NO_ERROR_MESSAGEwhenever PayPal'sdetails[]array was empty, discarding thetop-level
name/messagethat PayPal always sends on a non-2xx envelope. This PR adds.or(name)/.or(message)fallbacks so real error codes surface instead of the placeholders.This is strictly additive — the existing
details[].issuepriority path is unchanged, so any errorthat already produced a real code produces exactly the same code as before. Only the
previously-placeholder case changes.
Files Modified
crates/integrations/connector-integration/src/connectors/paypal.rscrates/integrations/connector-integration/src/connectors/paypal/transformers.rscrates/types-traits/domain_types/src/types.rsgRPC Test Results
Status: PASS
grpcurl output (credentials redacted)
Validation Checklist
cargo build -p connector-integrationpassed with zero errorscargo clippy -p connector-integration -p domain_typescleancargo fmt --checkcleanREAUTHORIZATION_TOO_SOON— the only reachable live outcome inside the honor period) and on boththe 201 and 200 success paths against a stub replaying PayPal's documented response body
Follow-up: Wallet(PayPal) coverage for IncrementalAuthorization
(Added by commit
39c85e1on this same branch — an add-payment-method follow-up to the Card workdescribed above, which landed in
e0a969e.)Summary
Extends IncrementalAuthorization for Paypal to Wallet(PayPal), covering both the
PaypalRedirectandPaypalSdksub-variants.No functional change was required. PayPal's REAUTHORIZE endpoint
(
POST /v2/payments/authorizations/{id}/reauthorize) is funding-source agnostic, and theexisting implementation was verified — in code and at runtime — to already work for Wallet(PayPal).
The diff is documentation only.
Why the endpoint cannot need a funding-source branch:
amount;authorization-2response carries no funding-instrument block;"Reauthorizes an authorized PayPal account payment, by ID."
What genuinely differs between funding sources is only which Authorize leg mints the
authorization id, and that is upstream of this flow:
POST /v2/checkout/orderswithpayment_source.card)PaypalRedirectconnector_order_id→POST /v2/checkout/orders/{order_id}/authorize(create-order returnsPAYER_ACTION_REQUIREDwith nopaymentsblock, so no id exists pre-approval)PaypalSdkPOST /v2/checkout/orders/{sdk_token}/authorizeChanges
paypal.rs— comment on theIncrementalAuthorizationmacro_connector_implementation!blockrecording why it deliberately carries no funding-source branch.
paypal/transformers.rs— doc comment onextract_incremental_authorization_idrecording whichAuthorize leg mints the authorization id per funding source, and why the function returns
Nonerather than erroring for orders that legitimately have no authorization yet.
Files Modified
crates/integrations/connector-integration/src/connectors/paypal.rscrates/integrations/connector-integration/src/connectors/paypal/transformers.rsgRPC Test Results — Wallet(PayPal)
Status: PASS
Summary of what was verified:
PAYER_ACTION_REQUIRED,incremental_authorization_id: null. Correct — no authorization exists before buyer approval.connector_order_idagainst real PayPal → 422ORDER_NOT_APPROVED(expected; buyer approval requires a browser). Proves the wallet leg routes to
/v2/checkout/orders/{id}/authorizewith no card gating, and that the real PayPal error codeis surfaced verbatim rather than
NO_ERROR_CODE.wallet order-authorize response does contain
"intent": "AUTHORIZE", so it deserializes intoPaypalOrdersResponse(the first untagged variant) and reachesextract_incremental_authorization_id— the id is not silently lost to thePaypalThreeDsResponsefallback.AUTHORIZED,incrementalAuthorizationAllowed: true, andconnectorFeatureDatacarryingauthorize_id/incremental_authorization_id. Same result for thePaypalSdksub-variant.POST /v2/payments/authorizations/3XY12345AB6789012/reauthorizewith body{"amount":{"currency_code":"USD","value":"15.00"}}; response mapped toconnectorAuthorizationId: "9WV12345KL6789012",status: AUTHORIZATION_SUCCESS,statusCode: 201.REAUTHORIZATION_TOO_SOON. Expected and correctly mapped — PayPal only permits reauthorizationon days 4–29 after the original authorization, so a freshly minted sandbox authorization can
never succeed.
cargo check -p connector-integrationclean; clippy clean.Full grpcurl transcript (credentials redacted)
Known items flagged but deliberately NOT fixed here
Both are pre-existing and affect Card identically; they are called out as follow-ups rather than
being bundled into a documentation-only change.
crates/types-traits/domain_types/src/types.rs:9386hardcodespayment_method: PaymentMethod::Card, //TODOwhen buildingPaymentFlowDatafor theIncrementalAuthorizationrequest. Harmless for PayPal (nothing on the reauthorize path readsit) but semantically wrong for a wallet payment. Fixing it properly needs a proto field that does
not currently exist.
PaymentServiceIncrementalAuthorizationResponsehas noconnector_feature_datafield, so acaller's stored
PaypalMeta.authorize_idstill holds the superseded id for a laterCapture/Void.
Validation Checklist — Wallet(PayPal)
cargo check -p connector-integrationpassed with zero errorscargo clippyclean"intent": "AUTHORIZE", so it reachesextract_incremental_authorization_id)PayPal returns the expected
REAUTHORIZATION_TOO_SOONinside the honor period)