Skip to content

feat(connector): implement IncrementalAuthorization for paypal (Card + Wallet(PayPal)) - #2032

Open
shuklatushar226 wants to merge 5 commits into
mainfrom
feat/paypal_grace
Open

feat(connector): implement IncrementalAuthorization for paypal (Card + Wallet(PayPal))#2032
shuklatushar226 wants to merge 5 commits into
mainfrom
feat/paypal_grace

Conversation

@shuklatushar226

@shuklatushar226 shuklatushar226 commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

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:

POST {base}/v2/payments/authorizations/{authorization_id}/reauthorize

IncrementalAuthorization was removed from paypal.rs's
macro_connector_flow_status_impls!(not_implemented: [...]) list and implemented with the standard UCS
macro_connector_implementation! idiom (cybersource-shaped). The authorization id is read out of
PaypalMeta in connector_feature_data. Both HTTP 201 (normal) and HTTP 200 (PayPal's
idempotent 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

  • Added IncrementalAuthorization to create_all_prerequisites! and a new
    macro_connector_implementation! block in paypal.rs, with get_headers (OAuth bearer +
    partner-attribution headers) and get_url (authorization id from PaypalMeta).
  • Added PaypalIncrementalAuthRequest / PaypalIncrementalAuthResponse (authorization-2 schema) and
    their TryFrom implementations in paypal/transformers.rs, plus the
    AUTHORIZATIONS_PATH / REAUTHORIZE_ACTION constants and a paypal_err_ctx helper that attaches
    doc URLs and suggested actions to every IntegrationError raised by the flow.
  • Fixed a latent bug in the previously-dead PaypalIncrementalStatus enum: the PARTIALLYCAPTURED /
    PENDINGREVIEW variants serialized without the underscore PayPal actually sends. Renamed to
    CamelCase so SCREAMING_SNAKE_CASE produces the correct wire values (PARTIALLY_CAPTURED,
    PENDING_REVIEW), added Expired, and added #[serde(other)] Unknown with a tracing::warn so an
    undocumented status is reconciled as Unresolved rather than failing deserialization.

Why domain_types/src/types.rs is in scope (required core plumbing)

PaymentFlowData::foreign_try_from(PaymentServiceIncrementalAuthorizationRequest, ...) hardcoded
access_token: None and connector_feature_data: None. As a result PayPal's OAuth bearer header could
never be built and the flow was unreachable — it returned FailedToObtainAuthType regardless of what
the connector code did. The fix mirrors the existing Capture impl exactly: it threads
state.access_token and the caller-supplied connector_feature_data through onto PaymentFlowData.

Behaviour change that affects other flows — please read

Paypal::build_error_response is shared by Capture / Void / Refund / RSync. It previously hardcoded
NO_ERROR_CODE / NO_ERROR_MESSAGE whenever PayPal's details[] array was empty, discarding the
top-level name / message that 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[].issue priority path is unchanged, so any error
that 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.rs
  • crates/integrations/connector-integration/src/connectors/paypal/transformers.rs
  • crates/types-traits/domain_types/src/types.rs

gRPC Test Results

Status: PASS

grpcurl output (credentials redacted)
1. Access token (prerequisite)
   types.MerchantAuthenticationService/CreateServerAuthenticationToken

   Response:
   {
     "accessToken": { ... },
     "expiresInSeconds": "32400",
     "status": "OPERATION_STATUS_SUCCESS",
     "statusCode": 200
   }

--------------------------------------------------------------------------------

2. Authorize (Card, MANUAL capture, request_incremental_authorization=true)
   types.PaymentService/Authorize

   grpcurl -plaintext \
     -H 'x-connector: paypal' -H 'x-auth: body-key' \
     -H 'x-api-key: <REDACTED>' -H 'x-key1: <REDACTED>' \
     -H 'x-merchant-id: <REDACTED>' -H 'x-tenant-id: default' \
     -H 'x-request-id: auth_paypal_incauth_001' \
     -d '{
       "merchant_transaction_id": "test_paypal_incauth_001",
       "amount": {"minor_amount": 1000, "currency": "USD"},
       "payment_method": {"card": {
         "card_number": {"value": "<REDACTED>"},
         "card_exp_month": {"value": "<REDACTED>"},
         "card_exp_year": {"value": "<REDACTED>"},
         "card_cvc": {"value": "<REDACTED>"},
         "card_holder_name": {"value": "John Doe"}
       }},
       "capture_method": "MANUAL",
       "request_incremental_authorization": true,
       "address": {"billing_address": {
         "first_name": {"value": "John"}, "last_name": {"value": "Doe"},
         "line1": {"value": "123 Test St"}, "city": {"value": "San Jose"},
         "state": {"value": "CA"}, "zip_code": {"value": "95131"},
         "country_alpha2_code": "US", "email": {"value": "test@example.com"}
       }},
       "auth_type": "NO_THREE_DS", "enrolled_for_3ds": false,
       "return_url": "https://example.com/return",
       "webhook_url": "https://example.com/webhook",
       "state": {"access_token": {"token": {"value": "<REDACTED>"},
                 "expires_in_seconds": 32400, "token_type": "Bearer"}}
     }' localhost:8101 types.PaymentService/Authorize

   Response:
   {
     "merchantTransactionId": "test_paypal_incauth_001",
     "connectorTransactionId": "52Y49830NP854041V",
     "status": "AUTHORIZED",
     "statusCode": 201,
     "incrementalAuthorizationAllowed": true,
     "connectorFeatureData": {"value": "{\"authorize_id\":\"4LH39595US586263G\",\"capture_id\":null,\"incremental_authorization_id\":\"4LH39595US586263G\",\"psync_flow\":\"AUTHORIZE\",\"next_action\":null,\"order_id\":null}"},
     "connectorReferenceId": "test_paypal_incauth_001"
   }

--------------------------------------------------------------------------------

3. IncrementalAuthorization against LIVE PayPal sandbox
   types.PaymentService/IncrementalAuthorization

   grpcurl -plaintext \
     -H 'x-connector: paypal' -H 'x-auth: body-key' \
     -H 'x-api-key: <REDACTED>' -H 'x-key1: <REDACTED>' \
     -H 'x-merchant-id: <REDACTED>' -H 'x-tenant-id: default' \
     -H 'x-request-id: incauth_paypal_002' \
     -d '{
       "merchant_authorization_id": "test_paypal_incauth_001",
       "connector_transaction_id": "52Y49830NP854041V",
       "amount": {"minor_amount": 1500, "currency": "USD"},
       "reason": "order total increased",
       "connector_feature_data": {"value": "{\"authorize_id\":\"4LH39595US586263G\",\"capture_id\":null,\"incremental_authorization_id\":\"4LH39595US586263G\",\"psync_flow\":\"AUTHORIZE\",\"next_action\":null,\"order_id\":null}"},
       "state": {"access_token": {"token": {"value": "<REDACTED>"},
                 "expires_in_seconds": 32400, "token_type": "Bearer"}}
     }' localhost:8101 types.PaymentService/IncrementalAuthorization

   Response — HTTP 422 REAUTHORIZATION_TOO_SOON:
     Code: Internal, grpc-message="REAUTHORIZATION_TOO_SOON"
     CONNECTOR_ERROR_RESPONSE code="REAUTHORIZATION_TOO_SOON" status=422
     reason="description - A reauthorization is only allowed once from Day 4 to Day 29 since the
             date of the original authorization. ;"
     debug_id="f854943117ab3"

   This is the expected and acceptable live result: PayPal only permits reauthorization from Day 4
   to Day 29 after the original authorization, so a freshly created sandbox authorization can never
   succeed. It proves the request was built, signed, routed and the error correctly parsed.

   Exact wire contract emitted (matches the hyperswitch reference):
     POST https://api-m.sandbox.paypal.com/v2/payments/authorizations/4LH39595US586263G/reauthorize
     headers: Content-Type: application/json
              Authorization: Bearer <REDACTED>
              PayPal-Request-Id: <REDACTED>
              Prefer: return=representation
              PayPal-Partner-Attribution-Id: HyperSwitchlegacy_Ecom
     body:    {"amount":{"currency_code":"USD","value":"15.00"}}
     -> real error code from details[].issue (not NO_ERROR_CODE), real HTTP status (not None),
        real debug_id.

--------------------------------------------------------------------------------

4. Success path verification (HTTP 201)
   Same binary with base_url pointed at a local stub replaying PayPal's documented
   `authorization-2` 201 response body, since live PayPal cannot return 201 inside the honor period.

   Outbound request observed:
     POST /v2/payments/authorizations/4LH39595US586263G/reauthorize
     body={"amount":{"currency_code":"USD","value":"15.00"}}

   Response:
   {
     "connectorAuthorizationId": "0AW2184448108334S",
     "status": "AUTHORIZATION_SUCCESS",
     "statusCode": 201
   }

--------------------------------------------------------------------------------

5. Idempotent replay path (HTTP 200)
   Identical call, stub answers 200.

   Response:
   {
     "connectorAuthorizationId": "0AW2184448108334S",
     "status": "AUTHORIZATION_SUCCESS",
     "statusCode": 200
   }

   Confirms 201 and 200 are both treated as success, the full `authorization-2` body deserializes,
   CREATED maps to AUTHORIZATION_SUCCESS, and the NEW authorization id is surfaced as
   `connector_authorization_id`.

Validation Checklist

  • cargo build -p connector-integration passed with zero errors
  • cargo clippy -p connector-integration -p domain_types clean
  • cargo fmt --check clean
  • grpcurl Authorize returned success status (201)
  • IncrementalAuthorization verified end-to-end against live PayPal sandbox (422
    REAUTHORIZATION_TOO_SOON — the only reachable live outcome inside the honor period) and on both
    the 201 and 200 success paths against a stub replaying PayPal's documented response body
  • No credentials in committed source code
  • Only the three intended files modified

Follow-up: Wallet(PayPal) coverage for IncrementalAuthorization

(Added by commit 39c85e1 on this same branch — an add-payment-method follow-up to the Card work
described above, which landed in e0a969e.)

Summary

Extends IncrementalAuthorization 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, and the
existing 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:

  • the path selector is an authorization id, not an order id or a funding instrument;
  • the request body accepts only amount;
  • the authorization-2 response carries no funding-instrument block;
  • PayPal's own OpenAPI operation description is in fact written for the wallet case —
    "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:

Funding source Authorize leg that mints the authorization id
Card the create-order call itself (POST /v2/checkout/orders with payment_source.card)
Wallet — PaypalRedirect the second Authorize carrying connector_order_idPOST /v2/checkout/orders/{order_id}/authorize (create-order returns PAYER_ACTION_REQUIRED with no payments block, so no id exists pre-approval)
Wallet — PaypalSdk the single Authorize, with the SDK token used as the order id → POST /v2/checkout/orders/{sdk_token}/authorize

Changes

  • paypal.rs — comment on the IncrementalAuthorization macro_connector_implementation! block
    recording why it deliberately carries no funding-source branch.
  • paypal/transformers.rs — doc comment on extract_incremental_authorization_id recording which
    Authorize leg mints the authorization id per funding source, and why the function returns None
    rather than erroring for orders that legitimately have no authorization yet.

Files Modified

  • crates/integrations/connector-integration/src/connectors/paypal.rs
  • crates/integrations/connector-integration/src/connectors/paypal/transformers.rs

gRPC Test Results — Wallet(PayPal)

Status: PASS

Summary of what was verified:

  • Wallet(PaypalRedirect) create-order against real PayPal sandboxPAYER_ACTION_REQUIRED,
    incremental_authorization_id: null. Correct — no authorization exists before buyer approval.
  • Second Authorize with connector_order_id against real PayPal → 422 ORDER_NOT_APPROVED
    (expected; buyer approval requires a browser). Proves the wallet leg routes to
    /v2/checkout/orders/{id}/authorize with no card gating, and that the real PayPal error code
    is surfaced verbatim rather than NO_ERROR_CODE.
  • Direct sandbox call (create order → confirm-payment-source → authorize) confirmed the live
    wallet order-authorize response does contain "intent": "AUTHORIZE", so it deserializes into
    PaypalOrdersResponse (the first untagged variant) and reaches
    extract_incremental_authorization_id — the id is not silently lost to the
    PaypalThreeDsResponse fallback.
  • Post-approval wallet order-authorize body replayed against a local stubAUTHORIZED,
    incrementalAuthorizationAllowed: true, and connectorFeatureData carrying
    authorize_id / incremental_authorization_id. Same result for the PaypalSdk sub-variant.
  • IncrementalAuthorization driven from that wallet-derived id → stub received
    POST /v2/payments/authorizations/3XY12345AB6789012/reauthorize with body
    {"amount":{"currency_code":"USD","value":"15.00"}}; response mapped to
    connectorAuthorizationId: "9WV12345KL6789012", status: AUTHORIZATION_SUCCESS,
    statusCode: 201.
  • IncrementalAuthorization against real PayPal with a real authorization id → 422
    REAUTHORIZATION_TOO_SOON. Expected and correctly mapped — PayPal only permits reauthorization
    on days 4–29 after the original authorization, so a freshly minted sandbox authorization can
    never succeed.
  • cargo check -p connector-integration clean; clippy clean.
Full grpcurl transcript (credentials redacted)
Common headers ($H):
  -H 'x-connector: paypal' -H 'x-auth: body-key' -H 'x-api-key: <REDACTED>'
  -H 'x-key1: <REDACTED>' -H 'x-merchant-id: <REDACTED>' -H 'x-tenant-id: default'

--- T0: CreateServerAuthenticationToken (REAL PayPal) ---
grpcurl -plaintext $H -H 'x-request-id: req_pp_final_000' -d '{"connector":"PAYPAL"}' \
  localhost:27311 types.MerchantAuthenticationService/CreateServerAuthenticationToken
{
  "accessToken": { "value": "<REDACTED>" },
  "expiresInSeconds": "29429",
  "status": "OPERATION_STATUS_SUCCESS",
  "statusCode": 200
}

--- T1: Authorize — Wallet(PaypalRedirect) create-order, intent=AUTHORIZE (REAL PayPal) ---
grpcurl -plaintext $H -H 'x-request-id: req_pp_final_001' -d '{
  "merchant_transaction_id": "pp_wallet_final_001",
  "amount": {"minor_amount": 1000, "currency": "USD"},
  "payment_method": {"paypal_redirect": {}},
  "capture_method": "MANUAL",
  "auth_type": "NO_THREE_DS",
  "request_incremental_authorization": true,
  "return_url": "https://example.com/return",
  "complete_authorize_url": "https://example.com/complete",
  "webhook_url": "https://example.com/webhook",
  "address": {"billing_address": {"first_name":{"value":"John"},"last_name":{"value":"Doe"},
      "line1":{"value":"123 Test St"},"city":{"value":"San Jose"},"state":{"value":"CA"},
      "zip_code":{"value":"95131"},"country_alpha2_code":"US","email":{"value":"test@example.com"}}},
  "state": {"access_token": {"token": {"value":"<REDACTED>"},
      "expires_in_seconds": 30240, "token_type": "Bearer"}}
}' localhost:27311 types.PaymentService/Authorize
{
  "merchantTransactionId": "pp_wallet_final_001",
  "connectorTransactionId": "69X69486CY2070043",
  "status": "AUTHENTICATION_PENDING",
  "statusCode": 200,
  "redirectionData": { "form": { "endpoint": "https://www.sandbox.paypal.com/checkoutnow",
      "method": "HTTP_METHOD_GET", "formFields": { "token": "69X69486CY2070043" } } },
  "connectorFeatureData": { "value": "{\"authorize_id\":null,\"capture_id\":null,\"incremental_authorization_id\":null,\"psync_flow\":\"AUTHORIZE\",\"next_action\":null,\"order_id\":null}" },
  "connectorReferenceId": "pp_wallet_final_001"
}
=> incremental_authorization_id is null, which is CORRECT: no authorization exists before the
   buyer approves at PayPal.

--- T2: Authorize #2 — order-authorize on the still-unapproved order (REAL PayPal) ---
grpcurl -plaintext $H -H 'x-request-id: req_pp_final_002' \
  -d '<T1 payload + "connector_order_id":"69X69486CY2070043">' \
  localhost:27311 types.PaymentService/Authorize
ERROR: grpc-message="ORDER_NOT_APPROVED", status=422
  details: ORDER_NOT_APPROVED / "Payer has not yet approved the Order for payment. Please redirect
  the payer to the 'rel':'approve' url returned as part of the HATEOAS links within the Create Order
  call or provide a valid payment_source in the request."  debug_id=f739497009865
(EXPECTED — buyer approval needs a browser. Proves the wallet leg routes to
 /v2/checkout/orders/{id}/authorize with no card gating, and that the real PayPal issue code is
 surfaced instead of NO_ERROR_CODE.)

--- T3: Authorize #2 — post-approval PayPal wallet body replayed via local stub ---
grpcurl -plaintext $H -H 'x-request-id: req_pp_final_003' -d '<same payload as T2>' \
  localhost:27321 types.PaymentService/Authorize
{
  "merchantTransactionId": "pp_wallet_final_001",
  "connectorTransactionId": "4N051495R5567400G",
  "status": "AUTHORIZED",
  "statusCode": 201,
  "incrementalAuthorizationAllowed": true,
  "connectorFeatureData": { "value": "{\"authorize_id\":\"3XY12345AB6789012\",\"capture_id\":null,\"incremental_authorization_id\":\"3XY12345AB6789012\",\"psync_flow\":\"AUTHORIZE\",\"next_action\":null,\"order_id\":null}" },
  "connectorReferenceId": "pp_wallet_ia_001"
}
Stub observed: POST /v2/checkout/orders/4N051495R5567400G/authorize, empty body,
headers `prefer: return=representation`, `paypal-request-id: <REDACTED>`,
`paypal-partner-attribution-id: HyperSwitchlegacy_Ecom`.

--- T4: Authorize — Wallet(PaypalSdk) single-call variant (stub) ---
grpcurl -plaintext $H -H 'x-request-id: req_pp_final_004' -d '{
  ..., "merchant_transaction_id":"pp_wallet_sdk_001",
  "payment_method": {"paypal_sdk": {"token": {"value":"4N051495R5567400G"}}}, ...
}' localhost:27321 types.PaymentService/Authorize
{
  "status": "AUTHORIZED",
  "statusCode": 201,
  "incrementalAuthorizationAllowed": true,
  "connectorFeatureData": { "value": "{\"authorize_id\":\"3XY12345AB6789012\",\"capture_id\":null,\"incremental_authorization_id\":\"3XY12345AB6789012\",\"psync_flow\":\"AUTHORIZE\",\"next_action\":null,\"order_id\":null}" }
}
Stub observed: POST /v2/checkout/orders/4N051495R5567400G/authorize (SDK token used as order id).

--- T5: IncrementalAuthorization — wallet-derived authorization id, PayPal 201 replayed (stub) ---
grpcurl -plaintext $H -H 'x-request-id: req_pp_final_005' -d '{
  "merchant_authorization_id": "pp_wallet_ia_001",
  "connector_transaction_id": "4N051495R5567400G",
  "amount": {"minor_amount": 1500, "currency": "USD"},
  "reason": "wallet incremental authorization test",
  "connector_feature_data": {"value": "{\"authorize_id\":\"3XY12345AB6789012\",\"capture_id\":null,\"incremental_authorization_id\":\"3XY12345AB6789012\",\"psync_flow\":\"AUTHORIZE\",\"next_action\":null,\"order_id\":null}"},
  "state": {"access_token": {"token": {"value":"<REDACTED>"},
      "expires_in_seconds": 30240, "token_type": "Bearer"}}
}' localhost:27321 types.PaymentService/IncrementalAuthorization
{
  "connectorAuthorizationId": "9WV12345KL6789012",
  "status": "AUTHORIZATION_SUCCESS",
  "statusCode": 201
}
Stub observed: POST /v2/payments/authorizations/3XY12345AB6789012/reauthorize
  body {"amount":{"currency_code":"USD","value":"15.00"}}

--- T6: IncrementalAuthorization — REAL PayPal, real authorization id ---
grpcurl -plaintext $H -H 'x-request-id: req_pp_final_006' -d '{
  "merchant_authorization_id": "probe_shape_001",
  "connector_transaction_id": "2LU033217V089323S",
  "amount": {"minor_amount": 1500, "currency": "USD"},
  "reason": "real paypal reauthorize",
  "connector_feature_data": {"value": "{\"authorize_id\":\"1FR795384L687463D\",\"capture_id\":null,\"incremental_authorization_id\":\"1FR795384L687463D\",\"psync_flow\":\"AUTHORIZE\",\"next_action\":null,\"order_id\":null}"},
  "state": {"access_token": {"token": {"value":"<REDACTED>"},
      "expires_in_seconds": 30240, "token_type": "Bearer"}}
}' localhost:27311 types.PaymentService/IncrementalAuthorization
ERROR: grpc-message="REAUTHORIZATION_TOO_SOON", status=422
  details: REAUTHORIZATION_TOO_SOON / "A reauthorization is only allowed once from Day 4 to Day 29
  since the date of the original authorization."  debug_id=f457760674c19
(EXPECTED and correctly mapped — real PayPal issue code surfaced, not NO_ERROR_CODE.)

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.

  1. crates/types-traits/domain_types/src/types.rs:9386 hardcodes
    payment_method: PaymentMethod::Card, //TODO when building PaymentFlowData for the
    IncrementalAuthorization request. Harmless for PayPal (nothing on the reauthorize path reads
    it) but semantically wrong for a wallet payment. Fixing it properly needs a proto field that does
    not currently exist.
  2. Reauthorization mints a new authorization id, but
    PaymentServiceIncrementalAuthorizationResponse has no connector_feature_data field, so a
    caller's stored PaypalMeta.authorize_id still holds the superseded id for a later
    Capture/Void.

Validation Checklist — Wallet(PayPal)

  • cargo check -p connector-integration passed with zero errors
  • cargo clippy clean
  • Wallet(PaypalRedirect) Authorize verified against live PayPal sandbox
  • Wallet order-authorize response shape confirmed against live PayPal (carries
    "intent": "AUTHORIZE", so it reaches extract_incremental_authorization_id)
  • Wallet-derived IncrementalAuthorization verified end-to-end (201 success path via stub; live
    PayPal returns the expected REAUTHORIZATION_TOO_SOON inside the honor period)
  • No credentials in committed source code
  • Only the two intended connector files modified by this follow-up commit

@shuklatushar226
shuklatushar226 requested a review from a team as a code owner August 2, 2026 22:14
@shuklatushar226
shuklatushar226 requested a review from a team as a code owner August 2, 2026 22:14
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.
@hyperswitch-bot
hyperswitch-bot Bot requested a review from a team as a code owner August 2, 2026 22:34
@XyneSpaces

Copy link
Copy Markdown

[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, connector_feature_data parsing, and each PaypalIncrementalStatusAuthorizationStatus branch so regressions are caught in CI.

…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).
@shuklatushar226 shuklatushar226 changed the title feat(connector): implement IncrementalAuthorization for paypal feat(connector): implement IncrementalAuthorization for paypal (Card + Wallet(PayPal)) Aug 2, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants