Skip to content

refactor(payment_methods): Refactor PM update API to allow multiple updates in a session#11209

Open
Sarthak1799 wants to merge 4 commits intomainfrom
pm-update-refactor
Open

refactor(payment_methods): Refactor PM update API to allow multiple updates in a session#11209
Sarthak1799 wants to merge 4 commits intomainfrom
pm-update-refactor

Conversation

@Sarthak1799
Copy link
Contributor

@Sarthak1799 Sarthak1799 commented Feb 10, 2026

Type of Change

  • Bugfix
  • New feature
  • Enhancement
  • Refactoring
  • Dependency updates
  • Documentation
  • CI/CD

Description

This pull request refactors and modularizes the payment method update logic, especially under the v2 feature flag, and improves the management of associated payment method tokens in sessions. The most significant change is the introduction of the PaymentMethodUpdateHandler, which encapsulates the update flow for payment methods, making the code more maintainable and testable. Additionally, the handling of associated payment method tokens in session update and delete flows is improved to ensure proper ordering and removal.

Payment Method Update Refactor:

  • Introduced a new PaymentMethodUpdateHandler struct (behind the v2 feature flag) in payment_methods.rs and types/payment_methods.rs, encapsulating the logic for validating, updating CVC, performing vaulting operations, and generating responses for payment method updates. This modularizes and streamlines the update flow. [1] [2]
  • Refactored update_payment_method_core to use the new handler, replacing inlined update logic with method calls on the handler for validation, CVC update, vaulting, and response generation. This reduces code duplication and improves readability.

Session Token Management Improvements:

  • Updated payment_methods_session_update_payment_method to ensure that the session token from the request is inserted at the beginning of the associated payment methods list, and that duplicates are avoided.
  • Modified payment_methods_session_delete_payment_method to properly remove the specified session token from the associated payment methods list and update the session in the database. [1] [2]

Minor Code Cleanups:

  • Removed an unnecessary variable assignment in payment_methods_session_update_payment_method.

Additional Changes

  • This PR modifies the API contract
  • This PR modifies the database schema
  • This PR modifies application configuration/environment variables

Motivation and Context

How did you test it?

Create a PM session
curl --location 'http://localhost:8080/v2/payment-method-sessions' \
--header 'x-profile-id: pro_6OGrLOtLhmE7WO45YuOH' \
--header 'Authorization: api-key=dev_pilXv1urqN1c9ATw1zUDoyJbjQzDkuuP52reiaI8Bu4BoPgwHSQxBzU1EKcSrftj' \
--header 'Content-Type: application/json' \
--header 'api-key: dev_pilXv1urqN1c9ATw1zUDoyJbjQzDkuuP52reiaI8Bu4BoPgwHSQxBzU1EKcSrftj' \
--data-raw '{
    "customer_id": "12345_cus_019c3299cbf777a3a9e77c78c1ebe037",
    "billing": {
        "address": {
            "first_name": "John",
            "last_name": "Dough"
        },
        "email": "example@example.com"
    }  
}'

response -

{
   "id":"12345_pms_019c48020da37c33b0f1fe6a65445890",
   "customer_id":"12345_cus_019c3299cbf777a3a9e77c78c1ebe037",
   "billing":{
      "address":{
         "city":null,
         "country":null,
         "line1":null,
         "line2":null,
         "line3":null,
         "zip":null,
         "state":null,
         "first_name":"John",
         "last_name":"Dough",
         "origin_zip":null
      },
      "phone":null,
      "email":"example@example.com"
   },
   "psp_tokenization":null,
   "network_tokenization":null,
   "tokenization_data":null,
   "expires_at":"2026-02-10T14:58:40.067Z",
   "client_secret":"cs_019c48020da37c33b0f1fe7eeaad24f7",
   "return_url":null,
   "next_action":null,
   "authentication_details":null,
   "associated_payment_methods":null,
   "associated_token_id":null,
   "storage_type":null,
   "card_cvc_token_storage":null
}
List PM
curl --location 'http://localhost:8080/v2/payment-method-sessions/12345_pms_019c48020da37c33b0f1fe6a65445890/list-payment-methods' \
--header 'x-profile-id: pro_6OGrLOtLhmE7WO45YuOH' \
--header 'Authorization: publishable-key=pk_dev_ff6051c0dba64ef48243dfb9403f9598,client-secret=cs_019c48020da37c33b0f1fe7eeaad24f7' \
--data ''"

response -

{
   "payment_methods_enabled":[
      {
         "payment_method_type":"card_redirect",
         "payment_method_subtype":"card_redirect",
         "required_fields":[
            
         ]
      },
      {
         "payment_method_type":"card",
         "payment_method_subtype":"credit",
         "required_fields":[
            {
               "required_field":"payment_method_data.card.card_number",
               "display_name":"card_number",
               "field_type":"user_card_number",
               "value":null
            },
            {
               "required_field":"payment_method_data.card.card_exp_month",
               "display_name":"card_exp_month",
               "field_type":"user_card_expiry_month",
               "value":null
            },
            {
               "required_field":"payment_method_data.card.card_exp_year",
               "display_name":"card_exp_year",
               "field_type":"user_card_expiry_year",
               "value":null
            },
            {
               "required_field":"payment_method_data.card.card_cvc",
               "display_name":"card_cvc",
               "field_type":"user_card_cvc",
               "value":null
            }
         ]
      },
      {
         "payment_method_type":"card",
         "payment_method_subtype":"debit",
         "required_fields":[
            {
               "required_field":"payment_method_data.card.card_number",
               "display_name":"card_number",
               "field_type":"user_card_number",
               "value":null
            },
            {
               "required_field":"payment_method_data.card.card_exp_month",
               "display_name":"card_exp_month",
               "field_type":"user_card_expiry_month",
               "value":null
            },
            {
               "required_field":"payment_method_data.card.card_exp_year",
               "display_name":"card_exp_year",
               "field_type":"user_card_expiry_year",
               "value":null
            },
            {
               "required_field":"payment_method_data.card.card_cvc",
               "display_name":"card_cvc",
               "field_type":"user_card_cvc",
               "value":null
            }
         ]
      },
      {
         "payment_method_type":"wallet",
         "payment_method_subtype":"google_pay",
         "required_fields":[
            
         ]
      },
      {
         "payment_method_type":"wallet",
         "payment_method_subtype":"apple_pay",
         "required_fields":[
            
         ]
      },
      {
         "payment_method_type":"wallet",
         "payment_method_subtype":"we_chat_pay",
         "required_fields":[
            
         ]
      },
      {
         "payment_method_type":"wallet",
         "payment_method_subtype":"ali_pay",
         "required_fields":[
            
         ]
      },
      {
         "payment_method_type":"wallet",
         "payment_method_subtype":"paypal",
         "required_fields":[
            
         ]
      },
      {
         "payment_method_type":"wallet",
         "payment_method_subtype":"mb_way",
         "required_fields":[
            
         ]
      },
      {
         "payment_method_type":"pay_later",
         "payment_method_subtype":"klarna",
         "required_fields":[
            
         ]
      },
      {
         "payment_method_type":"pay_later",
         "payment_method_subtype":"affirm",
         "required_fields":[
            
         ]
      },
      {
         "payment_method_type":"pay_later",
         "payment_method_subtype":"afterpay_clearpay",
         "required_fields":[
            
         ]
      },
      {
         "payment_method_type":"pay_later",
         "payment_method_subtype":"walley",
         "required_fields":[
            
         ]
      },
      {
         "payment_method_type":"bank_redirect",
         "payment_method_subtype":"giropay",
         "required_fields":[
            {
               "required_field":"payment_method_data.billing.address.first_name",
               "display_name":"billing_name",
               "field_type":"user_billing_name",
               "value":null
            },
            {
               "required_field":"payment_method_data.billing.address.last_name",
               "display_name":"billing_name",
               "field_type":"user_billing_name",
               "value":null
            }
         ]
      },
      {
         "payment_method_type":"bank_redirect",
         "payment_method_subtype":"ideal",
         "required_fields":[
            {
               "required_field":"payment_method_data.billing.address.first_name",
               "display_name":"billing_name",
               "field_type":"user_full_name",
               "value":null
            },
            {
               "required_field":"payment_method_data.billing.address.last_name",
               "display_name":"billing_name",
               "field_type":"user_full_name",
               "value":null
            },
            {
               "required_field":"payment_method_data.billing.email",
               "display_name":"billing_email",
               "field_type":"user_email_address",
               "value":null
            }
         ]
      },
      {
         "payment_method_type":"bank_redirect",
         "payment_method_subtype":"eps",
         "required_fields":[
            
         ]
      },
      {
         "payment_method_type":"bank_redirect",
         "payment_method_subtype":"bancontact_card",
         "required_fields":[
            {
               "required_field":"payment_method_data.billing.address.first_name",
               "display_name":"billing_name",
               "field_type":"user_full_name",
               "value":null
            },
            {
               "required_field":"payment_method_data.billing.address.last_name",
               "display_name":"billing_name",
               "field_type":"user_full_name",
               "value":null
            },
            {
               "required_field":"payment_method_data.billing.email",
               "display_name":"email",
               "field_type":"user_email_address",
               "value":null
            }
         ]
      },
      {
         "payment_method_type":"bank_redirect",
         "payment_method_subtype":"przelewy24",
         "required_fields":[
            
         ]
      },
      {
         "payment_method_type":"bank_redirect",
         "payment_method_subtype":"sofort",
         "required_fields":[
            {
               "required_field":"payment_method_data.billing.address.country",
               "display_name":"country",
               "field_type":"text",
               "value":null
            },
            {
               "required_field":"payment_method_data.billing.address.first_name",
               "display_name":"account_holder_name",
               "field_type":"user_billing_name",
               "value":null
            },
            {
               "required_field":"payment_method_data.billing.address.last_name",
               "display_name":"account_holder_name",
               "field_type":"user_billing_name",
               "value":null
            },
            {
               "required_field":"payment_method_data.billing.email",
               "display_name":"email",
               "field_type":"user_email_address",
               "value":null
            }
         ]
      },
      {
         "payment_method_type":"bank_redirect",
         "payment_method_subtype":"blik",
         "required_fields":[
            {
               "required_field":"payment_method_data.bank_redirect.blik.blik_code",
               "display_name":"blik_code",
               "field_type":"user_blik_code",
               "value":null
            }
         ]
      },
      {
         "payment_method_type":"bank_redirect",
         "payment_method_subtype":"trustly",
         "required_fields":[
            
         ]
      },
      {
         "payment_method_type":"bank_redirect",
         "payment_method_subtype":"online_banking_finland",
         "required_fields":[
            
         ]
      },
      {
         "payment_method_type":"bank_redirect",
         "payment_method_subtype":"online_banking_poland",
         "required_fields":[
            
         ]
      },
      {
         "payment_method_type":"bank_transfer",
         "payment_method_subtype":"ach",
         "required_fields":[
            {
               "required_field":"payment_method_data.billing.email",
               "display_name":"email",
               "field_type":"user_email_address",
               "value":null
            }
         ]
      },
      {
         "payment_method_type":"bank_transfer",
         "payment_method_subtype":"sepa",
         "required_fields":[
            
         ]
      },
      {
         "payment_method_type":"bank_transfer",
         "payment_method_subtype":"bacs",
         "required_fields":[
            
         ]
      },
      {
         "payment_method_type":"bank_debit",
         "payment_method_subtype":"ach",
         "required_fields":[
            {
               "required_field":"payment_method_data.billing.address.first_name",
               "display_name":"billing_address_first_name",
               "field_type":"text",
               "value":null
            },
            {
               "required_field":"payment_method_data.billing.address.last_name",
               "display_name":"billing_address_last_name",
               "field_type":"text",
               "value":null
            },
            {
               "required_field":"payment_method_data.bank_debit.ach.account_number",
               "display_name":"account_number",
               "field_type":"text",
               "value":null
            },
            {
               "required_field":"payment_method_data.bank_debit.ach.routing_number",
               "display_name":"routing_number",
               "field_type":"text",
               "value":null
            }
         ]
      },
      {
         "payment_method_type":"bank_debit",
         "payment_method_subtype":"sepa",
         "required_fields":[
            {
               "required_field":"payment_method_data.billing.address.first_name",
               "display_name":"owner_name",
               "field_type":"user_billing_name",
               "value":null
            },
            {
               "required_field":"payment_method_data.billing.address.last_name",
               "display_name":"owner_name",
               "field_type":"user_billing_name",
               "value":null
            },
            {
               "required_field":"payment_method_data.bank_debit.sepa_bank_debit.iban",
               "display_name":"iban",
               "field_type":"user_iban",
               "value":null
            },
            {
               "required_field":"email",
               "display_name":"email",
               "field_type":"user_iban",
               "value":null
            }
         ]
      },
      {
         "payment_method_type":"bank_debit",
         "payment_method_subtype":"bacs",
         "required_fields":[
            {
               "required_field":"payment_method_data.billing.address.first_name",
               "display_name":"billing_address_first_name",
               "field_type":"text",
               "value":null
            },
            {
               "required_field":"payment_method_data.bank_debit.bacs.account_number",
               "display_name":"account_number",
               "field_type":"text",
               "value":null
            },
            {
               "required_field":"payment_method_data.bank_debit.bacs.sort_code",
               "display_name":"sort_code",
               "field_type":"text",
               "value":null
            },
            {
               "required_field":"payment_method_data.billing.address.country",
               "display_name":"country",
               "field_type":"text",
               "value":null
            },
            {
               "required_field":"payment_method_data.billing.address.zip",
               "display_name":"zip",
               "field_type":"text",
               "value":null
            },
            {
               "required_field":"payment_method_data.billing.address.line1",
               "display_name":"line1",
               "field_type":"text",
               "value":null
            }
         ]
      },
      {
         "payment_method_type":"bank_debit",
         "payment_method_subtype":"becs",
         "required_fields":[
            {
               "required_field":"payment_method_data.billing.address.first_name",
               "display_name":"billing_address_first_name",
               "field_type":"text",
               "value":null
            },
            {
               "required_field":"payment_method_data.billing.address.last_name",
               "display_name":"billing_address_last_name",
               "field_type":"text",
               "value":null
            },
            {
               "required_field":"payment_method_data.bank_debit.becs.account_number",
               "display_name":"account_number",
               "field_type":"text",
               "value":null
            },
            {
               "required_field":"payment_method_data.bank_debit.becs.bsb_number",
               "display_name":"bsb_number",
               "field_type":"text",
               "value":null
            },
            {
               "required_field":"email",
               "display_name":"email",
               "field_type":"text",
               "value":null
            }
         ]
      }
   ],
   "customer_payment_methods":[
      {
         "payment_method_token":"token_hseqI29Z5l5XZU4kjAa9",
         "customer_id":"12345_cus_019c3299cbf777a3a9e77c78c1ebe037",
         "payment_method_type":"card",
         "payment_method_subtype":"credit",
         "recurring_enabled":true,
         "payment_method_data":{
            "card":{
               "issuer_country":null,
               "last4_digits":"4242",
               "expiry_month":"03",
               "expiry_year":"28",
               "card_holder_name":"joseph Doe",
               "card_fingerprint":null,
               "nick_name":null,
               "card_network":null,
               "card_isin":null,
               "card_issuer":null,
               "card_type":null,
               "saved_to_locker":true
            }
         },
         "bank":null,
         "created":"2026-02-10T14:45:25.756Z",
         "requires_cvv":true,
         "last_used_at":"2026-02-10T14:45:25.756Z",
         "is_default":false,
         "billing":{
            "address":{
               "city":null,
               "country":null,
               "line1":null,
               "line2":null,
               "line3":null,
               "zip":null,
               "state":null,
               "first_name":"John",
               "last_name":"Dough",
               "origin_zip":null
            },
            "phone":null,
            "email":"example@example.com"
         }
      },
      {
         "payment_method_token":"token_G4L17T6vUtCVS12K2M3v",
         "customer_id":"12345_cus_019c3299cbf777a3a9e77c78c1ebe037",
         "payment_method_type":"card",
         "payment_method_subtype":"credit",
         "recurring_enabled":true,
         "payment_method_data":{
            "card":{
               "issuer_country":null,
               "last4_digits":"4242",
               "expiry_month":"02",
               "expiry_year":"28",
               "card_holder_name":"Sarthak2",
               "card_fingerprint":null,
               "nick_name":null,
               "card_network":null,
               "card_isin":null,
               "card_issuer":null,
               "card_type":null,
               "saved_to_locker":true
            }
         },
         "bank":null,
         "created":"2026-02-10T12:02:02.588Z",
         "requires_cvv":true,
         "last_used_at":"2026-02-10T12:02:02.588Z",
         "is_default":false,
         "billing":{
            "address":{
               "city":null,
               "country":null,
               "line1":null,
               "line2":null,
               "line3":null,
               "zip":null,
               "state":null,
               "first_name":"John",
               "last_name":"Dough",
               "origin_zip":null
            },
            "phone":null,
            "email":"example@example.com"
         }
      }
   ]
}
Update a PM
curl --location --request PUT 'http://localhost:8080/v2/payment-method-sessions/12345_pms_019c48020da37c33b0f1fe6a65445890/update-saved-payment-method' \
--header 'x-profile-id: pro_6OGrLOtLhmE7WO45YuOH' \
--header 'Authorization: publishable-key=pk_dev_ff6051c0dba64ef48243dfb9403f9598,client-secret=cs_019c48020da37c33b0f1fe7eeaad24f7' \
--header 'Content-Type: application/json' \
--header 'api-key: dev_pilXv1urqN1c9ATw1zUDoyJbjQzDkuuP52reiaI8Bu4BoPgwHSQxBzU1EKcSrftj' \
--data '{
    "payment_method_token": "token_hseqI29Z5l5XZU4kjAa9",
    "payment_method_data": {
        "card": {
            "card_cvc": "123",
            "card_holder_name": "Sarthak2"
        }
    },
    "network_transaction_id": "nt-123123",
    "connector_token_details": {
        "connector_id": "mca_aKrdOOYPudjJswiyx4lM",
        "token_type": "multi_use",
        "token": "token-1234",
        "status": "active"
    }
}'

response -

{
   "id":"12345_pms_019c48020da37c33b0f1fe6a65445890",
   "customer_id":"12345_cus_019c3299cbf777a3a9e77c78c1ebe037",
   "billing":{
      "address":{
         "city":null,
         "country":null,
         "line1":null,
         "line2":null,
         "line3":null,
         "zip":null,
         "state":null,
         "first_name":"John",
         "last_name":"Dough",
         "origin_zip":null
      },
      "phone":null,
      "email":"example@example.com"
   },
   "psp_tokenization":null,
   "network_tokenization":null,
   "tokenization_data":null,
   "expires_at":"2026-02-10T14:58:40.067Z",
   "client_secret":"CLIENT_SECRET_REDACTED",
   "return_url":null,
   "next_action":null,
   "authentication_details":null,
   "associated_payment_methods":[
      {
         "payment_method_token":{
            "type":"payment_method_session_token",
            "data":"token_hseqI29Z5l5XZU4kjAa9"
         }
      },
      {
         "payment_method_token":{
            "type":"payment_method_session_token",
            "data":"token_G4L17T6vUtCVS12K2M3v"
         }
      }
   ],
   "associated_token_id":null,
   "storage_type":null,
   "card_cvc_token_storage":{
      "is_stored":true,
      "expires_at":"2026-02-10T15:03:27.654Z"
   },
   "payment_method_data":{
      "card":{
         "issuer_country":null,
         "last4_digits":"4242",
         "expiry_month":"03",
         "expiry_year":"28",
         "card_holder_name":"Sarthak2",
         "card_fingerprint":null,
         "nick_name":null,
         "card_network":null,
         "card_isin":null,
         "card_issuer":null,
         "card_type":null,
         "saved_to_locker":true
      }
   }
}
Update a different PM in the same session
curl --location --request PUT 'http://localhost:8080/v2/payment-method-sessions/12345_pms_019c48020da37c33b0f1fe6a65445890/update-saved-payment-method' \
--header 'x-profile-id: pro_6OGrLOtLhmE7WO45YuOH' \
--header 'Authorization: publishable-key=pk_dev_ff6051c0dba64ef48243dfb9403f9598,client-secret=cs_019c48020da37c33b0f1fe7eeaad24f7' \
--header 'Content-Type: application/json' \
--header 'api-key: dev_pilXv1urqN1c9ATw1zUDoyJbjQzDkuuP52reiaI8Bu4BoPgwHSQxBzU1EKcSrftj' \
--data '{
    "payment_method_token": "token_G4L17T6vUtCVS12K2M3v",
    "payment_method_data": {
        "card": {
            "card_cvc": "123",
            "card_holder_name": "Sarthak3"
        }
    },
    "network_transaction_id": "nt-123123",
    "connector_token_details": {
        "connector_id": "mca_aKrdOOYPudjJswiyx4lM",
        "token_type": "multi_use",
        "token": "token-1234",
        "status": "active"
    }
}'

response -

{
   "id":"12345_pms_019c48020da37c33b0f1fe6a65445890",
   "customer_id":"12345_cus_019c3299cbf777a3a9e77c78c1ebe037",
   "billing":{
      "address":{
         "city":null,
         "country":null,
         "line1":null,
         "line2":null,
         "line3":null,
         "zip":null,
         "state":null,
         "first_name":"John",
         "last_name":"Dough",
         "origin_zip":null
      },
      "phone":null,
      "email":"example@example.com"
   },
   "psp_tokenization":null,
   "network_tokenization":null,
   "tokenization_data":null,
   "expires_at":"2026-02-10T14:58:40.067Z",
   "client_secret":"CLIENT_SECRET_REDACTED",
   "return_url":null,
   "next_action":null,
   "authentication_details":null,
   "associated_payment_methods":[
      {
         "payment_method_token":{
            "type":"payment_method_session_token",
            "data":"token_G4L17T6vUtCVS12K2M3v"
         }
      },
      {
         "payment_method_token":{
            "type":"payment_method_session_token",
            "data":"token_hseqI29Z5l5XZU4kjAa9"
         }
      }
   ],
   "associated_token_id":null,
   "storage_type":null,
   "card_cvc_token_storage":{
      "is_stored":true,
      "expires_at":"2026-02-10T15:06:36.997Z"
   },
   "payment_method_data":{
      "card":{
         "issuer_country":null,
         "last4_digits":"4242",
         "expiry_month":"02",
         "expiry_year":"28",
         "card_holder_name":"Sarthak3",
         "card_fingerprint":null,
         "nick_name":null,
         "card_network":null,
         "card_isin":null,
         "card_issuer":null,
         "card_type":null,
         "saved_to_locker":true
      }
   }
}

Checklist

  • I formatted the code cargo +nightly fmt --all
  • I addressed lints thrown by cargo clippy
  • I reviewed the submitted code
  • I added unit tests for my changes where possible

@Sarthak1799 Sarthak1799 self-assigned this Feb 10, 2026
@Sarthak1799 Sarthak1799 added the A-payment-methods Area: Payment Methods label Feb 10, 2026
@Sarthak1799 Sarthak1799 requested review from a team as code owners February 10, 2026 12:39
@semanticdiff-com
Copy link

semanticdiff-com bot commented Feb 10, 2026

Review changes with  SemanticDiff

Changed Files
File Status
  crates/router/src/core/payment_methods.rs  27% smaller
  crates/router/src/types/payment_methods.rs  0% smaller

Comment on lines +5475 to +5478
if !self.request.is_payment_method_update_required() {
return Ok(());
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avoid early return

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried to use when here but that didn't work.
This return would be required as want to break the flow here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-payment-methods Area: Payment Methods

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants