feat(paysafe): support Apple Pay / Google Pay recurring (CIT→MIT) - #13410
feat(paysafe): support Apple Pay / Google Pay recurring (CIT→MIT)#13410shuklatushar226 wants to merge 9 commits into
Conversation
12c0b54 to
3e349a1
Compare
|
|
||
| [grpc_client.unified_connector_service] | ||
| ucs_only_connectors = "imerchantsolutions, paytm, phonepe, hyperpg, revolv3, fiservcommercehub, absa_sanlam, interpayments, payconex, dlocal, barclaycard, tsys_transit, jpmorgan, datatrans" # Comma-separated list of connectors that use UCS only | ||
| ucs_only_connectors = "imerchantsolutions, paytm, phonepe, hyperpg, revolv3, fiservcommercehub, absa_sanlam, interpayments, payconex, dlocal, barclaycard, tsys_transit, jpmorgan, datatrans, paysafe" # Comma-separated list of connectors that use UCS only |
There was a problem hiding this comment.
is it confirmed that no merchant uses paysafe through HS
3e349a1 to
84ae16f
Compare
| // integration-test/sandbox/production. The catch is intentionally | ||
| // broad: under `mock_locker` every wallet vault attempt fails the | ||
| // same way, so there is no narrower error to match on. |
There was a problem hiding this comment.
Mock locker behaviour must be changed to accommodate wallet use case as well. instead of handling here separately.
There was a problem hiding this comment.
Done — moved the mock support into the vault layer instead of the call site. vault::call_to_vault now has a mock_locker branch that synthesizes the fingerprint / add-vault responses, mirroring the legacy card locker's add_card_to_vault mock branch. Since wallets, bank_debit and v2 all share call_to_vault, this fixes them together at one choke point, so the special-case wallet fallback (and the broad Err(_) if mock_locker catch) in tokenization.rs is removed entirely. Pushed in 01b77ac.
| .merchant_reference_id(leg2_merchant_reference_id) | ||
| .resource_id(leg2_resource_id) | ||
| .lineage_ids(leg2_lineage_ids); | ||
| let leg2_outcome = Box::pin(unified_connector_service::ucs_logging_wrapper_granular( |
There was a problem hiding this comment.
Here we are moving from a granular approach to composite. Should not be done. Core must handle this.
There was a problem hiding this comment.
Done — the second (wallet vault-conversion) call is no longer in the gateway. execute() is a single granular UCS call again, matching every other granular gateway. Core now sequences the conversion: tokenization::add_payment_method_token runs a second execute_payment_gateway after leg-1, gated by the requires_wallet_vault_conversion capability flag, fully fail-soft. The leg-1 token is handed to the second call via a new connector_feature_data field on PaymentMethodTokenizationData (mapped in the tokenize request builder) instead of the gateway mutating the gRPC request — so each gateway call stays one gRPC call and core owns the sequence, mirroring the create_order_at_connector → add_payment_method_token idiom. Pushed in 01b77ac.
Wires the hyperswitch side of Paysafe wallet recurring so a wallet CIT vaults a reusable payment method that a later MIT can replay. Card recurring already worked; this closes the gaps for Apple Pay and Google Pay. Pairs with the UCS-side connector change (juspay/hyperswitch-prism#1986). - paysafe.rs: `should_call_connector_customer` fires for Apple Pay / Google Pay too (not just Card); new `requires_wallet_vault_conversion` capability opts Paysafe into the wallet-vault-conversion leg. - ConnectorSpecifications: add `requires_wallet_vault_conversion` (default false) + ConnectorEnum forwarding, so core stays connector-agnostic instead of hardcoding a connector name in the token gateway. - payment_method_token_create_gateway.rs: after leg-1 mints a single-use wallet handle, a second tokenize (its own UCS logging wrapper) converts it into a customer-vaulted MULTI_USE handle via connector_feature_data. Fully fail-soft — any leg-2 failure keeps the single-use handle so the payment still succeeds (only a future MIT is not replayable). Needs Clone on GrpcHeadersUcs / UcsReferenceId / UcsResourceId to reuse the request. - payments.rs: run connector tokenization for a one-shot PaymentCreate with confirm=true, not just the standalone Confirm operation, via a new `is_operation_create` helper (+ unit test guarding the Debug-name match). NOTE: this is a general behavior change — any connector with a [tokenization] entry now tokenizes on one-shot create+confirm. - tokenization.rs: under mock_locker (local dev only) the wallet vault call has no backend; degrade to a locker-less payment method so the mandate (connector_mandate_details) is still saved. - config: enable google_pay in the paysafe [tokenization] filter, add paysafe to the apple_pay/google_pay mandate connector lists, and route paysafe UCS-only (the native connector does not implement the wallet flows) — in development.toml and all deployment configs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
84ae16f to
01b77ac
Compare
| // v2 vaulting work in local dev instead of failing on a live call to a vault | ||
| // that isn't running. Only the flows the tokenization path exercises are | ||
| // mocked; the rest fall through to the real call (unchanged behaviour). | ||
| if locker.mock_locker { |
There was a problem hiding this comment.
this again seems like a workaround style implementation.
There was a problem hiding this comment.
Done — moved the mock off inline synthesis onto the established DB-backed convention. The mock_locker branch in call_to_vault now delegates to a dedicated mock_call_to_vault that persists to / reads back from the locker_mock_up table via StorageInterface, mirroring the legacy card locker's mock_call_to_locker_hs. So an add actually inserts a row (genuinely retrievable and deletable) instead of returning throwaway ids, and all five vault flows are covered (add / get-fingerprint / retrieve / delete / entity-create).
Kept it central in call_to_vault rather than at the typed wrappers because ~15 call sites hit call_to_vault directly (cards.rs, payment_methods.rs, tokenization.rs, workflows) — the choke point is the only place that covers them uniformly.
Verified under mock_locker = true: a wallet CIT inserts a locker_mock_up row holding the decrypted vaulting data keyed by the vault id, and CIT→MIT still replays. Pushed in 231d213.
… instead of inline synthesis The `mock_locker` branch in `call_to_vault` synthesized throwaway JSON inline for only the add/get-fingerprint flows. Replace it with a dedicated `mock_call_to_vault` that persists to / reads back from the `locker_mock_up` table via `StorageInterface` — mirroring the legacy card locker's `mock_call_to_locker_hs` — so an add is genuinely retrievable and deletable, and all vault flows (add, get-fingerprint, retrieve, delete, entity-create) are covered. Kept central in `call_to_vault` because ~15 call sites hit it directly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…token stays under clippy::large_futures The leg-2 (Paysafe wallet MULTI_USE conversion) block held an extra RouterData clone plus a second execute_payment_gateway future inline, pushing add_payment_method_token's future to 18176 bytes and tripping clippy::large_futures (-D warnings under v2) in the four flows that await it (authorize / complete_authorize / setup_mandate / external_proxy). Box the cloned router data and the second gateway future so leg-2's state is smaller than leg-1's, which was already under the limit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ta literals in connector tests The PR added `connector_feature_data` to `PaymentMethodTokenizationData` but the square/stax/utils connector-test constructors still built it without the field, failing compilation of the router test target (E0063). Default the field to `None` in those 10 literals. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
deepanshu-iiitu
left a comment
There was a problem hiding this comment.
Connector changes look good
| let is_confirm_leg = is_operation_confirm(&operation) | ||
| || (is_operation_create(&operation) && payment_data.get_payment_attempt().confirm); | ||
|
|
There was a problem hiding this comment.
In case of Payment create with confirm = true / s2s flows the operation will be changed to confirm. Check if_not_create_change_operation in payment create get_trackers
| if locker.mock_locker { | ||
| return mock_call_to_vault::<V>(state, payload).await; | ||
| } |
There was a problem hiding this comment.
Why do we want to have mock_locker flow ?
| match Box::pin(gateway::execute_payment_gateway( | ||
| state, | ||
| conversion_integration, | ||
| &conversion_router_data, | ||
| payments::CallConnectorAction::Trigger, | ||
| None, | ||
| None, | ||
| gateway_context.clone(), | ||
| )) | ||
| .await |
There was a problem hiding this comment.
I was thinking that any additional connector calls for setup_future_usage could be handled within the Payments Core itself. With this modular flow, these calls are expected to be handled by the Payments Core logic rather than by the Payment Methods layer.
Three review comments from @ShankarSinghC on #13410. 1. Remove the dead `is_operation_create` gate (payments.rs) For POST /payments with confirm=true the operation is already swapped to PaymentConfirm before this code runs: PaymentCreate::get_trackers calls if_not_create_change_operation and returns the swapped op in GetTrackerResponse, which payments_operation_core shadows its binding with. Independently, should_call_connector has no "PaymentCreate" arm, so a still-PaymentCreate operation would yield no connector and never reach this function. The disjunct was unreachable; the guard is back to is_operation_confirm and the helper is gone. Note: payments.rs:5465-5472 uses the same ("PaymentCreate", true) match and is dead for the same reason, but is already on main (12fd469, #13163). Left alone here; follow-up to be raised. 2. Drop the mock_locker vault work (payment_methods/vault.rs) Reverted to main. It is not needed for this feature in any real environment (integration_test/sandbox/production carry no [locker] section), and the guard sat at the shared call_to_vault, changing behaviour for ~16 call sites that previously always reached the real vault. 3. Sequence the wallet vault conversion from Payments Core (Feature step) The second tokenization pass no longer fires from inside tokenization::add_payment_method_token, which core only reaches through the Feature trait. It is now its own step declared on Feature with a no-op default, implemented for authorize and setup_mandate, and sequenced from call_connector_service (v1 and v2) alongside create_order_at_connector and the connector-customer call - the same shape as should_call_connector_customer, which is the sibling setup_future_usage connector call for this feature and already lived in core. add_payment_method_token returns to exactly what it was on main. The step runs before update_router_data_with_payment_method_token_result and takes/returns PaymentMethodTokenResult. Order matters: build_unified_connector_service_payment_method early-returns a bare Token payment method when payment_method_token is PaymentMethodToken::Token, so folding the first pass's handle in first would make the conversion request carry payment_method: Token instead of the wallet variant the connector matches on, silently degrading to the single-use endpoint. Also drops the Box::pin/Box::new large_futures workaround, which only existed because the call sat in a function generic over four flows, and adds a CONNECTOR_WALLET_VAULT_CONVERSION metric so the fail-soft conversion leg is observable. No UCS-side change: juspay/hyperswitch-prism#1986 keys off request fields only and is unaffected by where hyperswitch sequences the call.
|
UCS follow-up is ready: juspay/hyperswitch-prism#2023 This HS branch now runs the Paysafe wallet conversion only after a successful CIT, persists only the validated MULTI_USE handle, and leaves every other connector on the default no-op path. The rebased branch passed |
What
Wires the hyperswitch side of Paysafe wallet recurring so a wallet CIT vaults a reusable payment method that a later MIT can replay. Card recurring already worked; this closes the gaps for Apple Pay and Google Pay. Pairs with the UCS-side connector change (juspay/hyperswitch-prism#1986).
Changes
paysafe.rs—should_call_connector_customerfires for Apple Pay / Google Pay too (not just Card), so wallets get a Paysafe connector customer to vault under. Newrequires_wallet_vault_conversioncapability opts Paysafe into the conversion leg.ConnectorSpecifications— addsrequires_wallet_vault_conversion(defaultfalse) +ConnectorEnumforwarding, so core stays connector-agnostic instead of hardcoding a connector name in the token gateway.payment_method_token_create_gateway.rs— after leg-1 mints a single-use wallet handle, a second tokenize (in its own UCS logging wrapper) converts it into a customer-vaulted MULTI_USE handle viaconnector_feature_data. Fully fail-soft: any leg-2 failure keeps the single-use handle so the payment still succeeds (only a future MIT is not replayable). RequiresCloneonGrpcHeadersUcs/UcsReferenceId/UcsResourceId.payments.rs— runs connector tokenization for a one-shotPaymentCreatewithconfirm=true(not just standaloneConfirm), via a newis_operation_createhelper with a unit test.[tokenization]entry configured — a general fix, but worth noting for connectors beyond Paysafe.tokenization.rs— undermock_locker(local dev only; deployment configs omit it → prod/CI use the real locker) the wallet vault call has no backend; degrade to a locker-less payment method so the mandate (connector_mandate_details) is still saved.google_payin the paysafe[tokenization]filter, addpaysafeto theapple_pay/google_paymandate connector lists, and route paysafe UCS-only (the native connector does not implement the wallet flows) — applied todevelopment.tomland all deployment configs (integration_test/sandbox/production). If your org uses a separate deployment-config repo, those need the same edits.Testing
Verified end-to-end (hyperswitch → UCS → Paysafe sandbox):
succeededwithpayment_method_id(mandate is a converted MULTI_USE handle) → MITsucceeded.succeededwithpayment_method_id→ MIT ×2succeeded.🤖 Generated with Claude Code
Closes
Closes juspay/hyperswitch-cloud#22283